diff --git a/src/components/Dashboard/Finance/Invoices/InvoiceInfo.jsx b/src/components/Dashboard/Finance/Invoices/InvoiceInfo.jsx
index 01d210e..c66d4e0 100644
--- a/src/components/Dashboard/Finance/Invoices/InvoiceInfo.jsx
+++ b/src/components/Dashboard/Finance/Invoices/InvoiceInfo.jsx
@@ -1,6 +1,6 @@
import { useRef, useState } from 'react'
import { useLocation } from 'react-router-dom'
-import { Space, Flex, Card } from 'antd'
+import { Space, Flex, Card, Modal } from 'antd'
import { LoadingOutlined } from '@ant-design/icons'
import loglevel from 'loglevel'
import config from '../../../../config.js'
@@ -8,6 +8,7 @@ import useCollapseState from '../../hooks/useCollapseState.js'
import NotesPanel from '../../common/NotesPanel.jsx'
import InfoCollapse from '../../common/InfoCollapse.jsx'
import ObjectInfo from '../../common/ObjectInfo.jsx'
+import ObjectProperty from '../../common/ObjectProperty.jsx'
import ViewButton from '../../common/ViewButton.jsx'
import InfoCircleIcon from '../../../Icons/InfoCircleIcon.jsx'
import NoteIcon from '../../../Icons/NoteIcon.jsx'
@@ -21,7 +22,13 @@ import ObjectTable from '../../common/ObjectTable.jsx'
import InfoCollapsePlaceholder from '../../common/InfoCollapsePlaceholder.jsx'
import DocumentPrintButton from '../../common/DocumentPrintButton.jsx'
import ScrollBox from '../../common/ScrollBox.jsx'
-import { getModelByName } from '../../../../database/ObjectModels.js'
+import {
+ getModelByName,
+ getModelProperty
+} from '../../../../database/ObjectModels.js'
+import OrderItemIcon from '../../../Icons/OrderItemIcon.jsx'
+import ShipmentIcon from '../../../Icons/ShipmentIcon.jsx'
+import PostInvoice from './PostInvoice.jsx'
const log = loglevel.getLogger('InvoiceInfo')
log.setLevel(config.logLevel)
@@ -31,14 +38,13 @@ const InvoiceInfo = () => {
const objectFormRef = useRef(null)
const actionHandlerRef = useRef(null)
const invoiceId = new URLSearchParams(location.search).get('invoiceId')
- const [collapseState, updateCollapseState] = useCollapseState(
- 'InvoiceInfo',
- {
- info: true,
- notes: true,
- auditLogs: true
- }
- )
+ const [collapseState, updateCollapseState] = useCollapseState('InvoiceInfo', {
+ info: true,
+ invoiceOrderItems: true,
+ invoiceShipments: true,
+ notes: true,
+ auditLogs: true
+ })
const [objectFormState, setEditFormState] = useState({
isEditing: false,
@@ -48,6 +54,7 @@ const InvoiceInfo = () => {
loading: false,
objectData: {}
})
+ const [postInvoiceOpen, setPostInvoiceOpen] = useState(false)
const actions = {
reload: () => {
@@ -69,12 +76,17 @@ const InvoiceInfo = () => {
delete: () => {
objectFormRef?.current?.handleDelete?.()
return true
+ },
+ post: () => {
+ setPostInvoiceOpen(true)
+ return true
}
}
- const editDisabled = getModelByName('invoice')
- ?.actions?.find((action) => action.name === 'edit')
- ?.disabled(objectFormState.objectData) ?? false
+ const editDisabled =
+ getModelByName('invoice')
+ ?.actions?.find((action) => action.name === 'edit')
+ ?.disabled(objectFormState.objectData) ?? false
return (
<>
@@ -99,6 +111,8 @@ const InvoiceInfo = () => {
disabled={objectFormState.loading}
items={[
{ key: 'info', label: 'Invoice Information' },
+ { key: 'invoiceOrderItems', label: 'Invoice Order Items' },
+ { key: 'invoiceShipments', label: 'Invoice Shipments' },
{ key: 'notes', label: 'Notes' },
{ key: 'auditLogs', label: 'Audit Logs' }
]}
@@ -171,6 +185,42 @@ const InvoiceInfo = () => {
type='invoice'
labelWidth='225px'
objectData={objectData}
+ visibleProperties={{
+ invoiceOrderItems: false,
+ invoiceShipments: false
+ }}
+ />
+
+ }
+ active={collapseState.invoiceOrderItems}
+ onToggle={(expanded) =>
+ updateCollapseState('invoiceOrderItems', expanded)
+ }
+ collapseKey='invoiceOrderItems'
+ >
+
+
+ }
+ active={collapseState.invoiceShipments}
+ onToggle={(expanded) =>
+ updateCollapseState('invoiceShipments', expanded)
+ }
+ collapseKey='invoiceShipments'
+ >
+
@@ -210,9 +260,26 @@ const InvoiceInfo = () => {
+ {
+ setPostInvoiceOpen(false)
+ }}
+ width={500}
+ footer={null}
+ destroyOnHidden={true}
+ centered={true}
+ >
+ {
+ setPostInvoiceOpen(false)
+ actions.reload()
+ }}
+ objectData={objectFormState.objectData}
+ />
+
>
)
}
export default InvoiceInfo
-
diff --git a/src/components/Dashboard/Finance/Invoices/NewInvoice.jsx b/src/components/Dashboard/Finance/Invoices/NewInvoice.jsx
index 620cdaf..ebe0699 100644
--- a/src/components/Dashboard/Finance/Invoices/NewInvoice.jsx
+++ b/src/components/Dashboard/Finance/Invoices/NewInvoice.jsx
@@ -1,4 +1,5 @@
import PropTypes from 'prop-types'
+import dayjs from 'dayjs'
import ObjectInfo from '../../common/ObjectInfo'
import NewObjectForm from '../../common/NewObjectForm'
import WizardView from '../../common/WizardView'
@@ -10,7 +11,8 @@ const NewInvoice = ({ onOk, reset, defaultValues }) => {
reset={reset}
defaultValues={{
state: { type: 'draft' },
- invoiceType: 'sales',
+ issuedAt: new Date(),
+ dueAt: dayjs().add(3, 'day').toDate(),
...defaultValues
}}
>
@@ -30,27 +32,10 @@ const NewInvoice = ({ onOk, reset, defaultValues }) => {
visibleProperties={{
orderType: true,
order: true,
- vendor: true,
- invoiceDate: true,
- dueDate: true
- }}
- />
- )
- },
- {
- title: 'Optional',
- key: 'optional',
- content: (
-
)
@@ -66,6 +51,8 @@ const NewInvoice = ({ onOk, reset, defaultValues }) => {
visibleProperties={{
_id: false,
createdAt: false,
+ invoiceOrderItems: false,
+ invoiceShipments: false,
updatedAt: false,
_reference: false,
totalAmount: false,
diff --git a/src/components/Dashboard/Finance/Invoices/PostInvoice.jsx b/src/components/Dashboard/Finance/Invoices/PostInvoice.jsx
new file mode 100644
index 0000000..8f18dd5
--- /dev/null
+++ b/src/components/Dashboard/Finance/Invoices/PostInvoice.jsx
@@ -0,0 +1,42 @@
+import { useState, useContext } from 'react'
+import PropTypes from 'prop-types'
+import { ApiServerContext } from '../../context/ApiServerContext'
+import { message } from 'antd'
+import MessageDialogView from '../../common/MessageDialogView.jsx'
+
+const PostInvoice = ({ onOk, objectData }) => {
+ const [postLoading, setPostLoading] = useState(false)
+ const { sendObjectFunction } = useContext(ApiServerContext)
+
+ const handlePost = async () => {
+ setPostLoading(true)
+ try {
+ const result = await sendObjectFunction(objectData._id, 'Invoice', 'post')
+ if (result) {
+ message.success('Invoice posted successfully')
+ onOk(result)
+ }
+ } catch (error) {
+ console.error('Error posting invoice:', error)
+ } finally {
+ setPostLoading(false)
+ }
+ }
+
+ return (
+
+ )
+}
+
+PostInvoice.propTypes = {
+ onOk: PropTypes.func.isRequired,
+ objectData: PropTypes.object
+}
+
+export default PostInvoice
diff --git a/src/database/models/Invoice.js b/src/database/models/Invoice.js
index eed44be..2522707 100644
--- a/src/database/models/Invoice.js
+++ b/src/database/models/Invoice.js
@@ -72,30 +72,16 @@ export const Invoice = {
},
{ type: 'divider' },
{
- name: 'send',
- label: 'Send',
+ name: 'post',
+ label: 'Post',
type: 'button',
icon: CheckIcon,
url: (_id) =>
- `/dashboard/finance/invoices/info?invoiceId=${_id}&action=send`,
+ `/dashboard/finance/invoices/info?invoiceId=${_id}&action=post`,
visible: (objectData) => {
return objectData?.state?.type == 'draft'
}
},
- {
- name: 'markPaid',
- label: 'Mark Paid',
- type: 'button',
- icon: CheckIcon,
- url: (_id) =>
- `/dashboard/finance/invoices/info?invoiceId=${_id}&action=markPaid`,
- visible: (objectData) => {
- return (
- objectData?.state?.type == 'sent' ||
- objectData?.state?.type == 'partiallyPaid'
- )
- }
- },
{
name: 'cancel',
label: 'Cancel',
@@ -111,10 +97,7 @@ export const Invoice = {
)
},
visible: (objectData) => {
- return (
- objectData?.state?.type == 'draft' ||
- objectData?.state?.type == 'sent'
- )
+ return objectData?.state?.type == 'sent'
}
}
],
@@ -172,16 +155,11 @@ export const Invoice = {
},
{ name: 'state', label: 'State', type: 'state', readOnly: true },
{
- name: 'invoiceDate',
- label: 'Invoice Date',
- type: 'date',
- readOnly: false
- },
- {
- name: 'dueDate',
- label: 'Due Date',
- type: 'date',
- readOnly: false
+ name: 'issuedAt',
+ label: 'Issued At',
+ type: 'dateTime',
+ readOnly: false,
+ required: true
},
{
name: 'orderType',
@@ -190,6 +168,12 @@ export const Invoice = {
masterFilter: ['purchaseOrder', 'salesOrder'],
required: true
},
+ {
+ name: 'dueAt',
+ label: 'Due At',
+ type: 'dateTime',
+ required: true
+ },
{
name: 'order',
label: 'Order',
@@ -201,8 +185,14 @@ export const Invoice = {
showHyperlink: true
},
{
- name: 'vendor',
- label: 'Vendor',
+ name: 'postedAt',
+ label: 'Posted At',
+ type: 'dateTime',
+ readOnly: true
+ },
+ {
+ name: 'from',
+ label: 'From',
required: true,
type: 'object',
objectType: 'vendor',
@@ -212,34 +202,39 @@ export const Invoice = {
if (objectData?.orderType == 'purchaseOrder') {
return objectData?.order?.vendor
} else {
- return objectData?.vendor
+ return null
}
}
},
- {
- name: 'sentAt',
- label: 'Sent At',
- type: 'dateTime',
- readOnly: true
- },
{
name: 'paidAt',
label: 'Paid At',
type: 'dateTime',
readOnly: true
},
+ {
+ name: 'to',
+ label: 'To',
+ required: true,
+ type: 'object',
+ objectType: 'client',
+ showHyperlink: true,
+ readOnly: true,
+ value: (objectData) => {
+ if (objectData?.orderType == 'salesOrder') {
+ return objectData?.order?.client
+ } else {
+ return null
+ }
+ }
+ },
{
name: 'cancelledAt',
label: 'Cancelled At',
type: 'dateTime',
readOnly: true
},
- {
- name: 'overdueAt',
- label: 'Overdue At',
- type: 'dateTime',
- readOnly: true
- },
+
{
name: 'totalTaxAmount',
label: 'Total Tax Amount',
@@ -293,44 +288,277 @@ export const Invoice = {
roundNumber: 2,
columnWidth: 175,
readOnly: true
+ },
+ {
+ name: 'invoiceOrderItems',
+ label: 'Invoice Order Items',
+ type: 'objectChildren',
+ objectType: 'orderItem',
+ properties: [
+ {
+ name: 'orderItem',
+ label: 'Order Item',
+ type: 'object',
+ objectType: 'orderItem',
+ required: true,
+ showHyperlink: true
+ },
+ {
+ name: 'invoiceQuantity',
+ label: 'Quantity',
+ type: 'number',
+ required: true
+ },
+ {
+ name: 'invoiceAmount',
+ label: 'Invoice Amount',
+ type: 'number',
+ prefix: '£',
+ roundNumber: 2,
+ required: true
+ },
+ {
+ name: 'taxRate',
+ label: 'Tax Rate',
+ type: 'object',
+ objectType: 'taxRate',
+ required: false,
+ showHyperlink: true
+ },
+ {
+ name: 'invoiceAmountWithTax',
+ label: 'Invoice Amount w/ Tax',
+ type: 'number',
+ prefix: '£',
+ roundNumber: 2,
+ required: true,
+ readOnly: true,
+ value: (objectData) => {
+ const invoiceAmount = objectData?.invoiceAmount || 0
+ if (objectData?.taxRate?.rateType == 'percentage') {
+ return (
+ (invoiceAmount * (1 + objectData?.taxRate?.rate / 100)).toFixed(
+ 2
+ ) || undefined
+ )
+ } else if (objectData?.taxRate?.rateType == 'amount') {
+ return (
+ (invoiceAmount + objectData?.taxRate?.rate).toFixed(2) ||
+ undefined
+ )
+ } else {
+ return invoiceAmount || 0
+ }
+ }
+ }
+ ],
+ rollups: [
+ {
+ name: 'totalQuantity',
+ label: 'Total Quantity',
+ type: 'number',
+ property: 'invoiceQuantity',
+ value: (objectData) => {
+ return objectData?.invoiceOrderItems?.reduce(
+ (acc, item) => acc + (item.invoiceQuantity || 0),
+ 0
+ )
+ }
+ },
+ {
+ name: 'totalAmount',
+ label: 'Total Amount',
+ type: 'number',
+ property: 'invoiceAmount',
+ prefix: '£',
+ fixedNumber: 2,
+ value: (objectData) => {
+ return objectData?.invoiceOrderItems
+ ?.reduce(
+ (acc, item) =>
+ acc + (Number.parseFloat(item.invoiceAmount) || 0),
+ 0
+ )
+ .toFixed(2)
+ }
+ },
+ {
+ name: 'totalAmountWithTax',
+ label: 'Total Amount w/ Tax',
+ type: 'number',
+ property: 'invoiceAmountWithTax',
+ prefix: '£',
+ fixedNumber: 2,
+ value: (objectData) => {
+ return objectData?.invoiceOrderItems
+ ?.reduce(
+ (acc, item) =>
+ acc + (Number.parseFloat(item.invoiceAmountWithTax) || 0),
+ 0
+ )
+ .toFixed(2)
+ }
+ }
+ ]
+ },
+ {
+ name: 'invoiceShipments',
+ label: 'Invoice Shipments',
+ type: 'objectChildren',
+ objectType: 'shipment',
+ properties: [
+ {
+ name: 'shipment',
+ label: 'Shipment',
+ type: 'object',
+ objectType: 'shipment',
+ required: true,
+ showHyperlink: true
+ },
+ {
+ name: 'invoiceAmount',
+ label: 'Invoice Amount',
+ type: 'number',
+ prefix: '£',
+ roundNumber: 2,
+ required: true
+ },
+ {
+ name: 'taxRate',
+ label: 'Tax Rate',
+ type: 'object',
+ objectType: 'taxRate',
+ required: false,
+ showHyperlink: true
+ },
+ {
+ name: 'invoiceAmountWithTax',
+ label: 'Invoice Amount w/ Tax',
+ type: 'number',
+ prefix: '£',
+ roundNumber: 2,
+ required: true,
+ readOnly: true,
+ value: (objectData) => {
+ const invoiceAmount = objectData?.invoiceAmount || 0
+ if (objectData?.taxRate?.rateType == 'percentage') {
+ return (
+ (invoiceAmount * (1 + objectData?.taxRate?.rate / 100)).toFixed(
+ 2
+ ) || undefined
+ )
+ } else if (objectData?.taxRate?.rateType == 'amount') {
+ return (
+ (invoiceAmount + objectData?.taxRate?.rate).toFixed(2) ||
+ undefined
+ )
+ } else {
+ return invoiceAmount || 0
+ }
+ }
+ }
+ ],
+ rollups: [
+ {
+ name: 'totalAmount',
+ label: 'Total Amount',
+ type: 'number',
+ property: 'invoiceAmount',
+ prefix: '£',
+ roundNumber: 2,
+ value: (objectData) => {
+ return objectData?.invoiceShipments
+ ?.reduce(
+ (acc, shipment) => acc + (shipment.invoiceAmount || 0),
+ 0
+ )
+ .toFixed(2)
+ }
+ },
+ {
+ name: 'totalAmountWithTax',
+ label: 'Total Amount w/ Tax',
+ type: 'number',
+ property: 'invoiceAmountWithTax',
+ prefix: '£',
+ roundNumber: 2,
+ value: (objectData) => {
+ return objectData?.invoiceShipments
+ ?.reduce(
+ (acc, shipment) =>
+ acc + (Number.parseFloat(shipment.invoiceAmountWithTax) || 0),
+ 0
+ )
+ .toFixed(2)
+ }
+ }
+ ]
}
],
stats: [
{
- name: 'draft.count',
+ name: 'draft.draftCount.count',
label: 'Draft',
type: 'number',
color: 'default'
},
{
- name: 'sent.count',
+ name: 'draft.draftGrandTotalAmount.sum',
+ label: 'Draft Grand Total Amount',
+ type: 'number',
+ prefix: '£',
+ roundNumber: 2,
+ color: 'default'
+ },
+ {
+ name: 'sent.sentCount.count',
label: 'Sent',
type: 'number',
color: 'cyan'
},
{
- name: 'partiallyPaid.count',
+ name: 'due.dueCount.count',
+ label: 'Due',
+ type: 'number',
+ color: 'warning',
+ sum: [
+ 'sent.sentCount.count',
+ 'partiallyPaid.partiallyPaidCount.count',
+ 'overdue.overdueCount.count'
+ ]
+ },
+ {
+ name: 'due.dueGrandTotalAmount.sum',
+ label: 'Due Grand Total Amount',
+ type: 'number',
+ prefix: '£',
+ roundNumber: 2,
+ color: 'warning',
+ sum: [
+ 'sent.sentGrandTotalAmount.sum',
+ 'partiallyPaid.partiallyPaidGrandTotalAmount.sum',
+ 'overdue.overdueGrandTotalAmount.sum'
+ ]
+ },
+ {
+ name: 'partiallyPaid.partiallyPaidCount.count',
label: 'Partially Paid',
type: 'number',
color: 'processing'
},
{
- name: 'paid.count',
- label: 'Paid',
- type: 'number',
- color: 'success'
- },
- {
- name: 'overdue.count',
+ name: 'overdue.overdueCount.count',
label: 'Overdue',
type: 'number',
color: 'error'
},
{
- name: 'cancelled.count',
- label: 'Cancelled',
+ name: 'overdue.overdueGrandTotalAmount.sum',
+ label: 'Overdue Grand Total Amount',
type: 'number',
- color: 'default'
+ prefix: '£',
+ roundNumber: 2,
+ color: 'error'
}
]
}