From aa21da88b30b7bd3dbd960ffb92b2e465fa04c5f Mon Sep 17 00:00:00 2001 From: elforjani3 Date: Mon, 14 Jun 2021 15:45:11 +0200 Subject: [PATCH 01/24] fix : common rtl --- client/src/common/countries.js | 7 +++- client/src/common/currencies.js | 2 +- client/src/common/dateFormatsOptions.js | 2 +- client/src/common/fiscalYearOptions.js | 2 +- client/src/common/languagesOptions.js | 2 +- .../Preferences/General/GeneralForm.js | 35 +++++++++++++------ .../containers/Setup/SetupOrganizationForm.js | 17 +++++---- 7 files changed, 44 insertions(+), 23 deletions(-) diff --git a/client/src/common/countries.js b/client/src/common/countries.js index c86568887..c1bfe5c5a 100644 --- a/client/src/common/countries.js +++ b/client/src/common/countries.js @@ -1,3 +1,8 @@ import intl from 'react-intl-universal'; -export default [{ name: intl.get('libya'), value: 'libya' }]; +export const getCountries = () => [ + { + name: intl.get('libya'), + value: 'libya', + }, +]; diff --git a/client/src/common/currencies.js b/client/src/common/currencies.js index ae1dbd6d9..b47f16807 100644 --- a/client/src/common/currencies.js +++ b/client/src/common/currencies.js @@ -1,6 +1,6 @@ import intl from 'react-intl-universal'; -export default [ +export const getCurrencies = () => [ { name: intl.get('us_dollar'), code: 'USD' }, { name: intl.get('euro'), code: 'EUR' }, { name: intl.get('libyan_diner'), code: 'LYD' }, diff --git a/client/src/common/dateFormatsOptions.js b/client/src/common/dateFormatsOptions.js index 8c6ed99a2..3a194a790 100644 --- a/client/src/common/dateFormatsOptions.js +++ b/client/src/common/dateFormatsOptions.js @@ -1,7 +1,7 @@ import moment from 'moment'; import intl from 'react-intl-universal'; -export default [ +export const getDateFormats =()=> [ { id: 1, name: intl.get('mm_dd_yy'), diff --git a/client/src/common/fiscalYearOptions.js b/client/src/common/fiscalYearOptions.js index 24043ec80..9fd5c3845 100644 --- a/client/src/common/fiscalYearOptions.js +++ b/client/src/common/fiscalYearOptions.js @@ -1,6 +1,6 @@ import intl from 'react-intl-universal'; -export const getFiscalYearOptions = () => [ +export const getFiscalYear = () => [ { id: 0, name: `${intl.get('january')} - ${intl.get('december')}`, diff --git a/client/src/common/languagesOptions.js b/client/src/common/languagesOptions.js index 77ddb2f6b..76f630e76 100644 --- a/client/src/common/languagesOptions.js +++ b/client/src/common/languagesOptions.js @@ -1,6 +1,6 @@ import intl from 'react-intl-universal'; -export default [ +export const getLanguages = () => [ { name: intl.get('english'), value: 'en' }, { name: intl.get('arabic'), value: 'ar' }, ]; diff --git a/client/src/containers/Preferences/General/GeneralForm.js b/client/src/containers/Preferences/General/GeneralForm.js index 06c164b2a..6446ecacb 100644 --- a/client/src/containers/Preferences/General/GeneralForm.js +++ b/client/src/containers/Preferences/General/GeneralForm.js @@ -22,15 +22,20 @@ import { handleDateChange, } from 'utils'; import { CLASSES } from 'common/classes'; -import countriesOptions from 'common/countries'; -import currencies from 'common/currencies'; -import { getFiscalYearOptions } from 'common/fiscalYearOptions'; -import languages from 'common/languagesOptions'; -import dateFormatsOptions from 'common/dateFormatsOptions'; +import { getCountries } from 'common/countries'; +import { getCurrencies } from 'common/currencies'; +import { getFiscalYear } from 'common/fiscalYearOptions'; +import { getLanguages } from 'common/languagesOptions'; +import { getDateFormats } from 'common/dateFormatsOptions'; export default function PreferencesGeneralForm({}) { const history = useHistory(); - const fiscalYearOptions = getFiscalYearOptions(); + + const FiscalYear = getFiscalYear(); + const Countries = getCountries(); + const Languages = getLanguages(); + const Currencies = getCurrencies(); + const DataFormats = getDateFormats(); const handleCloseClick = () => { history.go(-1); @@ -38,6 +43,7 @@ export default function PreferencesGeneralForm({}) { return (
+ {/* ---------- Organization name ---------- */} {({ field, meta: { error, touched } }) => ( + {/* ---------- Financial starting date ---------- */} {({ form, field: { value }, meta: { error, touched } }) => ( + {/* ---------- Location ---------- */} {({ form, field: { value }, meta: { error, touched } }) => ( { form.setFieldValue('location', value); }} @@ -116,6 +124,7 @@ export default function PreferencesGeneralForm({}) { )} + {/* ---------- Base currency ---------- */} {({ form, field: { value }, meta: { error, touched } }) => ( { form.setFieldValue('base_currency', currency.code); }} @@ -148,6 +157,7 @@ export default function PreferencesGeneralForm({}) { )} + {/* --------- Fiscal Year ----------- */} {({ form, field: { value }, meta: { error, touched } }) => ( form.setFieldValue('fiscal_year', value) } @@ -173,6 +183,7 @@ export default function PreferencesGeneralForm({}) { )} + {/* ---------- Language ---------- */} {({ form, field: { value }, meta: { error, touched } }) => ( } > } @@ -198,6 +209,7 @@ export default function PreferencesGeneralForm({}) { )} + {/* ---------- Time zone ---------- */} {({ form, field: { value }, meta: { error, touched } }) => ( + {/* --------- Data format ----------- */} {({ form, field: { value }, meta: { error, touched } }) => ( } > { form.setFieldValue('date_format', dateFormat.value); }} diff --git a/client/src/containers/Setup/SetupOrganizationForm.js b/client/src/containers/Setup/SetupOrganizationForm.js index 4fa0a6a46..4630cd9d2 100644 --- a/client/src/containers/Setup/SetupOrganizationForm.js +++ b/client/src/containers/Setup/SetupOrganizationForm.js @@ -22,16 +22,19 @@ import { handleDateChange } from 'utils'; -import { getFiscalYearOptions } from 'common/fiscalYearOptions'; -import languages from 'common/languagesOptions'; -import currencies from 'common/currencies'; +import { getFiscalYear } from 'common/fiscalYearOptions'; +import { getLanguages } from 'common/languagesOptions'; +import { getCurrencies } from 'common/currencies'; + /** * Setup organization form. */ export default function SetupOrganizationForm({ isSubmitting, values }) { - const fiscalYearOptions = getFiscalYearOptions(); + const FiscalYear = getFiscalYear(); + const Languages = getLanguages(); + const Currencies = getCurrencies(); return ( @@ -97,7 +100,7 @@ export default function SetupOrganizationForm({ isSubmitting, values }) { helperText={} > } />} popoverProps={{ minimal: true }} onItemSelect={(item) => { @@ -132,7 +135,7 @@ export default function SetupOrganizationForm({ isSubmitting, values }) { helperText={} > } />} onItemSelect={(item) => { setFieldValue('language', item.value); @@ -164,7 +167,7 @@ export default function SetupOrganizationForm({ isSubmitting, values }) { helperText={} > } />} selectedItem={value} selectedItemProp={'value'} From de2cf8e8f3ff7a5a840d220e21f007f53b01cec3 Mon Sep 17 00:00:00 2001 From: elforjani3 Date: Tue, 15 Jun 2021 15:33:22 +0200 Subject: [PATCH 02/24] fix: payment viaVoucherDialog lang. --- .../PaymentViaVoucherDialogContent.js | 7 +-- client/src/lang/en/index.json | 3 +- client/src/store/plans/plans.reducer.js | 47 +++++++++++++------ .../style/pages/Subscription/PlanRadio.scss | 2 +- 4 files changed, 37 insertions(+), 22 deletions(-) diff --git a/client/src/containers/Dialogs/PaymentViaVoucherDialog/PaymentViaVoucherDialogContent.js b/client/src/containers/Dialogs/PaymentViaVoucherDialog/PaymentViaVoucherDialogContent.js index 58cf04831..2a2202422 100644 --- a/client/src/containers/Dialogs/PaymentViaVoucherDialog/PaymentViaVoucherDialogContent.js +++ b/client/src/containers/Dialogs/PaymentViaVoucherDialog/PaymentViaVoucherDialogContent.js @@ -26,13 +26,10 @@ function PaymentViaLicenseDialogContent({ // #withDialog closeDialog, }) { - const history = useHistory(); // Payment via voucher - const { - mutateAsync: paymentViaVoucherMutate, - } = usePaymentByVoucher(); + const { mutateAsync: paymentViaVoucherMutate } = usePaymentByVoucher(); // Handle submit. const handleSubmit = (values, { setSubmitting, setErrors }) => { @@ -41,7 +38,7 @@ function PaymentViaLicenseDialogContent({ paymentViaVoucherMutate({ ...values }) .then(() => { Toaster.show({ - message: 'Payment has been done successfully.', + message: intl.get('payment_has_been_done_successfully'), intent: Intent.SUCCESS, }); return closeDialog('payment-via-voucher'); diff --git a/client/src/lang/en/index.json b/client/src/lang/en/index.json index 49af9e971..793b102b0 100644 --- a/client/src/lang/en/index.json +++ b/client/src/lang/en/index.json @@ -1121,5 +1121,6 @@ "something_went_wrong": "Something went wrong!", "please_refresh_the_page": "Please refresh the page", "waiting_to_redirect": "Waiting to redirect", - "refresh_the_page_if_redirect_not_worked": "Refresh the page if redirect not worked." + "refresh_the_page_if_redirect_not_worked": "Refresh the page if redirect not worked.", + "payment_has_been_done_successfully":"Payment has been done successfully." } \ No newline at end of file diff --git a/client/src/store/plans/plans.reducer.js b/client/src/store/plans/plans.reducer.js index 5f661231a..db0f1951f 100644 --- a/client/src/store/plans/plans.reducer.js +++ b/client/src/store/plans/plans.reducer.js @@ -1,4 +1,6 @@ +import React from 'react'; import { createReducer } from '@reduxjs/toolkit'; +import { FormattedMessage as T } from 'components'; const initialState = { plans: [ @@ -6,11 +8,12 @@ const initialState = { name: 'Free', slug: 'free', description: [ - 'Sales/purchases module.', - 'Expense module.', - 'Inventory module.', - 'Unlimited status pages.', - 'Unlimited status pages.', + , + , + , + , + , + , ], price: { month: '100', @@ -19,14 +22,15 @@ const initialState = { currencyCode: 'LYD', }, { - name: 'Pro', - slug: 'pro', + name: 'Plus', + slug: 'plus', description: [ - 'Sales/purchases module.', - 'Expense module.', - 'Inventory module.', - 'Unlimited status pages.', - 'Unlimited status pages.', + , + , + , + , + , + , ], price: { month: '200', @@ -34,6 +38,21 @@ const initialState = { }, currencyCode: 'LYD', }, + { + name: 'Enterprise', + slug: 'enterprise', + description: [ + , + , + , + , + ], + price: { + month: '300', + year: '3,400', + }, + currencyCode: 'LYD', + }, ], periods: [ { @@ -47,6 +66,4 @@ const initialState = { ], }; -export default createReducer(initialState, { - -}); +export default createReducer(initialState, {}); diff --git a/client/src/style/pages/Subscription/PlanRadio.scss b/client/src/style/pages/Subscription/PlanRadio.scss index ba8c85d45..87e8783f0 100644 --- a/client/src/style/pages/Subscription/PlanRadio.scss +++ b/client/src/style/pages/Subscription/PlanRadio.scss @@ -9,7 +9,7 @@ display: flex; flex-direction: column; width: 215px; - height: 267px; + height: 277px; border-radius: 5px; padding: 15px; border: 1px solid #dcdcdc; From c42a134b76f92def28ff3eec62475b4a2350a306 Mon Sep 17 00:00:00 2001 From: elforjani3 Date: Wed, 23 Jun 2021 22:36:44 +0200 Subject: [PATCH 03/24] fix: setup page rtl. --- .../src/containers/Setup/SetupLeftSection.js | 28 +++++++++++++------ client/src/store/plans/plans.reducer.js | 4 +-- client/src/style/pages/Setup/SetupPage.scss | 3 ++ 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/client/src/containers/Setup/SetupLeftSection.js b/client/src/containers/Setup/SetupLeftSection.js index 68054e1e9..c724dbb33 100644 --- a/client/src/containers/Setup/SetupLeftSection.js +++ b/client/src/containers/Setup/SetupLeftSection.js @@ -8,7 +8,9 @@ import { useAuthActions, useAuthOrganizationId } from 'hooks/state'; function FooterLinkItem({ title, link }) { return ( ); } @@ -28,7 +30,12 @@ export default function SetupLeftSection() {
- +

@@ -42,17 +49,22 @@ export default function SetupLeftSection() {
- : { organizationId }, + :{' '} + {organizationId},
- + + +
-
-

{'+21892-791-8381'}

+
+

+ {'+21892-738-1987'} +

@@ -61,5 +73,5 @@ export default function SetupLeftSection() {

- ) -} \ No newline at end of file + ); +} diff --git a/client/src/store/plans/plans.reducer.js b/client/src/store/plans/plans.reducer.js index db0f1951f..cfc2c6acc 100644 --- a/client/src/store/plans/plans.reducer.js +++ b/client/src/store/plans/plans.reducer.js @@ -57,11 +57,11 @@ const initialState = { periods: [ { slug: 'month', - label: 'Monthly', + label: }, { slug: 'year', - label: 'Yearly', + label: }, ], }; diff --git a/client/src/style/pages/Setup/SetupPage.scss b/client/src/style/pages/Setup/SetupPage.scss index 7393911ed..a8767d46f 100644 --- a/client/src/style/pages/Setup/SetupPage.scss +++ b/client/src/style/pages/Setup/SetupPage.scss @@ -101,6 +101,9 @@ opacity: 0.75; padding-bottom: 5px; border-bottom: 1px solid rgba(255, 255, 255, 0.15); + p > span { + unicode-bidi: plaintext; + } } &__links { From a6770c145b5f524f0be34a507ab7da686706c356 Mon Sep 17 00:00:00 2001 From: elforjani3 Date: Sun, 27 Jun 2021 14:08:32 +0200 Subject: [PATCH 04/24] feat: manual journal drawer & paper footer RLT. --- client/src/containers/Drawers/ExpenseDrawer/index.js | 3 ++- .../ManualJournalDrawer/ManualJournalDrawerProvider.js | 5 ++++- .../Drawers/PaperTemplate/PaperTemplateFooter.js | 3 ++- client/src/lang/en/index.json | 8 +++++--- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/client/src/containers/Drawers/ExpenseDrawer/index.js b/client/src/containers/Drawers/ExpenseDrawer/index.js index 6e97a5d60..07031c210 100644 --- a/client/src/containers/Drawers/ExpenseDrawer/index.js +++ b/client/src/containers/Drawers/ExpenseDrawer/index.js @@ -1,6 +1,7 @@ import React, { lazy } from 'react'; import { Drawer, DrawerSuspense } from 'components'; import withDrawers from 'containers/Drawer/withDrawers'; +import intl from 'react-intl-universal'; import { compose } from 'utils'; @@ -17,7 +18,7 @@ function ExpenseDrawer({ payload: { expenseId, title }, }) { return ( - + diff --git a/client/src/containers/Drawers/ManualJournalDrawer/ManualJournalDrawerProvider.js b/client/src/containers/Drawers/ManualJournalDrawer/ManualJournalDrawerProvider.js index ebbf206cd..966971e19 100644 --- a/client/src/containers/Drawers/ManualJournalDrawer/ManualJournalDrawerProvider.js +++ b/client/src/containers/Drawers/ManualJournalDrawer/ManualJournalDrawerProvider.js @@ -1,6 +1,7 @@ import React from 'react'; import { useJournal } from 'hooks/query'; import { DashboardInsider, DrawerHeaderContent } from 'components'; +import intl from 'react-intl-universal'; const ManualJournalDrawerContext = React.createContext(); @@ -25,7 +26,9 @@ function ManualJournalDrawerProvider({ manualJournalId, ...props }) { diff --git a/client/src/containers/Drawers/PaperTemplate/PaperTemplateFooter.js b/client/src/containers/Drawers/PaperTemplate/PaperTemplateFooter.js index cedf261a2..5481982e3 100644 --- a/client/src/containers/Drawers/PaperTemplate/PaperTemplateFooter.js +++ b/client/src/containers/Drawers/PaperTemplate/PaperTemplateFooter.js @@ -1,5 +1,6 @@ import React from 'react'; import { If } from 'components'; +import intl from 'react-intl-universal'; export default function PaperTemplateFooter({ footerData: { terms_conditions }, @@ -8,7 +9,7 @@ export default function PaperTemplateFooter({
-

Conditions and terms

+

{intl.get('conditions_and_terms')}

    diff --git a/client/src/lang/en/index.json b/client/src/lang/en/index.json index 793b102b0..f4566c3d0 100644 --- a/client/src/lang/en/index.json +++ b/client/src/lang/en/index.json @@ -1102,7 +1102,7 @@ "mm_dd_yy_": "MM-DD-YY", "dd_mm_yy_": "DD-MM-YY", "yy_mm_dd_": "YY-MM-DD", - "plan_radio_name":"{name}", + "plan_radio_name": "{name}", "customers_payments": "Customers Payments", "receiving_customer_payments_is_one_pleasant_accounting_tasks": "Receiving payments is one of your more pleasant accounting tasks. The payments transactions will appear once receive payments to invoices.", "estimate_is_used_to_create_bid_proposal_or_quote": "An estimate is used to create a bid, proposal, or quote. The estimate can later be turned into a sales order or an invoice.", @@ -1122,5 +1122,7 @@ "please_refresh_the_page": "Please refresh the page", "waiting_to_redirect": "Waiting to redirect", "refresh_the_page_if_redirect_not_worked": "Refresh the page if redirect not worked.", - "payment_has_been_done_successfully":"Payment has been done successfully." -} \ No newline at end of file + "payment_has_been_done_successfully": "Payment has been done successfully.", + "manual_journal_number": "Manual journal {number}", +"conditions_and_terms":"Conditions and terms" +} From 0937f11326b6d5f10a8214941101d340ca6cd181 Mon Sep 17 00:00:00 2001 From: elforjani3 Date: Tue, 29 Jun 2021 17:06:57 +0200 Subject: [PATCH 05/24] fix: reducers Rtl --- client/src/containers/FinancialStatements/reducers.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/src/containers/FinancialStatements/reducers.js b/client/src/containers/FinancialStatements/reducers.js index 7d6770764..6159722f1 100644 --- a/client/src/containers/FinancialStatements/reducers.js +++ b/client/src/containers/FinancialStatements/reducers.js @@ -113,7 +113,8 @@ export const profitLossSheetReducer = (profitLoss) => { } if (profitLoss.other_income) { results.push({ - name: 'other_income', + + name:, total: profitLoss.other_income.total, total_periods: profitLoss.other_income.total_periods, children: [ From 2b5d00ed600b709e5ea9c8797b92bf1934b398ab Mon Sep 17 00:00:00 2001 From: elforjani3 Date: Wed, 21 Jul 2021 23:47:40 +0200 Subject: [PATCH 06/24] feature/ allocate landed cost. --- client/src/common/allocateLandedCostType.js | 6 + client/src/components/DialogsContainer.js | 2 + client/src/components/index.js | 2 + .../AllocateLandedCostDialogContent.js | 18 ++ .../AllocateLandedCostDialogProvider.js | 38 ++++ .../AllocateLandedCostEntriesTable.js | 72 ++++++++ .../AllocateLandedCostFloatingActions.js | 43 +++++ .../AllocateLandedCostForm.js | 57 ++++++ .../AllocateLandedCostForm.schema.js | 19 ++ .../AllocateLandedCostFormBody.js | 26 +++ .../AllocateLandedCostFormContent.js | 15 ++ .../AllocateLandedCostFormFields.js | 171 ++++++++++++++++++ .../Dialogs/AllocateLandedCostDialog/index.js | 36 ++++ .../Bills/BillsLanding/BillsTable.js | 15 +- .../Bills/BillsLanding/components.js | 8 +- client/src/lang/en/index.json | 13 +- .../AllocateLandedCostForm.scss | 42 +++++ 17 files changed, 572 insertions(+), 11 deletions(-) create mode 100644 client/src/common/allocateLandedCostType.js create mode 100644 client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostDialogContent.js create mode 100644 client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostDialogProvider.js create mode 100644 client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostEntriesTable.js create mode 100644 client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFloatingActions.js create mode 100644 client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostForm.js create mode 100644 client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostForm.schema.js create mode 100644 client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormBody.js create mode 100644 client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormContent.js create mode 100644 client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormFields.js create mode 100644 client/src/containers/Dialogs/AllocateLandedCostDialog/index.js create mode 100644 client/src/style/pages/AllocateLandedCost/AllocateLandedCostForm.scss diff --git a/client/src/common/allocateLandedCostType.js b/client/src/common/allocateLandedCostType.js new file mode 100644 index 000000000..4e915bfec --- /dev/null +++ b/client/src/common/allocateLandedCostType.js @@ -0,0 +1,6 @@ +import intl from 'react-intl-universal'; + +export default [ + { name: intl.get('bills'), value: 'bills' }, + { name: intl.get('expenses'), value: 'expenses' }, +] \ No newline at end of file diff --git a/client/src/components/DialogsContainer.js b/client/src/components/DialogsContainer.js index 1419f7638..203a769b6 100644 --- a/client/src/components/DialogsContainer.js +++ b/client/src/components/DialogsContainer.js @@ -13,6 +13,7 @@ import KeyboardShortcutsDialog from 'containers/Dialogs/keyboardShortcutsDialog' import ContactDuplicateDialog from 'containers/Dialogs/ContactDuplicateDialog'; import QuickPaymentReceiveFormDialog from 'containers/Dialogs/QuickPaymentReceiveFormDialog'; import QuickPaymentMadeFormDialog from 'containers/Dialogs/QuickPaymentMadeFormDialog'; +import AllocateLandedCostDialog from 'containers/Dialogs/AllocateLandedCostDialog'; /** * Dialogs container. @@ -32,6 +33,7 @@ export default function DialogsContainer() { +
); } diff --git a/client/src/components/index.js b/client/src/components/index.js index c8d56dfe9..8567a384b 100644 --- a/client/src/components/index.js +++ b/client/src/components/index.js @@ -56,6 +56,7 @@ import DrawerHeaderContent from './Drawer/DrawerHeaderContent'; import Postbox from './Postbox'; import AccountsSuggestField from './AccountsSuggestField'; import MaterialProgressBar from './MaterialProgressBar'; +import { MoneyFieldCell } from './DataTableCells'; const Hint = FieldHint; @@ -123,4 +124,5 @@ export { Postbox, AccountsSuggestField, MaterialProgressBar, + MoneyFieldCell, }; diff --git a/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostDialogContent.js b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostDialogContent.js new file mode 100644 index 000000000..1f574d9fa --- /dev/null +++ b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostDialogContent.js @@ -0,0 +1,18 @@ +import React from 'react'; +import { AllocateLandedCostDialogProvider } from './AllocateLandedCostDialogProvider'; +import AllocateLandedCostForm from './AllocateLandedCostForm'; + +/** + * Allocate landed cost dialog content. + */ +export default function AllocateLandedCostDialogContent({ + // #ownProps + dialogName, + bill, +}) { + return ( + + + + ); +} diff --git a/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostDialogProvider.js b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostDialogProvider.js new file mode 100644 index 000000000..aec4f85eb --- /dev/null +++ b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostDialogProvider.js @@ -0,0 +1,38 @@ +import React from 'react'; +import { DialogContent } from 'components'; +import { useBill } from 'hooks/query'; + +import { pick } from 'lodash'; + +const AllocateLandedCostDialogContext = React.createContext(); + +/** + * Allocate landed cost provider. + */ +function AllocateLandedCostDialogProvider({ billId, dialogName, ...props }) { + // Handle fetch bill details. + const { isLoading: isBillLoading, data: bill } = useBill(billId, { + enabled: !!billId, + }); + + // provider payload. + const provider = { + bill: { + ...pick(bill, ['entries']), + }, + dialogName, + }; + return ( + + + + ); +} + +const useAllocateLandedConstDialogContext = () => + React.useContext(AllocateLandedCostDialogContext); + +export { + AllocateLandedCostDialogProvider, + useAllocateLandedConstDialogContext, +}; diff --git a/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostEntriesTable.js b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostEntriesTable.js new file mode 100644 index 000000000..2275883ad --- /dev/null +++ b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostEntriesTable.js @@ -0,0 +1,72 @@ +import React from 'react'; +import intl from 'react-intl-universal'; +import { DataTable, MoneyFieldCell, DataTableEditable } from 'components'; +import { compose, updateTableRow } from 'utils'; + +/** + * Allocate landed cost entries table. + */ +export default function AllocateLandedCostEntriesTable({ + onUpdateData, + entries, +}) { + // allocate landed cost entries table columns. + const columns = React.useMemo( + () => [ + { + Header: intl.get('item'), + accessor: 'item_id', + disableSortBy: true, + width: '150', + }, + { + Header: intl.get('quantity'), + accessor: 'quantity', + disableSortBy: true, + width: '100', + }, + { + Header: intl.get('rate'), + accessor: 'rate', + disableSortBy: true, + width: '100', + }, + { + Header: intl.get('amount'), + accessor: 'amount', + disableSortBy: true, + width: '100', + }, + { + Header: intl.get('cost'), + accessor: 'cost', + width: '150', + Cell: MoneyFieldCell, + disableSortBy: true, + }, + ], + [], + ); + + // Handle update data. + const handleUpdateData = React.useCallback( + (rowIndex, columnId, value) => { + const newRows = compose(updateTableRow(rowIndex, columnId, value))( + entries, + ); + onUpdateData(newRows); + }, + [onUpdateData, entries], + ); + + const LL = [ + { + item_id: 'ITEM', + quantity: '30000', + rate: '100000', + amount: '400', + }, + ]; + + return ; +} diff --git a/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFloatingActions.js b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFloatingActions.js new file mode 100644 index 000000000..ddc400903 --- /dev/null +++ b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFloatingActions.js @@ -0,0 +1,43 @@ +import React from 'react'; +import { Intent, Button, Classes } from '@blueprintjs/core'; +import { FormattedMessage as T } from 'components'; + +import { useFormikContext } from 'formik'; +import { useAllocateLandedConstDialogContext } from './AllocateLandedCostDialogProvider'; +import withDialogActions from 'containers/Dialog/withDialogActions'; +import { compose } from 'utils'; + +function AllocateLandedCostFloatingActions({ + // #withDialogActions + closeDialog, +}) { + // Formik context. + const { isSubmitting } = useFormikContext(); + + const { dialogName } = useAllocateLandedConstDialogContext(); + + // Handle cancel button click. + const handleCancelBtnClick = (event) => { + closeDialog(dialogName); + }; + + return ( +
+
+ + +
+
+ ); +} + +export default compose(withDialogActions)(AllocateLandedCostFloatingActions); diff --git a/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostForm.js b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostForm.js new file mode 100644 index 000000000..5ccffca48 --- /dev/null +++ b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostForm.js @@ -0,0 +1,57 @@ +import React from 'react'; +import { Formik } from 'formik'; +import moment from 'moment'; + +import 'style/pages/AllocateLandedCost/AllocateLandedCostForm.scss'; + +import { AllocateLandedCostFormSchema } from './AllocateLandedCostForm.schema'; +import { useAllocateLandedConstDialogContext } from './AllocateLandedCostDialogProvider'; +import AllocateLandedCostFormContent from './AllocateLandedCostFormContent'; +import withDialogActions from 'containers/Dialog/withDialogActions'; + +import { compose } from 'utils'; + +const defaultInitialValues = { + transaction_type: 'bills', + transaction_date: moment(new Date()).format('YYYY-MM-DD'), + transaction_id: '', + transaction_entry_id: '', + amount: '', + allocation_method: 'quantity', + entries: { + entry_id: '', + cost: '', + }, +}; + +/** + * Allocate landed cost form. + */ +function AllocateLandedCostForm({ + // #withDialogActions + closeDialog, +}) { + const { bill, dialogName } = useAllocateLandedConstDialogContext(); + + // Initial form values. + const initialValues = { + ...defaultInitialValues, + ...bill, + }; + + + // Handle form submit. + const handleFormSubmit = (values, { setSubmitting }) => {}; + + return ( + + + + ); +} + +export default compose(withDialogActions)(AllocateLandedCostForm); diff --git a/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostForm.schema.js b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostForm.schema.js new file mode 100644 index 000000000..176514808 --- /dev/null +++ b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostForm.schema.js @@ -0,0 +1,19 @@ +import * as Yup from 'yup'; +import intl from 'react-intl-universal'; + +const Schema = Yup.object().shape({ + transaction_type: Yup.string().label(intl.get('transaction_type')), + transaction_date: Yup.date().label(intl.get('transaction_date')), + transaction_id: Yup.string().label(intl.get('transaction_number')), + transaction_entry_id: Yup.string().label(intl.get('transaction_line')), + amount: Yup.number().label(intl.get('amount')), + allocation_method: Yup.string().trim(), + entries: Yup.array().of( + Yup.object().shape({ + entry_id: Yup.number().nullable(), + cost: Yup.number().nullable(), + }), + ), +}); + +export const AllocateLandedCostFormSchema = Schema; diff --git a/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormBody.js b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormBody.js new file mode 100644 index 000000000..13029efd5 --- /dev/null +++ b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormBody.js @@ -0,0 +1,26 @@ +import React from 'react'; +import { FastField } from 'formik'; +import classNames from 'classnames'; +import { CLASSES } from 'common/classes'; +import AllocateLandedCostEntriesTable from './AllocateLandedCostEntriesTable'; + +export default function AllocateLandedCostFormBody() { + return ( +
+ + {({ + form: { setFieldValue, values }, + field: { value }, + meta: { error, touched }, + }) => ( + { + setFieldValue('entries', newEntries); + }} + /> + )} + +
+ ); +} diff --git a/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormContent.js b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormContent.js new file mode 100644 index 000000000..c06d05a67 --- /dev/null +++ b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormContent.js @@ -0,0 +1,15 @@ +import React from 'react'; +import { Form } from 'formik'; +import AllocateLandedCostFormFields from './AllocateLandedCostFormFields'; +import AllocateLandedCostFloatingActions from './AllocateLandedCostFloatingActions'; +/** + * Allocate landed cost form content. + */ +export default function AllocateLandedCostFormContent() { + return ( + + + + + ); +} diff --git a/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormFields.js b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormFields.js new file mode 100644 index 000000000..a1639ca96 --- /dev/null +++ b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormFields.js @@ -0,0 +1,171 @@ +import React from 'react'; +import { FastField, ErrorMessage } from 'formik'; +import { + Classes, + FormGroup, + RadioGroup, + Radio, + InputGroup, + Position, +} from '@blueprintjs/core'; +import { DateInput } from '@blueprintjs/datetime'; +import classNames from 'classnames'; +import { FormattedMessage as T } from 'components'; +import intl from 'react-intl-universal'; +import { + inputIntent, + momentFormatter, + tansformDateValue, + handleDateChange, + handleStringChange, +} from 'utils'; +import { FieldRequiredHint, ListSelect } from 'components'; +import { CLASSES } from 'common/classes'; +import allocateLandedCostType from 'common/allocateLandedCostType'; +import AccountsSuggestField from 'components/AccountsSuggestField'; +import AllocateLandedCostFormBody from './AllocateLandedCostFormBody'; + +/** + * Allocate landed cost form fields. + */ +export default function AllocateLandedCostFormFields() { + return ( +
+ {/*------------Transaction type -----------*/} + + {({ + form: { values, setFieldValue }, + field: { value }, + meta: { error, touched }, + }) => ( + } + labelInfo={} + helperText={} + intent={inputIntent({ error, touched })} + inline={true} + className={classNames(CLASSES.FILL, 'form-group--transaction_type')} + > + { + setFieldValue('transaction_type', type.value); + }} + filterable={false} + selectedItem={value} + selectedItemProp={'value'} + textProp={'name'} + popoverProps={{ minimal: true }} + /> + + )} + + + {/*------------Transaction date -----------*/} + + {({ form, field: { value }, meta: { error, touched } }) => ( + } + // labelInfo={} + intent={inputIntent({ error, touched })} + helperText={} + minimal={true} + className={classNames(CLASSES.FILL, 'form-group--transaction_date')} + inline={true} + > + { + form.setFieldValue('transaction_date', formattedDate); + })} + value={tansformDateValue(value)} + popoverProps={{ + position: Position.BOTTOM, + minimal: true, + }} + /> + + )} + + {/*------------ Transaction -----------*/} + + {({ form, field, meta: { error, touched } }) => ( + } + // labelInfo={} + intent={inputIntent({ error, touched })} + helperText={} + className={'form-group--transaction_id'} + inline={true} + > + + form.setFieldValue('transaction_id', id) + } + inputProps={{ + placeholder: intl.get('select_transaction'), + }} + /> + + )} + + {/*------------ Transaction line -----------*/} + + {({ form, field, meta: { error, touched } }) => ( + } + intent={inputIntent({ error, touched })} + helperText={} + className={'form-group--transaction_entry_id'} + inline={true} + > + + + )} + + {/*------------ Amount -----------*/} + + {({ form, field, meta: { error, touched } }) => ( + } + intent={inputIntent({ error, touched })} + helperText={} + className={'form-group--amount'} + inline={true} + > + + + )} + + {/*------------ Allocation method -----------*/} + + {({ form, field: { value }, meta: { touched, error } }) => ( + } + labelInfo={} + className={'form-group--allocation_method'} + intent={inputIntent({ error, touched })} + helperText={} + inline={true} + > + { + form.setFieldValue('allocation_method', _value); + })} + selectedValue={value} + inline={true} + > + } value="quantity" /> + } value="valuation" /> + + + )} + + + {/*------------ Allocate Landed cost Table -----------*/} + +
+ ); +} diff --git a/client/src/containers/Dialogs/AllocateLandedCostDialog/index.js b/client/src/containers/Dialogs/AllocateLandedCostDialog/index.js new file mode 100644 index 000000000..5798a5aba --- /dev/null +++ b/client/src/containers/Dialogs/AllocateLandedCostDialog/index.js @@ -0,0 +1,36 @@ +import React, { lazy } from 'react'; +import { FormattedMessage as T, Dialog, DialogSuspense } from 'components'; +import withDialogRedux from 'components/DialogReduxConnect'; +import { compose } from 'utils'; + +const AllocateLandedCostDialogContent = lazy(() => + import('./AllocateLandedCostDialogContent'), +); + +/** + * Allocate landed cost dialog. + */ +function AllocateLandedCostDialog({ + dialogName, + payload = { billId: null }, + isOpen, +}) { + return ( + } + canEscapeKeyClose={true} + isOpen={isOpen} + className="dialog--allocate-landed-cost-form" + > + + + + + ); +} + +export default compose(withDialogRedux())(AllocateLandedCostDialog); diff --git a/client/src/containers/Purchases/Bills/BillsLanding/BillsTable.js b/client/src/containers/Purchases/Bills/BillsLanding/BillsTable.js index cbf514012..03f49bb09 100644 --- a/client/src/containers/Purchases/Bills/BillsLanding/BillsTable.js +++ b/client/src/containers/Purchases/Bills/BillsLanding/BillsTable.js @@ -34,13 +34,8 @@ function BillsDataTable({ openDialog, }) { // Bills list context. - const { - bills, - pagination, - isBillsLoading, - isBillsFetching, - isEmptyStatus, - } = useBillsListContext(); + const { bills, pagination, isBillsLoading, isBillsFetching, isEmptyStatus } = + useBillsListContext(); const history = useHistory(); @@ -78,6 +73,11 @@ function BillsDataTable({ openDialog('quick-payment-made', { billId: id }); }; + // handle allocate landed cost. + const handleAllocateLandedCost = ({ id }) => { + openDialog('allocate-landed-cost', { billId: id }); + }; + if (isEmptyStatus) { return ; } @@ -105,6 +105,7 @@ function BillsDataTable({ onEdit: handleEditBill, onOpen: handleOpenBill, onQuick: handleQuickPaymentMade, + onAllocateLandedCost: handleAllocateLandedCost, }} /> ); diff --git a/client/src/containers/Purchases/Bills/BillsLanding/components.js b/client/src/containers/Purchases/Bills/BillsLanding/components.js index 34d299000..fa4c2a76b 100644 --- a/client/src/containers/Purchases/Bills/BillsLanding/components.js +++ b/client/src/containers/Purchases/Bills/BillsLanding/components.js @@ -20,7 +20,7 @@ import moment from 'moment'; * Actions menu. */ export function ActionsMenu({ - payload: { onEdit, onOpen, onDelete, onQuick }, + payload: { onEdit, onOpen, onDelete, onQuick, onAllocateLandedCost }, row: { original }, }) { return ( @@ -50,7 +50,11 @@ export function ActionsMenu({ onClick={safeCallback(onQuick, original)} /> - + } + text={intl.get('allocate_landed_coast')} + onClick={safeCallback(onAllocateLandedCost, original)} + /> Date: Wed, 21 Jul 2021 23:49:00 +0200 Subject: [PATCH 07/24] feature(expense)/ cost landed checkbox. --- .../ExpenseForm/ExpenseForm.schema.js | 5 +-- .../Expenses/ExpenseForm/components.js | 40 ++++++++++++++++++- .../containers/Expenses/ExpenseForm/utils.js | 1 + 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/client/src/containers/Expenses/ExpenseForm/ExpenseForm.schema.js b/client/src/containers/Expenses/ExpenseForm/ExpenseForm.schema.js index d05661cb6..8fb55b94a 100644 --- a/client/src/containers/Expenses/ExpenseForm/ExpenseForm.schema.js +++ b/client/src/containers/Expenses/ExpenseForm/ExpenseForm.schema.js @@ -8,9 +8,7 @@ const Schema = Yup.object().shape({ payment_account_id: Yup.number() .required() .label(intl.get('payment_account_')), - payment_date: Yup.date() - .required() - .label(intl.get('payment_date_')), + payment_date: Yup.date().required().label(intl.get('payment_date_')), reference_no: Yup.string().min(1).max(DATATYPES_LENGTH.STRING).nullable(), currency_code: Yup.string() .nullable() @@ -33,6 +31,7 @@ const Schema = Yup.object().shape({ is: (amount) => !isBlank(amount), then: Yup.number().required(), }), + landed_cost: Yup.boolean(), description: Yup.string().max(DATATYPES_LENGTH.TEXT).nullable(), }), ), diff --git a/client/src/containers/Expenses/ExpenseForm/components.js b/client/src/containers/Expenses/ExpenseForm/components.js index dfd0c96d2..0ed35114c 100644 --- a/client/src/containers/Expenses/ExpenseForm/components.js +++ b/client/src/containers/Expenses/ExpenseForm/components.js @@ -1,5 +1,5 @@ import React from 'react'; -import { Button, Tooltip, Intent, Position } from '@blueprintjs/core'; +import { Button, Tooltip, Intent, Position, Checkbox } from '@blueprintjs/core'; import { FormattedMessage as T } from 'components'; import { Icon, Hint } from 'components'; import intl from 'react-intl-universal'; @@ -49,6 +49,35 @@ const ActionsCellRenderer = ({ ); }; +/** + * Landed cost cell. + */ +const LandedCostCell = ({ + row: { index }, + column: { id }, + cell: { value: initialValue }, + data, + payload, +}) => { + return ( + + + + ); +}; + +/** + * Landed cost header cell. + */ +const LandedCostHeaderCell = () => { + return ( + <> + + + + ); +}; + /** * Amount footer cell. */ @@ -114,6 +143,15 @@ export function useExpenseFormTableColumns() { className: 'description', width: 100, }, + { + Header: LandedCostHeaderCell, + accessor: 'landed_cost', + Cell: LandedCostCell, + disableSortBy: true, + disableResizing: true, + width: 70, + className: 'landed_cost', + }, { Header: '', accessor: 'action', diff --git a/client/src/containers/Expenses/ExpenseForm/utils.js b/client/src/containers/Expenses/ExpenseForm/utils.js index a2a045abd..36653842c 100644 --- a/client/src/containers/Expenses/ExpenseForm/utils.js +++ b/client/src/containers/Expenses/ExpenseForm/utils.js @@ -27,6 +27,7 @@ export const defaultExpenseEntry = { amount: '', expense_account_id: '', description: '', + landed_cost: false, }; export const defaultExpense = { From 1091e3f99697cc0ffcc14fed927fc4791168cd5c Mon Sep 17 00:00:00 2001 From: elforjani3 Date: Thu, 22 Jul 2021 13:33:10 +0200 Subject: [PATCH 08/24] feat : checkbox landed cost. --- client/src/containers/Entries/components.js | 43 +++++++++++++++++-- .../Expenses/ExpenseForm/components.js | 6 +-- 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/client/src/containers/Entries/components.js b/client/src/containers/Entries/components.js index 44cb44d11..a9c53596e 100644 --- a/client/src/containers/Entries/components.js +++ b/client/src/containers/Entries/components.js @@ -1,7 +1,7 @@ import React from 'react'; import { FormattedMessage as T } from 'components'; import intl from 'react-intl-universal'; -import { Tooltip, Button, Intent, Position } from '@blueprintjs/core'; +import { Tooltip, Button, Checkbox, Intent, Position } from '@blueprintjs/core'; import { Hint, Icon } from 'components'; import { formattedAmount, safeSumBy } from 'utils'; import { @@ -28,7 +28,11 @@ export function ItemHeaderCell() { * Item column footer cell. */ export function ItemFooterCell() { - return ; + return ( + + + + ); } /** @@ -86,12 +90,35 @@ export function IndexTableCell({ row: { index } }) { return {index + 1}; } +/** + * Landed cost cell. + */ +const LandedCostCell = ({ + row: { index }, + column: { id }, + cell: { value: initialValue }, + data, + payload, +}) => { + return ; +}; + +/** + * Landed cost header cell. + */ +const LandedCostHeaderCell = () => { + return ( + <> + + + + ); +}; + /** * Retrieve editable items entries columns. */ export function useEditableItemsEntriesColumns() { - - return React.useMemo( () => [ { @@ -155,6 +182,14 @@ export function useEditableItemsEntriesColumns() { width: 100, className: 'total', }, + { + Header: '', + accessor: 'landed_cost', + Cell: LandedCostCell, + width: 70, + disableSortBy: true, + disableResizing: true, + }, { Header: '', accessor: 'action', diff --git a/client/src/containers/Expenses/ExpenseForm/components.js b/client/src/containers/Expenses/ExpenseForm/components.js index 0ed35114c..b60949896 100644 --- a/client/src/containers/Expenses/ExpenseForm/components.js +++ b/client/src/containers/Expenses/ExpenseForm/components.js @@ -59,11 +59,7 @@ const LandedCostCell = ({ data, payload, }) => { - return ( - - - - ); + return ; }; /** From 1eacc254d895df11999a697edbaedc99501c3a05 Mon Sep 17 00:00:00 2001 From: elforjani3 Date: Thu, 22 Jul 2021 17:42:43 +0200 Subject: [PATCH 09/24] feat: Bill drawer. --- client/src/components/DrawersContainer.js | 2 + .../AllocateLandedCostEntriesTable.js | 24 ++++++ .../Drawers/BillDrawer/BillDrawerContent.js | 16 ++++ .../Drawers/BillDrawer/BillDrawerDetails.js | 25 ++++++ .../Drawers/BillDrawer/BillDrawerProvider.js | 26 ++++++ .../BillDrawer/LocatedLandedCostTable.js | 22 +++++ .../Drawers/BillDrawer/components.js | 41 ++++++++++ .../containers/Drawers/BillDrawer/index.js | 27 +++++++ .../Bills/BillsLanding/BillsTable.js | 11 +++ .../Bills/BillsLanding/components.js | 10 ++- client/src/lang/en/index.json | 5 +- .../style/components/Drawer/BillDrawer.scss | 81 +++++++++++++++++++ .../AllocateLandedCostForm.scss | 3 +- 13 files changed, 290 insertions(+), 3 deletions(-) create mode 100644 client/src/containers/Drawers/BillDrawer/BillDrawerContent.js create mode 100644 client/src/containers/Drawers/BillDrawer/BillDrawerDetails.js create mode 100644 client/src/containers/Drawers/BillDrawer/BillDrawerProvider.js create mode 100644 client/src/containers/Drawers/BillDrawer/LocatedLandedCostTable.js create mode 100644 client/src/containers/Drawers/BillDrawer/components.js create mode 100644 client/src/containers/Drawers/BillDrawer/index.js create mode 100644 client/src/style/components/Drawer/BillDrawer.scss diff --git a/client/src/components/DrawersContainer.js b/client/src/components/DrawersContainer.js index 177101057..63dcbdb6e 100644 --- a/client/src/components/DrawersContainer.js +++ b/client/src/components/DrawersContainer.js @@ -6,6 +6,7 @@ import PaymentReceiveDrawer from 'containers/Sales/PaymentReceives/PaymentDetail import AccountDrawer from 'containers/Drawers/AccountDrawer'; import ManualJournalDrawer from 'containers/Drawers/ManualJournalDrawer'; import ExpenseDrawer from 'containers/Drawers/ExpenseDrawer'; +import BillDrawer from 'containers/Drawers/BillDrawer'; export default function DrawersContainer() { return ( @@ -17,6 +18,7 @@ export default function DrawersContainer() { + ); } diff --git a/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostEntriesTable.js b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostEntriesTable.js index 2275883ad..171d88ce1 100644 --- a/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostEntriesTable.js +++ b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostEntriesTable.js @@ -66,6 +66,30 @@ export default function AllocateLandedCostEntriesTable({ rate: '100000', amount: '400', }, + { + item_id: 'ITEM', + quantity: '30000', + rate: '100000', + amount: '400', + }, + { + item_id: 'ITEM', + quantity: '30000', + rate: '100000', + amount: '400', + }, + { + item_id: 'ITEM', + quantity: '30000', + rate: '100000', + amount: '400', + }, + { + item_id: 'ITEM', + quantity: '30000', + rate: '100000', + amount: '400', + }, ]; return ; diff --git a/client/src/containers/Drawers/BillDrawer/BillDrawerContent.js b/client/src/containers/Drawers/BillDrawer/BillDrawerContent.js new file mode 100644 index 000000000..0e93746a5 --- /dev/null +++ b/client/src/containers/Drawers/BillDrawer/BillDrawerContent.js @@ -0,0 +1,16 @@ +import React from 'react'; +import { BillDrawerProvider } from './BillDrawerProvider'; +import BillDrawerDetails from './BillDrawerDetails'; +/** + * Bill drawer content. + */ +export default function BillDrawerContent({ + // #ownProp + billId, +}) { + return ( + + + + ); +} diff --git a/client/src/containers/Drawers/BillDrawer/BillDrawerDetails.js b/client/src/containers/Drawers/BillDrawer/BillDrawerDetails.js new file mode 100644 index 000000000..28bc4fc3a --- /dev/null +++ b/client/src/containers/Drawers/BillDrawer/BillDrawerDetails.js @@ -0,0 +1,25 @@ +import React from 'react'; +import { Tabs, Tab } from '@blueprintjs/core'; +import intl from 'react-intl-universal'; + +import LocatedLandedCostTable from './LocatedLandedCostTable'; + +import 'style/components/Drawer/BillDrawer.scss'; + +/** + * Bill view details. + */ +export default function BillDrawerDetails() { + return ( +
+ + + } + /> + +
+ ); +} diff --git a/client/src/containers/Drawers/BillDrawer/BillDrawerProvider.js b/client/src/containers/Drawers/BillDrawer/BillDrawerProvider.js new file mode 100644 index 000000000..689508ad0 --- /dev/null +++ b/client/src/containers/Drawers/BillDrawer/BillDrawerProvider.js @@ -0,0 +1,26 @@ +import React from 'react'; +import intl from 'react-intl-universal'; +import { DrawerHeaderContent, DashboardInsider } from 'components'; + +const BillDrawerContext = React.createContext(); + +/** + * Bill drawer provider. + */ +function BillDrawerProvider({ billId, ...props }) { + //provider. + const provider = {}; + return ( + + + + + ); +} + +const useBillDrawerContext = () => React.useContext(BillDrawerContext); + +export { BillDrawerProvider, useBillDrawerContext }; diff --git a/client/src/containers/Drawers/BillDrawer/LocatedLandedCostTable.js b/client/src/containers/Drawers/BillDrawer/LocatedLandedCostTable.js new file mode 100644 index 000000000..c50da77e6 --- /dev/null +++ b/client/src/containers/Drawers/BillDrawer/LocatedLandedCostTable.js @@ -0,0 +1,22 @@ +import React from 'react'; +import { DataTable } from 'components'; +import { useLocatedLandedCostColumns, ActionsMenu } from './components'; + +/** + * Located landed cost table. + */ +function LocatedLandedCostTable() { + const columns = useLocatedLandedCostColumns(); + + const DATA = [ + { + name: 'INV-1000', + amount: '10.000.000', + allocation_method: 'Bill', + }, + ]; + + return ; +} + +export default LocatedLandedCostTable; diff --git a/client/src/containers/Drawers/BillDrawer/components.js b/client/src/containers/Drawers/BillDrawer/components.js new file mode 100644 index 000000000..1e3816c3c --- /dev/null +++ b/client/src/containers/Drawers/BillDrawer/components.js @@ -0,0 +1,41 @@ +import React from 'react'; +import intl from 'react-intl-universal'; +import { Intent, MenuItem, Menu } from '@blueprintjs/core'; +import { safeCallback } from 'utils'; +import { Icon } from 'components'; + +/** + * Actions menu. + */ +export function ActionsMenu({ row: { original }, payload: { onDelete } }) { + return ( + + } + text={intl.get('delete_transaction')} + intent={Intent.DANGER} + // onClick={safeCallback(onDelete, original)} + /> + + ); +} + +export function useLocatedLandedCostColumns() { + return React.useMemo(() => [ + { + Header: intl.get('name'), + accessor: 'name', + width: 150, + }, + { + Header: intl.get('amount'), + accessor: 'amount', + width: 100, + }, + { + Header: intl.get('allocation_method'), + accessor: 'allocation_method', + width: 100, + }, + ]); +} diff --git a/client/src/containers/Drawers/BillDrawer/index.js b/client/src/containers/Drawers/BillDrawer/index.js new file mode 100644 index 000000000..c712e4a15 --- /dev/null +++ b/client/src/containers/Drawers/BillDrawer/index.js @@ -0,0 +1,27 @@ +import React from 'react'; +import { Drawer, DrawerSuspense } from 'components'; +import withDrawers from 'containers/Drawer/withDrawers'; + +import { compose } from 'utils'; + +const BillDrawerContent = React.lazy(() => import('./BillDrawerContent')); + +/** + * Bill drawer. + */ +function BillDrawer({ + name, + // #withDrawer + isOpen, + payload: { billId }, +}) { + return ( + + + + + + ); +} + +export default compose(withDrawers())(BillDrawer); diff --git a/client/src/containers/Purchases/Bills/BillsLanding/BillsTable.js b/client/src/containers/Purchases/Bills/BillsLanding/BillsTable.js index 03f49bb09..0bb5de39b 100644 --- a/client/src/containers/Purchases/Bills/BillsLanding/BillsTable.js +++ b/client/src/containers/Purchases/Bills/BillsLanding/BillsTable.js @@ -14,6 +14,7 @@ import withBillActions from './withBillsActions'; import withSettings from 'containers/Settings/withSettings'; import withAlertsActions from 'containers/Alert/withAlertActions'; import withDialogActions from 'containers/Dialog/withDialogActions'; +import withDrawerActions from 'containers/Drawer/withDrawerActions'; import { useBillsTableColumns, ActionsMenu } from './components'; import { useBillsListContext } from './BillsListProvider'; @@ -32,6 +33,9 @@ function BillsDataTable({ // #withDialogActions openDialog, + + // #withDrawerActions + openDrawer, }) { // Bills list context. const { bills, pagination, isBillsLoading, isBillsFetching, isEmptyStatus } = @@ -78,6 +82,11 @@ function BillsDataTable({ openDialog('allocate-landed-cost', { billId: id }); }; + // Handle view detail bill. + const handleViewDetailBill = ({ id }) => { + openDrawer('bill-drawer', { billId: id }); + }; + if (isEmptyStatus) { return ; } @@ -106,6 +115,7 @@ function BillsDataTable({ onOpen: handleOpenBill, onQuick: handleQuickPaymentMade, onAllocateLandedCost: handleAllocateLandedCost, + onViewDetails: handleViewDetailBill, }} /> ); @@ -115,6 +125,7 @@ export default compose( withBills(({ billsTableState }) => ({ billsTableState })), withBillActions, withAlertsActions, + withDrawerActions, withDialogActions, withSettings(({ organizationSettings }) => ({ baseCurrency: organizationSettings?.baseCurrency, diff --git a/client/src/containers/Purchases/Bills/BillsLanding/components.js b/client/src/containers/Purchases/Bills/BillsLanding/components.js index fa4c2a76b..cf2b6c73a 100644 --- a/client/src/containers/Purchases/Bills/BillsLanding/components.js +++ b/client/src/containers/Purchases/Bills/BillsLanding/components.js @@ -20,7 +20,14 @@ import moment from 'moment'; * Actions menu. */ export function ActionsMenu({ - payload: { onEdit, onOpen, onDelete, onQuick, onAllocateLandedCost }, + payload: { + onEdit, + onOpen, + onDelete, + onQuick, + onViewDetails, + onAllocateLandedCost, + }, row: { original }, }) { return ( @@ -28,6 +35,7 @@ export function ActionsMenu({ } text={intl.get('view_details')} + onClick={safeCallback(onViewDetails, original)} /> *:not(:last-child) { + margin-right: 25px; + } + + &.bp3-large > .bp3-tab { + font-size: 15px; + color: #555; + margin: 0 0.8rem; + + &[aria-selected='true'], + &:not([aria-disabled='true']):hover { + color: $pt-link-color; + } + } + } + } + + .bigcapital-datatable { + .table { + max-height: 500px; + border: 1px solid #d1dee2; + min-width: auto; + margin: 12px; + + .tbody, + .tbody-inner { + height: auto; + scrollbar-width: none; + &::-webkit-scrollbar { + display: none; + } + } + .tbody { + .tr .td { + padding: 0.8rem; + } + } + } + } +} + +.bp3-drawer.bp3-position-right { + bottom: 0; + right: 0; + top: 0; + overflow: auto; + height: 100%; + + scrollbar-width: none; + + &::-webkit-scrollbar { + display: none; + } + + .bp3-drawer-header { + margin-bottom: 2px; + box-shadow: (0, 0, 0); + background-color: #6a7993; + + .bp3-heading, + .bp3-icon { + color: white; + } + } +} diff --git a/client/src/style/pages/AllocateLandedCost/AllocateLandedCostForm.scss b/client/src/style/pages/AllocateLandedCost/AllocateLandedCostForm.scss index 4785113d6..fe15f6d73 100644 --- a/client/src/style/pages/AllocateLandedCost/AllocateLandedCostForm.scss +++ b/client/src/style/pages/AllocateLandedCost/AllocateLandedCostForm.scss @@ -19,12 +19,13 @@ .bigcapital-datatable { .table { - max-height: 300px; + // max-height: 300px; border: 1px solid #d1dee2; min-width: auto; .tbody, .tbody-inner { + height: auto; scrollbar-width: none; &::-webkit-scrollbar { display: none; From 76c6cb36996fe31f2cd401c5a5d3bda2b250d6fe Mon Sep 17 00:00:00 2001 From: "a.bouhuolia" Date: Thu, 22 Jul 2021 18:11:17 +0200 Subject: [PATCH 10/24] WIP: Allocate landed cost. --- server/src/api/controllers/Expenses.ts | 13 +- server/src/api/controllers/Purchases/Bills.ts | 34 +- .../api/controllers/Purchases/LandedCost.ts | 289 ++++++++++ server/src/api/controllers/Purchases/index.ts | 2 + .../api/controllers/Sales/SalesInvoices.ts | 2 +- .../20190822214306_create_items_table.js | 1 + .../20200105014405_create_expenses_table.js | 39 +- ...e_expense_transactions_categories_table.js | 37 +- .../20200719152005_create_bills_table.js | 14 +- ...20200722164252_create_landed_cost_table.js | 21 + ...164253_create_landed_cost_entries_table.js | 11 + ...200722173423_create_items_entries_table.js | 2 + server/src/interfaces/Bill.ts | 101 ++-- server/src/interfaces/Expenses.ts | 6 + server/src/interfaces/ItemEntry.ts | 4 +- server/src/interfaces/LandedCost.ts | 85 +++ server/src/interfaces/index.ts | 1 + server/src/loaders/tenantModels.ts | 4 + server/src/models/Bill.js | 19 + server/src/models/BillLandedCost.js | 36 ++ server/src/models/BillLandedCostEntry.js | 10 + server/src/models/Expense.js | 105 ++-- server/src/models/ExpenseCategory.js | 15 + .../src/services/Expenses/ExpensesService.ts | 63 ++- server/src/services/Purchases/Bills.ts | 42 +- .../Purchases/LandedCost/BillLandedCost.ts | 55 ++ .../Purchases/LandedCost/ExpenseLandedCost.ts | 53 ++ .../Purchases/LandedCost/LandedCostListing.ts | 78 +++ .../LandedCost/TransctionLandedCost.ts | 61 +++ .../Purchases/LandedCost/constants.ts | 15 + .../services/Purchases/LandedCost/index.ts | 504 ++++++++++++++++++ server/src/subscribers/events.ts | 10 + server/src/utils/index.ts | 8 +- 33 files changed, 1577 insertions(+), 163 deletions(-) create mode 100644 server/src/api/controllers/Purchases/LandedCost.ts create mode 100644 server/src/database/migrations/20200722164252_create_landed_cost_table.js create mode 100644 server/src/database/migrations/20200722164253_create_landed_cost_entries_table.js create mode 100644 server/src/interfaces/LandedCost.ts create mode 100644 server/src/models/BillLandedCost.js create mode 100644 server/src/models/BillLandedCostEntry.js create mode 100644 server/src/services/Purchases/LandedCost/BillLandedCost.ts create mode 100644 server/src/services/Purchases/LandedCost/ExpenseLandedCost.ts create mode 100644 server/src/services/Purchases/LandedCost/LandedCostListing.ts create mode 100644 server/src/services/Purchases/LandedCost/TransctionLandedCost.ts create mode 100644 server/src/services/Purchases/LandedCost/constants.ts create mode 100644 server/src/services/Purchases/LandedCost/index.ts diff --git a/server/src/api/controllers/Expenses.ts b/server/src/api/controllers/Expenses.ts index c30dca97e..a207b6029 100644 --- a/server/src/api/controllers/Expenses.ts +++ b/server/src/api/controllers/Expenses.ts @@ -111,6 +111,7 @@ export default class ExpensesController extends BaseController { .trim() .escape() .isLength({ max: DATATYPES_LENGTH.STRING }), + check('categories.*.landed_cost').optional().isBoolean().toBoolean(), ]; } @@ -251,11 +252,8 @@ export default class ExpensesController extends BaseController { } try { - const { - expenses, - pagination, - filterMeta, - } = await this.expensesService.getExpensesList(tenantId, filter); + const { expenses, pagination, filterMeta } = + await this.expensesService.getExpensesList(tenantId, filter); return res.status(200).send({ expenses, @@ -345,6 +343,11 @@ export default class ExpensesController extends BaseController { errors: [{ type: 'CONTACT_NOT_FOUND', code: 800 }], }); } + if (error.errorType === 'EXPENSE_HAS_ASSOCIATED_LANDED_COST') { + return res.status(400).send({ + errors: [{ type: 'EXPENSE_HAS_ASSOCIATED_LANDED_COST', code: 900 }], + }); + } } next(error); } diff --git a/server/src/api/controllers/Purchases/Bills.ts b/server/src/api/controllers/Purchases/Bills.ts index d8f8affde..7100bd3ad 100644 --- a/server/src/api/controllers/Purchases/Bills.ts +++ b/server/src/api/controllers/Purchases/Bills.ts @@ -110,6 +110,10 @@ export default class BillsController extends BaseController { .optional({ nullable: true }) .trim() .escape(), + check('entries.*.landed_cost') + .optional({ nullable: true }) + .isBoolean() + .toBoolean(), ]; } @@ -141,6 +145,10 @@ export default class BillsController extends BaseController { .optional({ nullable: true }) .trim() .escape(), + check('entries.*.landedCost') + .optional({ nullable: true }) + .isBoolean() + .toBoolean(), ]; } @@ -301,11 +309,8 @@ export default class BillsController extends BaseController { filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles); } try { - const { - bills, - pagination, - filterMeta, - } = await this.billsService.getBills(tenantId, filter); + const { bills, pagination, filterMeta } = + await this.billsService.getBills(tenantId, filter); return res.status(200).send({ bills, @@ -397,17 +402,24 @@ export default class BillsController extends BaseController { if (error.errorType === 'contact_not_found') { return res.boom.badRequest(null, { errors: [ - { type: 'VENDOR_NOT_FOUND', message: 'Vendor not found.', code: 1200 }, + { + type: 'VENDOR_NOT_FOUND', + message: 'Vendor not found.', + code: 1200, + }, ], }); } if (error.errorType === 'BILL_HAS_ASSOCIATED_PAYMENT_ENTRIES') { return res.status(400).send({ - errors: [{ - type: 'BILL_HAS_ASSOCIATED_PAYMENT_ENTRIES', - message: 'Cannot delete bill that has associated payment transactions.', - code: 1200 - }], + errors: [ + { + type: 'BILL_HAS_ASSOCIATED_PAYMENT_ENTRIES', + message: + 'Cannot delete bill that has associated payment transactions.', + code: 1200, + }, + ], }); } } diff --git a/server/src/api/controllers/Purchases/LandedCost.ts b/server/src/api/controllers/Purchases/LandedCost.ts new file mode 100644 index 000000000..da32f2b8b --- /dev/null +++ b/server/src/api/controllers/Purchases/LandedCost.ts @@ -0,0 +1,289 @@ +import { Router, Request, Response, NextFunction } from 'express'; +import { check, param, query } from 'express-validator'; +import { Service, Inject } from 'typedi'; +import { ServiceError } from 'exceptions'; +import AllocateLandedCostService from 'services/Purchases/LandedCost'; +import LandedCostListing from 'services/Purchases/LandedCost/LandedCostListing'; +import BaseController from '../BaseController'; +import { ResultSetDependencies } from 'mathjs'; + +@Service() +export default class BillAllocateLandedCost extends BaseController { + @Inject() + allocateLandedCost: AllocateLandedCostService; + + @Inject() + landedCostListing: LandedCostListing; + + /** + * Router constructor. + */ + public router() { + const router = Router(); + + router.post( + '/bills/:billId/allocate', + [ + check('transaction_id').exists().isInt(), + check('transaction_type').exists().isIn(['Expense', 'Bill']), + check('transaction_entry_id').exists().isInt(), + + check('allocation_method').exists().isIn(['value', 'quantity']), + check('description').optional({ nullable: true }), + + check('items').isArray({ min: 1 }), + check('items.*.entry_id').isInt(), + check('items.*.cost').isDecimal(), + ], + this.validationResult, + this.calculateLandedCost.bind(this), + this.handleServiceErrors + ); + router.delete( + '/:allocatedLandedCostId', + [param('allocatedLandedCostId').exists().isInt()], + this.validationResult, + this.deleteAllocatedLandedCost.bind(this), + this.handleServiceErrors + ); + router.get( + '/transactions', + [query('transaction_type').exists().isIn(['Expense', 'Bill'])], + this.validationResult, + this.getLandedCostTransactions.bind(this), + this.handleServiceErrors + ); + router.get( + '/bills/:billId/transactions', + [param('billId').exists()], + this.validationResult, + this.getBillLandedCostTransactions.bind(this), + this.handleServiceErrors + ); + return router; + } + + /** + * Retrieve the landed cost transactions of the given query. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + private async getLandedCostTransactions( + req: Request, + res: Response, + next: NextFunction + ) { + const { tenantId } = req; + const query = this.matchedQueryData(req); + + try { + const transactions = + await this.landedCostListing.getLandedCostTransactions(tenantId, query); + return res.status(200).send({ transactions }); + } catch (error) { + next(error); + } + } + + /** + * Allocate landed cost. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + * @returns {Response} + */ + public async calculateLandedCost( + req: Request, + res: Response, + next: NextFunction + ) { + const { tenantId } = req; + const { billId: purchaseInvoiceId } = req.params; + const landedCostDTO = this.matchedBodyData(req); + + try { + const { billLandedCost } = + await this.allocateLandedCost.allocateLandedCost( + tenantId, + landedCostDTO, + purchaseInvoiceId + ); + + return res.status(200).send({ + id: billLandedCost.id, + message: 'The items cost are located successfully.', + }); + } catch (error) { + next(error); + } + } + + /** + * Deletes the allocated landed cost. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + * @returns {Response} + */ + public async deleteAllocatedLandedCost( + req: Request, + res: Response, + next: NextFunction + ): Promise { + const { tenantId } = req; + const { allocatedLandedCostId } = req.params; + + try { + await this.allocateLandedCost.deleteAllocatedLandedCost( + tenantId, + allocatedLandedCostId + ); + + return res.status(200).send({ + id: allocatedLandedCostId, + message: 'The allocated landed cost are delete successfully.', + }); + } catch (error) { + next(error); + } + } + + /** + * Retrieve the list unlocated landed costs. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + public async listLandedCosts( + req: Request, + res: Response, + next: NextFunction + ) { + const query = this.matchedQueryData(req); + const { tenantId } = req; + + try { + const transactions = + await this.landedCostListing.getLandedCostTransactions(tenantId, query); + return res.status(200).send({ transactions }); + } catch (error) { + next(error); + } + } + + /** + * Retrieve the bill landed cost transactions. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + public async getBillLandedCostTransactions( + req: Request, + res: Response, + next: NextFunction + ): Promise { + const { tenantId } = req; + const { billId } = req.params; + + try { + const transactions = + await this.landedCostListing.getBillLandedCostTransactions( + tenantId, + billId + ); + + return res.status(200).send({ + billId, + transactions, + }); + } catch (error) { + next(error); + } + } + + /** + * Handle service errors. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + * @param {Error} error + */ + public handleServiceErrors( + error: Error, + req: Request, + res: Response, + next: NextFunction + ) { + if (error instanceof ServiceError) { + if (error.errorType === 'BILL_NOT_FOUND') { + return res.status(400).send({ + errors: [ + { + type: 'BILL_NOT_FOUND', + code: 400, + message: 'The give bill id not found.', + }, + ], + }); + } + if (error.errorType === 'LANDED_COST_TRANSACTION_NOT_FOUND') { + return res.status(400).send({ + errors: [ + { + type: 'LANDED_COST_TRANSACTION_NOT_FOUND', + code: 200, + message: 'The given landed cost transaction id not found.', + }, + ], + }); + } + if (error.errorType === 'LANDED_COST_ENTRY_NOT_FOUND') { + return res.status(400).send({ + errors: [ + { + type: 'LANDED_COST_ENTRY_NOT_FOUND', + code: 300, + message: 'The given landed cost tranasction entry id not found.', + }, + ], + }); + } + if (error.errorType === 'COST_AMOUNT_BIGGER_THAN_UNALLOCATED_AMOUNT') { + return res.status(400).send({ + errors: [ + { type: 'COST_AMOUNT_BIGGER_THAN_UNALLOCATED_AMOUNT', code: 300 }, + ], + }); + } + if (error.errorType === 'LANDED_COST_ITEMS_IDS_NOT_FOUND') { + return res.status(400).send({ + errors: [ + { + type: 'LANDED_COST_ITEMS_IDS_NOT_FOUND', + code: 200, + message: 'The given entries ids of purchase invoice not found.', + }, + ], + }); + } + if (error.errorType === 'BILL_LANDED_COST_NOT_FOUND') { + return res.status(400).send({ + errors: [ + { + type: 'BILL_LANDED_COST_NOT_FOUND', + code: 200, + message: 'The given bill located landed cost not found.', + }, + ], + }); + } + if (error.errorType === 'COST_TRASNACTION_NOT_FOUND') { + return res.status(400).send({ + errors: [{ type: 'COST_TRASNACTION_NOT_FOUND', code: 500 }], + }); + } + } + next(error); + } +} diff --git a/server/src/api/controllers/Purchases/index.ts b/server/src/api/controllers/Purchases/index.ts index a56ac1261..2ee3686f2 100644 --- a/server/src/api/controllers/Purchases/index.ts +++ b/server/src/api/controllers/Purchases/index.ts @@ -2,6 +2,7 @@ import { Router } from 'express'; import { Container, Service } from 'typedi'; import Bills from 'api/controllers/Purchases/Bills' import BillPayments from 'api/controllers/Purchases/BillsPayments'; +import BillAllocateLandedCost from './LandedCost'; @Service() export default class PurchasesController { @@ -11,6 +12,7 @@ export default class PurchasesController { router.use('/bills', Container.get(Bills).router()); router.use('/bill_payments', Container.get(BillPayments).router()); + router.use('/landed-cost', Container.get(BillAllocateLandedCost).router()); return router; } diff --git a/server/src/api/controllers/Sales/SalesInvoices.ts b/server/src/api/controllers/Sales/SalesInvoices.ts index c2fbc8aa1..7b8641c36 100644 --- a/server/src/api/controllers/Sales/SalesInvoices.ts +++ b/server/src/api/controllers/Sales/SalesInvoices.ts @@ -347,7 +347,7 @@ export default class SaleInvoicesController extends BaseController { * @param {Response} res * @param {NextFunction} next */ - handleServiceErrors( + private handleServiceErrors( error: Error, req: Request, res: Response, diff --git a/server/src/database/migrations/20190822214306_create_items_table.js b/server/src/database/migrations/20190822214306_create_items_table.js index 7d0100506..16ac1ed66 100644 --- a/server/src/database/migrations/20190822214306_create_items_table.js +++ b/server/src/database/migrations/20190822214306_create_items_table.js @@ -17,6 +17,7 @@ exports.up = function (knex) { table.text('sell_description').nullable(); table.text('purchase_description').nullable(); table.integer('quantity_on_hand'); + table.boolean('landed_cost').nullable(); table.text('note').nullable(); table.boolean('active'); diff --git a/server/src/database/migrations/20200105014405_create_expenses_table.js b/server/src/database/migrations/20200105014405_create_expenses_table.js index 5f4382e35..169856f33 100644 --- a/server/src/database/migrations/20200105014405_create_expenses_table.js +++ b/server/src/database/migrations/20200105014405_create_expenses_table.js @@ -1,20 +1,29 @@ +exports.up = function (knex) { + return knex.schema + .createTable('expenses_transactions', (table) => { + table.increments(); + table.string('currency_code', 3); + table.text('description'); + table + .integer('payment_account_id') + .unsigned() + .references('id') + .inTable('accounts'); + table.integer('payee_id').unsigned().references('id').inTable('contacts'); + table.string('reference_no'); -exports.up = function(knex) { - return knex.schema.createTable('expenses_transactions', (table) => { - table.increments(); - table.decimal('total_amount', 13, 3); - table.string('currency_code', 3); - table.text('description'); - table.integer('payment_account_id').unsigned().references('id').inTable('accounts'); - table.integer('payee_id').unsigned().references('id').inTable('contacts');; - table.string('reference_no'); - table.date('published_at').index(); - table.integer('user_id').unsigned().index(); - table.date('payment_date').index(); - table.timestamps(); - }).raw('ALTER TABLE `EXPENSES_TRANSACTIONS` AUTO_INCREMENT = 1000'); + table.decimal('total_amount', 13, 3); + table.decimal('landed_cost_amount', 13, 3).defaultTo(0); + table.decimal('allocated_cost_amount', 13, 3).defaultTo(0); + + table.date('published_at').index(); + table.integer('user_id').unsigned().index(); + table.date('payment_date').index(); + table.timestamps(); + }) + .raw('ALTER TABLE `EXPENSES_TRANSACTIONS` AUTO_INCREMENT = 1000'); }; -exports.down = function(knex) { +exports.down = function (knex) { return knex.schema.dropTableIfExists('expenses'); }; diff --git a/server/src/database/migrations/20200606113848_create_expense_transactions_categories_table.js b/server/src/database/migrations/20200606113848_create_expense_transactions_categories_table.js index b383bd668..a1bc88052 100644 --- a/server/src/database/migrations/20200606113848_create_expense_transactions_categories_table.js +++ b/server/src/database/migrations/20200606113848_create_expense_transactions_categories_table.js @@ -1,16 +1,29 @@ - -exports.up = function(knex) { - return knex.schema.createTable('expense_transaction_categories', table => { - table.increments(); - table.integer('expense_account_id').unsigned().index().references('id').inTable('accounts'); - table.integer('index').unsigned(); - table.text('description'); - table.decimal('amount', 13, 3); - table.integer('expense_id').unsigned().index().references('id').inTable('expenses_transactions'); - table.timestamps(); - }).raw('ALTER TABLE `EXPENSE_TRANSACTION_CATEGORIES` AUTO_INCREMENT = 1000');; +exports.up = function (knex) { + return knex.schema + .createTable('expense_transaction_categories', (table) => { + table.increments(); + table + .integer('expense_account_id') + .unsigned() + .index() + .references('id') + .inTable('accounts'); + table.integer('index').unsigned(); + table.text('description'); + table.decimal('amount', 13, 3); + table.decimal('allocated_cost_amount', 13, 3).defaultTo(0); + table.boolean('landed_cost').defaultTo(false); + table + .integer('expense_id') + .unsigned() + .index() + .references('id') + .inTable('expenses_transactions'); + table.timestamps(); + }) + .raw('ALTER TABLE `EXPENSE_TRANSACTION_CATEGORIES` AUTO_INCREMENT = 1000'); }; -exports.down = function(knex) { +exports.down = function (knex) { return knex.schema.dropTableIfExists('expense_transaction_categories'); }; diff --git a/server/src/database/migrations/20200719152005_create_bills_table.js b/server/src/database/migrations/20200719152005_create_bills_table.js index c8c432a32..34cb845ef 100644 --- a/server/src/database/migrations/20200719152005_create_bills_table.js +++ b/server/src/database/migrations/20200719152005_create_bills_table.js @@ -1,8 +1,12 @@ - -exports.up = function(knex) { +exports.up = function (knex) { return knex.schema.createTable('bills', (table) => { table.increments(); - table.integer('vendor_id').unsigned().index().references('id').inTable('contacts'); + table + .integer('vendor_id') + .unsigned() + .index() + .references('id') + .inTable('contacts'); table.string('bill_number'); table.date('bill_date').index(); table.date('due_date').index(); @@ -12,6 +16,8 @@ exports.up = function(knex) { table.decimal('amount', 13, 3).defaultTo(0); table.string('currency_code'); table.decimal('payment_amount', 13, 3).defaultTo(0); + table.decimal('landed_cost_amount', 13, 3).defaultTo(0); + table.decimal('allocated_cost_amount', 13, 3).defaultTo(0); table.string('inv_lot_number').index(); table.date('opened_at').index(); table.integer('user_id').unsigned(); @@ -19,6 +25,6 @@ exports.up = function(knex) { }); }; -exports.down = function(knex) { +exports.down = function (knex) { return knex.schema.dropTableIfExists('bills'); }; diff --git a/server/src/database/migrations/20200722164252_create_landed_cost_table.js b/server/src/database/migrations/20200722164252_create_landed_cost_table.js new file mode 100644 index 000000000..f315e1bde --- /dev/null +++ b/server/src/database/migrations/20200722164252_create_landed_cost_table.js @@ -0,0 +1,21 @@ +exports.up = function (knex) { + return knex.schema.createTable('bill_located_costs', (table) => { + table.increments(); + + table.decimal('amount', 13, 3).unsigned(); + + table.integer('fromTransactionId').unsigned(); + table.string('fromTransactionType'); + table.integer('fromTransactionEntryId').unsigned(); + + table.string('allocationMethod'); + table.integer('costAccountId').unsigned(); + table.text('description'); + + table.integer('billId').unsigned(); + + table.timestamps(); + }); +}; + +exports.down = function (knex) {}; diff --git a/server/src/database/migrations/20200722164253_create_landed_cost_entries_table.js b/server/src/database/migrations/20200722164253_create_landed_cost_entries_table.js new file mode 100644 index 000000000..96cdc5d77 --- /dev/null +++ b/server/src/database/migrations/20200722164253_create_landed_cost_entries_table.js @@ -0,0 +1,11 @@ +exports.up = function (knex) { + return knex.schema.createTable('bill_located_cost_entries', (table) => { + table.increments(); + + table.decimal('cost', 13, 3).unsigned(); + table.integer('entry_id').unsigned(); + table.integer('bill_located_cost_id').unsigned(); + }); +}; + +exports.down = function (knex) {}; diff --git a/server/src/database/migrations/20200722173423_create_items_entries_table.js b/server/src/database/migrations/20200722173423_create_items_entries_table.js index b6313eaba..a4c809cec 100644 --- a/server/src/database/migrations/20200722173423_create_items_entries_table.js +++ b/server/src/database/migrations/20200722173423_create_items_entries_table.js @@ -15,6 +15,8 @@ exports.up = function(knex) { table.integer('sell_account_id').unsigned().references('id').inTable('accounts'); table.integer('cost_account_id').unsigned().references('id').inTable('accounts'); + table.boolean('landed_cost').defaultTo(false); + table.decimal('allocated_cost_amount', 13, 3); table.timestamps(); }); }; diff --git a/server/src/interfaces/Bill.ts b/server/src/interfaces/Bill.ts index 0598dde6a..ea68100c3 100644 --- a/server/src/interfaces/Bill.ts +++ b/server/src/interfaces/Bill.ts @@ -1,64 +1,69 @@ -import { IDynamicListFilterDTO } from "./DynamicFilter"; -import { IItemEntry, IItemEntryDTO } from "./ItemEntry"; +import { IDynamicListFilterDTO } from './DynamicFilter'; +import { IItemEntry, IItemEntryDTO } from './ItemEntry'; export interface IBillDTO { - vendorId: number, - billNumber: string, - billDate: Date, - dueDate: Date, - referenceNo: string, - status: string, - note: string, - amount: number, - paymentAmount: number, - open: boolean, - entries: IItemEntryDTO[], -}; + vendorId: number; + billNumber: string; + billDate: Date; + dueDate: Date; + referenceNo: string; + status: string; + note: string; + amount: number; + paymentAmount: number; + open: boolean; + entries: IItemEntryDTO[]; +} export interface IBillEditDTO { - vendorId: number, - billNumber: string, - billDate: Date, - dueDate: Date, - referenceNo: string, - status: string, - note: string, - amount: number, - paymentAmount: number, - open: boolean, - entries: IItemEntryDTO[], -}; + vendorId: number; + billNumber: string; + billDate: Date; + dueDate: Date; + referenceNo: string; + status: string; + note: string; + amount: number; + paymentAmount: number; + open: boolean; + entries: IItemEntryDTO[]; +} export interface IBill { - id?: number, + id?: number; - vendorId: number, - billNumber: string, - billDate: Date, - dueDate: Date, - referenceNo: string, - status: string, - note: string, - amount: number, - paymentAmount: number, - currencyCode: string, + vendorId: number; + billNumber: string; + billDate: Date; + dueDate: Date; + referenceNo: string; + status: string; + note: string; - dueAmount: number, - overdueDays: number, + amount: number; + allocatedCostAmount: number; + landedCostAmount: number; + unallocatedCostAmount: number; - openedAt: Date | string, + paymentAmount: number; + currencyCode: string; - entries: IItemEntry[], - userId: number, + dueAmount: number; + overdueDays: number; - createdAt: Date, - updateAt: Date, -}; + openedAt: Date | string; -export interface IBillsFilter extends IDynamicListFilterDTO { - stringifiedFilterRoles?: string, + entries: IItemEntry[]; + userId: number; + + createdAt: Date; + updateAt: Date; +} + +export interface IBillsFilter extends IDynamicListFilterDTO { + stringifiedFilterRoles?: string; } export interface IBillsService { validateVendorHasNoBills(tenantId: number, vendorId: number): Promise; -} \ No newline at end of file +} diff --git a/server/src/interfaces/Expenses.ts b/server/src/interfaces/Expenses.ts index 0d1a11a1f..a1bea49ac 100644 --- a/server/src/interfaces/Expenses.ts +++ b/server/src/interfaces/Expenses.ts @@ -27,15 +27,20 @@ export interface IExpense { userId: number; paymentDate: Date; payeeId: number; + landedCostAmount: number; + allocatedCostAmount: number; + unallocatedCostAmount: number; categories: IExpenseCategory[]; } export interface IExpenseCategory { + id?: number; expenseAccountId: number; index: number; description: string; expenseId: number; amount: number; + landedCost: boolean; } export interface IExpenseDTO { @@ -56,6 +61,7 @@ export interface IExpenseCategoryDTO { index: number; description?: string; expenseId: number; + landedCost?: boolean; } export interface IExpensesService { diff --git a/server/src/interfaces/ItemEntry.ts b/server/src/interfaces/ItemEntry.ts index 3e082c716..f9df14934 100644 --- a/server/src/interfaces/ItemEntry.ts +++ b/server/src/interfaces/ItemEntry.ts @@ -17,8 +17,10 @@ export interface IItemEntry { sellAccountId: number, costAccountId: number, + + landedCost?: boolean, } export interface IItemEntryDTO { - + landedCost?: boolean } \ No newline at end of file diff --git a/server/src/interfaces/LandedCost.ts b/server/src/interfaces/LandedCost.ts new file mode 100644 index 000000000..a719b31c2 --- /dev/null +++ b/server/src/interfaces/LandedCost.ts @@ -0,0 +1,85 @@ +export interface IBillLandedCost { + fromTransactionId: number; + fromTransactionType: string; + amount: number; + BillId: number; +} + +export interface IBillLandedCostEntry { + id?: number, + cost: number, + entryId: number, + billLocatedCostId: number, +} + +export interface ILandedCostItemDTO { + entryId: number, + cost: number; +} +export type ILandedCostType = 'Expense' | 'Bill'; + +export interface ILandedCostDTO { + transactionType: ILandedCostType; + transactionId: number; + transactionEntryId: number, + allocationMethod: string; + description: string; + items: ILandedCostItemDTO[]; +} + +export interface ILandedCostQueryDTO { + vendorId: number; + fromDate: Date; + toDate: Date; +} + +export interface IUnallocatedListCost { + costNumber: string; + costAmount: number; + unallocatedAmount: number; +} + +export interface ILandedCostTransactionsQueryDTO { + transactionType: string, + date: Date, +} + +export interface ILandedCostEntriesQueryDTO { + transactionType: string, + transactionId: number, +} + +export interface ILandedCostTransaction { + id: number; + name: string; + amount: number; + allocatedCostAmount: number; + unallocatedCostAmount: number; + transactionType: string; + entries?: ILandedCostTransactionEntry[]; +} + +export interface ILandedCostTransactionEntry { + id: number; + name: string; + code: string; + amount: number; + description: string; +} + +interface ILandedCostEntry { + id: number; + landedCost?: boolean; +} + +export interface IBillLandedCostTransaction { + id: number, + fromTranscationId: number, + fromTransactionType: string; + fromTransactionEntryId: number; + + billId: number, + allocationMethod: string; + costAccountId: number, + description: string; +}; \ No newline at end of file diff --git a/server/src/interfaces/index.ts b/server/src/interfaces/index.ts index 388668041..0ce5f414c 100644 --- a/server/src/interfaces/index.ts +++ b/server/src/interfaces/index.ts @@ -53,6 +53,7 @@ export * from './Table'; export * from './Ledger'; export * from './CashFlow'; export * from './InventoryDetails'; +export * from './LandedCost'; export interface I18nService { __: (input: string) => string; diff --git a/server/src/loaders/tenantModels.ts b/server/src/loaders/tenantModels.ts index 86adacdab..c71242b02 100644 --- a/server/src/loaders/tenantModels.ts +++ b/server/src/loaders/tenantModels.ts @@ -36,6 +36,8 @@ import Media from 'models/Media'; import MediaLink from 'models/MediaLink'; import InventoryAdjustment from 'models/InventoryAdjustment'; import InventoryAdjustmentEntry from 'models/InventoryAdjustmentEntry'; +import BillLandedCost from 'models/BillLandedCost'; +import BillLandedCostEntry from 'models/BillLandedCostEntry'; export default (knex) => { const models = { @@ -75,6 +77,8 @@ export default (knex) => { Contact, InventoryAdjustment, InventoryAdjustmentEntry, + BillLandedCost, + BillLandedCostEntry }; return mapValues(models, (model) => model.bindKnex(knex)); } \ No newline at end of file diff --git a/server/src/models/Bill.js b/server/src/models/Bill.js index 9b69d0c3e..f7e050eb2 100644 --- a/server/src/models/Bill.js +++ b/server/src/models/Bill.js @@ -103,6 +103,7 @@ export default class Bill extends TenantModel { 'remainingDays', 'overdueDays', 'isOverdue', + 'unallocatedCostAmount' ]; } @@ -178,6 +179,14 @@ export default class Bill extends TenantModel { return this.overdueDays > 0; } + /** + * Retrieve the unallocated cost amount. + * @return {number} + */ + get unallocatedCostAmount() { + return Math.max(this.landedCostAmount - this.allocatedCostAmount, 0); + } + getOverdueDays(asDate = moment().format('YYYY-MM-DD')) { // Can't continue in case due date not defined. if (!this.dueDate) { @@ -195,6 +204,7 @@ export default class Bill extends TenantModel { static get relationMappings() { const Contact = require('models/Contact'); const ItemEntry = require('models/ItemEntry'); + const BillLandedCost = require('models/BillLandedCost'); return { vendor: { @@ -220,6 +230,15 @@ export default class Bill extends TenantModel { builder.where('reference_type', 'Bill'); }, }, + + locatedLandedCosts: { + relation: Model.HasManyRelation, + modelClass: BillLandedCost.default, + join: { + from: 'bills.id', + to: 'bill_located_costs.billId', + }, + }, }; } diff --git a/server/src/models/BillLandedCost.js b/server/src/models/BillLandedCost.js new file mode 100644 index 000000000..c7a0624fc --- /dev/null +++ b/server/src/models/BillLandedCost.js @@ -0,0 +1,36 @@ +import { Model } from 'objection'; +import TenantModel from 'models/TenantModel'; + +export default class BillLandedCost extends TenantModel { + /** + * Table name + */ + static get tableName() { + return 'bill_located_costs'; + } + + /** + * Model timestamps. + */ + get timestamps() { + return ['createdAt', 'updatedAt']; + } + + /** + * Relationship mapping. + */ + static get relationMappings() { + const BillLandedCostEntry = require('models/BillLandedCostEntry'); + + return { + allocateEntries: { + relation: Model.HasManyRelation, + modelClass: BillLandedCostEntry.default, + join: { + from: 'bill_located_costs.id', + to: 'bill_located_cost_entries.billLocatedCostId', + }, + }, + }; + } +} diff --git a/server/src/models/BillLandedCostEntry.js b/server/src/models/BillLandedCostEntry.js new file mode 100644 index 000000000..aca0a87b7 --- /dev/null +++ b/server/src/models/BillLandedCostEntry.js @@ -0,0 +1,10 @@ +import TenantModel from 'models/TenantModel'; + +export default class BillLandedCostEntry extends TenantModel { + /** + * Table name + */ + static get tableName() { + return 'bill_located_cost_entries'; + } +} diff --git a/server/src/models/Expense.js b/server/src/models/Expense.js index c95d9e57f..e8bc02545 100644 --- a/server/src/models/Expense.js +++ b/server/src/models/Expense.js @@ -1,27 +1,27 @@ -import { Model } from "objection"; -import TenantModel from "models/TenantModel"; -import { viewRolesBuilder } from "lib/ViewRolesBuilder"; +import { Model } from 'objection'; +import TenantModel from 'models/TenantModel'; +import { viewRolesBuilder } from 'lib/ViewRolesBuilder'; export default class Expense extends TenantModel { /** * Table name */ static get tableName() { - return "expenses_transactions"; + return 'expenses_transactions'; } /** * Account transaction reference type. */ static get referenceType() { - return "Expense"; + return 'Expense'; } /** * Model timestamps. */ get timestamps() { - return ["createdAt", "updatedAt"]; + return ['createdAt', 'updatedAt']; } /** @@ -37,14 +37,19 @@ export default class Expense extends TenantModel { static get media() { return true; } - + static get virtualAttributes() { - return ["isPublished"]; + return ['isPublished', 'unallocatedLandedCost']; } + isPublished() { return Boolean(this.publishedAt); } + unallocatedLandedCost() { + return Math.max(this.amount - this.allocatedCostAmount, 0); + } + /** * Model modifiers. */ @@ -52,28 +57,28 @@ export default class Expense extends TenantModel { return { filterByDateRange(query, startDate, endDate) { if (startDate) { - query.where("date", ">=", startDate); + query.where('date', '>=', startDate); } if (endDate) { - query.where("date", "<=", endDate); + query.where('date', '<=', endDate); } }, filterByAmountRange(query, from, to) { if (from) { - query.where("amount", ">=", from); + query.where('amount', '>=', from); } if (to) { - query.where("amount", "<=", to); + query.where('amount', '<=', to); } }, filterByExpenseAccount(query, accountId) { if (accountId) { - query.where("expense_account_id", accountId); + query.where('expense_account_id', accountId); } }, filterByPaymentAccount(query, accountId) { if (accountId) { - query.where("payment_account_id", accountId); + query.where('payment_account_id', accountId); } }, viewRolesBuilder(query, conditionals, expression) { @@ -94,40 +99,40 @@ export default class Expense extends TenantModel { * Relationship mapping. */ static get relationMappings() { - const Account = require("models/Account"); - const ExpenseCategory = require("models/ExpenseCategory"); - const Media = require("models/Media"); + const Account = require('models/Account'); + const ExpenseCategory = require('models/ExpenseCategory'); + const Media = require('models/Media'); return { paymentAccount: { relation: Model.BelongsToOneRelation, modelClass: Account.default, join: { - from: "expenses_transactions.paymentAccountId", - to: "accounts.id", + from: 'expenses_transactions.paymentAccountId', + to: 'accounts.id', }, }, categories: { relation: Model.HasManyRelation, modelClass: ExpenseCategory.default, join: { - from: "expenses_transactions.id", - to: "expense_transaction_categories.expenseId", + from: 'expenses_transactions.id', + to: 'expense_transaction_categories.expenseId', }, }, media: { relation: Model.ManyToManyRelation, modelClass: Media.default, join: { - from: "expenses_transactions.id", + from: 'expenses_transactions.id', through: { - from: "media_links.model_id", - to: "media_links.media_id", + from: 'media_links.model_id', + to: 'media_links.media_id', }, - to: "media.id", + to: 'media.id', }, filter(query) { - query.where("model_name", "Expense"); + query.where('model_name', 'Expense'); }, }, }; @@ -139,39 +144,39 @@ export default class Expense extends TenantModel { static get fields() { return { payment_date: { - label: "Payment date", - column: "payment_date", - columnType: "date", + label: 'Payment date', + column: 'payment_date', + columnType: 'date', }, payment_account: { - label: "Payment account", - column: "payment_account_id", - relation: "accounts.id", - optionsResource: "account", + label: 'Payment account', + column: 'payment_account_id', + relation: 'accounts.id', + optionsResource: 'account', }, amount: { - label: "Amount", - column: "total_amount", - columnType: "number", + label: 'Amount', + column: 'total_amount', + columnType: 'number', }, currency_code: { - label: "Currency", - column: "currency_code", - optionsResource: "currency", + label: 'Currency', + column: 'currency_code', + optionsResource: 'currency', }, reference_no: { - label: "Reference No.", - column: "reference_no", - columnType: "string", + label: 'Reference No.', + column: 'reference_no', + columnType: 'string', }, description: { - label: "Description", - column: "description", - columnType: "string", + label: 'Description', + column: 'description', + columnType: 'string', }, published: { - label: "Published", - column: "published_at", + label: 'Published', + column: 'published_at', }, status: { label: 'Status', @@ -194,9 +199,9 @@ export default class Expense extends TenantModel { }, }, created_at: { - label: "Created at", - column: "created_at", - columnType: "date", + label: 'Created at', + column: 'created_at', + columnType: 'date', }, }; } diff --git a/server/src/models/ExpenseCategory.js b/server/src/models/ExpenseCategory.js index 80b89e89f..a761acb23 100644 --- a/server/src/models/ExpenseCategory.js +++ b/server/src/models/ExpenseCategory.js @@ -9,6 +9,21 @@ export default class ExpenseCategory extends TenantModel { return 'expense_transaction_categories'; } + /** + * Virtual attributes. + */ + static get virtualAttributes() { + return ['unallocatedLandedCost']; + } + + /** + * Remain unallocated landed cost. + * @return {number} + */ + get unallocatedLandedCost() { + return Math.max(this.amount - this.allocatedCostAmount, 0); + } + /** * Relationship mapping. */ diff --git a/server/src/services/Expenses/ExpensesService.ts b/server/src/services/Expenses/ExpensesService.ts index 1e3670e08..1ac1623b0 100644 --- a/server/src/services/Expenses/ExpensesService.ts +++ b/server/src/services/Expenses/ExpensesService.ts @@ -17,11 +17,12 @@ import { IExpensesService, ISystemUser, IPaginationMeta, + IExpenseCategory, } from 'interfaces'; import DynamicListingService from 'services/DynamicListing/DynamicListService'; import events from 'subscribers/events'; import ContactsService from 'services/Contacts/ContactsService'; -import { ACCOUNT_PARENT_TYPE, ACCOUNT_ROOT_TYPE } from 'data/AccountTypes' +import { ACCOUNT_PARENT_TYPE, ACCOUNT_ROOT_TYPE } from 'data/AccountTypes'; const ERRORS = { EXPENSE_NOT_FOUND: 'expense_not_found', @@ -32,6 +33,7 @@ const ERRORS = { PAYMENT_ACCOUNT_HAS_INVALID_TYPE: 'payment_account_has_invalid_type', EXPENSES_ACCOUNT_HAS_INVALID_TYPE: 'expenses_account_has_invalid_type', EXPENSE_ALREADY_PUBLISHED: 'expense_already_published', + EXPENSE_HAS_ASSOCIATED_LANDED_COST: 'EXPENSE_HAS_ASSOCIATED_LANDED_COST', }; @Service() @@ -308,6 +310,27 @@ export default class ExpensesService implements IExpensesService { } } + /** + * Retrieve the expense landed cost amount. + * @param {IExpenseDTO} expenseDTO + * @return {number} + */ + private getExpenseLandedCostAmount(expenseDTO: IExpenseDTO): number { + const landedCostEntries = expenseDTO.categories.filter((entry) => { + return entry.landedCost === true; + }); + return this.getExpenseCategoriesTotal(landedCostEntries); + } + + /** + * Retrieve the given expense categories total. + * @param {IExpenseCategory} categories + * @returns {number} + */ + private getExpenseCategoriesTotal(categories): number { + return sumBy(categories, 'amount'); + } + /** * Mapping expense DTO to model. * @param {IExpenseDTO} expenseDTO @@ -315,12 +338,14 @@ export default class ExpensesService implements IExpensesService { * @return {IExpense} */ private expenseDTOToModel(expenseDTO: IExpenseDTO, user?: ISystemUser) { - const totalAmount = sumBy(expenseDTO.categories, 'amount'); + const landedCostAmount = this.getExpenseLandedCostAmount(expenseDTO); + const totalAmount = this.getExpenseCategoriesTotal(expenseDTO.categories); return { categories: [], ...omit(expenseDTO, ['publish']), totalAmount, + landedCostAmount, paymentDate: moment(expenseDTO.paymentDate).toMySqlDateTime(), ...(user ? { @@ -340,7 +365,7 @@ export default class ExpensesService implements IExpensesService { * @param {IExpenseDTO} expenseDTO * @return {number[]} */ - mapExpensesAccountsIdsFromDTO(expenseDTO: IExpenseDTO) { + private mapExpensesAccountsIdsFromDTO(expenseDTO: IExpenseDTO) { return expenseDTO.categories.map((category) => category.expenseAccountId); } @@ -544,15 +569,16 @@ export default class ExpensesService implements IExpensesService { authorizedUser: ISystemUser ): Promise { const oldExpense = await this.getExpenseOrThrowError(tenantId, expenseId); - const { - expenseRepository, - expenseEntryRepository, - } = this.tenancy.repositories(tenantId); + const { expenseRepository, expenseEntryRepository } = + this.tenancy.repositories(tenantId); this.logger.info('[expense] trying to delete the expense.', { tenantId, expenseId, }); + // Validates the expense has no associated landed cost. + await this.validateNoAssociatedLandedCost(tenantId, expenseId); + await expenseEntryRepository.deleteBy({ expenseId }); await expenseRepository.deleteById(expenseId); @@ -572,7 +598,7 @@ export default class ExpensesService implements IExpensesService { /** * Filters the not published expenses. - * @param {IExpense[]} expenses - + * @param {IExpense[]} expenses - */ public getNonePublishedExpenses(expenses: IExpense[]): IExpense[] { return expenses.filter((expense) => !expense.publishedAt); @@ -648,4 +674,25 @@ export default class ExpensesService implements IExpensesService { } return expense; } + + /** + * Validates the expense has not associated landed cost + * references to the given expense. + * @param {number} tenantId + * @param {number} expenseId + */ + public async validateNoAssociatedLandedCost( + tenantId: number, + expenseId: number + ) { + const { BillLandedCost } = this.tenancy.models(tenantId); + + const associatedLandedCosts = await BillLandedCost.query() + .where('fromTransactionType', 'Expense') + .where('fromTransactionId', expenseId); + + if (associatedLandedCosts.length > 0) { + throw new ServiceError(ERRORS.EXPENSE_HAS_ASSOCIATED_LANDED_COST); + } + } } diff --git a/server/src/services/Purchases/Bills.ts b/server/src/services/Purchases/Bills.ts index b35284e40..12a63ad67 100644 --- a/server/src/services/Purchases/Bills.ts +++ b/server/src/services/Purchases/Bills.ts @@ -40,7 +40,8 @@ import { ERRORS } from './constants'; @Service('Bills') export default class BillsService extends SalesInvoicesCost - implements IBillsService { + implements IBillsService +{ @Inject() inventoryService: InventoryService; @@ -100,7 +101,7 @@ export default class BillsService * @param {number} tenantId - * @param {number} billId - */ - private async getBillOrThrowError(tenantId: number, billId: number) { + public async getBillOrThrowError(tenantId: number, billId: number) { const { Bill } = this.tenancy.models(tenantId); this.logger.info('[bill] trying to get bill.', { tenantId, billId }); @@ -194,6 +195,28 @@ export default class BillsService }; } + /** + * Retrieve the bill entries total. + * @param {IItemEntry[]} entries + * @returns {number} + */ + private getBillEntriesTotal(tenantId: number, entries: IItemEntry[]): number { + const { ItemEntry } = this.tenancy.models(tenantId); + + return sumBy(entries, (e) => ItemEntry.calcAmount(e)); + } + + /** + * Retrieve the bill landed cost amount. + * @param {IBillDTO} billDTO + * @returns {number} + */ + private getBillLandedCostAmount(tenantId: number, billDTO: IBillDTO): number { + const costEntries = billDTO.entries.filter((entry) => entry.landedCost); + + return this.getBillEntriesTotal(tenantId, costEntries); + } + /** * Converts create bill DTO to model. * @param {number} tenantId @@ -211,6 +234,9 @@ export default class BillsService const amount = sumBy(billDTO.entries, (e) => ItemEntry.calcAmount(e)); + // Retrieve the landed cost amount from landed cost entries. + const landedCostAmount = this.getBillLandedCostAmount(tenantId, billDTO); + // Bill number from DTO or from auto-increment. const billNumber = billDTO.billNumber || oldBill?.billNumber; @@ -234,6 +260,7 @@ export default class BillsService 'dueDate', ]), amount, + landedCostAmount, currencyCode: vendor.currencyCode, billNumber, entries, @@ -498,7 +525,7 @@ export default class BillsService const bill = await Bill.query() .findById(billId) .withGraphFetched('vendor') - .withGraphFetched('entries'); + .withGraphFetched('entries.item'); if (!bill) { throw new ServiceError(ERRORS.BILL_NOT_FOUND); @@ -538,10 +565,11 @@ export default class BillsService override?: boolean ): Promise { // Loads the inventory items entries of the given sale invoice. - const inventoryEntries = await this.itemsEntriesService.filterInventoryEntries( - tenantId, - bill.entries - ); + const inventoryEntries = + await this.itemsEntriesService.filterInventoryEntries( + tenantId, + bill.entries + ); const transaction = { transactionId: bill.id, transactionType: 'Bill', diff --git a/server/src/services/Purchases/LandedCost/BillLandedCost.ts b/server/src/services/Purchases/LandedCost/BillLandedCost.ts new file mode 100644 index 000000000..0aacc5ecf --- /dev/null +++ b/server/src/services/Purchases/LandedCost/BillLandedCost.ts @@ -0,0 +1,55 @@ +import { Service } from 'typedi'; +import { isEmpty } from 'lodash'; +import { + IBill, + IItem, + ILandedCostTransactionEntry, + ILandedCostTransaction, + IItemEntry, +} from 'interfaces'; + +@Service() +export default class BillLandedCost { + /** + * Retrieve the landed cost transaction from the given bill transaction. + * @param {IBill} bill + * @returns {ILandedCostTransaction} + */ + public transformToLandedCost = (bill: IBill): ILandedCostTransaction => { + const number = bill.billNumber || bill.referenceNo; + const name = [ + number, + bill.currencyCode + ' ' + bill.unallocatedCostAmount, + ].join(' - '); + + return { + id: bill.id, + name, + allocatedCostAmount: bill.allocatedCostAmount, + amount: bill.landedCostAmount, + unallocatedCostAmount: bill.unallocatedCostAmount, + transactionType: 'Bill', + + ...(!isEmpty(bill.entries)) && { + entries: bill.entries.map(this.transformToLandedCostEntry), + }, + }; + }; + + /** + * Transformes bill entry to landed cost entry. + * @param {IItemEntry} billEntry - Bill entry. + * @return {ILandedCostTransactionEntry} + */ + public transformToLandedCostEntry( + billEntry: IItemEntry & { item: IItem } + ): ILandedCostTransactionEntry { + return { + id: billEntry.id, + name: billEntry.item.name, + code: billEntry.item.code, + amount: billEntry.amount, + description: billEntry.description, + }; + } +} diff --git a/server/src/services/Purchases/LandedCost/ExpenseLandedCost.ts b/server/src/services/Purchases/LandedCost/ExpenseLandedCost.ts new file mode 100644 index 000000000..8931e500c --- /dev/null +++ b/server/src/services/Purchases/LandedCost/ExpenseLandedCost.ts @@ -0,0 +1,53 @@ +import { Service } from 'typedi'; +import { isEmpty } from 'lodash'; +import { + IExpense, + ILandedCostTransactionEntry, + IExpenseCategory, + IAccount, + ILandedCostTransaction, +} from 'interfaces'; + +@Service() +export default class ExpenseLandedCost { + /** + * Retrieve the landed cost transaction from the given expense transaction. + * @param {IExpense} expense + * @returns {ILandedCostTransaction} + */ + public transformToLandedCost = ( + expense: IExpense + ): ILandedCostTransaction => { + const name = [expense.currencyCode + ' ' + expense.totalAmount].join(' - '); + + return { + id: expense.id, + name, + allocatedCostAmount: expense.allocatedCostAmount, + amount: expense.landedCostAmount, + unallocatedCostAmount: expense.unallocatedCostAmount, + transactionType: 'Expense', + + ...(!isEmpty(expense.categories) && { + entries: expense.categories.map(this.transformToLandedCostEntry), + }), + }; + }; + + /** + * Transformes expense entry to landed cost entry. + * @param {IExpenseCategory & { expenseAccount: IAccount }} expenseEntry - + * @return {ILandedCostTransactionEntry} + */ + public transformToLandedCostEntry = ( + expenseEntry: IExpenseCategory & { expenseAccount: IAccount } + ): ILandedCostTransactionEntry => { + return { + id: expenseEntry.id, + name: expenseEntry.expenseAccount.name, + code: expenseEntry.expenseAccount.code, + amount: expenseEntry.amount, + description: expenseEntry.description, + }; + }; +} diff --git a/server/src/services/Purchases/LandedCost/LandedCostListing.ts b/server/src/services/Purchases/LandedCost/LandedCostListing.ts new file mode 100644 index 000000000..e476b9f93 --- /dev/null +++ b/server/src/services/Purchases/LandedCost/LandedCostListing.ts @@ -0,0 +1,78 @@ +import { Inject, Service } from 'typedi'; +import { ref } from 'objection'; +import { + ILandedCostTransactionsQueryDTO, + ILandedCostTransaction, + IBillLandedCostTransaction, +} from 'interfaces'; +import TransactionLandedCost from './TransctionLandedCost'; +import BillsService from '../Bills'; +import HasTenancyService from 'services/Tenancy/TenancyService'; + +@Service() +export default class LandedCostListing { + @Inject() + transactionLandedCost: TransactionLandedCost; + + @Inject() + billsService: BillsService; + + @Inject() + tenancy: HasTenancyService; + + /** + * Retrieve the landed costs based on the given query. + * @param {number} tenantId + * @param {ILandedCostTransactionsQueryDTO} query + * @returns {Promise} + */ + public getLandedCostTransactions = async ( + tenantId: number, + query: ILandedCostTransactionsQueryDTO + ): Promise => { + const { transactionType } = query; + const Model = this.transactionLandedCost.getModel( + tenantId, + query.transactionType + ); + + // Retrieve the model entities. + const transactions = await Model.query().onBuild((q) => { + q.where('allocated_cost_amount', '<', ref('landed_cost_amount')); + + if (query.transactionType === 'Bill') { + q.withGraphFetched('entries.item'); + } else if (query.transactionType === 'Expense') { + q.withGraphFetched('categories.expenseAccount'); + } + }); + return transactions.map((transaction) => ({ + ...this.transactionLandedCost.transformToLandedCost( + transactionType, + transaction + ), + })); + }; + + /** + * Retrieve the bill associated landed cost transactions. + * @param {number} tenantId - Tenant id. + * @param {number} billId - Bill id. + * @return {Promise} + */ + public getBillLandedCostTransactions = async ( + tenantId: number, + billId: number + ): Promise => { + const { BillLandedCost } = this.tenancy.models(tenantId); + + // Retrieve the given bill id or throw not found service error. + const bill = await this.billsService.getBillOrThrowError(tenantId, billId); + + const landedCostTransactions = await BillLandedCost.query() + .where('bill_id', billId) + .withGraphFetched('allocateEntries'); + + return landedCostTransactions; + }; +} diff --git a/server/src/services/Purchases/LandedCost/TransctionLandedCost.ts b/server/src/services/Purchases/LandedCost/TransctionLandedCost.ts new file mode 100644 index 000000000..57c8e13c6 --- /dev/null +++ b/server/src/services/Purchases/LandedCost/TransctionLandedCost.ts @@ -0,0 +1,61 @@ +import { Inject, Service } from 'typedi'; +import * as R from 'ramda'; +import { IBill, IExpense, ILandedCostTransaction } from 'interfaces'; +import { ServiceError } from 'exceptions'; +import BillLandedCost from './BillLandedCost'; +import ExpenseLandedCost from './ExpenseLandedCost'; +import HasTenancyService from 'services/Tenancy/TenancyService'; +import { ERRORS } from './constants'; + +@Service() +export default class TransactionLandedCost { + @Inject() + billLandedCost: BillLandedCost; + + @Inject() + expenseLandedCost: ExpenseLandedCost; + + @Inject() + tenancy: HasTenancyService; + + /** + * Retrieve the cost transaction code model. + * @param {number} tenantId - Tenant id. + * @param {string} transactionType - Transaction type. + * @returns + */ + public getModel = ( + tenantId: number, + transactionType: string + ): IBill | IExpense => { + const Models = this.tenancy.models(tenantId); + const Model = Models[transactionType]; + + if (!Model) { + throw new ServiceError(ERRORS.COST_TYPE_UNDEFINED); + } + return Model; + } + + /** + * Mappes the given expense or bill transaction to landed cost transaction. + * @param {string} transactionType - Transaction type. + * @param {IBill|IExpense} transaction - Expense or bill transaction. + * @returns {ILandedCostTransaction} + */ + public transformToLandedCost = ( + transactionType: string, + transaction: IBill | IExpense + ): ILandedCostTransaction => { + return R.compose( + R.when( + R.always(transactionType === 'Bill'), + this.billLandedCost.transformToLandedCost, + ), + R.when( + R.always(transactionType === 'Expense'), + this.expenseLandedCost.transformToLandedCost, + ), + )(transaction); + } +} diff --git a/server/src/services/Purchases/LandedCost/constants.ts b/server/src/services/Purchases/LandedCost/constants.ts new file mode 100644 index 000000000..be247f943 --- /dev/null +++ b/server/src/services/Purchases/LandedCost/constants.ts @@ -0,0 +1,15 @@ + + + +export const ERRORS = { + COST_TYPE_UNDEFINED: 'COST_TYPE_UNDEFINED', + LANDED_COST_ITEMS_IDS_NOT_FOUND: 'LANDED_COST_ITEMS_IDS_NOT_FOUND', + COST_TRANSACTION_HAS_NO_ENOUGH_TO_LOCATE: + 'COST_TRANSACTION_HAS_NO_ENOUGH_TO_LOCATE', + BILL_LANDED_COST_NOT_FOUND: 'BILL_LANDED_COST_NOT_FOUND', + COST_ENTRY_ID_NOT_FOUND: 'COST_ENTRY_ID_NOT_FOUND', + LANDED_COST_TRANSACTION_NOT_FOUND: 'LANDED_COST_TRANSACTION_NOT_FOUND', + LANDED_COST_ENTRY_NOT_FOUND: 'LANDED_COST_ENTRY_NOT_FOUND', + COST_AMOUNT_BIGGER_THAN_UNALLOCATED_AMOUNT: 'COST_AMOUNT_BIGGER_THAN_UNALLOCATED_AMOUNT', + ALLOCATE_COST_SHOULD_NOT_BE_BILL: 'ALLOCATE_COST_SHOULD_NOT_BE_BILL' +}; diff --git a/server/src/services/Purchases/LandedCost/index.ts b/server/src/services/Purchases/LandedCost/index.ts new file mode 100644 index 000000000..604ddc3e9 --- /dev/null +++ b/server/src/services/Purchases/LandedCost/index.ts @@ -0,0 +1,504 @@ +import { Inject, Service } from 'typedi'; +import { difference, sumBy } from 'lodash'; +import BillsService from '../Bills'; +import { ServiceError } from 'exceptions'; +import { + IItemEntry, + IBill, + IBillLandedCost, + ILandedCostItemDTO, + ILandedCostDTO, +} from 'interfaces'; +import InventoryService from 'services/Inventory/Inventory'; +import HasTenancyService from 'services/Tenancy/TenancyService'; +import { ERRORS } from './constants'; +import { mergeObjectsBykey } from 'utils'; +import JournalPoster from 'services/Accounting/JournalPoster'; +import JournalEntry from 'services/Accounting/JournalEntry'; +import TransactionLandedCost from './TransctionLandedCost'; + +const CONFIG = { + COST_TYPES: { + Expense: { + entries: 'categories', + }, + Bill: { + entries: 'entries', + }, + }, +}; + +@Service() +export default class AllocateLandedCostService { + @Inject() + public billsService: BillsService; + + @Inject() + public inventoryService: InventoryService; + + @Inject() + public tenancy: HasTenancyService; + + @Inject('logger') + public logger: any; + + @Inject() + public transactionLandedCost: TransactionLandedCost; + + /** + * Validates allocate cost items association with the purchase invoice entries. + * @param {IItemEntry[]} purchaseInvoiceEntries + * @param {ILandedCostItemDTO[]} landedCostItems + */ + private validateAllocateCostItems = ( + purchaseInvoiceEntries: IItemEntry[], + landedCostItems: ILandedCostItemDTO[] + ): void => { + // Purchase invoice entries items ids. + const purchaseInvoiceItems = purchaseInvoiceEntries.map((e) => e.id); + const landedCostItemsIds = landedCostItems.map((item) => item.entryId); + + // Not found items ids. + const notFoundItemsIds = difference( + purchaseInvoiceItems, + landedCostItemsIds + ); + // Throw items ids not found service error. + if (notFoundItemsIds.length > 0) { + throw new ServiceError(ERRORS.LANDED_COST_ITEMS_IDS_NOT_FOUND); + } + }; + + /** + * Saves the bill landed cost model. + * @param {number} tenantId + * @param {ILandedCostDTO} landedCostDTO + * @param {number} purchaseInvoiceId + * @returns {Promise} + */ + private saveBillLandedCostModel = ( + tenantId: number, + landedCostDTO: ILandedCostDTO, + purchaseInvoiceId: number + ): Promise => { + const { BillLandedCost } = this.tenancy.models(tenantId); + const amount = sumBy(landedCostDTO.items, 'cost'); + + // Inserts the bill landed cost to the storage. + return BillLandedCost.query().insertGraph({ + billId: purchaseInvoiceId, + fromTransactionType: landedCostDTO.transactionType, + fromTransactionId: landedCostDTO.transactionId, + fromTransactionEntryId: landedCostDTO.transactionEntryId, + amount, + allocationMethod: landedCostDTO.allocationMethod, + description: landedCostDTO.description, + allocateEntries: landedCostDTO.items, + }); + }; + + /** + * Allocate the landed cost amount to cost transactions. + * @param {number} tenantId - + * @param {string} transactionType + * @param {number} transactionId + */ + private incrementLandedCostAmount = async ( + tenantId: number, + transactionType: string, + transactionId: number, + transactionEntryId: number, + amount: number + ): Promise => { + const Model = this.transactionLandedCost.getModel( + tenantId, + transactionType + ); + const relation = CONFIG.COST_TYPES[transactionType].entries; + + // Increment the landed cost transaction amount. + await Model.query() + .where('id', transactionId) + .increment('allocatedCostAmount', amount); + + // Increment the landed cost entry. + await Model.relatedQuery(relation) + .for(transactionId) + .where('id', transactionEntryId) + .increment('allocatedCostAmount', amount); + }; + + /** + * Reverts the landed cost amount to cost transaction. + * @param {number} tenantId - Tenant id. + * @param {string} transactionType - Transaction type. + * @param {number} transactionId - Transaction id. + * @param {number} amount - Amount + */ + private revertLandedCostAmount = ( + tenantId: number, + transactionType: string, + transactionId: number, + amount: number + ) => { + const Model = this.transactionLandedCost.getModel(tenantId, transactionType); + + // Decrement the allocate cost amount of cost transaction. + return Model.query() + .where('id', transactionId) + .decrement('allocatedCostAmount', amount); + }; + + /** + * Retrieve the cost transaction or throw not found error. + * @param {number} tenantId + * @param {transactionType} transactionType - + * @param {transactionId} transactionId - + */ + public getLandedCostOrThrowError = async ( + tenantId: number, + transactionType: string, + transactionId: number + ) => { + const Model = this.transactionLandedCost.getModel( + tenantId, + transactionType + ); + const model = await Model.query().findById(transactionId); + + if (!model) { + throw new ServiceError(ERRORS.LANDED_COST_TRANSACTION_NOT_FOUND); + } + return this.transactionLandedCost.transformToLandedCost( + transactionType, + model + ); + }; + + /** + * Retrieve the landed cost entries. + * @param {number} tenantId + * @param {string} transactionType + * @param {number} transactionId + * @returns + */ + public getLandedCostEntry = async ( + tenantId: number, + transactionType: string, + transactionId: number, + transactionEntryId: number + ): Promise => { + const Model = this.transactionLandedCost.getModel( + tenantId, + transactionType + ); + const relation = CONFIG.COST_TYPES[transactionType].entries; + + const entry = await Model.relatedQuery(relation) + .for(transactionId) + .findOne('id', transactionEntryId) + .where('landedCost', true); + + if (!entry) { + throw new ServiceError(ERRORS.LANDED_COST_ENTRY_NOT_FOUND); + } + return entry; + }; + + /** + * Retrieve allocate items cost total. + * @param {ILandedCostDTO} landedCostDTO + * @returns {number} + */ + private getAllocateItemsCostTotal = ( + landedCostDTO: ILandedCostDTO + ): number => { + return sumBy(landedCostDTO.items, 'cost'); + }; + + /** + * Validate allocate cost transaction should not be bill transaction. + * @param {number} purchaseInvoiceId + * @param {string} transactionType + * @param {number} transactionId + */ + private validateAllocateCostNotSameBill = ( + purchaseInvoiceId: number, + transactionType: string, + transactionId: number + ): void => { + if (transactionType === 'Bill' && transactionId === purchaseInvoiceId) { + throw new ServiceError(ERRORS.ALLOCATE_COST_SHOULD_NOT_BE_BILL); + } + }; + + /** + * Validates the landed cost entry amount. + * @param {number} unallocatedCost - + * @param {number} amount - + */ + private validateLandedCostEntryAmount = ( + unallocatedCost: number, + amount: number + ): void => { + console.log(unallocatedCost, amount, '123'); + + if (unallocatedCost < amount) { + throw new ServiceError(ERRORS.COST_AMOUNT_BIGGER_THAN_UNALLOCATED_AMOUNT); + } + }; + + /** + * Records inventory transactions. + * @param {number} tenantId + * @param {} allocateEntries + */ + private recordInventoryTransactions = async ( + tenantId: number, + allocateEntries, + purchaseInvoice: IBill, + landedCostId: number + ) => { + const costEntries = mergeObjectsBykey( + purchaseInvoice.entries, + allocateEntries.map((e) => ({ ...e, id: e.itemId })), + 'id' + ); + // Inventory transaction. + const inventoryTransactions = costEntries.map((entry) => ({ + date: purchaseInvoice.billDate, + itemId: entry.itemId, + direction: 'IN', + quantity: 0, + rate: entry.cost, + transactionType: 'LandedCost', + transactionId: landedCostId, + entryId: entry.id, + })); + + return this.inventoryService.recordInventoryTransactions( + tenantId, + inventoryTransactions + ); + }; + + /** + * ================================= + * Allocate landed cost. + * ================================= + * - Validates the allocate cost not the same purchase invoice id. + * - Get the given bill (purchase invoice) or throw not found error. + * - Get the given landed cost transaction or throw not found error. + * - Validate landed cost transaction has enough unallocated cost amount. + * - Validate landed cost transaction entry has enough unallocated cost amount. + * - Validate allocate entries existance and associated with cost bill transaction. + * - Writes inventory landed cost transaction. + * - Increment the allocated landed cost transaction. + * - Increment the allocated landed cost transaction entry. + * + * @param {ILandedCostDTO} landedCostDTO - Landed cost DTO. + * @param {number} tenantId - Tenant id. + * @param {number} purchaseInvoiceId - Purchase invoice id. + */ + public allocateLandedCost = async ( + tenantId: number, + allocateCostDTO: ILandedCostDTO, + purchaseInvoiceId: number + ): Promise<{ + billLandedCost: IBillLandedCost; + }> => { + // Retrieve total cost of allocated items. + const amount = this.getAllocateItemsCostTotal(allocateCostDTO); + + // Retrieve the purchase invoice or throw not found error. + const purchaseInvoice = await this.billsService.getBillOrThrowError( + tenantId, + purchaseInvoiceId + ); + // Retrieve landed cost transaction or throw not found service error. + const landedCostTransaction = await this.getLandedCostOrThrowError( + tenantId, + allocateCostDTO.transactionType, + allocateCostDTO.transactionId + ); + // Retrieve landed cost transaction entries. + const landedCostEntry = await this.getLandedCostEntry( + tenantId, + allocateCostDTO.transactionType, + allocateCostDTO.transactionId, + allocateCostDTO.transactionEntryId + ); + // Validates allocate cost items association with the purchase invoice entries. + this.validateAllocateCostItems( + purchaseInvoice.entries, + allocateCostDTO.items + ); + // Validate the amount of cost with unallocated landed cost. + this.validateLandedCostEntryAmount( + landedCostEntry.unallocatedLandedCost, + amount + ); + // Save the bill landed cost model. + const billLandedCost = await this.saveBillLandedCostModel( + tenantId, + allocateCostDTO, + purchaseInvoiceId + ); + // Records the inventory transactions. + // await this.recordInventoryTransactions( + // tenantId, + // allocateCostDTO.items, + // purchaseInvoice, + // landedCostTransaction.id + // ); + // Increment landed cost amount on transaction and entry. + await this.incrementLandedCostAmount( + tenantId, + allocateCostDTO.transactionType, + allocateCostDTO.transactionId, + allocateCostDTO.transactionEntryId, + amount + ); + // Write the landed cost journal entries. + // await this.writeJournalEntry(tenantId, purchaseInvoice, billLandedCost); + + return { billLandedCost }; + }; + + /** + * Write journal entries of the given purchase invoice landed cost. + * @param tenantId + * @param purchaseInvoice + * @param landedCost + */ + private writeJournalEntry = async ( + tenantId: number, + purchaseInvoice: IBill, + landedCost: IBillLandedCost + ) => { + const journal = new JournalPoster(tenantId); + const billEntriesById = purchaseInvoice.entries; + + const commonEntry = { + referenceType: 'Bill', + referenceId: purchaseInvoice.id, + date: purchaseInvoice.billDate, + indexGroup: 300, + }; + const costEntry = new JournalEntry({ + ...commonEntry, + credit: landedCost.amount, + account: landedCost.costAccountId, + index: 1, + }); + journal.credit(costEntry); + + landedCost.allocateEntries.forEach((entry, index) => { + const billEntry = billEntriesById[entry.entryId]; + + const inventoryEntry = new JournalEntry({ + ...commonEntry, + debit: entry.cost, + account: billEntry.item.inventoryAccountId, + index: 1 + index, + }); + journal.debit(inventoryEntry); + }); + return journal; + }; + + /** + * Retrieve the give bill landed cost or throw not found service error. + * @param {number} tenantId - Tenant id. + * @param {number} landedCostId - Landed cost id. + * @returns {Promise} + */ + public getBillLandedCostOrThrowError = async ( + tenantId: number, + landedCostId: number + ): Promise => { + const { BillLandedCost } = this.tenancy.models(tenantId); + + // Retrieve the bill landed cost model. + const billLandedCost = await BillLandedCost.query().findById(landedCostId); + + if (!billLandedCost) { + throw new ServiceError(ERRORS.BILL_LANDED_COST_NOT_FOUND); + } + return billLandedCost; + }; + + /** + * Deletes the landed cost transaction with assocaited allocate entries. + * @param {number} tenantId + * @param {number} landedCostId + */ + public deleteLandedCost = async ( + tenantId: number, + landedCostId: number + ): Promise => { + const { BillLandedCost, BillLandedCostEntry } = + this.tenancy.models(tenantId); + + // Deletes the bill landed cost allocated entries associated to landed cost. + await BillLandedCostEntry.query() + .where('bill_located_cost_id', landedCostId) + .delete(); + + // Delete the bill landed cost from the storage. + await BillLandedCost.query().where('id', landedCostId).delete(); + }; + + /** + * Deletes the allocated landed cost. + * ================================== + * - Delete bill landed cost transaction with associated allocate entries. + * - Delete the associated inventory transactions. + * - Decrement allocated amount of landed cost transaction and entry. + * - Revert journal entries. + * + * @param {number} tenantId - Tenant id. + * @param {number} landedCostId - Landed cost id. + * @return {Promise} + */ + public deleteAllocatedLandedCost = async ( + tenantId: number, + landedCostId: number + ): Promise<{ + landedCostId: number; + }> => { + // Retrieves the bill landed cost. + const oldBillLandedCost = await this.getBillLandedCostOrThrowError( + tenantId, + landedCostId + ); + // Delete landed cost transaction with assocaited locate entries. + await this.deleteLandedCost(tenantId, landedCostId); + + // Removes the inventory transactions. + await this.removeInventoryTransactions(tenantId, landedCostId); + + // Reverts the landed cost amount to the cost transaction. + await this.revertLandedCostAmount( + tenantId, + oldBillLandedCost.fromTransactionType, + oldBillLandedCost.fromTransactionId, + oldBillLandedCost.amount + ); + return { landedCostId }; + }; + + /** + * Deletes the inventory transaction. + * @param {number} tenantId + * @param {number} landedCostId + * @returns + */ + private removeInventoryTransactions = (tenantId, landedCostId: number) => { + return this.inventoryService.deleteInventoryTransactions( + tenantId, + landedCostId, + 'LandedCost' + ); + }; +} diff --git a/server/src/subscribers/events.ts b/server/src/subscribers/events.ts index 48f8836b5..fe6a74f0f 100644 --- a/server/src/subscribers/events.ts +++ b/server/src/subscribers/events.ts @@ -203,5 +203,15 @@ export default { onQuickCreated: 'onInventoryAdjustmentQuickCreated', onDeleted: 'onInventoryAdjustmentDeleted', onPublished: 'onInventoryAdjustmentPublished', + }, + + /** + * Bill landed cost. + */ + billLandedCost: { + onCreate: 'onBillLandedCostCreate', + onCreated: 'onBillLandedCostCreated', + onDelete: 'onBillLandedCostDelete', + onDeleted: 'onBillLandedCostDeleted' } } diff --git a/server/src/utils/index.ts b/server/src/utils/index.ts index a5f894765..a18bc93e2 100644 --- a/server/src/utils/index.ts +++ b/server/src/utils/index.ts @@ -373,6 +373,11 @@ const accumSum = (data, callback) => { }, 0) } +const mergeObjectsBykey = (object1, object2, key) => { + var merged = _.merge(_.keyBy(object1, key), _.keyBy(object2, key)); + return _.values(merged); +} + export { accumSum, increment, @@ -400,5 +405,6 @@ export { transactionIncrement, transformToMapBy, dateRangeFromToCollection, - transformToMapKeyValue + transformToMapKeyValue, + mergeObjectsBykey }; From 70aea9bf2dd0f0dfae8ec1f6bc057bef8515f9b3 Mon Sep 17 00:00:00 2001 From: "a.bouhuolia" Date: Fri, 23 Jul 2021 01:32:10 +0200 Subject: [PATCH 11/24] WIP: Allocate landed cost. --- .../api/controllers/Purchases/LandedCost.ts | 16 ++-- ...200722173423_create_items_entries_table.js | 24 ++++-- server/src/interfaces/LandedCost.ts | 10 ++- server/src/models/BillLandedCost.js | 11 ++- .../services/Purchases/LandedCost/index.ts | 84 ++++++++++--------- 5 files changed, 91 insertions(+), 54 deletions(-) diff --git a/server/src/api/controllers/Purchases/LandedCost.ts b/server/src/api/controllers/Purchases/LandedCost.ts index da32f2b8b..b1f403c3c 100644 --- a/server/src/api/controllers/Purchases/LandedCost.ts +++ b/server/src/api/controllers/Purchases/LandedCost.ts @@ -5,7 +5,6 @@ import { ServiceError } from 'exceptions'; import AllocateLandedCostService from 'services/Purchases/LandedCost'; import LandedCostListing from 'services/Purchases/LandedCost/LandedCostListing'; import BaseController from '../BaseController'; -import { ResultSetDependencies } from 'mathjs'; @Service() export default class BillAllocateLandedCost extends BaseController { @@ -221,8 +220,8 @@ export default class BillAllocateLandedCost extends BaseController { errors: [ { type: 'BILL_NOT_FOUND', - code: 400, message: 'The give bill id not found.', + code: 100, }, ], }); @@ -232,8 +231,8 @@ export default class BillAllocateLandedCost extends BaseController { errors: [ { type: 'LANDED_COST_TRANSACTION_NOT_FOUND', - code: 200, message: 'The given landed cost transaction id not found.', + code: 200, }, ], }); @@ -243,8 +242,8 @@ export default class BillAllocateLandedCost extends BaseController { errors: [ { type: 'LANDED_COST_ENTRY_NOT_FOUND', - code: 300, message: 'The given landed cost tranasction entry id not found.', + code: 300, }, ], }); @@ -252,7 +251,10 @@ export default class BillAllocateLandedCost extends BaseController { if (error.errorType === 'COST_AMOUNT_BIGGER_THAN_UNALLOCATED_AMOUNT') { return res.status(400).send({ errors: [ - { type: 'COST_AMOUNT_BIGGER_THAN_UNALLOCATED_AMOUNT', code: 300 }, + { + type: 'COST_AMOUNT_BIGGER_THAN_UNALLOCATED_AMOUNT', + code: 400, + }, ], }); } @@ -261,8 +263,8 @@ export default class BillAllocateLandedCost extends BaseController { errors: [ { type: 'LANDED_COST_ITEMS_IDS_NOT_FOUND', - code: 200, message: 'The given entries ids of purchase invoice not found.', + code: 500, }, ], }); @@ -272,8 +274,8 @@ export default class BillAllocateLandedCost extends BaseController { errors: [ { type: 'BILL_LANDED_COST_NOT_FOUND', - code: 200, message: 'The given bill located landed cost not found.', + code: 600, }, ], }); diff --git a/server/src/database/migrations/20200722173423_create_items_entries_table.js b/server/src/database/migrations/20200722173423_create_items_entries_table.js index a4c809cec..616234ac9 100644 --- a/server/src/database/migrations/20200722173423_create_items_entries_table.js +++ b/server/src/database/migrations/20200722173423_create_items_entries_table.js @@ -1,19 +1,31 @@ - -exports.up = function(knex) { +exports.up = function (knex) { return knex.schema.createTable('items_entries', (table) => { table.increments(); table.string('reference_type').index(); table.string('reference_id').index(); table.integer('index').unsigned(); - table.integer('item_id').unsigned().index().references('id').inTable('items'); + table + .integer('item_id') + .unsigned() + .index() + .references('id') + .inTable('items'); table.text('description'); table.integer('discount').unsigned(); table.integer('quantity').unsigned(); table.integer('rate').unsigned(); - table.integer('sell_account_id').unsigned().references('id').inTable('accounts'); - table.integer('cost_account_id').unsigned().references('id').inTable('accounts'); + table + .integer('sell_account_id') + .unsigned() + .references('id') + .inTable('accounts'); + table + .integer('cost_account_id') + .unsigned() + .references('id') + .inTable('accounts'); table.boolean('landed_cost').defaultTo(false); table.decimal('allocated_cost_amount', 13, 3); @@ -21,6 +33,6 @@ exports.up = function(knex) { }); }; -exports.down = function(knex) { +exports.down = function (knex) { return knex.schema.dropTableIfExists('items_entries'); }; diff --git a/server/src/interfaces/LandedCost.ts b/server/src/interfaces/LandedCost.ts index a719b31c2..141e0f721 100644 --- a/server/src/interfaces/LandedCost.ts +++ b/server/src/interfaces/LandedCost.ts @@ -82,4 +82,12 @@ export interface IBillLandedCostTransaction { allocationMethod: string; costAccountId: number, description: string; -}; \ No newline at end of file + + allocatedEntries?: IBillLandedCostTransactionEntry[], +}; + +export interface IBillLandedCostTransactionEntry { + cost: number; + entryId: number; + billLocatedCostId: number, +} \ No newline at end of file diff --git a/server/src/models/BillLandedCost.js b/server/src/models/BillLandedCost.js index c7a0624fc..f517dbbaf 100644 --- a/server/src/models/BillLandedCost.js +++ b/server/src/models/BillLandedCost.js @@ -19,10 +19,19 @@ export default class BillLandedCost extends TenantModel { /** * Relationship mapping. */ - static get relationMappings() { + static get relationMappings() { const BillLandedCostEntry = require('models/BillLandedCostEntry'); + const Bill = require('models/Bill'); return { + bill: { + relation: Model.BelongsToOneRelation, + modelClass: Bill.default, + join: { + from: 'bill_located_costs.billId', + to: 'bills.id', + }, + }, allocateEntries: { relation: Model.HasManyRelation, modelClass: BillLandedCostEntry.default, diff --git a/server/src/services/Purchases/LandedCost/index.ts b/server/src/services/Purchases/LandedCost/index.ts index 604ddc3e9..9b4c1ffb7 100644 --- a/server/src/services/Purchases/LandedCost/index.ts +++ b/server/src/services/Purchases/LandedCost/index.ts @@ -8,11 +8,13 @@ import { IBillLandedCost, ILandedCostItemDTO, ILandedCostDTO, + IBillLandedCostTransaction, + IBillLandedCostTransactionEntry, } from 'interfaces'; import InventoryService from 'services/Inventory/Inventory'; import HasTenancyService from 'services/Tenancy/TenancyService'; import { ERRORS } from './constants'; -import { mergeObjectsBykey } from 'utils'; +import { transformToMap } from 'utils'; import JournalPoster from 'services/Accounting/JournalPoster'; import JournalEntry from 'services/Accounting/JournalEntry'; import TransactionLandedCost from './TransctionLandedCost'; @@ -141,7 +143,10 @@ export default class AllocateLandedCostService { transactionId: number, amount: number ) => { - const Model = this.transactionLandedCost.getModel(tenantId, transactionType); + const Model = this.transactionLandedCost.getModel( + tenantId, + transactionType + ); // Decrement the allocate cost amount of cost transaction. return Model.query() @@ -216,22 +221,6 @@ export default class AllocateLandedCostService { return sumBy(landedCostDTO.items, 'cost'); }; - /** - * Validate allocate cost transaction should not be bill transaction. - * @param {number} purchaseInvoiceId - * @param {string} transactionType - * @param {number} transactionId - */ - private validateAllocateCostNotSameBill = ( - purchaseInvoiceId: number, - transactionType: string, - transactionId: number - ): void => { - if (transactionType === 'Bill' && transactionId === purchaseInvoiceId) { - throw new ServiceError(ERRORS.ALLOCATE_COST_SHOULD_NOT_BE_BILL); - } - }; - /** * Validates the landed cost entry amount. * @param {number} unallocatedCost - @@ -248,6 +237,24 @@ export default class AllocateLandedCostService { } }; + /** + * Merges item entry to bill located landed cost entry. + * @param {IBillLandedCostTransactionEntry[]} locatedEntries - + * @param {IItemEntry[]} billEntries - + * @returns {(IBillLandedCostTransactionEntry & { entry: IItemEntry })[]} + */ + private mergeLocatedWithBillEntries = ( + locatedEntries: IBillLandedCostTransactionEntry[], + billEntries: IItemEntry[] + ): (IBillLandedCostTransactionEntry & { entry: IItemEntry })[] => { + const billEntriesByEntryId = transformToMap(billEntries, 'id'); + + return locatedEntries.map((entry) => ({ + ...entry, + entry: billEntriesByEntryId.get(entry.entryId), + })); + }; + /** * Records inventory transactions. * @param {number} tenantId @@ -255,25 +262,24 @@ export default class AllocateLandedCostService { */ private recordInventoryTransactions = async ( tenantId: number, - allocateEntries, - purchaseInvoice: IBill, - landedCostId: number + billLandedCost: IBillLandedCostTransaction, + bill: IBill ) => { - const costEntries = mergeObjectsBykey( - purchaseInvoice.entries, - allocateEntries.map((e) => ({ ...e, id: e.itemId })), - 'id' + // Retrieve the merged allocated entries with bill entries. + const allocateEntries = this.mergeLocatedWithBillEntries( + billLandedCost.allocateEntries, + bill.entries ); - // Inventory transaction. - const inventoryTransactions = costEntries.map((entry) => ({ - date: purchaseInvoice.billDate, - itemId: entry.itemId, + // Mappes the allocate cost entries to inventory transactions. + const inventoryTransactions = allocateEntries.map((allocateEntry) => ({ + date: bill.billDate, + itemId: allocateEntry.entry.itemId, direction: 'IN', quantity: 0, - rate: entry.cost, + rate: allocateEntry.cost, transactionType: 'LandedCost', - transactionId: landedCostId, - entryId: entry.id, + transactionId: billLandedCost.id, + entryId: allocateEntry.entryId, })); return this.inventoryService.recordInventoryTransactions( @@ -345,12 +351,11 @@ export default class AllocateLandedCostService { purchaseInvoiceId ); // Records the inventory transactions. - // await this.recordInventoryTransactions( - // tenantId, - // allocateCostDTO.items, - // purchaseInvoice, - // landedCostTransaction.id - // ); + await this.recordInventoryTransactions( + tenantId, + billLandedCost, + purchaseInvoice + ); // Increment landed cost amount on transaction and entry. await this.incrementLandedCostAmount( tenantId, @@ -360,7 +365,7 @@ export default class AllocateLandedCostService { amount ); // Write the landed cost journal entries. - // await this.writeJournalEntry(tenantId, purchaseInvoice, billLandedCost); + // await this.writeJournalEntry(tenantId, billLandedCost, purchaseInvoice); return { billLandedCost }; }; @@ -373,6 +378,7 @@ export default class AllocateLandedCostService { */ private writeJournalEntry = async ( tenantId: number, + landedCostEntry: any, purchaseInvoice: IBill, landedCost: IBillLandedCost ) => { From d0c2be90bf9562bb6d445ffd37f789eb0466e11b Mon Sep 17 00:00:00 2001 From: elforjani3 Date: Fri, 23 Jul 2021 22:30:36 +0200 Subject: [PATCH 12/24] feat : bill transaction delete alert. --- client/src/common/allocateLandedCostType.js | 6 +- .../Bills/BillTransactionDeleteAlert.js | 67 +++++++++++++++ .../BillDrawer/LocatedLandedCostTable.js | 35 +++++--- .../Drawers/BillDrawer/components.js | 4 +- client/src/hooks/query/index.js | 1 + client/src/hooks/query/landedCost.js | 86 +++++++++++++++++++ client/src/hooks/query/types.js | 10 ++- 7 files changed, 193 insertions(+), 16 deletions(-) create mode 100644 client/src/containers/Alerts/Bills/BillTransactionDeleteAlert.js create mode 100644 client/src/hooks/query/landedCost.js diff --git a/client/src/common/allocateLandedCostType.js b/client/src/common/allocateLandedCostType.js index 4e915bfec..74bcffacf 100644 --- a/client/src/common/allocateLandedCostType.js +++ b/client/src/common/allocateLandedCostType.js @@ -1,6 +1,6 @@ import intl from 'react-intl-universal'; export default [ - { name: intl.get('bills'), value: 'bills' }, - { name: intl.get('expenses'), value: 'expenses' }, -] \ No newline at end of file + { name: intl.get('bills'), value: 'Bill' }, + { name: intl.get('expenses'), value: 'Expense' }, +]; diff --git a/client/src/containers/Alerts/Bills/BillTransactionDeleteAlert.js b/client/src/containers/Alerts/Bills/BillTransactionDeleteAlert.js new file mode 100644 index 000000000..20c93183c --- /dev/null +++ b/client/src/containers/Alerts/Bills/BillTransactionDeleteAlert.js @@ -0,0 +1,67 @@ +import React from 'react'; +import { Intent, Alert } from '@blueprintjs/core'; +import { FormattedMessage as T } from 'components'; +import intl from 'react-intl-universal'; +import { useDeleteLandedCost } from 'hooks/query'; + +import { AppToaster } from 'components'; + +import withAlertActions from 'containers/Alert/withAlertActions'; +import withAlertStoreConnect from 'containers/Alert/withAlertStoreConnect'; + +import { compose } from 'utils'; + +/** + * Bill transaction delete alert. + */ +function BillTransactionDeleteAlert({ + name, + // #withAlertStoreConnect + isOpen, + payload: { BillId }, + // #withAlertActions + closeAlert, +}) { + const { mutateAsync: deleteLandedCostMutate, isLoading } = + useDeleteLandedCost(); + + // Handle cancel delete. + const handleCancelAlert = () => { + closeAlert(name); + }; + + // Handle confirm delete . + const handleConfirmLandedCostDelete = () => { + deleteLandedCostMutate(BillId) + .then(() => { + AppToaster.show({ + message: intl.get('the_landed_cost_has_been_deleted_successfully'), + intent: Intent.SUCCESS, + }); + closeAlert(name); + }) + .catch(() => { + closeAlert(name); + }); + }; + + return ( + } + confirmButtonText={} + icon="trash" + intent={Intent.DANGER} + isOpen={isOpen} + onCancel={handleCancelAlert} + onConfirm={handleConfirmLandedCostDelete} + loading={isLoading} + > +

{/* */}

+
+ ); +} + +export default compose( + withAlertStoreConnect(), + withAlertActions, +)(BillTransactionDeleteAlert); diff --git a/client/src/containers/Drawers/BillDrawer/LocatedLandedCostTable.js b/client/src/containers/Drawers/BillDrawer/LocatedLandedCostTable.js index c50da77e6..1c7c98405 100644 --- a/client/src/containers/Drawers/BillDrawer/LocatedLandedCostTable.js +++ b/client/src/containers/Drawers/BillDrawer/LocatedLandedCostTable.js @@ -1,22 +1,37 @@ import React from 'react'; import { DataTable } from 'components'; import { useLocatedLandedCostColumns, ActionsMenu } from './components'; +import { useBillDrawerContext } from './BillDrawerProvider'; + +import withAlertsActions from 'containers/Alert/withAlertActions'; + +import { compose } from 'utils'; /** * Located landed cost table. */ -function LocatedLandedCostTable() { +function LocatedLandedCostTable({ + // #withAlertsActions + openAlert, +}) { const columns = useLocatedLandedCostColumns(); + const { transactions } = useBillDrawerContext(); - const DATA = [ - { - name: 'INV-1000', - amount: '10.000.000', - allocation_method: 'Bill', - }, - ]; + // Handle the transaction delete action. + const handleDeleteTransaction = ({ id }) => { + openAlert('transaction-delete', { BillId: id }); + }; - return ; + return ( + + ); } -export default LocatedLandedCostTable; +export default compose(withAlertsActions)(LocatedLandedCostTable); diff --git a/client/src/containers/Drawers/BillDrawer/components.js b/client/src/containers/Drawers/BillDrawer/components.js index 1e3816c3c..fb181da03 100644 --- a/client/src/containers/Drawers/BillDrawer/components.js +++ b/client/src/containers/Drawers/BillDrawer/components.js @@ -14,7 +14,7 @@ export function ActionsMenu({ row: { original }, payload: { onDelete } }) { icon={} text={intl.get('delete_transaction')} intent={Intent.DANGER} - // onClick={safeCallback(onDelete, original)} + onClick={safeCallback(onDelete, original)} /> ); @@ -24,7 +24,7 @@ export function useLocatedLandedCostColumns() { return React.useMemo(() => [ { Header: intl.get('name'), - accessor: 'name', + accessor: 'description', width: 150, }, { diff --git a/client/src/hooks/query/index.js b/client/src/hooks/query/index.js index 1ce3eef49..cb9f8301c 100644 --- a/client/src/hooks/query/index.js +++ b/client/src/hooks/query/index.js @@ -23,3 +23,4 @@ export * from './exchangeRates'; export * from './contacts'; export * from './subscriptions'; export * from './organization'; +export * from './landedCost'; diff --git a/client/src/hooks/query/landedCost.js b/client/src/hooks/query/landedCost.js new file mode 100644 index 000000000..860835f4c --- /dev/null +++ b/client/src/hooks/query/landedCost.js @@ -0,0 +1,86 @@ +import { useQueryClient, useMutation } from 'react-query'; +import useApiRequest from '../useRequest'; +import { useRequestQuery } from '../useQueryRequest'; + +import t from './types'; + +const commonInvalidateQueries = (queryClient) => { + // Invalidate bills. + queryClient.invalidateQueries(t.BILLS); + queryClient.invalidateQueries(t.BILL); + // Invalidate landed cost. + queryClient.invalidateQueries(t.LANDED_COST); + queryClient.invalidateQueries(t.LANDED_COST_TRANSACTION); +}; + +/** + * Creates a new landed cost. + */ +export function useCreateLandedCost(props) { + const queryClient = useQueryClient(); + const apiRequest = useApiRequest(); + + return useMutation( + (id) => apiRequest.post(`purchases/landed-cost/bills/${id}/allocate`), + { + onSuccess: (res, id) => { + // Common invalidate queries. + commonInvalidateQueries(queryClient); + }, + ...props, + }, + ); +} + +/** + * Deletes the given landed cost. + */ +export function useDeleteLandedCost(props) { + const queryClient = useQueryClient(); + const apiRequest = useApiRequest(); + return useMutation( + (landedCostId) => + apiRequest.delete(`purchases/landed-cost/${landedCostId}`), + { + onSuccess: (res, id) => { + // Common invalidate queries. + commonInvalidateQueries(queryClient); + }, + ...props, + }, + ); +} + +/** + * Retrieve the landed cost transactions. + */ +export function useLandedCostTransaction(query, props) { + return useRequestQuery( + [t.LANDED_COST, query], + { + method: 'get', + url: 'purchases/landed-cost/transactions', + params: { transaction_type: query }, + }, + { + select: (res) => res.data.transactions, + defaultData: [], + ...props, + }, + ); +} + +/** + * Retrieve the bill located landed cost transactions. + */ +export function useBillLocatedLandedCost(id, props) { + return useRequestQuery( + [t.LANDED_COST_TRANSACTION, id], + { method: 'get', url: `purchases/landed-cost/bills/${id}/transactions` }, + { + select: (res) => res.data.transactions, + defaultData: {}, + ...props, + }, + ); +} diff --git a/client/src/hooks/query/types.js b/client/src/hooks/query/types.js index c2bfb67d4..886c07dd3 100644 --- a/client/src/hooks/query/types.js +++ b/client/src/hooks/query/types.js @@ -22,7 +22,7 @@ const FINANCIAL_REPORTS = { PURCHASES_BY_ITEMS: 'PURCHASES_BY_ITEMS', INVENTORY_VALUATION: 'INVENTORY_VALUATION', CASH_FLOW_STATEMENT: 'CASH_FLOW_STATEMENT', - INVENTORY_ITEM_DETAILS:'INVENTORY_ITEM_DETAILS' + INVENTORY_ITEM_DETAILS: 'INVENTORY_ITEM_DETAILS', }; const BILLS = { @@ -117,6 +117,13 @@ const MANUAL_JOURNALS = { MANUAL_JOURNALS: 'MANUAL_JOURNALS', MANUAL_JOURNAL: 'MANUAL_JOURNAL', }; + +const LANDED_COSTS = { + LANDED_COST: 'LANDED_COST', + LANDED_COSTS: 'LANDED_COSTS', + LANDED_COST_TRANSACTION: 'LANDED_COST_TRANSACTION', +}; + export default { ...ACCOUNTS, ...BILLS, @@ -137,4 +144,5 @@ export default { ...SUBSCRIPTIONS, ...EXPENSES, ...MANUAL_JOURNALS, + ...LANDED_COSTS, }; From cf2ebe9597447dd72e84053a82b0c9bf1ec4e0e7 Mon Sep 17 00:00:00 2001 From: "a.bouhuolia" Date: Sat, 24 Jul 2021 03:10:32 +0200 Subject: [PATCH 13/24] WIP: Allocate landed cost. --- server/src/api/controllers/Expenses.ts | 77 +++++++- server/src/api/controllers/Items.ts | 2 +- server/src/api/controllers/Purchases/Bills.ts | 38 +++- .../api/controllers/Purchases/LandedCost.ts | 11 +- server/src/api/index.ts | 1 - ...200722173423_create_items_entries_table.js | 3 +- server/src/interfaces/Entry.ts | 18 ++ server/src/interfaces/Expenses.ts | 5 + server/src/interfaces/ItemEntry.ts | 33 ++-- server/src/interfaces/LandedCost.ts | 5 +- server/src/interfaces/index.ts | 1 + server/src/loaders/events.ts | 4 +- server/src/models/BillLandedCost.js | 20 ++ server/src/models/BillLandedCostEntry.js | 22 +++ server/src/models/Expense.js | 8 +- server/src/models/ExpenseCategory.js | 4 +- server/src/models/ItemEntry.js | 13 +- .../services/Accounting/JournalCommands.ts | 54 ++---- server/src/services/Entries/index.ts | 78 ++++++++ .../src/services/Expenses/ExpensesService.ts | 37 +++- server/src/services/Purchases/Bills.ts | 73 +++++++- .../Purchases/LandedCost/BillLandedCost.ts | 7 +- .../Purchases/LandedCost/ExpenseLandedCost.ts | 3 + .../LandedCost/TransctionLandedCost.ts | 29 ++- .../Purchases/LandedCost/constants.ts | 15 -- .../services/Purchases/LandedCost/index.ts | 175 +++++++----------- .../services/Purchases/LandedCost/utils.ts | 34 ++++ server/src/services/Purchases/constants.ts | 5 +- .../subscribers/Bills/WriteJournalEntries.ts | 8 +- server/src/subscribers/LandedCost/index.ts | 37 ++++ 30 files changed, 602 insertions(+), 218 deletions(-) create mode 100644 server/src/interfaces/Entry.ts create mode 100644 server/src/services/Entries/index.ts delete mode 100644 server/src/services/Purchases/LandedCost/constants.ts create mode 100644 server/src/services/Purchases/LandedCost/utils.ts create mode 100644 server/src/subscribers/LandedCost/index.ts diff --git a/server/src/api/controllers/Expenses.ts b/server/src/api/controllers/Expenses.ts index a207b6029..b6010d706 100644 --- a/server/src/api/controllers/Expenses.ts +++ b/server/src/api/controllers/Expenses.ts @@ -39,7 +39,7 @@ export default class ExpensesController extends BaseController { ); router.post( '/:id', - [...this.expenseDTOSchema, ...this.expenseParamSchema], + [...this.editExpenseDTOSchema, ...this.expenseParamSchema], this.validationResult, asyncMiddleware(this.editExpense.bind(this)), this.catchServiceErrors @@ -116,12 +116,62 @@ export default class ExpensesController extends BaseController { } /** - * Expense param schema. + * Edit expense validation schema. + */ + get editExpenseDTOSchema() { + return [ + check('reference_no') + .optional({ nullable: true }) + .trim() + .escape() + .isLength({ max: DATATYPES_LENGTH.STRING }), + check('payment_date').exists().isISO8601(), + check('payment_account_id') + .exists() + .isInt({ max: DATATYPES_LENGTH.INT_10 }) + .toInt(), + check('description') + .optional({ nullable: true }) + .isString() + .isLength({ max: DATATYPES_LENGTH.TEXT }), + check('currency_code').optional().isString().isLength({ max: 3 }), + check('exchange_rate').optional({ nullable: true }).isNumeric().toFloat(), + check('publish').optional().isBoolean().toBoolean(), + check('payee_id').optional({ nullable: true }).isNumeric().toInt(), + + check('categories').exists().isArray({ min: 1 }), + check('categories.*.id').optional().isNumeric().toInt(), + check('categories.*.index') + .exists() + .isInt({ max: DATATYPES_LENGTH.INT_10 }) + .toInt(), + check('categories.*.expense_account_id') + .exists() + .isInt({ max: DATATYPES_LENGTH.INT_10 }) + .toInt(), + check('categories.*.amount') + .optional({ nullable: true }) + .isFloat({ max: DATATYPES_LENGTH.DECIMAL_13_3 }) // 13, 3 + .toFloat(), + check('categories.*.description') + .optional() + .trim() + .escape() + .isLength({ max: DATATYPES_LENGTH.STRING }), + check('categories.*.landed_cost').optional().isBoolean().toBoolean(), + ]; + } + + /** + * Expense param validation schema. */ get expenseParamSchema() { return [param('id').exists().isNumeric().toInt()]; } - + + /** + * Expenses list validation schema. + */ get expensesListSchema() { return [ query('custom_view_id').optional().isNumeric().toInt(), @@ -291,7 +341,7 @@ export default class ExpensesController extends BaseController { * @param {Response} res * @param {ServiceError} error */ - catchServiceErrors( + private catchServiceErrors( error: Error, req: Request, res: Response, @@ -348,6 +398,25 @@ export default class ExpensesController extends BaseController { errors: [{ type: 'EXPENSE_HAS_ASSOCIATED_LANDED_COST', code: 900 }], }); } + if (error.errorType === 'ENTRIES_ALLOCATED_COST_COULD_NOT_DELETED') { + return res.status(400).send({ + errors: [ + { type: 'ENTRIES_ALLOCATED_COST_COULD_NOT_DELETED', code: 1000 }, + ], + }); + } + if ( + error.errorType === 'LOCATED_COST_ENTRIES_SHOULD_BIGGE_THAN_NEW_ENTRIES' + ) { + return res.status(400).send({ + errors: [ + { + type: 'LOCATED_COST_ENTRIES_SHOULD_BIGGE_THAN_NEW_ENTRIES', + code: 1100, + }, + ], + }); + } } next(error); } diff --git a/server/src/api/controllers/Items.ts b/server/src/api/controllers/Items.ts index 44f782ce7..ff30657e3 100644 --- a/server/src/api/controllers/Items.ts +++ b/server/src/api/controllers/Items.ts @@ -406,7 +406,7 @@ export default class ItemsController extends BaseController { * @param {Response} res * @param {NextFunction} next */ - handlerServiceErrors( + private handlerServiceErrors( error: Error, req: Request, res: Response, diff --git a/server/src/api/controllers/Purchases/Bills.ts b/server/src/api/controllers/Purchases/Bills.ts index 7100bd3ad..e190d7d84 100644 --- a/server/src/api/controllers/Purchases/Bills.ts +++ b/server/src/api/controllers/Purchases/Bills.ts @@ -145,7 +145,7 @@ export default class BillsController extends BaseController { .optional({ nullable: true }) .trim() .escape(), - check('entries.*.landedCost') + check('entries.*.landed_cost') .optional({ nullable: true }) .isBoolean() .toBoolean(), @@ -347,7 +347,7 @@ export default class BillsController extends BaseController { * @param {Response} res * @param {NextFunction} next */ - handleServiceError( + private handleServiceError( error: Error, req: Request, res: Response, @@ -422,6 +422,40 @@ export default class BillsController extends BaseController { ], }); } + if (error.errorType === 'BILL_HAS_ASSOCIATED_LANDED_COSTS') { + return res.status(400).send({ + errors: [ + { + type: 'BILL_HAS_ASSOCIATED_LANDED_COSTS', + message: + 'Cannot delete bill that has associated landed cost transactions.', + code: 1300, + }, + ], + }); + } + if (error.errorType === 'ENTRIES_ALLOCATED_COST_COULD_NOT_DELETED') { + return res.status(400).send({ + errors: [ + { + type: 'ENTRIES_ALLOCATED_COST_COULD_NOT_DELETED', + code: 1400, + message: + 'Bill entries that have landed cost type can not be deleted.', + }, + ], + }); + } + if (error.errorType === 'LOCATED_COST_ENTRIES_SHOULD_BIGGE_THAN_NEW_ENTRIES') { + return res.status(400).send({ + errors: [ + { + type: 'LOCATED_COST_ENTRIES_SHOULD_BIGGE_THAN_NEW_ENTRIES', + code: 1500, + }, + ], + }); + } } next(error); } diff --git a/server/src/api/controllers/Purchases/LandedCost.ts b/server/src/api/controllers/Purchases/LandedCost.ts index b1f403c3c..5471be78c 100644 --- a/server/src/api/controllers/Purchases/LandedCost.ts +++ b/server/src/api/controllers/Purchases/LandedCost.ts @@ -64,9 +64,9 @@ export default class BillAllocateLandedCost extends BaseController { /** * Retrieve the landed cost transactions of the given query. - * @param {Request} req - * @param {Response} res - * @param {NextFunction} next + * @param {Request} req - Request + * @param {Response} res - Response. + * @param {NextFunction} next - Next function. */ private async getLandedCostTransactions( req: Request, @@ -192,10 +192,7 @@ export default class BillAllocateLandedCost extends BaseController { billId ); - return res.status(200).send({ - billId, - transactions, - }); + return res.status(200).send({ billId, transactions }); } catch (error) { next(error); } diff --git a/server/src/api/index.ts b/server/src/api/index.ts index 649e67f0a..1e95d7a05 100644 --- a/server/src/api/index.ts +++ b/server/src/api/index.ts @@ -40,7 +40,6 @@ import Ping from 'api/controllers/Ping'; import Subscription from 'api/controllers/Subscription'; import Licenses from 'api/controllers/Subscription/Licenses'; import InventoryAdjustments from 'api/controllers/Inventory/InventoryAdjustments'; - import Setup from 'api/controllers/Setup'; export default () => { diff --git a/server/src/database/migrations/20200722173423_create_items_entries_table.js b/server/src/database/migrations/20200722173423_create_items_entries_table.js index 616234ac9..b480540de 100644 --- a/server/src/database/migrations/20200722173423_create_items_entries_table.js +++ b/server/src/database/migrations/20200722173423_create_items_entries_table.js @@ -28,7 +28,8 @@ exports.up = function (knex) { .inTable('accounts'); table.boolean('landed_cost').defaultTo(false); - table.decimal('allocated_cost_amount', 13, 3); + table.decimal('allocated_cost_amount', 13, 3).defaultTo(0); + table.timestamps(); }); }; diff --git a/server/src/interfaces/Entry.ts b/server/src/interfaces/Entry.ts new file mode 100644 index 000000000..b55bb0aa1 --- /dev/null +++ b/server/src/interfaces/Entry.ts @@ -0,0 +1,18 @@ +export interface ICommonEntry { + id: number; + amount: number; +} + +export interface ICommonLandedCostEntry extends ICommonEntry { + landedCost: boolean; + allocatedCostAmount: number; +} + +export interface ICommonEntryDTO { + id?: number; + amount: number; +} + +export interface ICommonLandedCostEntryDTO extends ICommonEntryDTO { + landedCost?: boolean; +} diff --git a/server/src/interfaces/Expenses.ts b/server/src/interfaces/Expenses.ts index a1bea49ac..78dba8946 100644 --- a/server/src/interfaces/Expenses.ts +++ b/server/src/interfaces/Expenses.ts @@ -40,6 +40,9 @@ export interface IExpenseCategory { description: string; expenseId: number; amount: number; + + allocatedCostAmount: number; + unallocatedCostAmount: number; landedCost: boolean; } @@ -57,8 +60,10 @@ export interface IExpenseDTO { } export interface IExpenseCategoryDTO { + id?: number; expenseAccountId: number; index: number; + amount: number; description?: string; expenseId: number; landedCost?: boolean; diff --git a/server/src/interfaces/ItemEntry.ts b/server/src/interfaces/ItemEntry.ts index f9df14934..3cb07e4a7 100644 --- a/server/src/interfaces/ItemEntry.ts +++ b/server/src/interfaces/ItemEntry.ts @@ -1,26 +1,29 @@ - export type IItemEntryTransactionType = 'SaleInvoice' | 'Bill' | 'SaleReceipt'; export interface IItemEntry { - id?: number, + id?: number; - referenceType: string, - referenceId: number, + referenceType: string; + referenceId: number; - index: number, + index: number; - itemId: number, - description: string, - discount: number, - quantity: number, - rate: number, + itemId: number; + description: string; + discount: number; + quantity: number; + rate: number; + amount: number; - sellAccountId: number, - costAccountId: number, + landedCost: number; + allocatedCostAmount: number; + unallocatedCostAmount: number; - landedCost?: boolean, + sellAccountId: number; + costAccountId: number; } export interface IItemEntryDTO { - landedCost?: boolean -} \ No newline at end of file + id?: number, + landedCost?: boolean; +} diff --git a/server/src/interfaces/LandedCost.ts b/server/src/interfaces/LandedCost.ts index 141e0f721..8e9ce2e77 100644 --- a/server/src/interfaces/LandedCost.ts +++ b/server/src/interfaces/LandedCost.ts @@ -64,7 +64,10 @@ export interface ILandedCostTransactionEntry { name: string; code: string; amount: number; + unallocatedCostAmount: number; + allocatedCostAmount: number; description: string; + costAccountId: number; } interface ILandedCostEntry { @@ -83,7 +86,7 @@ export interface IBillLandedCostTransaction { costAccountId: number, description: string; - allocatedEntries?: IBillLandedCostTransactionEntry[], + allocateEntries?: IBillLandedCostTransactionEntry[], }; export interface IBillLandedCostTransactionEntry { diff --git a/server/src/interfaces/index.ts b/server/src/interfaces/index.ts index 0ce5f414c..4e8dc9e78 100644 --- a/server/src/interfaces/index.ts +++ b/server/src/interfaces/index.ts @@ -54,6 +54,7 @@ export * from './Ledger'; export * from './CashFlow'; export * from './InventoryDetails'; export * from './LandedCost'; +export * from './Entry'; export interface I18nService { __: (input: string) => string; diff --git a/server/src/loaders/events.ts b/server/src/loaders/events.ts index e0478145f..92a8427a4 100644 --- a/server/src/loaders/events.ts +++ b/server/src/loaders/events.ts @@ -26,4 +26,6 @@ import 'subscribers/vendors'; import 'subscribers/paymentMades'; import 'subscribers/paymentReceives'; import 'subscribers/saleEstimates'; -import 'subscribers/items'; \ No newline at end of file +import 'subscribers/items'; + +import 'subscribers/LandedCost'; \ No newline at end of file diff --git a/server/src/models/BillLandedCost.js b/server/src/models/BillLandedCost.js index f517dbbaf..ffa69eaeb 100644 --- a/server/src/models/BillLandedCost.js +++ b/server/src/models/BillLandedCost.js @@ -1,4 +1,5 @@ import { Model } from 'objection'; +import { lowerCase } from 'lodash'; import TenantModel from 'models/TenantModel'; export default class BillLandedCost extends TenantModel { @@ -16,6 +17,25 @@ export default class BillLandedCost extends TenantModel { return ['createdAt', 'updatedAt']; } + /** + * Virtual attributes. + */ + static get virtualAttributes() { + return ['allocationMethodFormatted']; + } + + /** + * Allocation method formatted. + */ + get allocationMethodFormatted() { + const allocationMethod = lowerCase(this.allocationMethod); + const keyLabelsPairs = { + value: 'Value', + quantity: 'Quantity', + }; + return keyLabelsPairs[allocationMethod] || ''; + } + /** * Relationship mapping. */ diff --git a/server/src/models/BillLandedCostEntry.js b/server/src/models/BillLandedCostEntry.js index aca0a87b7..d4f3fc833 100644 --- a/server/src/models/BillLandedCostEntry.js +++ b/server/src/models/BillLandedCostEntry.js @@ -1,3 +1,4 @@ +import { Model } from 'objection'; import TenantModel from 'models/TenantModel'; export default class BillLandedCostEntry extends TenantModel { @@ -7,4 +8,25 @@ export default class BillLandedCostEntry extends TenantModel { static get tableName() { return 'bill_located_cost_entries'; } + + /** + * Relationship mapping. + */ + static get relationMappings() { + const ItemEntry = require('models/ItemEntry'); + + return { + itemEntry: { + relation: Model.BelongsToOneRelation, + modelClass: ItemEntry.default, + join: { + from: 'bill_located_cost_entries.entryId', + to: 'items_entries.referenceId', + }, + filter(builder) { + builder.where('reference_type', 'Bill'); + }, + }, + }; + } } diff --git a/server/src/models/Expense.js b/server/src/models/Expense.js index e8bc02545..efadc2bbb 100644 --- a/server/src/models/Expense.js +++ b/server/src/models/Expense.js @@ -39,14 +39,18 @@ export default class Expense extends TenantModel { } static get virtualAttributes() { - return ['isPublished', 'unallocatedLandedCost']; + return ['isPublished', 'unallocatedCostAmount']; } isPublished() { return Boolean(this.publishedAt); } - unallocatedLandedCost() { + /** + * Retrieve the unallocated cost amount. + * @return {number} + */ + get unallocatedCostAmount() { return Math.max(this.amount - this.allocatedCostAmount, 0); } diff --git a/server/src/models/ExpenseCategory.js b/server/src/models/ExpenseCategory.js index a761acb23..50416805e 100644 --- a/server/src/models/ExpenseCategory.js +++ b/server/src/models/ExpenseCategory.js @@ -13,14 +13,14 @@ export default class ExpenseCategory extends TenantModel { * Virtual attributes. */ static get virtualAttributes() { - return ['unallocatedLandedCost']; + return ['unallocatedCostAmount']; } /** * Remain unallocated landed cost. * @return {number} */ - get unallocatedLandedCost() { + get unallocatedCostAmount() { return Math.max(this.amount - this.allocatedCostAmount, 0); } diff --git a/server/src/models/ItemEntry.js b/server/src/models/ItemEntry.js index 3434b01af..a8156fc01 100644 --- a/server/src/models/ItemEntry.js +++ b/server/src/models/ItemEntry.js @@ -21,8 +21,8 @@ export default class ItemEntry extends TenantModel { return ['amount']; } - static amount() { - return this.calcAmount(this); + get amount() { + return ItemEntry.calcAmount(this); } static calcAmount(itemEntry) { @@ -34,6 +34,7 @@ export default class ItemEntry extends TenantModel { static get relationMappings() { const Item = require('models/Item'); + const BillLandedCostEntry = require('models/BillLandedCostEntry'); return { item: { @@ -44,6 +45,14 @@ export default class ItemEntry extends TenantModel { to: 'items.id', }, }, + allocatedCostEntries: { + relation: Model.HasManyRelation, + modelClass: BillLandedCostEntry.default, + join: { + from: 'items_entries.referenceId', + to: 'bill_located_cost_entries.entryId', + }, + }, }; } } diff --git a/server/src/services/Accounting/JournalCommands.ts b/server/src/services/Accounting/JournalCommands.ts index 15f731f3c..4a5df9bf0 100644 --- a/server/src/services/Accounting/JournalCommands.ts +++ b/server/src/services/Accounting/JournalCommands.ts @@ -1,9 +1,11 @@ import moment from 'moment'; +import { sumBy } from 'lodash'; import { IBill, IManualJournalEntry, ISaleReceipt, ISystemUser, + IAccount, } from 'interfaces'; import JournalPoster from './JournalPoster'; import JournalEntry from './JournalEntry'; @@ -17,7 +19,6 @@ import { IItemEntry, } from 'interfaces'; import { increment } from 'utils'; - export default class JournalCommands { journal: JournalPoster; models: any; @@ -37,45 +38,20 @@ export default class JournalCommands { /** * Records the bill journal entries. * @param {IBill} bill - * @param {boolean} override - Override the old bill entries. + * @param {IAccount} payableAccount - */ - async bill(bill: IBill, override: boolean = false): Promise { - const { transactionsRepository, accountRepository } = this.repositories; - const { Item, ItemEntry } = this.models; - - const entriesItemsIds = bill.entries.map((entry) => entry.itemId); - - // Retrieve the bill transaction items. - const storedItems = await Item.query().whereIn('id', entriesItemsIds); - - const storedItemsMap = new Map(storedItems.map((item) => [item.id, item])); - const payableAccount = await accountRepository.findOne({ - slug: 'accounts-payable', - }); - const formattedDate = moment(bill.billDate).format('YYYY-MM-DD'); - + bill(bill: IBill, payableAccount: IAccount): void { const commonJournalMeta = { debit: 0, credit: 0, referenceId: bill.id, referenceType: 'Bill', - date: formattedDate, + date: moment(bill.billDate).format('YYYY-MM-DD'), userId: bill.userId, - referenceNumber: bill.referenceNo, transactionNumber: bill.billNumber, - createdAt: bill.createdAt, }; - // Overrides the old bill entries. - if (override) { - const entries = await transactionsRepository.journal({ - referenceType: ['Bill'], - referenceId: [bill.id], - }); - this.journal.fromTransactions(entries); - this.journal.removeEntries(); - } const payableEntry = new JournalEntry({ ...commonJournalMeta, credit: bill.amount, @@ -86,15 +62,15 @@ export default class JournalCommands { this.journal.credit(payableEntry); bill.entries.forEach((entry, index) => { - const item: IItem = storedItemsMap.get(entry.itemId); - const amount = ItemEntry.calcAmount(entry); + const landedCostAmount = sumBy(entry.allocatedCostEntries, 'cost'); + // Inventory or cost entry. const debitEntry = new JournalEntry({ ...commonJournalMeta, - debit: amount, + debit: entry.amount + landedCostAmount, account: - ['inventory'].indexOf(item.type) !== -1 - ? item.inventoryAccountId + ['inventory'].indexOf(entry.item.type) !== -1 + ? entry.item.inventoryAccountId : entry.costAccountId, index: index + 2, itemId: entry.itemId, @@ -102,6 +78,16 @@ export default class JournalCommands { }); this.journal.debit(debitEntry); }); + + // Allocate cost entries journal entries. + bill.locatedLandedCosts.forEach((landedCost) => { + const creditEntry = new JournalEntry({ + ...commonJournalMeta, + credit: landedCost.amount, + account: landedCost.costAccountId, + }); + this.journal.credit(creditEntry); + }); } /** diff --git a/server/src/services/Entries/index.ts b/server/src/services/Entries/index.ts new file mode 100644 index 000000000..b4277dabb --- /dev/null +++ b/server/src/services/Entries/index.ts @@ -0,0 +1,78 @@ +import { Service } from 'typedi'; +import { ServiceError } from 'exceptions'; +import { transformToMap } from 'utils'; +import { + ICommonLandedCostEntry, + ICommonLandedCostEntryDTO +} from 'interfaces'; + +const ERRORS = { + ENTRIES_ALLOCATED_COST_COULD_NOT_DELETED: + 'ENTRIES_ALLOCATED_COST_COULD_NOT_DELETED', + LOCATED_COST_ENTRIES_SHOULD_BIGGE_THAN_NEW_ENTRIES: + 'LOCATED_COST_ENTRIES_SHOULD_BIGGE_THAN_NEW_ENTRIES', +}; + +@Service() +export default class EntriesService { + /** + * Validates bill entries that has allocated landed cost amount not deleted. + * @param {IItemEntry[]} oldBillEntries - + * @param {IItemEntry[]} newBillEntries - + */ + public getLandedCostEntriesDeleted( + oldBillEntries: ICommonLandedCostEntry[], + newBillEntriesDTO: ICommonLandedCostEntryDTO[] + ): ICommonLandedCostEntry[] { + const newBillEntriesById = transformToMap(newBillEntriesDTO, 'id'); + + return oldBillEntries.filter((entry) => { + const newEntry = newBillEntriesById.get(entry.id); + + if (entry.allocatedCostAmount > 0 && typeof newEntry === 'undefined') { + return true; + } + return false; + }); + } + + /** + * Validates the bill entries that have located cost amount should not be deleted. + * @param {IItemEntry[]} oldBillEntries - Old bill entries. + * @param {IItemEntryDTO[]} newBillEntries - New DTO bill entries. + */ + public validateLandedCostEntriesNotDeleted( + oldBillEntries: ICommonLandedCostEntry[], + newBillEntriesDTO: ICommonLandedCostEntryDTO[] + ): void { + const entriesDeleted = this.getLandedCostEntriesDeleted( + oldBillEntries, + newBillEntriesDTO + ); + if (entriesDeleted.length > 0) { + throw new ServiceError(ERRORS.ENTRIES_ALLOCATED_COST_COULD_NOT_DELETED); + } + } + + /** + * Validate allocated cost amount entries should be smaller than new entries amount. + * @param {IItemEntry[]} oldBillEntries - Old bill entries. + * @param {IItemEntryDTO[]} newBillEntries - New DTO bill entries. + */ + public validateLocatedCostEntriesSmallerThanNewEntries( + oldBillEntries: ICommonLandedCostEntry[], + newBillEntriesDTO: ICommonLandedCostEntryDTO[] + ): void { + const oldBillEntriesById = transformToMap(oldBillEntries, 'id'); + + newBillEntriesDTO.forEach((entry) => { + const oldEntry = oldBillEntriesById.get(entry.id); + + if (oldEntry && oldEntry.allocatedCostAmount > entry.amount) { + throw new ServiceError( + ERRORS.LOCATED_COST_ENTRIES_SHOULD_BIGGE_THAN_NEW_ENTRIES + ); + } + }); + } +} diff --git a/server/src/services/Expenses/ExpensesService.ts b/server/src/services/Expenses/ExpensesService.ts index 1ac1623b0..39a8e8fdd 100644 --- a/server/src/services/Expenses/ExpensesService.ts +++ b/server/src/services/Expenses/ExpensesService.ts @@ -23,6 +23,7 @@ import DynamicListingService from 'services/DynamicListing/DynamicListService'; import events from 'subscribers/events'; import ContactsService from 'services/Contacts/ContactsService'; import { ACCOUNT_PARENT_TYPE, ACCOUNT_ROOT_TYPE } from 'data/AccountTypes'; +import EntriesService from 'services/Entries'; const ERRORS = { EXPENSE_NOT_FOUND: 'expense_not_found', @@ -53,6 +54,9 @@ export default class ExpensesService implements IExpensesService { @Inject() contactsService: ContactsService; + @Inject() + entriesService: EntriesService; + /** * Retrieve the payment account details or returns not found server error in case the * given account not found on the storage. @@ -251,14 +255,16 @@ export default class ExpensesService implements IExpensesService { * @returns {IExpense|ServiceError} */ private async getExpenseOrThrowError(tenantId: number, expenseId: number) { - const { expenseRepository } = this.tenancy.repositories(tenantId); + const { Expense } = this.tenancy.models(tenantId); this.logger.info('[expense] trying to get the given expense.', { tenantId, expenseId, }); // Retrieve the given expense by id. - const expense = await expenseRepository.findOneById(expenseId); + const expense = await Expense.query() + .findById(expenseId) + .withGraphFetched('categories'); if (!expense) { this.logger.info('[expense] the given expense not found.', { @@ -459,36 +465,47 @@ export default class ExpensesService implements IExpensesService { const { expenseRepository } = this.tenancy.repositories(tenantId); const oldExpense = await this.getExpenseOrThrowError(tenantId, expenseId); - // - Validate payment account existance on the storage. + // Validate payment account existance on the storage. const paymentAccount = await this.getPaymentAccountOrThrowError( tenantId, expenseDTO.paymentAccountId ); - // - Validate expense accounts exist on the storage. + // Validate expense accounts exist on the storage. const expensesAccounts = await this.getExpensesAccountsOrThrowError( tenantId, this.mapExpensesAccountsIdsFromDTO(expenseDTO) ); - // - Validate payment account type. + // Validate payment account type. await this.validatePaymentAccountType(tenantId, paymentAccount); - // - Validate expenses accounts type. + // Validate expenses accounts type. await this.validateExpensesAccountsType(tenantId, expensesAccounts); - // - Validate the expense payee contact id existance on storage. + // Validate the expense payee contact id existance on storage. if (expenseDTO.payeeId) { await this.contactsService.getContactByIdOrThrowError( tenantId, expenseDTO.payeeId ); } - // - Validate the given expense categories not equal zero. + // Validate the given expense categories not equal zero. this.validateCategoriesNotEqualZero(expenseDTO); - // - Update the expense on the storage. + // Update the expense on the storage. const expenseObj = this.expenseDTOToModel(expenseDTO); - // - Upsert the expense object with expense entries. + // Validate expense entries that have allocated landed cost cannot be deleted. + this.entriesService.validateLandedCostEntriesNotDeleted( + oldExpense.categories, + expenseDTO.categories, + ); + // Validate expense entries that have allocated cost amount should be bigger than amount. + this.entriesService.validateLocatedCostEntriesSmallerThanNewEntries( + oldExpense.categories, + expenseDTO.categories, + ); + + // Upsert the expense object with expense entries. const expense = await expenseRepository.upsertGraph({ id: expenseId, ...expenseObj, diff --git a/server/src/services/Purchases/Bills.ts b/server/src/services/Purchases/Bills.ts index 12a63ad67..7cd4dfe34 100644 --- a/server/src/services/Purchases/Bills.ts +++ b/server/src/services/Purchases/Bills.ts @@ -1,4 +1,4 @@ -import { omit, sumBy } from 'lodash'; +import { omit, runInContext, sumBy } from 'lodash'; import moment from 'moment'; import { Inject, Service } from 'typedi'; import composeAsync from 'async/compose'; @@ -24,6 +24,7 @@ import { IBillsFilter, IBillsService, IItemEntry, + IItemEntryDTO, } from 'interfaces'; import { ServiceError } from 'exceptions'; import ItemsService from 'services/Items/ItemsService'; @@ -32,6 +33,7 @@ import JournalCommands from 'services/Accounting/JournalCommands'; import JournalPosterService from 'services/Sales/JournalPosterService'; import VendorsService from 'services/Contacts/VendorsService'; import { ERRORS } from './constants'; +import EntriesService from 'services/Entries'; /** * Vendor bills services. @@ -72,6 +74,9 @@ export default class BillsService @Inject() vendorsService: VendorsService; + @Inject() + entriesService: EntriesService; + /** * Validates whether the vendor is exist. * @async @@ -166,16 +171,33 @@ export default class BillsService * Validate the bill number require. * @param {string} billNo - */ - validateBillNoRequire(billNo: string) { + private validateBillNoRequire(billNo: string) { if (!billNo) { throw new ServiceError(ERRORS.BILL_NO_IS_REQUIRED); } } + /** + * Validate bill transaction has no associated allocated landed cost transactions. + * @param {number} tenantId + * @param {number} billId + */ + private async validateBillHasNoLandedCost(tenantId: number, billId: number) { + const { BillLandedCost } = this.tenancy.models(tenantId); + + const billLandedCosts = await BillLandedCost.query().where( + 'billId', + billId + ); + if (billLandedCosts.length > 0) { + throw new ServiceError(ERRORS.BILL_HAS_ASSOCIATED_LANDED_COSTS); + } + } + /** * Sets the default cost account to the bill entries. */ - setBillEntriesDefaultAccounts(tenantId: number) { + private setBillEntriesDefaultAccounts(tenantId: number) { return async (entries: IItemEntry[]) => { const { Item } = this.tenancy.models(tenantId); @@ -246,6 +268,7 @@ export default class BillsService billDTO.vendorId ); const initialEntries = billDTO.entries.map((entry) => ({ + amount: ItemEntry.calcAmount(entry), reference_type: 'Bill', ...omit(entry, ['amount']), })); @@ -397,6 +420,16 @@ export default class BillsService authorizedUser, oldBill ); + // Validate landed cost entries that have allocated cost could not be deleted. + await this.entriesService.validateLandedCostEntriesNotDeleted( + oldBill.entries, + billObj.entries, + ); + // Validate new landed cost entries should be bigger than new entries. + await this.entriesService.validateLocatedCostEntriesSmallerThanNewEntries( + oldBill.entries, + billObj.entries + ); // Update the bill transaction. const bill = await billRepository.upsertGraph({ id: billId, @@ -429,6 +462,9 @@ export default class BillsService // Retrieve the given bill or throw not found error. const oldBill = await this.getBillOrThrowError(tenantId, billId); + // Validate the givne bill has no associated landed cost transactions. + await this.validateBillHasNoLandedCost(tenantId, billId); + // Validate the purchase bill has no assocaited payments transactions. await this.validateBillHasNoEntries(tenantId, billId); @@ -561,9 +597,16 @@ export default class BillsService */ public async recordInventoryTransactions( tenantId: number, - bill: IBill, + billId: number, override?: boolean ): Promise { + const { Bill } = this.tenancy.models(tenantId); + + // Retireve bill with assocaited entries and allocated cost entries. + const bill = await Bill.query() + .findById(billId) + .withGraphFetched('entries.allocatedCostEntries'); + // Loads the inventory items entries of the given sale invoice. const inventoryEntries = await this.itemsEntriesService.filterInventoryEntries( @@ -573,7 +616,6 @@ export default class BillsService const transaction = { transactionId: bill.id, transactionType: 'Bill', - date: bill.billDate, direction: 'IN', entries: inventoryEntries, @@ -609,13 +651,30 @@ export default class BillsService */ public async recordJournalTransactions( tenantId: number, - bill: IBill, + billId: number, override: boolean = false ) { + const { Bill, Account } = this.tenancy.models(tenantId); + const journal = new JournalPoster(tenantId); const journalCommands = new JournalCommands(journal); - await journalCommands.bill(bill, override); + const bill = await Bill.query() + .findById(billId) + .withGraphFetched('entries.item') + .withGraphFetched('entries.allocatedCostEntries') + .withGraphFetched('locatedLandedCosts.allocateEntries'); + + const payableAccount = await Account.query().findOne({ + slug: 'accounts-payable', + }); + + // Overrides the bill journal entries. + if (override) { + await journalCommands.revertJournalEntries(billId, 'Bill'); + } + // Writes the bill journal entries. + journalCommands.bill(bill, payableAccount); return Promise.all([ journal.deleteEntries(), diff --git a/server/src/services/Purchases/LandedCost/BillLandedCost.ts b/server/src/services/Purchases/LandedCost/BillLandedCost.ts index 0aacc5ecf..92fcb89ee 100644 --- a/server/src/services/Purchases/LandedCost/BillLandedCost.ts +++ b/server/src/services/Purchases/LandedCost/BillLandedCost.ts @@ -12,8 +12,8 @@ import { export default class BillLandedCost { /** * Retrieve the landed cost transaction from the given bill transaction. - * @param {IBill} bill - * @returns {ILandedCostTransaction} + * @param {IBill} bill - Bill transaction. + * @returns {ILandedCostTransaction} - Landed cost transaction. */ public transformToLandedCost = (bill: IBill): ILandedCostTransaction => { const number = bill.billNumber || bill.referenceNo; @@ -49,7 +49,10 @@ export default class BillLandedCost { name: billEntry.item.name, code: billEntry.item.code, amount: billEntry.amount, + unallocatedCostAmount: billEntry.unallocatedCostAmount, + allocatedCostAmount: billEntry.allocatedCostAmount, description: billEntry.description, + costAccountId: billEntry.costAccountId || billEntry.item.costAccountId, }; } } diff --git a/server/src/services/Purchases/LandedCost/ExpenseLandedCost.ts b/server/src/services/Purchases/LandedCost/ExpenseLandedCost.ts index 8931e500c..078ae6627 100644 --- a/server/src/services/Purchases/LandedCost/ExpenseLandedCost.ts +++ b/server/src/services/Purchases/LandedCost/ExpenseLandedCost.ts @@ -48,6 +48,9 @@ export default class ExpenseLandedCost { code: expenseEntry.expenseAccount.code, amount: expenseEntry.amount, description: expenseEntry.description, + allocatedCostAmount: expenseEntry.allocatedCostAmount, + unallocatedCostAmount: expenseEntry.unallocatedCostAmount, + costAccountId: expenseEntry.expenseAccount.id, }; }; } diff --git a/server/src/services/Purchases/LandedCost/TransctionLandedCost.ts b/server/src/services/Purchases/LandedCost/TransctionLandedCost.ts index 57c8e13c6..9362aae1d 100644 --- a/server/src/services/Purchases/LandedCost/TransctionLandedCost.ts +++ b/server/src/services/Purchases/LandedCost/TransctionLandedCost.ts @@ -1,11 +1,12 @@ import { Inject, Service } from 'typedi'; import * as R from 'ramda'; -import { IBill, IExpense, ILandedCostTransaction } from 'interfaces'; +import { Model } from 'objection'; +import { IBill, IExpense, ILandedCostTransaction, ILandedCostTransactionEntry } from 'interfaces'; import { ServiceError } from 'exceptions'; import BillLandedCost from './BillLandedCost'; import ExpenseLandedCost from './ExpenseLandedCost'; import HasTenancyService from 'services/Tenancy/TenancyService'; -import { ERRORS } from './constants'; +import { ERRORS } from './utils'; @Service() export default class TransactionLandedCost { @@ -27,7 +28,7 @@ export default class TransactionLandedCost { public getModel = ( tenantId: number, transactionType: string - ): IBill | IExpense => { + ): Model => { const Models = this.tenancy.models(tenantId); const Model = Models[transactionType]; @@ -58,4 +59,26 @@ export default class TransactionLandedCost { ), )(transaction); } + + /** + * Transformes the given expense or bill entry to landed cost transaction entry. + * @param {string} transactionType + * @param {} transactionEntry + * @returns {ILandedCostTransactionEntry} + */ + public transformToLandedCostEntry = ( + transactionType: 'Bill' | 'Expense', + transactionEntry, + ): ILandedCostTransactionEntry => { + return R.compose( + R.when( + R.always(transactionType === 'Bill'), + this.billLandedCost.transformToLandedCostEntry, + ), + R.when( + R.always(transactionType === 'Expense'), + this.expenseLandedCost.transformToLandedCostEntry, + ), + )(transactionEntry); + } } diff --git a/server/src/services/Purchases/LandedCost/constants.ts b/server/src/services/Purchases/LandedCost/constants.ts deleted file mode 100644 index be247f943..000000000 --- a/server/src/services/Purchases/LandedCost/constants.ts +++ /dev/null @@ -1,15 +0,0 @@ - - - -export const ERRORS = { - COST_TYPE_UNDEFINED: 'COST_TYPE_UNDEFINED', - LANDED_COST_ITEMS_IDS_NOT_FOUND: 'LANDED_COST_ITEMS_IDS_NOT_FOUND', - COST_TRANSACTION_HAS_NO_ENOUGH_TO_LOCATE: - 'COST_TRANSACTION_HAS_NO_ENOUGH_TO_LOCATE', - BILL_LANDED_COST_NOT_FOUND: 'BILL_LANDED_COST_NOT_FOUND', - COST_ENTRY_ID_NOT_FOUND: 'COST_ENTRY_ID_NOT_FOUND', - LANDED_COST_TRANSACTION_NOT_FOUND: 'LANDED_COST_TRANSACTION_NOT_FOUND', - LANDED_COST_ENTRY_NOT_FOUND: 'LANDED_COST_ENTRY_NOT_FOUND', - COST_AMOUNT_BIGGER_THAN_UNALLOCATED_AMOUNT: 'COST_AMOUNT_BIGGER_THAN_UNALLOCATED_AMOUNT', - ALLOCATE_COST_SHOULD_NOT_BE_BILL: 'ALLOCATE_COST_SHOULD_NOT_BE_BILL' -}; diff --git a/server/src/services/Purchases/LandedCost/index.ts b/server/src/services/Purchases/LandedCost/index.ts index 9b4c1ffb7..b189610ee 100644 --- a/server/src/services/Purchases/LandedCost/index.ts +++ b/server/src/services/Purchases/LandedCost/index.ts @@ -1,5 +1,9 @@ import { Inject, Service } from 'typedi'; import { difference, sumBy } from 'lodash'; +import { + EventDispatcher, + EventDispatcherInterface, +} from 'decorators/eventDispatcher'; import BillsService from '../Bills'; import { ServiceError } from 'exceptions'; import { @@ -9,15 +13,14 @@ import { ILandedCostItemDTO, ILandedCostDTO, IBillLandedCostTransaction, - IBillLandedCostTransactionEntry, + ILandedCostTransaction, + ILandedCostTransactionEntry, } from 'interfaces'; +import events from 'subscribers/events'; import InventoryService from 'services/Inventory/Inventory'; import HasTenancyService from 'services/Tenancy/TenancyService'; -import { ERRORS } from './constants'; -import { transformToMap } from 'utils'; -import JournalPoster from 'services/Accounting/JournalPoster'; -import JournalEntry from 'services/Accounting/JournalEntry'; import TransactionLandedCost from './TransctionLandedCost'; +import { ERRORS, mergeLocatedWithBillEntries } from './utils'; const CONFIG = { COST_TYPES: { @@ -47,6 +50,9 @@ export default class AllocateLandedCostService { @Inject() public transactionLandedCost: TransactionLandedCost; + @EventDispatcher() + eventDispatcher: EventDispatcherInterface; + /** * Validates allocate cost items association with the purchase invoice entries. * @param {IItemEntry[]} purchaseInvoiceEntries @@ -72,23 +78,23 @@ export default class AllocateLandedCostService { }; /** - * Saves the bill landed cost model. - * @param {number} tenantId - * @param {ILandedCostDTO} landedCostDTO - * @param {number} purchaseInvoiceId - * @returns {Promise} + * Transformes DTO to bill landed cost model object. + * @param landedCostDTO + * @param bill + * @param costTransaction + * @param costTransactionEntry + * @returns */ - private saveBillLandedCostModel = ( - tenantId: number, + private transformToBillLandedCost( landedCostDTO: ILandedCostDTO, - purchaseInvoiceId: number - ): Promise => { - const { BillLandedCost } = this.tenancy.models(tenantId); + bill: IBill, + costTransaction: ILandedCostTransaction, + costTransactionEntry: ILandedCostTransactionEntry + ) { const amount = sumBy(landedCostDTO.items, 'cost'); - // Inserts the bill landed cost to the storage. - return BillLandedCost.query().insertGraph({ - billId: purchaseInvoiceId, + return { + billId: bill.id, fromTransactionType: landedCostDTO.transactionType, fromTransactionId: landedCostDTO.transactionId, fromTransactionEntryId: landedCostDTO.transactionEntryId, @@ -96,8 +102,9 @@ export default class AllocateLandedCostService { allocationMethod: landedCostDTO.allocationMethod, description: landedCostDTO.description, allocateEntries: landedCostDTO.items, - }); - }; + costAccountId: costTransactionEntry.costAccountId, + }; + } /** * Allocate the landed cost amount to cost transactions. @@ -147,7 +154,6 @@ export default class AllocateLandedCostService { tenantId, transactionType ); - // Decrement the allocate cost amount of cost transaction. return Model.query() .where('id', transactionId) @@ -202,12 +208,22 @@ export default class AllocateLandedCostService { const entry = await Model.relatedQuery(relation) .for(transactionId) .findOne('id', transactionEntryId) - .where('landedCost', true); + .where('landedCost', true) + .onBuild((q) => { + if (transactionType === 'Bill') { + q.withGraphFetched('item'); + } else if (transactionType === 'Expense') { + q.withGraphFetched('expenseAccount'); + } + }); if (!entry) { throw new ServiceError(ERRORS.LANDED_COST_ENTRY_NOT_FOUND); } - return entry; + return this.transactionLandedCost.transformToLandedCostEntry( + transactionType, + entry + ); }; /** @@ -230,31 +246,11 @@ export default class AllocateLandedCostService { unallocatedCost: number, amount: number ): void => { - console.log(unallocatedCost, amount, '123'); - if (unallocatedCost < amount) { throw new ServiceError(ERRORS.COST_AMOUNT_BIGGER_THAN_UNALLOCATED_AMOUNT); } }; - /** - * Merges item entry to bill located landed cost entry. - * @param {IBillLandedCostTransactionEntry[]} locatedEntries - - * @param {IItemEntry[]} billEntries - - * @returns {(IBillLandedCostTransactionEntry & { entry: IItemEntry })[]} - */ - private mergeLocatedWithBillEntries = ( - locatedEntries: IBillLandedCostTransactionEntry[], - billEntries: IItemEntry[] - ): (IBillLandedCostTransactionEntry & { entry: IItemEntry })[] => { - const billEntriesByEntryId = transformToMap(billEntries, 'id'); - - return locatedEntries.map((entry) => ({ - ...entry, - entry: billEntriesByEntryId.get(entry.entryId), - })); - }; - /** * Records inventory transactions. * @param {number} tenantId @@ -266,7 +262,7 @@ export default class AllocateLandedCostService { bill: IBill ) => { // Retrieve the merged allocated entries with bill entries. - const allocateEntries = this.mergeLocatedWithBillEntries( + const allocateEntries = mergeLocatedWithBillEntries( billLandedCost.allocateEntries, bill.entries ); @@ -304,22 +300,24 @@ export default class AllocateLandedCostService { * * @param {ILandedCostDTO} landedCostDTO - Landed cost DTO. * @param {number} tenantId - Tenant id. - * @param {number} purchaseInvoiceId - Purchase invoice id. + * @param {number} billId - Purchase invoice id. */ public allocateLandedCost = async ( tenantId: number, allocateCostDTO: ILandedCostDTO, - purchaseInvoiceId: number + billId: number ): Promise<{ billLandedCost: IBillLandedCost; }> => { + const { BillLandedCost } = this.tenancy.models(tenantId); + // Retrieve total cost of allocated items. const amount = this.getAllocateItemsCostTotal(allocateCostDTO); // Retrieve the purchase invoice or throw not found error. - const purchaseInvoice = await this.billsService.getBillOrThrowError( + const bill = await this.billsService.getBillOrThrowError( tenantId, - purchaseInvoiceId + billId ); // Retrieve landed cost transaction or throw not found service error. const landedCostTransaction = await this.getLandedCostOrThrowError( @@ -336,25 +334,36 @@ export default class AllocateLandedCostService { ); // Validates allocate cost items association with the purchase invoice entries. this.validateAllocateCostItems( - purchaseInvoice.entries, + bill.entries, allocateCostDTO.items ); // Validate the amount of cost with unallocated landed cost. this.validateLandedCostEntryAmount( - landedCostEntry.unallocatedLandedCost, + landedCostEntry.unallocatedCostAmount, amount ); - // Save the bill landed cost model. - const billLandedCost = await this.saveBillLandedCostModel( - tenantId, + // Transformes DTO to bill landed cost model object. + const billLandedCostObj = this.transformToBillLandedCost( allocateCostDTO, - purchaseInvoiceId + bill, + landedCostTransaction, + landedCostEntry ); + // Save the bill landed cost model. + const billLandedCost = await BillLandedCost.query().insertGraph( + billLandedCostObj + ); + // Triggers the event `onBillLandedCostCreated`. + await this.eventDispatcher.dispatch(events.billLandedCost.onCreated, { + tenantId, + billId, + billLandedCostId: billLandedCost.id, + }); // Records the inventory transactions. await this.recordInventoryTransactions( tenantId, billLandedCost, - purchaseInvoice + bill ); // Increment landed cost amount on transaction and entry. await this.incrementLandedCostAmount( @@ -364,55 +373,9 @@ export default class AllocateLandedCostService { allocateCostDTO.transactionEntryId, amount ); - // Write the landed cost journal entries. - // await this.writeJournalEntry(tenantId, billLandedCost, purchaseInvoice); - return { billLandedCost }; }; - /** - * Write journal entries of the given purchase invoice landed cost. - * @param tenantId - * @param purchaseInvoice - * @param landedCost - */ - private writeJournalEntry = async ( - tenantId: number, - landedCostEntry: any, - purchaseInvoice: IBill, - landedCost: IBillLandedCost - ) => { - const journal = new JournalPoster(tenantId); - const billEntriesById = purchaseInvoice.entries; - - const commonEntry = { - referenceType: 'Bill', - referenceId: purchaseInvoice.id, - date: purchaseInvoice.billDate, - indexGroup: 300, - }; - const costEntry = new JournalEntry({ - ...commonEntry, - credit: landedCost.amount, - account: landedCost.costAccountId, - index: 1, - }); - journal.credit(costEntry); - - landedCost.allocateEntries.forEach((entry, index) => { - const billEntry = billEntriesById[entry.entryId]; - - const inventoryEntry = new JournalEntry({ - ...commonEntry, - debit: entry.cost, - account: billEntry.item.inventoryAccountId, - index: 1 + index, - }); - journal.debit(inventoryEntry); - }); - return journal; - }; - /** * Retrieve the give bill landed cost or throw not found service error. * @param {number} tenantId - Tenant id. @@ -422,7 +385,7 @@ export default class AllocateLandedCostService { public getBillLandedCostOrThrowError = async ( tenantId: number, landedCostId: number - ): Promise => { + ): Promise => { const { BillLandedCost } = this.tenancy.models(tenantId); // Retrieve the bill landed cost model. @@ -462,7 +425,7 @@ export default class AllocateLandedCostService { * - Delete the associated inventory transactions. * - Decrement allocated amount of landed cost transaction and entry. * - Revert journal entries. - * + * ---------------------------------- * @param {number} tenantId - Tenant id. * @param {number} landedCostId - Landed cost id. * @return {Promise} @@ -481,6 +444,12 @@ export default class AllocateLandedCostService { // Delete landed cost transaction with assocaited locate entries. await this.deleteLandedCost(tenantId, landedCostId); + // Triggers the event `onBillLandedCostCreated`. + await this.eventDispatcher.dispatch(events.billLandedCost.onDeleted, { + tenantId, + billLandedCostId: oldBillLandedCost.id, + billId: oldBillLandedCost.billId, + }); // Removes the inventory transactions. await this.removeInventoryTransactions(tenantId, landedCostId); diff --git a/server/src/services/Purchases/LandedCost/utils.ts b/server/src/services/Purchases/LandedCost/utils.ts new file mode 100644 index 000000000..48d8cb683 --- /dev/null +++ b/server/src/services/Purchases/LandedCost/utils.ts @@ -0,0 +1,34 @@ +import { IItemEntry, IBillLandedCostTransactionEntry } from 'interfaces'; +import { transformToMap } from 'utils'; + +export const ERRORS = { + COST_TYPE_UNDEFINED: 'COST_TYPE_UNDEFINED', + LANDED_COST_ITEMS_IDS_NOT_FOUND: 'LANDED_COST_ITEMS_IDS_NOT_FOUND', + COST_TRANSACTION_HAS_NO_ENOUGH_TO_LOCATE: + 'COST_TRANSACTION_HAS_NO_ENOUGH_TO_LOCATE', + BILL_LANDED_COST_NOT_FOUND: 'BILL_LANDED_COST_NOT_FOUND', + COST_ENTRY_ID_NOT_FOUND: 'COST_ENTRY_ID_NOT_FOUND', + LANDED_COST_TRANSACTION_NOT_FOUND: 'LANDED_COST_TRANSACTION_NOT_FOUND', + LANDED_COST_ENTRY_NOT_FOUND: 'LANDED_COST_ENTRY_NOT_FOUND', + COST_AMOUNT_BIGGER_THAN_UNALLOCATED_AMOUNT: + 'COST_AMOUNT_BIGGER_THAN_UNALLOCATED_AMOUNT', + ALLOCATE_COST_SHOULD_NOT_BE_BILL: 'ALLOCATE_COST_SHOULD_NOT_BE_BILL', +}; + +/** + * Merges item entry to bill located landed cost entry. + * @param {IBillLandedCostTransactionEntry[]} locatedEntries - + * @param {IItemEntry[]} billEntries - + * @returns {(IBillLandedCostTransactionEntry & { entry: IItemEntry })[]} + */ +export const mergeLocatedWithBillEntries = ( + locatedEntries: IBillLandedCostTransactionEntry[], + billEntries: IItemEntry[] +): (IBillLandedCostTransactionEntry & { entry: IItemEntry })[] => { + const billEntriesByEntryId = transformToMap(billEntries, 'id'); + + return locatedEntries.map((entry) => ({ + ...entry, + entry: billEntriesByEntryId.get(entry.entryId), + })); +}; diff --git a/server/src/services/Purchases/constants.ts b/server/src/services/Purchases/constants.ts index 09b31979d..251598385 100644 --- a/server/src/services/Purchases/constants.ts +++ b/server/src/services/Purchases/constants.ts @@ -9,5 +9,8 @@ export const ERRORS = { BILL_ALREADY_OPEN: 'BILL_ALREADY_OPEN', BILL_NO_IS_REQUIRED: 'BILL_NO_IS_REQUIRED', BILL_HAS_ASSOCIATED_PAYMENT_ENTRIES: 'BILL_HAS_ASSOCIATED_PAYMENT_ENTRIES', - VENDOR_HAS_BILLS: 'VENDOR_HAS_BILLS' + VENDOR_HAS_BILLS: 'VENDOR_HAS_BILLS', + BILL_HAS_ASSOCIATED_LANDED_COSTS: 'BILL_HAS_ASSOCIATED_LANDED_COSTS', + BILL_ENTRIES_ALLOCATED_COST_COULD_DELETED: 'BILL_ENTRIES_ALLOCATED_COST_COULD_DELETED', + LOCATED_COST_ENTRIES_SHOULD_BIGGE_THAN_NEW_ENTRIES: 'LOCATED_COST_ENTRIES_SHOULD_BIGGE_THAN_NEW_ENTRIES' }; diff --git a/server/src/subscribers/Bills/WriteJournalEntries.ts b/server/src/subscribers/Bills/WriteJournalEntries.ts index 3a94f2c71..0741f6255 100644 --- a/server/src/subscribers/Bills/WriteJournalEntries.ts +++ b/server/src/subscribers/Bills/WriteJournalEntries.ts @@ -23,20 +23,20 @@ export default class BillSubscriber { * Handles writing journal entries once bill created. */ @On(events.bill.onCreated) - async handlerWriteJournalEntriesOnCreate({ tenantId, bill }) { + async handlerWriteJournalEntriesOnCreate({ tenantId, billId }) { // Writes the journal entries for the given bill transaction. this.logger.info('[bill] writing bill journal entries.', { tenantId }); - await this.billsService.recordJournalTransactions(tenantId, bill); + await this.billsService.recordJournalTransactions(tenantId, billId); } /** * Handles the overwriting journal entries once bill edited. */ @On(events.bill.onEdited) - async handleOverwriteJournalEntriesOnEdit({ tenantId, bill }) { + async handleOverwriteJournalEntriesOnEdit({ tenantId, billId }) { // Overwrite the journal entries for the given bill transaction. this.logger.info('[bill] overwriting bill journal entries.', { tenantId }); - await this.billsService.recordJournalTransactions(tenantId, bill, true); + await this.billsService.recordJournalTransactions(tenantId, billId, true); } /** diff --git a/server/src/subscribers/LandedCost/index.ts b/server/src/subscribers/LandedCost/index.ts new file mode 100644 index 000000000..ec31b83d9 --- /dev/null +++ b/server/src/subscribers/LandedCost/index.ts @@ -0,0 +1,37 @@ +import { Container } from 'typedi'; +import { On, EventSubscriber } from 'event-dispatch'; +import events from 'subscribers/events'; +import TenancyService from 'services/Tenancy/TenancyService'; +import BillsService from 'services/Purchases/Bills'; + +@EventSubscriber() +export default class BillLandedCostSubscriber { + logger: any; + tenancy: TenancyService; + billsService: BillsService; + + /** + * Constructor method. + */ + constructor() { + this.logger = Container.get('logger'); + this.tenancy = Container.get(TenancyService); + this.billsService = Container.get(BillsService); + } + + /** + * Marks the rewrite bill journal entries once the landed cost transaction + * be deleted or created. + */ + @On(events.billLandedCost.onCreated) + @On(events.billLandedCost.onDeleted) + public async handleRewriteBillJournalEntries({ + tenantId, + billId, + bilLandedCostId, + }) { + // Overwrite the journal entries for the given bill transaction. + this.logger.info('[bill] overwriting bill journal entries.', { tenantId }); + await this.billsService.recordJournalTransactions(tenantId, billId, true); + } +} From 504b380da64c84526ef907162d8f2d5100d86d4c Mon Sep 17 00:00:00 2001 From: "a.bouhuolia" Date: Sat, 24 Jul 2021 03:46:25 +0200 Subject: [PATCH 14/24] WIP: allocate landed cost. --- server/src/api/controllers/Purchases/Bills.ts | 16 +++++++- server/src/interfaces/ItemEntry.ts | 1 + server/src/services/Entries/index.ts | 30 +++++++-------- server/src/services/Purchases/Bills.ts | 38 ++++++++++++++++++- server/src/services/Purchases/constants.ts | 3 +- 5 files changed, 69 insertions(+), 19 deletions(-) diff --git a/server/src/api/controllers/Purchases/Bills.ts b/server/src/api/controllers/Purchases/Bills.ts index e190d7d84..c119bcfce 100644 --- a/server/src/api/controllers/Purchases/Bills.ts +++ b/server/src/api/controllers/Purchases/Bills.ts @@ -446,7 +446,9 @@ export default class BillsController extends BaseController { ], }); } - if (error.errorType === 'LOCATED_COST_ENTRIES_SHOULD_BIGGE_THAN_NEW_ENTRIES') { + if ( + error.errorType === 'LOCATED_COST_ENTRIES_SHOULD_BIGGE_THAN_NEW_ENTRIES' + ) { return res.status(400).send({ errors: [ { @@ -456,6 +458,18 @@ export default class BillsController extends BaseController { ], }); } + if (error.errorType === 'LANDED_COST_ENTRIES_SHOULD_BE_INVENTORY_ITEMS') { + return res.status(400).send({ + errors: [ + { + type: 'LANDED_COST_ENTRIES_SHOULD_BE_INVENTORY_ITEMS', + message: + 'Landed cost entries should be only with inventory items.', + code: 1600, + }, + ], + }); + } } next(error); } diff --git a/server/src/interfaces/ItemEntry.ts b/server/src/interfaces/ItemEntry.ts index 3cb07e4a7..348e5875d 100644 --- a/server/src/interfaces/ItemEntry.ts +++ b/server/src/interfaces/ItemEntry.ts @@ -25,5 +25,6 @@ export interface IItemEntry { export interface IItemEntryDTO { id?: number, + itemId: number; landedCost?: boolean; } diff --git a/server/src/services/Entries/index.ts b/server/src/services/Entries/index.ts index b4277dabb..482fffa5a 100644 --- a/server/src/services/Entries/index.ts +++ b/server/src/services/Entries/index.ts @@ -17,16 +17,16 @@ const ERRORS = { export default class EntriesService { /** * Validates bill entries that has allocated landed cost amount not deleted. - * @param {IItemEntry[]} oldBillEntries - + * @param {IItemEntry[]} oldCommonEntries - * @param {IItemEntry[]} newBillEntries - */ public getLandedCostEntriesDeleted( - oldBillEntries: ICommonLandedCostEntry[], - newBillEntriesDTO: ICommonLandedCostEntryDTO[] + oldCommonEntries: ICommonLandedCostEntry[], + newCommonEntriesDTO: ICommonLandedCostEntryDTO[] ): ICommonLandedCostEntry[] { - const newBillEntriesById = transformToMap(newBillEntriesDTO, 'id'); + const newBillEntriesById = transformToMap(newCommonEntriesDTO, 'id'); - return oldBillEntries.filter((entry) => { + return oldCommonEntries.filter((entry) => { const newEntry = newBillEntriesById.get(entry.id); if (entry.allocatedCostAmount > 0 && typeof newEntry === 'undefined') { @@ -38,16 +38,16 @@ export default class EntriesService { /** * Validates the bill entries that have located cost amount should not be deleted. - * @param {IItemEntry[]} oldBillEntries - Old bill entries. + * @param {IItemEntry[]} oldCommonEntries - Old bill entries. * @param {IItemEntryDTO[]} newBillEntries - New DTO bill entries. */ public validateLandedCostEntriesNotDeleted( - oldBillEntries: ICommonLandedCostEntry[], - newBillEntriesDTO: ICommonLandedCostEntryDTO[] + oldCommonEntries: ICommonLandedCostEntry[], + newCommonEntriesDTO: ICommonLandedCostEntryDTO[] ): void { const entriesDeleted = this.getLandedCostEntriesDeleted( - oldBillEntries, - newBillEntriesDTO + oldCommonEntries, + newCommonEntriesDTO ); if (entriesDeleted.length > 0) { throw new ServiceError(ERRORS.ENTRIES_ALLOCATED_COST_COULD_NOT_DELETED); @@ -56,16 +56,16 @@ export default class EntriesService { /** * Validate allocated cost amount entries should be smaller than new entries amount. - * @param {IItemEntry[]} oldBillEntries - Old bill entries. + * @param {IItemEntry[]} oldCommonEntries - Old bill entries. * @param {IItemEntryDTO[]} newBillEntries - New DTO bill entries. */ public validateLocatedCostEntriesSmallerThanNewEntries( - oldBillEntries: ICommonLandedCostEntry[], - newBillEntriesDTO: ICommonLandedCostEntryDTO[] + oldCommonEntries: ICommonLandedCostEntry[], + newCommonEntriesDTO: ICommonLandedCostEntryDTO[] ): void { - const oldBillEntriesById = transformToMap(oldBillEntries, 'id'); + const oldBillEntriesById = transformToMap(oldCommonEntries, 'id'); - newBillEntriesDTO.forEach((entry) => { + newCommonEntriesDTO.forEach((entry) => { const oldEntry = oldBillEntriesById.get(entry.id); if (oldEntry && oldEntry.allocatedCostAmount > entry.amount) { diff --git a/server/src/services/Purchases/Bills.ts b/server/src/services/Purchases/Bills.ts index 7cd4dfe34..439cdfb74 100644 --- a/server/src/services/Purchases/Bills.ts +++ b/server/src/services/Purchases/Bills.ts @@ -13,7 +13,7 @@ import InventoryService from 'services/Inventory/Inventory'; import SalesInvoicesCost from 'services/Sales/SalesInvoicesCost'; import TenancyService from 'services/Tenancy/TenancyService'; import DynamicListingService from 'services/DynamicListing/DynamicListService'; -import { formatDateFields } from 'utils'; +import { formatDateFields, transformToMap } from 'utils'; import { IBillDTO, IBill, @@ -194,6 +194,36 @@ export default class BillsService } } + /** + * Validate transaction entries that have landed cost type should not be + * inventory items. + * @param {number} tenantId - + * @param {IItemEntryDTO[]} newEntriesDTO - + */ + public async validateCostEntriesShouldBeInventoryItems( + tenantId: number, + newEntriesDTO: IItemEntryDTO[] + ) { + const { Item } = this.tenancy.models(tenantId); + + const entriesItemsIds = newEntriesDTO.map((e) => e.itemId); + const entriesItems = await Item.query().whereIn('id', entriesItemsIds); + + const entriesItemsById = transformToMap(entriesItems, 'id'); + + // Filter the landed cost entries that not associated with inventory item. + const nonInventoryHasCost = newEntriesDTO.filter((entry) => { + const item = entriesItemsById.get(entry.itemId); + + return entry.landedCost && item.type !== 'inventory'; + }); + if (nonInventoryHasCost.length > 0) { + throw new ServiceError( + ERRORS.LANDED_COST_ENTRIES_SHOULD_BE_INVENTORY_ITEMS + ); + } + } + /** * Sets the default cost account to the bill entries. */ @@ -334,6 +364,10 @@ export default class BillsService tenantId, billDTO.entries ); + await this.validateCostEntriesShouldBeInventoryItems( + tenantId, + billDTO.entries, + ); this.logger.info('[bill] trying to create a new bill', { tenantId, billDTO, @@ -423,7 +457,7 @@ export default class BillsService // Validate landed cost entries that have allocated cost could not be deleted. await this.entriesService.validateLandedCostEntriesNotDeleted( oldBill.entries, - billObj.entries, + billObj.entries ); // Validate new landed cost entries should be bigger than new entries. await this.entriesService.validateLocatedCostEntriesSmallerThanNewEntries( diff --git a/server/src/services/Purchases/constants.ts b/server/src/services/Purchases/constants.ts index 251598385..be1874a86 100644 --- a/server/src/services/Purchases/constants.ts +++ b/server/src/services/Purchases/constants.ts @@ -12,5 +12,6 @@ export const ERRORS = { VENDOR_HAS_BILLS: 'VENDOR_HAS_BILLS', BILL_HAS_ASSOCIATED_LANDED_COSTS: 'BILL_HAS_ASSOCIATED_LANDED_COSTS', BILL_ENTRIES_ALLOCATED_COST_COULD_DELETED: 'BILL_ENTRIES_ALLOCATED_COST_COULD_DELETED', - LOCATED_COST_ENTRIES_SHOULD_BIGGE_THAN_NEW_ENTRIES: 'LOCATED_COST_ENTRIES_SHOULD_BIGGE_THAN_NEW_ENTRIES' + LOCATED_COST_ENTRIES_SHOULD_BIGGE_THAN_NEW_ENTRIES: 'LOCATED_COST_ENTRIES_SHOULD_BIGGE_THAN_NEW_ENTRIES', + LANDED_COST_ENTRIES_SHOULD_BE_INVENTORY_ITEMS: 'LANDED_COST_ENTRIES_SHOULD_BE_INVENTORY_ITEMS' }; From fabc8d3b4ec930ec7d7188207d63b9f44f4815c3 Mon Sep 17 00:00:00 2001 From: elforjani3 Date: Sat, 24 Jul 2021 13:16:44 +0200 Subject: [PATCH 15/24] feat: bill Drawer. --- .../Drawers/BillDrawer/BillDrawerAlerts.js | 13 +++++++++++++ .../Drawers/BillDrawer/BillDrawerContent.js | 7 +++++-- .../Drawers/BillDrawer/BillDrawerProvider.js | 13 +++++++++++-- 3 files changed, 29 insertions(+), 4 deletions(-) create mode 100644 client/src/containers/Drawers/BillDrawer/BillDrawerAlerts.js diff --git a/client/src/containers/Drawers/BillDrawer/BillDrawerAlerts.js b/client/src/containers/Drawers/BillDrawer/BillDrawerAlerts.js new file mode 100644 index 000000000..278671574 --- /dev/null +++ b/client/src/containers/Drawers/BillDrawer/BillDrawerAlerts.js @@ -0,0 +1,13 @@ +import React from 'react'; +import BillTransactionDeleteAlert from 'containers/Alerts/Bills/BillTransactionDeleteAlert'; + +/** + * Bill drawer alert. + */ +export default function BillDrawerAlerts() { + return ( +
+ +
+ ); +} diff --git a/client/src/containers/Drawers/BillDrawer/BillDrawerContent.js b/client/src/containers/Drawers/BillDrawer/BillDrawerContent.js index 0e93746a5..ec1579009 100644 --- a/client/src/containers/Drawers/BillDrawer/BillDrawerContent.js +++ b/client/src/containers/Drawers/BillDrawer/BillDrawerContent.js @@ -1,16 +1,19 @@ import React from 'react'; import { BillDrawerProvider } from './BillDrawerProvider'; import BillDrawerDetails from './BillDrawerDetails'; +import BillDrawerAlerts from './BillDrawerAlerts'; + /** * Bill drawer content. */ export default function BillDrawerContent({ // #ownProp - billId, + bill, }) { return ( - + + ); } diff --git a/client/src/containers/Drawers/BillDrawer/BillDrawerProvider.js b/client/src/containers/Drawers/BillDrawer/BillDrawerProvider.js index 689508ad0..5bd4a8246 100644 --- a/client/src/containers/Drawers/BillDrawer/BillDrawerProvider.js +++ b/client/src/containers/Drawers/BillDrawer/BillDrawerProvider.js @@ -1,6 +1,7 @@ import React from 'react'; import intl from 'react-intl-universal'; import { DrawerHeaderContent, DashboardInsider } from 'components'; +import { useBillLocatedLandedCost } from 'hooks/query'; const BillDrawerContext = React.createContext(); @@ -8,10 +9,18 @@ const BillDrawerContext = React.createContext(); * Bill drawer provider. */ function BillDrawerProvider({ billId, ...props }) { + // Handle fetch bill located landed cost transaction. + const { isLoading: isLandedCostLoading, data: transactions } = + useBillLocatedLandedCost(billId, { + enabled: !!billId, + }); + //provider. - const provider = {}; + const provider = { + transactions, + }; return ( - + Date: Sat, 24 Jul 2021 15:03:13 +0200 Subject: [PATCH 16/24] feat: catch error. --- .../Alerts/Expenses/ExpenseDeleteAlert.js | 47 ++++++++++++------- .../Purchases/Bills/BillForm/utils.js | 10 ++++ client/src/lang/en/index.json | 5 +- 3 files changed, 44 insertions(+), 18 deletions(-) diff --git a/client/src/containers/Alerts/Expenses/ExpenseDeleteAlert.js b/client/src/containers/Alerts/Expenses/ExpenseDeleteAlert.js index b367fec84..f8604be99 100644 --- a/client/src/containers/Alerts/Expenses/ExpenseDeleteAlert.js +++ b/client/src/containers/Alerts/Expenses/ExpenseDeleteAlert.js @@ -21,11 +21,7 @@ function ExpenseDeleteAlert({ isOpen, payload: { expenseId }, }) { - - const { - mutateAsync: deleteExpenseMutate, - isLoading, - } = useDeleteExpense(); + const { mutateAsync: deleteExpenseMutate, isLoading } = useDeleteExpense(); // Handle cancel expense journal. const handleCancelExpenseDelete = () => { @@ -34,17 +30,34 @@ function ExpenseDeleteAlert({ // Handle confirm delete expense. const handleConfirmExpenseDelete = () => { - deleteExpenseMutate(expenseId).then(() => { - AppToaster.show({ - message: intl.get( - 'the_expense_has_been_deleted_successfully', - { number: expenseId }, - ), - intent: Intent.SUCCESS, - }); - }).finally(() => { - closeAlert('expense-delete'); - }); + deleteExpenseMutate(expenseId) + .then(() => { + AppToaster.show({ + message: intl.get('the_expense_has_been_deleted_successfully', { + number: expenseId, + }), + intent: Intent.SUCCESS, + }); + closeAlert('expense-delete'); + }) + .catch( + ({ + response: { + data: { errors }, + }, + }) => { + if ( + errors.find((e) => e.type === 'EXPENSE_HAS_ASSOCIATED_LANDED_COST') + ) { + AppToaster.show({ + intent: Intent.DANGER, + message: intl.get( + 'couldn_t_delete_expense_transaction_has_associated_located_landed_cost_transaction', + ), + }); + } + }, + ); }; return ( @@ -68,4 +81,4 @@ function ExpenseDeleteAlert({ export default compose( withAlertStoreConnect(), withAlertActions, -)(ExpenseDeleteAlert); \ No newline at end of file +)(ExpenseDeleteAlert); diff --git a/client/src/containers/Purchases/Bills/BillForm/utils.js b/client/src/containers/Purchases/Bills/BillForm/utils.js index 47ebec4a2..dba011cf7 100644 --- a/client/src/containers/Purchases/Bills/BillForm/utils.js +++ b/client/src/containers/Purchases/Bills/BillForm/utils.js @@ -51,4 +51,14 @@ export const handleDeleteErrors = (errors) => { intent: Intent.DANGER, }); } + if ( + errors.find((error) => error.type === 'BILL_HAS_ASSOCIATED_LANDED_COSTS') + ) { + AppToaster.show({ + message: intl.get( + 'cannot_delete_bill_that_has_associated_landed_cost_transactions', + ), + intent: Intent.DANGER, + }); + } }; diff --git a/client/src/lang/en/index.json b/client/src/lang/en/index.json index 1643a7891..6ad074587 100644 --- a/client/src/lang/en/index.json +++ b/client/src/lang/en/index.json @@ -1136,5 +1136,8 @@ "select_transaction":"Select transaction account", "details":"Details", "located_landed_cost":"Located Landed Cost", - "delete_transaction":"Delete transaction" + "delete_transaction":"Delete transaction", + "cannot_delete_bill_that_has_associated_landed_cost_transactions":"Cannot delete bill that has associated landed cost transactions.", + "couldn_t_delete_expense_transaction_has_associated_located_landed_cost_transaction":"Couldn't delete expense transaction has associated located landed cost transaction" + } \ No newline at end of file From c72918fb1f4c0cf67c3c88928d8ea7b48f197e27 Mon Sep 17 00:00:00 2001 From: elforjani3 Date: Sat, 24 Jul 2021 16:32:18 +0200 Subject: [PATCH 17/24] feat: checkbox field cell. --- .../DataTableCells/CheckBoxFieldCell.js | 41 +++++++++++++++++++ client/src/components/DataTableCells/index.js | 4 +- .../Expenses/ExpenseForm/ExpenseForm.js | 5 ++- .../Expenses/ExpenseForm/components.js | 16 +------- .../containers/Expenses/ExpenseForm/utils.js | 2 +- 5 files changed, 51 insertions(+), 17 deletions(-) create mode 100644 client/src/components/DataTableCells/CheckBoxFieldCell.js diff --git a/client/src/components/DataTableCells/CheckBoxFieldCell.js b/client/src/components/DataTableCells/CheckBoxFieldCell.js new file mode 100644 index 000000000..4cb58ffa1 --- /dev/null +++ b/client/src/components/DataTableCells/CheckBoxFieldCell.js @@ -0,0 +1,41 @@ +import React from 'react'; +import classNames from 'classnames'; +import { Classes, Checkbox, FormGroup, Intent } from '@blueprintjs/core'; + +const CheckboxEditableCell = ({ + row: { index }, + column: { id }, + cell: { value: initialValue }, + payload, +}) => { + const [value, setValue] = React.useState(initialValue); + + const onChange = (e) => { + setValue(e.target.value); + }; + const onBlur = () => { + payload.updateData(index, id, value); + }; + React.useEffect(() => { + setValue(initialValue); + }, [initialValue]); + + const error = payload.errors?.[index]?.[id]; + + return ( + + + + ); +}; + +export default CheckboxEditableCell; diff --git a/client/src/components/DataTableCells/index.js b/client/src/components/DataTableCells/index.js index 8cde83de0..2a0688ad2 100644 --- a/client/src/components/DataTableCells/index.js +++ b/client/src/components/DataTableCells/index.js @@ -6,6 +6,7 @@ import ItemsListCell from './ItemsListCell'; import PercentFieldCell from './PercentFieldCell'; import { DivFieldCell, EmptyDiv } from './DivFieldCell'; import NumericInputCell from './NumericInputCell'; +import CheckBoxFieldCell from './CheckBoxFieldCell' export { AccountsListFieldCell, @@ -16,5 +17,6 @@ export { PercentFieldCell, DivFieldCell, EmptyDiv, - NumericInputCell + NumericInputCell, + CheckBoxFieldCell }; diff --git a/client/src/containers/Expenses/ExpenseForm/ExpenseForm.js b/client/src/containers/Expenses/ExpenseForm/ExpenseForm.js index a75fd5dd6..2f0472447 100644 --- a/client/src/containers/Expenses/ExpenseForm/ExpenseForm.js +++ b/client/src/containers/Expenses/ExpenseForm/ExpenseForm.js @@ -79,7 +79,10 @@ function ExpenseForm({ } const categories = values.categories.filter( (category) => - category.amount && category.index && category.expense_account_id, + category.amount && + category.index && + category.expense_account_id && + category.landed_cost, ); const form = { diff --git a/client/src/containers/Expenses/ExpenseForm/components.js b/client/src/containers/Expenses/ExpenseForm/components.js index b60949896..21d71cf20 100644 --- a/client/src/containers/Expenses/ExpenseForm/components.js +++ b/client/src/containers/Expenses/ExpenseForm/components.js @@ -7,6 +7,7 @@ import { InputGroupCell, MoneyFieldCell, AccountsListFieldCell, + CheckBoxFieldCell, } from 'components/DataTableCells'; import { formattedAmount, safeSumBy } from 'utils'; @@ -49,19 +50,6 @@ const ActionsCellRenderer = ({ ); }; -/** - * Landed cost cell. - */ -const LandedCostCell = ({ - row: { index }, - column: { id }, - cell: { value: initialValue }, - data, - payload, -}) => { - return ; -}; - /** * Landed cost header cell. */ @@ -142,7 +130,7 @@ export function useExpenseFormTableColumns() { { Header: LandedCostHeaderCell, accessor: 'landed_cost', - Cell: LandedCostCell, + Cell: CheckBoxFieldCell, disableSortBy: true, disableResizing: true, width: 70, diff --git a/client/src/containers/Expenses/ExpenseForm/utils.js b/client/src/containers/Expenses/ExpenseForm/utils.js index 36653842c..d418c42a3 100644 --- a/client/src/containers/Expenses/ExpenseForm/utils.js +++ b/client/src/containers/Expenses/ExpenseForm/utils.js @@ -27,7 +27,7 @@ export const defaultExpenseEntry = { amount: '', expense_account_id: '', description: '', - landed_cost: false, + landed_cost: 0, }; export const defaultExpense = { From cd27864f6df3599bded5300333fb47ab49d3cf19 Mon Sep 17 00:00:00 2001 From: elforjani3 Date: Sun, 25 Jul 2021 01:16:24 +0200 Subject: [PATCH 18/24] feat : allocate landed cost. --- .../AllocateLandedCostDialogProvider.js | 31 ++++- .../AllocateLandedCostEntriesTable.js | 49 +++----- .../AllocateLandedCostFloatingActions.js | 1 - .../AllocateLandedCostForm.js | 56 +++++++-- .../AllocateLandedCostFormBody.js | 14 ++- .../AllocateLandedCostFormContent.js | 1 + .../AllocateLandedCostFormFields.js | 109 +++++++++--------- .../Dialogs/AllocateLandedCostDialog/utils.js | 4 + client/src/hooks/query/landedCost.js | 7 +- client/src/lang/en/index.json | 4 +- 10 files changed, 161 insertions(+), 115 deletions(-) create mode 100644 client/src/containers/Dialogs/AllocateLandedCostDialog/utils.js diff --git a/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostDialogProvider.js b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostDialogProvider.js index aec4f85eb..ba4be03fa 100644 --- a/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostDialogProvider.js +++ b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostDialogProvider.js @@ -1,27 +1,50 @@ import React from 'react'; import { DialogContent } from 'components'; -import { useBill } from 'hooks/query'; +import { useBill, useCreateLandedCost } from 'hooks/query'; -import { pick } from 'lodash'; +import { map, omit, pick } from 'lodash'; +import * as R from 'ramda'; const AllocateLandedCostDialogContext = React.createContext(); /** * Allocate landed cost provider. */ -function AllocateLandedCostDialogProvider({ billId, dialogName, ...props }) { +function AllocateLandedCostDialogProvider({ + billId, + query, + dialogName, + ...props +}) { // Handle fetch bill details. const { isLoading: isBillLoading, data: bill } = useBill(billId, { enabled: !!billId, }); + // Create landed cost mutations. + const { mutateAsync: createLandedCostMutate } = useCreateLandedCost(); + + // const L = [bill].map(({ entries: items }) => ({ + // items, + // })); + // let obj = { oldKey: 1, b: 2, c: 3 }; + // const { oldKey: newKey, ...rest } = obj; + // obj = { newKey, ...rest }; + + // const obj = { ...pick(bill, ['entries']).map((index) => index) }; + + // provider payload. const provider = { - bill: { + items: { ...pick(bill, ['entries']), }, dialogName, + query, + createLandedCostMutate, + billId, }; + return ( diff --git a/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostEntriesTable.js b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostEntriesTable.js index 171d88ce1..4400b0124 100644 --- a/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostEntriesTable.js +++ b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostEntriesTable.js @@ -1,6 +1,6 @@ import React from 'react'; import intl from 'react-intl-universal'; -import { DataTable, MoneyFieldCell, DataTableEditable } from 'components'; +import { MoneyFieldCell, DataTableEditable } from 'components'; import { compose, updateTableRow } from 'utils'; /** @@ -10,12 +10,13 @@ export default function AllocateLandedCostEntriesTable({ onUpdateData, entries, }) { + // allocate landed cost entries table columns. const columns = React.useMemo( () => [ { Header: intl.get('item'), - accessor: 'item_id', + accessor: 'item.name', disableSortBy: true, width: '150', }, @@ -59,38 +60,14 @@ export default function AllocateLandedCostEntriesTable({ [onUpdateData, entries], ); - const LL = [ - { - item_id: 'ITEM', - quantity: '30000', - rate: '100000', - amount: '400', - }, - { - item_id: 'ITEM', - quantity: '30000', - rate: '100000', - amount: '400', - }, - { - item_id: 'ITEM', - quantity: '30000', - rate: '100000', - amount: '400', - }, - { - item_id: 'ITEM', - quantity: '30000', - rate: '100000', - amount: '400', - }, - { - item_id: 'ITEM', - quantity: '30000', - rate: '100000', - amount: '400', - }, - ]; - - return ; + return ( + + ); } diff --git a/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFloatingActions.js b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFloatingActions.js index ddc400903..8f16e5ec2 100644 --- a/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFloatingActions.js +++ b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFloatingActions.js @@ -13,7 +13,6 @@ function AllocateLandedCostFloatingActions({ }) { // Formik context. const { isSubmitting } = useFormikContext(); - const { dialogName } = useAllocateLandedConstDialogContext(); // Handle cancel button click. diff --git a/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostForm.js b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostForm.js index 5ccffca48..d3f5ef983 100644 --- a/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostForm.js +++ b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostForm.js @@ -1,24 +1,27 @@ import React from 'react'; import { Formik } from 'formik'; +import { Intent } from '@blueprintjs/core'; +import intl from 'react-intl-universal'; import moment from 'moment'; +import { pick, omit } from 'lodash'; import 'style/pages/AllocateLandedCost/AllocateLandedCostForm.scss'; +import { AppToaster } from 'components'; import { AllocateLandedCostFormSchema } from './AllocateLandedCostForm.schema'; import { useAllocateLandedConstDialogContext } from './AllocateLandedCostDialogProvider'; import AllocateLandedCostFormContent from './AllocateLandedCostFormContent'; import withDialogActions from 'containers/Dialog/withDialogActions'; - import { compose } from 'utils'; const defaultInitialValues = { - transaction_type: 'bills', + transaction_type: 'Bill', transaction_date: moment(new Date()).format('YYYY-MM-DD'), transaction_id: '', transaction_entry_id: '', amount: '', allocation_method: 'quantity', - entries: { + items: { entry_id: '', cost: '', }, @@ -31,26 +34,59 @@ function AllocateLandedCostForm({ // #withDialogActions closeDialog, }) { - const { bill, dialogName } = useAllocateLandedConstDialogContext(); + const { items, dialogName, createLandedCostMutate } = + useAllocateLandedConstDialogContext(); // Initial form values. const initialValues = { ...defaultInitialValues, - ...bill, + ...items, }; - // Handle form submit. - const handleFormSubmit = (values, { setSubmitting }) => {}; + const handleFormSubmit = (values, { setSubmitting }) => { + setSubmitting(false); + closeDialog(dialogName); + + const entries = [values] + .filter((entry) => entry.id && entry.cost) + .map((entry) => ({ + entry_id: entry.id, + ...pick(entry, ['cost']), + })); + + const form = { + ...values, + // items:{entries}, + }; + + // Handle the request success. + const onSuccess = (response) => { + AppToaster.show({ + message: intl.get('the_landed_cost_has_been_created_successfully'), + intent: Intent.SUCCESS, + }); + setSubmitting(false); + }; + + // Handle the request error. + const onError = ({ + response: { + data: { errors }, + }, + }) => { + setSubmitting(false); + }; + createLandedCostMutate(form).then(onSuccess).catch(onError); + }; return ( - - + component={AllocateLandedCostFormContent} + /> ); } diff --git a/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormBody.js b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormBody.js index 13029efd5..a0df6980a 100644 --- a/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormBody.js +++ b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormBody.js @@ -13,12 +13,14 @@ export default function AllocateLandedCostFormBody() { field: { value }, meta: { error, touched }, }) => ( - { - setFieldValue('entries', newEntries); - }} - /> + <> + { + setFieldValue('entries', newEntries); + }} + /> + )}
diff --git a/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormContent.js b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormContent.js index c06d05a67..e1f5f08c0 100644 --- a/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormContent.js +++ b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormContent.js @@ -2,6 +2,7 @@ import React from 'react'; import { Form } from 'formik'; import AllocateLandedCostFormFields from './AllocateLandedCostFormFields'; import AllocateLandedCostFloatingActions from './AllocateLandedCostFloatingActions'; + /** * Allocate landed cost form content. */ diff --git a/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormFields.js b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormFields.js index a1639ca96..ea7b76a2f 100644 --- a/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormFields.js +++ b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormFields.js @@ -1,34 +1,39 @@ import React from 'react'; -import { FastField, ErrorMessage } from 'formik'; +import { FastField, Field, ErrorMessage, useFormikContext } from 'formik'; import { Classes, FormGroup, RadioGroup, Radio, InputGroup, - Position, } from '@blueprintjs/core'; -import { DateInput } from '@blueprintjs/datetime'; import classNames from 'classnames'; import { FormattedMessage as T } from 'components'; import intl from 'react-intl-universal'; -import { - inputIntent, - momentFormatter, - tansformDateValue, - handleDateChange, - handleStringChange, -} from 'utils'; +import { inputIntent, handleStringChange } from 'utils'; import { FieldRequiredHint, ListSelect } from 'components'; import { CLASSES } from 'common/classes'; import allocateLandedCostType from 'common/allocateLandedCostType'; -import AccountsSuggestField from 'components/AccountsSuggestField'; +import { useLandedCostTransaction } from 'hooks/query'; + import AllocateLandedCostFormBody from './AllocateLandedCostFormBody'; +import { getEntriesByTransactionId } from './utils'; /** * Allocate landed cost form fields. */ export default function AllocateLandedCostFormFields() { + const { values } = useFormikContext(); + + const { + data: { transactions }, + } = useLandedCostTransaction(values.transaction_type); + + const transactionEntry = getEntriesByTransactionId( + transactions, + values.transaction_id, + ); + return (
{/*------------Transaction type -----------*/} @@ -61,69 +66,64 @@ export default function AllocateLandedCostFormFields() { )} - {/*------------Transaction date -----------*/} - - {({ form, field: { value }, meta: { error, touched } }) => ( - } - // labelInfo={} - intent={inputIntent({ error, touched })} - helperText={} - minimal={true} - className={classNames(CLASSES.FILL, 'form-group--transaction_date')} - inline={true} - > - { - form.setFieldValue('transaction_date', formattedDate); - })} - value={tansformDateValue(value)} - popoverProps={{ - position: Position.BOTTOM, - minimal: true, - }} - /> - - )} - {/*------------ Transaction -----------*/} - - {({ form, field, meta: { error, touched } }) => ( + + {({ form, field: { value }, meta: { error, touched } }) => ( } // labelInfo={} intent={inputIntent({ error, touched })} helperText={} - className={'form-group--transaction_id'} + className={classNames(CLASSES.FILL, 'form-group--transaction_id')} inline={true} > - - form.setFieldValue('transaction_id', id) - } - inputProps={{ - placeholder: intl.get('select_transaction'), + { + form.setFieldValue('transaction_id', id); }} + filterable={false} + selectedItem={value} + selectedItemProp={'id'} + textProp={'name'} + labelProp={'id'} + defaultText={intl.get('select_transaction')} + popoverProps={{ minimal: true }} /> )} - + + {/*------------ Transaction line -----------*/} - - {({ form, field, meta: { error, touched } }) => ( + + {({ form, field: { value }, meta: { error, touched } }) => ( } intent={inputIntent({ error, touched })} helperText={} - className={'form-group--transaction_entry_id'} + className={classNames( + CLASSES.FILL, + 'form-group--transaction_entry_id', + )} inline={true} > - + { + form.setFieldValue('transaction_entry_id', id); + }} + filterable={false} + selectedItem={value} + selectedItemProp={'id'} + textProp={'name'} + labelProp={'id'} + defaultText={intl.get('select_transaction')} + popoverProps={{ minimal: true }} + /> )} - + + {/*------------ Amount -----------*/} {({ form, field, meta: { error, touched } }) => ( @@ -138,6 +138,7 @@ export default function AllocateLandedCostFormFields() { )} + {/*------------ Allocation method -----------*/} {({ form, field: { value }, meta: { touched, error } }) => ( @@ -158,7 +159,7 @@ export default function AllocateLandedCostFormFields() { inline={true} > } value="quantity" /> - } value="valuation" /> + } value="value" /> )} diff --git a/client/src/containers/Dialogs/AllocateLandedCostDialog/utils.js b/client/src/containers/Dialogs/AllocateLandedCostDialog/utils.js new file mode 100644 index 000000000..b906a6ba0 --- /dev/null +++ b/client/src/containers/Dialogs/AllocateLandedCostDialog/utils.js @@ -0,0 +1,4 @@ +export function getEntriesByTransactionId(transactions, id) { + const transaction = transactions.find((trans) => trans.id === id); + return transaction ? transaction.entries : []; +} diff --git a/client/src/hooks/query/landedCost.js b/client/src/hooks/query/landedCost.js index 860835f4c..f9bb28497 100644 --- a/client/src/hooks/query/landedCost.js +++ b/client/src/hooks/query/landedCost.js @@ -63,8 +63,11 @@ export function useLandedCostTransaction(query, props) { params: { transaction_type: query }, }, { - select: (res) => res.data.transactions, - defaultData: [], + select: (res) => res.data, + + defaultData: { + transactions: [], + }, ...props, }, ); diff --git a/client/src/lang/en/index.json b/client/src/lang/en/index.json index 6ad074587..8e4e8053e 100644 --- a/client/src/lang/en/index.json +++ b/client/src/lang/en/index.json @@ -1138,6 +1138,6 @@ "located_landed_cost":"Located Landed Cost", "delete_transaction":"Delete transaction", "cannot_delete_bill_that_has_associated_landed_cost_transactions":"Cannot delete bill that has associated landed cost transactions.", - "couldn_t_delete_expense_transaction_has_associated_located_landed_cost_transaction":"Couldn't delete expense transaction has associated located landed cost transaction" - + "couldn_t_delete_expense_transaction_has_associated_located_landed_cost_transaction":"Couldn't delete expense transaction has associated located landed cost transaction", + "the_landed_cost_has_been_created_successfully":"The landed cost has been created successfully" } \ No newline at end of file From 3a7f8a4512c06b01e284673a33f05b71e1b9f362 Mon Sep 17 00:00:00 2001 From: "a.bouhuolia" Date: Sun, 25 Jul 2021 03:59:02 +0200 Subject: [PATCH 19/24] fix: Filter financial reports by items, customers or vendors. --- client/src/components/ContactsMultiSelect.js | 60 ++--- client/src/components/FinancialStatement.js | 13 +- .../src/components/Items/ItemsMultiSelect.js | 77 +++++++ client/src/components/Items/index.js | 1 + client/src/components/MultiSelect.js | 214 +++++++++--------- client/src/components/index.js | 3 + .../APAgingSummary/APAgingSummary.js | 3 +- .../APAgingSummary/APAgingSummaryHeader.js | 36 +-- .../APAgingSummaryHeaderGeneral.js | 38 +++- .../ARAgingSummary/ARAgingSummary.js | 5 +- .../ARAgingSummary/ARAgingSummaryHeader.js | 17 +- .../ARAgingSummaryHeaderGeneral.js | 26 ++- .../BalanceSheet/BalanceSheetHeader.js | 33 +-- .../CashFlowStatementHeader.js | 18 +- .../CustomersBalanceSummaryGeneralPanel.js | 44 +++- .../CustomersBalanceSummaryHeader.js | 15 +- .../CustomersBalanceSummaryProvider.js | 15 +- .../CustomersTransactionsHeader.js | 21 +- ...CustomersTransactionsHeaderGeneralPanel.js | 32 +++ .../CustomersTransactionsProvider.js | 14 +- .../GeneralLedger/GeneralLedgerHeader.js | 21 +- .../InventoryItemDetails.js | 10 +- .../InventoryItemDetailsHeader.js | 23 +- .../InventoryItemDetailsHeaderGeneralPanel.js | 33 +++ .../InventoryItemDetailsProvider.js | 16 +- .../InventoryValuation/InventoryValuation.js | 2 +- .../InventoryValuationHeader.js | 20 +- .../InventoryValuationHeaderGeneralPanel.js | 37 ++- .../InventoryValuationProvider.js | 14 +- .../PurchasesByItems/PurchasesByItems.js | 2 +- .../PurchasesByItemsGeneralPanel.js | 34 +++ .../PurchasesByItemsHeader.js | 26 ++- .../PurchasesByItemsProvider.js | 17 +- .../SalesByItems/SalesByItemProvider.js | 14 +- .../SalesByItems/SalesByItems.js | 2 +- .../SalesByItemsHeaderGeneralPanel.js | 32 +++ .../TrialBalanceSheetHeader.js | 35 ++- .../VendorsBalanceSummaryHeader.js | 11 +- .../VendorsBalanceSummaryHeaderGeneral.js | 34 ++- .../VendorsBalanceSummaryProvider.js | 16 +- .../VendorsTransactionsHeader.js | 19 +- .../VendorsTransactionsHeaderGeneralPanel.js | 29 +++ .../VendorsTransactionsProvider.js | 15 +- .../containers/FinancialStatements/common.js | 2 +- client/src/lang/en/index.json | 9 +- .../FinancialStatements/ARAgingSummary.scss | 9 + .../SalesAndPurchasesSheet.scss | 20 ++ client/src/utils.js | 11 +- .../CustomerBalanceSummary/index.ts | 7 + .../InventoryDetails/index.ts | 4 + .../InventoryValuationSheet.ts | 4 + .../FinancialStatements/PurchasesByItem.ts | 6 + .../FinancialStatements/SalesByItems.ts | 4 + .../TransactionsByCustomers/index.ts | 23 +- .../TransactionsByVendors/index.ts | 16 +- .../VendorBalanceSummary/index.ts | 4 + .../interfaces/IInventoryValuationSheet.ts | 1 + server/src/interfaces/InventoryDetails.ts | 1 + server/src/interfaces/SalesByItemsSheet.ts | 1 + .../src/interfaces/TransactionsByCustomers.ts | 4 +- .../src/interfaces/TransactionsByVendors.ts | 4 +- .../AgingSummary/ARAgingSummaryService.ts | 2 +- .../InventoryDetailsRepository.ts | 11 +- .../InventoryDetailsService.ts | 7 +- .../InventoryValuationSheetService.ts | 13 +- .../PurchasesByItemsService.ts | 9 +- .../SalesByItems/SalesByItemsService.ts | 10 +- .../TransactionsByCustomersRepository.ts | 12 +- .../TransactionsByCustomersService.ts | 4 +- .../TransactionsByVendorRepository.ts | 17 +- .../TransactionsByVendorService.ts | 9 +- 71 files changed, 1021 insertions(+), 350 deletions(-) create mode 100644 client/src/components/Items/ItemsMultiSelect.js create mode 100644 client/src/components/Items/index.js diff --git a/client/src/components/ContactsMultiSelect.js b/client/src/components/ContactsMultiSelect.js index 5c8171294..75755ab9a 100644 --- a/client/src/components/ContactsMultiSelect.js +++ b/client/src/components/ContactsMultiSelect.js @@ -1,58 +1,57 @@ -import React, { useMemo, useCallback, useState } from 'react'; +import React, { useCallback, useState } from 'react'; import { MenuItem, Button } from '@blueprintjs/core'; -import { omit } from 'lodash'; +import intl from 'react-intl-universal'; import MultiSelect from 'components/MultiSelect'; import { FormattedMessage as T } from 'components'; -import intl from 'react-intl-universal'; +import { safeInvoke } from 'utils'; +/** + * Contacts multi-select component. + */ export default function ContactsMultiSelect({ contacts, defaultText = , buttonProps, - onCustomerSelected: onContactSelected, - ...selectProps + onContactSelect, + contactsSelected = [], + ...multiSelectProps }) { - const [selectedContacts, setSelectedContacts] = useState({}); + const [localSelected, setLocalSelected] = useState([ ...contactsSelected]); - const isContactSelect = useCallback( - (id) => typeof selectedContacts[id] !== 'undefined', - [selectedContacts], + // Detarmines the given id is selected. + const isItemSelected = useCallback( + (id) => localSelected.some(s => s === id), + [localSelected], ); + // Contact item renderer. const contactRenderer = useCallback( (contact, { handleClick }) => ( ), - [isContactSelect], + [isItemSelected], ); - const countSelected = useMemo( - () => Object.values(selectedContacts).length, - [selectedContacts], - ); + // Count selected items. + const countSelected = localSelected.length; - const onContactSelect = useCallback( + // Handle item selected. + const handleItemSelect = useCallback( ({ id }) => { - const selected = { - ...(isContactSelect(id) - ? { - ...omit(selectedContacts, [id]), - } - : { - ...selectedContacts, - [id]: true, - }), - }; - setSelectedContacts({ ...selected }); - onContactSelected && onContactSelected(selected); + const selected = isItemSelected(id) + ? localSelected.filter(s => s !== id) + : [...localSelected, id]; + + setLocalSelected([ ...selected ]); + safeInvoke(onContactSelect, selected); }, - [setSelectedContacts, selectedContacts, isContactSelect, onContactSelected], + [setLocalSelected, localSelected, isItemSelected, onContactSelect], ); return ( @@ -62,7 +61,8 @@ export default function ContactsMultiSelect({ itemRenderer={contactRenderer} popoverProps={{ minimal: true }} filterable={true} - onItemSelect={onContactSelect} + onItemSelect={handleItemSelect} + {...multiSelectProps} >
; +export default function FinancialStatements({ name, children }) { + return ( +
+ {children} +
+ ); } diff --git a/client/src/components/Items/ItemsMultiSelect.js b/client/src/components/Items/ItemsMultiSelect.js new file mode 100644 index 000000000..ba7e868bb --- /dev/null +++ b/client/src/components/Items/ItemsMultiSelect.js @@ -0,0 +1,77 @@ +import React, { useCallback, useState } from 'react'; +import { MenuItem, Button } from '@blueprintjs/core'; +import intl from 'react-intl-universal'; +import MultiSelect from 'components/MultiSelect'; +import { FormattedMessage as T } from 'components'; +import { safeInvoke } from 'utils'; + +/** + * Items multi-select. + */ +export function ItemsMultiSelect({ + items, + defaultText = , + buttonProps, + + selectedItems = [], + onItemSelect, + ...multiSelectProps +}) { + const [localSelected, setLocalSelected] = useState([...selectedItems]); + + // Detarmines the given id is selected. + const isItemSelected = useCallback( + (id) => localSelected.some((s) => s === id), + [localSelected], + ); + + // Contact item renderer. + const itemRenderer = useCallback( + (item, { handleClick }) => ( + + ), + [isItemSelected], + ); + + // Count selected items. + const countSelected = localSelected.length; + + // Handle item selected. + const handleItemSelect = useCallback( + ({ id }) => { + const selected = isItemSelected(id) + ? localSelected.filter((s) => s !== id) + : [...localSelected, id]; + + setLocalSelected([...selected]); + safeInvoke(onItemSelect, selected); + }, + [setLocalSelected, localSelected, isItemSelected, onItemSelect], + ); + + return ( + } />} + itemRenderer={itemRenderer} + popoverProps={{ minimal: true }} + filterable={true} + onItemSelect={handleItemSelect} + {...multiSelectProps} + > + diff --git a/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostForm.js b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostForm.js index d3f5ef983..3dccf250d 100644 --- a/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostForm.js +++ b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostForm.js @@ -3,7 +3,6 @@ import { Formik } from 'formik'; import { Intent } from '@blueprintjs/core'; import intl from 'react-intl-universal'; import moment from 'moment'; -import { pick, omit } from 'lodash'; import 'style/pages/AllocateLandedCost/AllocateLandedCostForm.scss'; @@ -12,8 +11,9 @@ import { AllocateLandedCostFormSchema } from './AllocateLandedCostForm.schema'; import { useAllocateLandedConstDialogContext } from './AllocateLandedCostDialogProvider'; import AllocateLandedCostFormContent from './AllocateLandedCostFormContent'; import withDialogActions from 'containers/Dialog/withDialogActions'; -import { compose } from 'utils'; +import { compose, transformToForm } from 'utils'; +// Default form initial values. const defaultInitialValues = { transaction_type: 'Bill', transaction_date: moment(new Date()).format('YYYY-MM-DD'), @@ -21,10 +21,12 @@ const defaultInitialValues = { transaction_entry_id: '', amount: '', allocation_method: 'quantity', - items: { - entry_id: '', - cost: '', - }, + items: [ + { + entry_id: '', + cost: '', + }, + ], }; /** @@ -34,32 +36,36 @@ function AllocateLandedCostForm({ // #withDialogActions closeDialog, }) { - const { items, dialogName, createLandedCostMutate } = + const { dialogName, bill, billId, createLandedCostMutate } = useAllocateLandedConstDialogContext(); // Initial form values. const initialValues = { ...defaultInitialValues, - ...items, + items: bill.entries.map((entry) => ({ + ...entry, + entry_id: entry.id, + cost: '', + })), }; // Handle form submit. const handleFormSubmit = (values, { setSubmitting }) => { setSubmitting(false); - closeDialog(dialogName); - const entries = [values] - .filter((entry) => entry.id && entry.cost) - .map((entry) => ({ - entry_id: entry.id, - ...pick(entry, ['cost']), - })); + // Filters the entries has no cost. + const entries = values.items + .filter((entry) => entry.entry_id && entry.cost) + .map((entry) => transformToForm(entry, defaultInitialValues.items[0])); + if (entries.length <= 0) { + AppToaster.show({ message: 'Something wrong!', intent: Intent.DANGER }); + return; + } const form = { ...values, - // items:{entries}, + items: entries, }; - // Handle the request success. const onSuccess = (response) => { AppToaster.show({ @@ -67,17 +73,15 @@ function AllocateLandedCostForm({ intent: Intent.SUCCESS, }); setSubmitting(false); + closeDialog(dialogName); }; // Handle the request error. - const onError = ({ - response: { - data: { errors }, - }, - }) => { + const onError = () => { setSubmitting(false); + AppToaster.show({ message: 'Something went wrong!', intent: Intent.DANGER }); }; - createLandedCostMutate(form).then(onSuccess).catch(onError); + createLandedCostMutate([billId, form]).then(onSuccess).catch(onError); }; return ( diff --git a/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostForm.schema.js b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostForm.schema.js index 176514808..0751bd67f 100644 --- a/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostForm.schema.js +++ b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostForm.schema.js @@ -8,7 +8,7 @@ const Schema = Yup.object().shape({ transaction_entry_id: Yup.string().label(intl.get('transaction_line')), amount: Yup.number().label(intl.get('amount')), allocation_method: Yup.string().trim(), - entries: Yup.array().of( + items: Yup.array().of( Yup.object().shape({ entry_id: Yup.number().nullable(), cost: Yup.number().nullable(), diff --git a/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormBody.js b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormBody.js index a0df6980a..2e57fa57e 100644 --- a/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormBody.js +++ b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormBody.js @@ -7,20 +7,18 @@ import AllocateLandedCostEntriesTable from './AllocateLandedCostEntriesTable'; export default function AllocateLandedCostFormBody() { return (
- + {({ form: { setFieldValue, values }, field: { value }, meta: { error, touched }, }) => ( - <> - { - setFieldValue('entries', newEntries); - }} - /> - + { + setFieldValue('items', newEntries); + }} + /> )}
diff --git a/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormFields.js b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormFields.js index ea7b76a2f..bb30785ce 100644 --- a/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormFields.js +++ b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormFields.js @@ -8,7 +8,7 @@ import { InputGroup, } from '@blueprintjs/core'; import classNames from 'classnames'; -import { FormattedMessage as T } from 'components'; +import { FormattedMessage as T, If } from 'components'; import intl from 'react-intl-universal'; import { inputIntent, handleStringChange } from 'utils'; import { FieldRequiredHint, ListSelect } from 'components'; @@ -29,10 +29,11 @@ export default function AllocateLandedCostFormFields() { data: { transactions }, } = useLandedCostTransaction(values.transaction_type); - const transactionEntry = getEntriesByTransactionId( + // Retrieve entries of the given transaction id. + const transactionEntries = React.useMemo(() => getEntriesByTransactionId( transactions, values.transaction_id, - ); + ), [transactions, values.transaction_id]); return (
@@ -71,7 +72,7 @@ export default function AllocateLandedCostFormFields() { {({ form, field: { value }, meta: { error, touched } }) => ( } - // labelInfo={} + labelInfo={} intent={inputIntent({ error, touched })} helperText={} className={classNames(CLASSES.FILL, 'form-group--transaction_id')} @@ -95,34 +96,37 @@ export default function AllocateLandedCostFormFields() { {/*------------ Transaction line -----------*/} - - {({ form, field: { value }, meta: { error, touched } }) => ( - } - intent={inputIntent({ error, touched })} - helperText={} - className={classNames( - CLASSES.FILL, - 'form-group--transaction_entry_id', - )} - inline={true} - > - { - form.setFieldValue('transaction_entry_id', id); - }} - filterable={false} - selectedItem={value} - selectedItemProp={'id'} - textProp={'name'} - labelProp={'id'} - defaultText={intl.get('select_transaction')} - popoverProps={{ minimal: true }} - /> - - )} - + 0}> + + {({ form, field: { value }, meta: { error, touched } }) => ( + } + intent={inputIntent({ error, touched })} + helperText={} + className={classNames( + CLASSES.FILL, + 'form-group--transaction_entry_id', + )} + inline={true} + > + { + form.setFieldValue('amount', amount) + form.setFieldValue('transaction_entry_id', id); + }} + filterable={false} + selectedItem={value} + selectedItemProp={'id'} + textProp={'name'} + labelProp={'id'} + defaultText={intl.get('select_transaction')} + popoverProps={{ minimal: true }} + /> + + )} + + {/*------------ Amount -----------*/} diff --git a/client/src/containers/Dialogs/AllocateLandedCostDialog/index.js b/client/src/containers/Dialogs/AllocateLandedCostDialog/index.js index 5798a5aba..de65ae01b 100644 --- a/client/src/containers/Dialogs/AllocateLandedCostDialog/index.js +++ b/client/src/containers/Dialogs/AllocateLandedCostDialog/index.js @@ -25,7 +25,7 @@ function AllocateLandedCostDialog({ > diff --git a/client/src/containers/Dialogs/AllocateLandedCostDialog/utils.js b/client/src/containers/Dialogs/AllocateLandedCostDialog/utils.js index b906a6ba0..fed205e5b 100644 --- a/client/src/containers/Dialogs/AllocateLandedCostDialog/utils.js +++ b/client/src/containers/Dialogs/AllocateLandedCostDialog/utils.js @@ -1,3 +1,6 @@ +/** + * Retrieve transaction entries of the given transaction id. + */ export function getEntriesByTransactionId(transactions, id) { const transaction = transactions.find((trans) => trans.id === id); return transaction ? transaction.entries : []; diff --git a/client/src/hooks/query/landedCost.js b/client/src/hooks/query/landedCost.js index f9bb28497..6249a6061 100644 --- a/client/src/hooks/query/landedCost.js +++ b/client/src/hooks/query/landedCost.js @@ -21,7 +21,8 @@ export function useCreateLandedCost(props) { const apiRequest = useApiRequest(); return useMutation( - (id) => apiRequest.post(`purchases/landed-cost/bills/${id}/allocate`), + ([id, values]) => + apiRequest.post(`purchases/landed-cost/bills/${id}/allocate`, values), { onSuccess: (res, id) => { // Common invalidate queries. diff --git a/client/src/lang/en/index.js b/client/src/lang/en/index.js index 8de51d797..897136ef1 100644 --- a/client/src/lang/en/index.js +++ b/client/src/lang/en/index.js @@ -1067,6 +1067,7 @@ export default { cash_flow_statement: 'Cash Flow Statement', statement_of_cash_flow: 'Statement of Cash Flow ', inventory_item_details: 'Inventory Item Details', - congratulations: 'Congratulations' + congratulations: 'Congratulations', + "all_items" }; diff --git a/client/src/style/pages/AllocateLandedCost/AllocateLandedCostForm.scss b/client/src/style/pages/AllocateLandedCost/AllocateLandedCostForm.scss index fe15f6d73..1040746e2 100644 --- a/client/src/style/pages/AllocateLandedCost/AllocateLandedCostForm.scss +++ b/client/src/style/pages/AllocateLandedCost/AllocateLandedCostForm.scss @@ -3,9 +3,12 @@ width: 700px; .bp3-dialog-body { + .bp3-form-group{ + margin-bottom: 18px; + } .bp3-form-group.bp3-inline { .bp3-label { - min-width: 140px; + min-width: 150px; } .bp3-form-content { width: 300px; @@ -17,6 +20,10 @@ } } + .bp3-dialog-footer{ + padding-top: 10px; + } + .bigcapital-datatable { .table { // max-height: 300px; @@ -37,6 +44,19 @@ margin-left: -1px; border-left: 1px solid #ececec; } + + .bp3-form-group{ + margin-bottom: 0; + + &:not(.bp3-intent-danger) .bp3-input{ + border: 1px solid #d0dfe2; + + &:focus{ + box-shadow: 0 0 0 1px #116cd0; + border-color: #116cd0; + } + } + } } } } From 9baf81f80385d11a2d1c012e0fddeb762be3e31b Mon Sep 17 00:00:00 2001 From: "a.bouhuolia" Date: Mon, 26 Jul 2021 19:45:16 +0200 Subject: [PATCH 21/24] fix: FastField re-rendering. fix: Allocate landed cost dialog. --- client/src/components/Card.js | 5 ++ .../components/Datatable/DatatableEditable.js | 14 +++- client/src/components/Details/index.js | 29 +++++++ client/src/components/Drawer/Drawer.js | 7 +- client/src/components/index.js | 4 +- .../Accounting/JournalsLanding/utils.js | 2 +- .../MakeJournal/MakeJournalEntriesField.js | 18 +++- .../MakeJournalEntriesHeaderFields.js | 11 ++- .../Accounting/MakeJournal/utils.js | 33 ++++++-- ...js => BillLocatedLandedCostDeleteAlert.js} | 2 +- .../AllocateLandedCostForm.js | 7 +- .../AllocateLandedCostForm.schema.js | 31 ++++--- .../AllocateLandedCostFormFields.js | 50 ++++++++--- .../Dialogs/AllocateLandedCostDialog/utils.js | 55 +++++++++++++ .../AccountDrawer/AccountDrawerContent.js | 3 + .../AccountDrawer/AccountDrawerDetails.js | 2 - .../Drawers/BillDrawer/BillDrawerAlerts.js | 4 +- .../Drawers/BillDrawer/BillDrawerContent.js | 3 + .../Drawers/BillDrawer/BillDrawerDetails.js | 2 - .../Drawers/BillDrawer/BillDrawerProvider.js | 2 + .../BillDrawer/LocatedLandedCostTable.js | 78 +++++++++++++++--- .../Drawers/BillDrawer/components.js | 72 +++++++++++----- .../ExpenseDrawer/ExpenseDrawerContent.js | 3 + .../ExpenseDrawer/ExpenseDrawerDetails.js | 1 - .../ManualJournalDrawerContent.js | 3 + .../ManualJournalDrawerDetails.js | 2 - .../ManualJournalDrawerProvider.js | 2 +- .../ManualJournalDrawerTable.js | 1 + .../Drawers/PaperTemplate/PaperTemplate.js | 4 +- .../PaymentPaperTemplate.js | 4 +- .../containers/Entries/ItemsEntriesTable.js | 3 +- client/src/containers/Entries/components.js | 45 +++++----- .../ExpenseForm/ExpenseFormEntriesField.js | 12 ++- .../ExpenseForm/ExpenseFormEntriesTable.js | 4 +- .../ExpenseForm/ExpenseFormHeaderFields.js | 13 ++- .../Expenses/ExpenseForm/components.js | 32 +++++--- .../containers/Expenses/ExpenseForm/utils.js | 28 ++++++- client/src/containers/Items/ItemFormBody.js | 49 +++++++++-- .../Items/ItemFormInventorySection.js | 7 +- .../Items/ItemFormPrimarySection.js | 7 +- client/src/containers/Items/utils.js | 82 ++++++++++++++++++- .../Purchases/Bills/BillForm/BillForm.js | 2 +- .../Bills/BillForm/BillFormHeaderFields.js | 7 +- .../Bills/BillForm/BillItemsEntriesEditor.js | 13 ++- .../Purchases/Bills/BillForm/utils.js | 27 +++++- .../Bills/BillsLanding/components.js | 2 +- .../PaymentMadeFormHeaderFields.js | 30 ++++--- .../PaymentMades/PaymentForm/utils.js | 35 ++++++-- .../EstimateForm/EstimateFormHeader.js | 1 + .../EstimateForm/EstimateFormHeaderFields.js | 11 ++- .../EstimateForm/EstimateItemsEntriesField.js | 7 +- .../Sales/Estimates/EstimateForm/utils.js | 29 ++++++- .../InvoiceForm/InvoiceFormHeaderFields.js | 20 +++-- .../InvoiceItemsEntriesEditorField.js | 7 +- .../Sales/Invoices/InvoiceForm/utils.js | 18 +++- .../PaymentReceiveHeaderFields.js | 15 +++- .../PaymentReceiveForm/utils.js | 27 +++++- .../ReceiptForm/ReceiptFormHeaderFields.js | 18 +++- .../ReceiptForm/ReceiptItemsEntriesEditor.js | 3 +- .../Sales/Receipts/ReceiptForm/utils.js | 40 ++++++++- client/src/lang/en/index.json | 8 +- .../DataTable/DataTableEditable.scss | 15 +++- client/src/style/components/Details.scss | 21 +++++ client/src/style/components/Drawer.scss | 17 ++++ .../{Drawer => Drawers}/AccountDrawer.scss | 35 -------- .../{Drawer => Drawers}/BillDrawer.scss | 52 ++++-------- .../{Drawer => Drawers}/DrawerTemplate.scss | 1 + .../{Drawer => Drawers}/ViewDetails.scss | 34 ++------ client/src/style/pages/Bills/PageForm.scss | 3 +- client/src/style/pages/Expense/PageForm.scss | 59 +++++++++---- client/src/style/pages/fonts.scss | 46 ++--------- client/src/style/variables.scss | 2 +- client/src/utils.js | 33 +++++++- .../FinancialStatements/CashFlow/CashFlow.ts | 1 - .../VendorBalanceSummary/index.ts | 16 ++-- .../api/controllers/Purchases/LandedCost.ts | 5 +- .../Purchases/LandedCost/LandedCostListing.ts | 14 +++- 77 files changed, 1046 insertions(+), 364 deletions(-) create mode 100644 client/src/components/Card.js create mode 100644 client/src/components/Details/index.js rename client/src/containers/Alerts/Bills/{BillTransactionDeleteAlert.js => BillLocatedLandedCostDeleteAlert.js} (91%) create mode 100644 client/src/style/components/Details.scss create mode 100644 client/src/style/components/Drawer.scss rename client/src/style/components/{Drawer => Drawers}/AccountDrawer.scss (72%) rename client/src/style/components/{Drawer => Drawers}/BillDrawer.scss (60%) rename client/src/style/components/{Drawer => Drawers}/DrawerTemplate.scss (99%) rename client/src/style/components/{Drawer => Drawers}/ViewDetails.scss (77%) diff --git a/client/src/components/Card.js b/client/src/components/Card.js new file mode 100644 index 000000000..e28d3816b --- /dev/null +++ b/client/src/components/Card.js @@ -0,0 +1,5 @@ +import React from 'react'; + +export default function Card({ children }) { + return
{children}
; +} diff --git a/client/src/components/Datatable/DatatableEditable.js b/client/src/components/Datatable/DatatableEditable.js index 911ef15cf..f067ea6af 100644 --- a/client/src/components/Datatable/DatatableEditable.js +++ b/client/src/components/Datatable/DatatableEditable.js @@ -4,14 +4,26 @@ import { CLASSES } from 'common/classes'; import { DataTable, If } from 'components'; import 'style/components/DataTable/DataTableEditable.scss'; +/** + * Editable datatable. + */ export default function DatatableEditable({ totalRow = false, actions, + name, className, ...tableProps }) { return ( -
+
diff --git a/client/src/components/Details/index.js b/client/src/components/Details/index.js new file mode 100644 index 000000000..ac6cc0d8e --- /dev/null +++ b/client/src/components/Details/index.js @@ -0,0 +1,29 @@ +import React from 'react'; +import className from 'classname'; + +/** + * Details menu. + */ +export function DetailsMenu({ children, vertical = false }) { + return ( +
+ {children} +
+ ); +} + +/** + * Detail item. + */ +export function DetailItem({ label, children }) { + return ( +
+
{label}
+
{children}
+
+ ); +} diff --git a/client/src/components/Drawer/Drawer.js b/client/src/components/Drawer/Drawer.js index 61c736375..09f7b93a5 100644 --- a/client/src/components/Drawer/Drawer.js +++ b/client/src/components/Drawer/Drawer.js @@ -1,9 +1,14 @@ import React from 'react'; import { Position, Drawer } from '@blueprintjs/core'; -import withDrawerActions from 'containers/Drawer/withDrawerActions'; +import 'style/components/Drawer.scss'; + +import withDrawerActions from 'containers/Drawer/withDrawerActions'; import { compose } from 'utils'; +/** + * Drawer component. + */ function DrawerComponent(props) { const { name, children, onClose, closeDrawer } = props; diff --git a/client/src/components/index.js b/client/src/components/index.js index fa0d566b7..c2d234be1 100644 --- a/client/src/components/index.js +++ b/client/src/components/index.js @@ -57,6 +57,7 @@ import Postbox from './Postbox'; import AccountsSuggestField from './AccountsSuggestField'; import MaterialProgressBar from './MaterialProgressBar'; import { MoneyFieldCell } from './DataTableCells'; +import Card from './Card'; import { ItemsMultiSelect } from './Items'; @@ -127,5 +128,6 @@ export { AccountsSuggestField, MaterialProgressBar, MoneyFieldCell, - ItemsMultiSelect + ItemsMultiSelect, + Card }; diff --git a/client/src/containers/Accounting/JournalsLanding/utils.js b/client/src/containers/Accounting/JournalsLanding/utils.js index dd6f3c21f..851eb92d8 100644 --- a/client/src/containers/Accounting/JournalsLanding/utils.js +++ b/client/src/containers/Accounting/JournalsLanding/utils.js @@ -67,4 +67,4 @@ export const useManualJournalsColumns = () => { ], [], ); -}; +}; \ No newline at end of file diff --git a/client/src/containers/Accounting/MakeJournal/MakeJournalEntriesField.js b/client/src/containers/Accounting/MakeJournal/MakeJournalEntriesField.js index 49f138903..8367dc2bd 100644 --- a/client/src/containers/Accounting/MakeJournal/MakeJournalEntriesField.js +++ b/client/src/containers/Accounting/MakeJournal/MakeJournalEntriesField.js @@ -3,16 +3,28 @@ import { FastField } from 'formik'; import classNames from 'classnames'; import { CLASSES } from 'common/classes'; import MakeJournalEntriesTable from './MakeJournalEntriesTable'; -import { defaultEntry, MIN_LINES_NUMBER } from './utils'; +import { entriesFieldShouldUpdate, defaultEntry, MIN_LINES_NUMBER } from './utils'; +import { useMakeJournalFormContext } from './MakeJournalProvider'; /** * Make journal entries field. */ export default function MakeJournalEntriesField() { + const { accounts, contacts } = useMakeJournalFormContext(); + return (
- - {({ form:{values ,setFieldValue}, field: { value }, meta: { error, touched } }) => ( + + {({ + form: { values, setFieldValue }, + field: { value }, + meta: { error, touched }, + }) => ( { setFieldValue('entries', entries); diff --git a/client/src/containers/Accounting/MakeJournal/MakeJournalEntriesHeaderFields.js b/client/src/containers/Accounting/MakeJournal/MakeJournalEntriesHeaderFields.js index 6ec4d25e4..02c329654 100644 --- a/client/src/containers/Accounting/MakeJournal/MakeJournalEntriesHeaderFields.js +++ b/client/src/containers/Accounting/MakeJournal/MakeJournalEntriesHeaderFields.js @@ -29,7 +29,10 @@ import { import withSettings from 'containers/Settings/withSettings'; import { useMakeJournalFormContext } from './MakeJournalProvider'; import withDialogActions from 'containers/Dialog/withDialogActions'; -import { useObserveJournalNoSettings } from './utils'; +import { + currenciesFieldShouldUpdate, + useObserveJournalNoSettings, +} from './utils'; /** * Make journal entries header. */ @@ -182,7 +185,11 @@ function MakeJournalEntriesHeader({ {/*------------ Currency -----------*/} - + {({ form, field: { value }, meta: { error, touched } }) => ( } diff --git a/client/src/containers/Accounting/MakeJournal/utils.js b/client/src/containers/Accounting/MakeJournal/utils.js index 6a7142f1e..143224e41 100644 --- a/client/src/containers/Accounting/MakeJournal/utils.js +++ b/client/src/containers/Accounting/MakeJournal/utils.js @@ -1,13 +1,13 @@ import React from 'react'; import { Intent } from '@blueprintjs/core'; -import { sumBy, setWith, toSafeInteger, get, values } from 'lodash'; +import { sumBy, setWith, toSafeInteger, get } from 'lodash'; import moment from 'moment'; - import { transactionNumber, updateTableRow, repeatValue, transformToForm, + defaultFastFieldShouldUpdate, } from 'utils'; import { AppToaster } from 'components'; import intl from 'react-intl-universal'; @@ -123,17 +123,17 @@ export const transformErrors = (resErrors, { setErrors, errors }) => { setEntriesErrors(error.indexes, 'contact_id', 'error'); } if ((error = getError(ERROR.ENTRIES_SHOULD_ASSIGN_WITH_CONTACT))) { - if (error.meta.find(meta => meta.contact_type === 'customer')) { + if (error.meta.find((meta) => meta.contact_type === 'customer')) { toastMessages.push( intl.get('receivable_accounts_should_assign_with_customers'), ); } - if (error.meta.find(meta => meta.contact_type === 'vendor')) { + if (error.meta.find((meta) => meta.contact_type === 'vendor')) { toastMessages.push( intl.get('payable_accounts_should_assign_with_vendors'), ); } - const indexes = error.meta.map((meta => meta.indexes)).flat(); + const indexes = error.meta.map((meta) => meta.indexes).flat(); setEntriesErrors(indexes, 'contact_id', 'error'); } if ((error = getError(ERROR.JOURNAL_NUMBER_ALREADY_EXISTS))) { @@ -159,7 +159,28 @@ export const useObserveJournalNoSettings = (prefix, nextNumber) => { const { setFieldValue } = useFormikContext(); React.useEffect(() => { - const journalNo = transactionNumber(prefix, nextNumber); + const journalNo = transactionNumber(prefix, nextNumber); setFieldValue('journal_number', journalNo); }, [setFieldValue, prefix, nextNumber]); }; + +/** + * Detarmines entries fast field should update. + */ +export const entriesFieldShouldUpdate = (newProps, oldProps) => { + return ( + newProps.accounts !== oldProps.accounts || + newProps.contacts !== oldProps.contacts || + defaultFastFieldShouldUpdate(newProps, oldProps) + ); +}; + +/** + * Detarmines currencies fast field should update. + */ +export const currenciesFieldShouldUpdate = (newProps, oldProps) => { + return ( + newProps.currencies !== oldProps.currencies || + defaultFastFieldShouldUpdate(newProps, oldProps) + ); +}; diff --git a/client/src/containers/Alerts/Bills/BillTransactionDeleteAlert.js b/client/src/containers/Alerts/Bills/BillLocatedLandedCostDeleteAlert.js similarity index 91% rename from client/src/containers/Alerts/Bills/BillTransactionDeleteAlert.js rename to client/src/containers/Alerts/Bills/BillLocatedLandedCostDeleteAlert.js index 20c93183c..826e9c925 100644 --- a/client/src/containers/Alerts/Bills/BillTransactionDeleteAlert.js +++ b/client/src/containers/Alerts/Bills/BillLocatedLandedCostDeleteAlert.js @@ -56,7 +56,7 @@ function BillTransactionDeleteAlert({ onConfirm={handleConfirmLandedCostDelete} loading={isLoading} > -

{/* */}

+

); } diff --git a/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostForm.js b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostForm.js index 3dccf250d..3b6ded70a 100644 --- a/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostForm.js +++ b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostForm.js @@ -3,6 +3,7 @@ import { Formik } from 'formik'; import { Intent } from '@blueprintjs/core'; import intl from 'react-intl-universal'; import moment from 'moment'; +import { sumBy } from 'lodash'; import 'style/pages/AllocateLandedCost/AllocateLandedCostForm.scss'; @@ -48,6 +49,7 @@ function AllocateLandedCostForm({ cost: '', })), }; + const amount = sumBy(initialValues.items, 'amount'); // Handle form submit. const handleFormSubmit = (values, { setSubmitting }) => { @@ -84,9 +86,12 @@ function AllocateLandedCostForm({ createLandedCostMutate([billId, form]).then(onSuccess).catch(onError); }; + // Computed validation schema. + const validationSchema = AllocateLandedCostFormSchema(amount); + return ( + Yup.object().shape({ + transaction_type: Yup.string().label(intl.get('transaction_type')), + transaction_date: Yup.date().label(intl.get('transaction_date')), + transaction_id: Yup.string().label(intl.get('transaction_number')), + transaction_entry_id: Yup.string().label(intl.get('transaction_line')), + amount: Yup.number().max(minAmount).label(intl.get('amount')), + allocation_method: Yup.string().trim(), + items: Yup.array().of( + Yup.object().shape({ + entry_id: Yup.number().nullable(), + cost: Yup.number().nullable(), + }), + ), + }); diff --git a/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormFields.js b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormFields.js index bb30785ce..4861a65eb 100644 --- a/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormFields.js +++ b/client/src/containers/Dialogs/AllocateLandedCostDialog/AllocateLandedCostFormFields.js @@ -17,7 +17,7 @@ import allocateLandedCostType from 'common/allocateLandedCostType'; import { useLandedCostTransaction } from 'hooks/query'; import AllocateLandedCostFormBody from './AllocateLandedCostFormBody'; -import { getEntriesByTransactionId } from './utils'; +import { getEntriesByTransactionId, allocateCostToEntries } from './utils'; /** * Allocate landed cost form fields. @@ -30,10 +30,10 @@ export default function AllocateLandedCostFormFields() { } = useLandedCostTransaction(values.transaction_type); // Retrieve entries of the given transaction id. - const transactionEntries = React.useMemo(() => getEntriesByTransactionId( - transactions, - values.transaction_id, - ), [transactions, values.transaction_id]); + const transactionEntries = React.useMemo( + () => getEntriesByTransactionId(transactions, values.transaction_id), + [transactions, values.transaction_id], + ); return (
@@ -56,6 +56,8 @@ export default function AllocateLandedCostFormFields() { items={allocateLandedCostType} onItemSelect={(type) => { setFieldValue('transaction_type', type.value); + setFieldValue('transaction_id', ''); + setFieldValue('transaction_entry_id', ''); }} filterable={false} selectedItem={value} @@ -82,13 +84,14 @@ export default function AllocateLandedCostFormFields() { items={transactions} onItemSelect={({ id }) => { form.setFieldValue('transaction_id', id); + form.setFieldValue('transaction_entry_id', ''); }} filterable={false} selectedItem={value} selectedItemProp={'id'} textProp={'name'} labelProp={'id'} - defaultText={intl.get('select_transaction')} + defaultText={intl.get('Select transaction')} popoverProps={{ minimal: true }} /> @@ -112,15 +115,21 @@ export default function AllocateLandedCostFormFields() { { - form.setFieldValue('amount', amount) + const { items, allocation_method } = form.values; + + form.setFieldValue('amount', amount); form.setFieldValue('transaction_entry_id', id); + + form.setFieldValue( + 'items', + allocateCostToEntries(amount, allocation_method, items), + ); }} filterable={false} selectedItem={value} selectedItemProp={'id'} textProp={'name'} - labelProp={'id'} - defaultText={intl.get('select_transaction')} + defaultText={intl.get('Select transaction entry')} popoverProps={{ minimal: true }} /> @@ -138,13 +147,24 @@ export default function AllocateLandedCostFormFields() { className={'form-group--amount'} inline={true} > - + { + const amount = e.target.value; + const { allocation_method, items } = form.values; + + form.setFieldValue( + 'items', + allocateCostToEntries(amount, allocation_method, items), + ); + }} + /> )} {/*------------ Allocation method -----------*/} - + {({ form, field: { value }, meta: { touched, error } }) => ( { + const { amount, items, allocation_method } = form.values; + form.setFieldValue('allocation_method', _value); + form.setFieldValue( + 'items', + allocateCostToEntries(amount, allocation_method, items), + ); })} selectedValue={value} inline={true} @@ -167,7 +193,7 @@ export default function AllocateLandedCostFormFields() { )} - + {/*------------ Allocate Landed cost Table -----------*/} diff --git a/client/src/containers/Dialogs/AllocateLandedCostDialog/utils.js b/client/src/containers/Dialogs/AllocateLandedCostDialog/utils.js index fed205e5b..21d5fa076 100644 --- a/client/src/containers/Dialogs/AllocateLandedCostDialog/utils.js +++ b/client/src/containers/Dialogs/AllocateLandedCostDialog/utils.js @@ -1,3 +1,5 @@ +import { sumBy, round } from 'lodash'; +import * as R from 'ramda'; /** * Retrieve transaction entries of the given transaction id. */ @@ -5,3 +7,56 @@ export function getEntriesByTransactionId(transactions, id) { const transaction = transactions.find((trans) => trans.id === id); return transaction ? transaction.entries : []; } + +export function allocateCostToEntries(total, allocateType, entries) { + return R.compose( + R.when( + R.always(allocateType === 'value'), + R.curry(allocateCostByValue)(total), + ), + R.when( + R.always(allocateType === 'quantity'), + R.curry(allocateCostByQuantity)(total), + ), + )(entries); +} + +/** + * Allocate total cost on entries on value. + * @param {*} entries + * @param {*} total + * @returns + */ +export function allocateCostByValue(total, entries) { + const totalAmount = sumBy(entries, 'amount'); + + const _entries = entries.map((entry) => ({ + ...entry, + percentageOfValue: entry.amount / totalAmount, + })); + + return _entries.map((entry) => ({ + ...entry, + cost: round(entry.percentageOfValue * total, 2), + })); +} + +/** + * Allocate total cost on entries by quantity. + * @param {*} entries + * @param {*} total + * @returns + */ +export function allocateCostByQuantity(total, entries) { + const totalQuantity = sumBy(entries, 'quantity'); + + const _entries = entries.map((entry) => ({ + ...entry, + percentageOfQuantity: entry.quantity / totalQuantity, + })); + + return _entries.map((entry) => ({ + ...entry, + cost: round(entry.percentageOfQuantity * total, 2), + })); +} diff --git a/client/src/containers/Drawers/AccountDrawer/AccountDrawerContent.js b/client/src/containers/Drawers/AccountDrawer/AccountDrawerContent.js index aa4bcc436..3bf0bf706 100644 --- a/client/src/containers/Drawers/AccountDrawer/AccountDrawerContent.js +++ b/client/src/containers/Drawers/AccountDrawer/AccountDrawerContent.js @@ -1,4 +1,7 @@ import React from 'react'; + +import 'style/components/Drawers/AccountDrawer.scss'; + import { AccountDrawerProvider } from './AccountDrawerProvider'; import AccountDrawerDetails from './AccountDrawerDetails'; diff --git a/client/src/containers/Drawers/AccountDrawer/AccountDrawerDetails.js b/client/src/containers/Drawers/AccountDrawer/AccountDrawerDetails.js index 230d89beb..30d5a30fb 100644 --- a/client/src/containers/Drawers/AccountDrawer/AccountDrawerDetails.js +++ b/client/src/containers/Drawers/AccountDrawer/AccountDrawerDetails.js @@ -5,8 +5,6 @@ import AccountDrawerHeader from './AccountDrawerHeader'; import AccountDrawerTable from './AccountDrawerTable'; import { useAccountDrawerContext } from './AccountDrawerProvider'; -import 'style/components/Drawer/AccountDrawer.scss'; - /** * Account view details. */ diff --git a/client/src/containers/Drawers/BillDrawer/BillDrawerAlerts.js b/client/src/containers/Drawers/BillDrawer/BillDrawerAlerts.js index 278671574..bc8ca4660 100644 --- a/client/src/containers/Drawers/BillDrawer/BillDrawerAlerts.js +++ b/client/src/containers/Drawers/BillDrawer/BillDrawerAlerts.js @@ -1,5 +1,5 @@ import React from 'react'; -import BillTransactionDeleteAlert from 'containers/Alerts/Bills/BillTransactionDeleteAlert'; +import BillLocatedLandedCostDeleteAlert from 'containers/Alerts/Bills/BillLocatedLandedCostDeleteAlert'; /** * Bill drawer alert. @@ -7,7 +7,7 @@ import BillTransactionDeleteAlert from 'containers/Alerts/Bills/BillTransactionD export default function BillDrawerAlerts() { return (
- +
); } diff --git a/client/src/containers/Drawers/BillDrawer/BillDrawerContent.js b/client/src/containers/Drawers/BillDrawer/BillDrawerContent.js index ec1579009..a9a49c0ce 100644 --- a/client/src/containers/Drawers/BillDrawer/BillDrawerContent.js +++ b/client/src/containers/Drawers/BillDrawer/BillDrawerContent.js @@ -1,4 +1,7 @@ import React from 'react'; + +import 'style/components/Drawers/BillDrawer.scss'; + import { BillDrawerProvider } from './BillDrawerProvider'; import BillDrawerDetails from './BillDrawerDetails'; import BillDrawerAlerts from './BillDrawerAlerts'; diff --git a/client/src/containers/Drawers/BillDrawer/BillDrawerDetails.js b/client/src/containers/Drawers/BillDrawer/BillDrawerDetails.js index 28bc4fc3a..bd9de99e1 100644 --- a/client/src/containers/Drawers/BillDrawer/BillDrawerDetails.js +++ b/client/src/containers/Drawers/BillDrawer/BillDrawerDetails.js @@ -4,8 +4,6 @@ import intl from 'react-intl-universal'; import LocatedLandedCostTable from './LocatedLandedCostTable'; -import 'style/components/Drawer/BillDrawer.scss'; - /** * Bill view details. */ diff --git a/client/src/containers/Drawers/BillDrawer/BillDrawerProvider.js b/client/src/containers/Drawers/BillDrawer/BillDrawerProvider.js index 5bd4a8246..47bdd53b1 100644 --- a/client/src/containers/Drawers/BillDrawer/BillDrawerProvider.js +++ b/client/src/containers/Drawers/BillDrawer/BillDrawerProvider.js @@ -18,7 +18,9 @@ function BillDrawerProvider({ billId, ...props }) { //provider. const provider = { transactions, + billId, }; + return ( { - openAlert('transaction-delete', { BillId: id }); + openAlert('bill-located-cost-delete', { BillId: id }); + }; + + // Handle allocate landed cost button click. + const handleAllocateCostClick = () => { + openDialog('allocate-landed-cost', { billId }); + }; + + // Handle from transaction link click. + const handleFromTransactionClick = (original) => { + const { from_transaction_type, from_transaction_id } = original; + + switch (from_transaction_type) { + case 'Expense': + openDrawer('expense-drawer', { expenseId: from_transaction_id }); + break; + + case 'Bill': + default: + openDrawer('bill-drawer', { billId: from_transaction_id }); + break; + } }; return ( - +
+ + +
); } -export default compose(withAlertsActions)(LocatedLandedCostTable); +export default compose( + withAlertsActions, + withDialogActions, + withDrawerActions, +)(LocatedLandedCostTable); diff --git a/client/src/containers/Drawers/BillDrawer/components.js b/client/src/containers/Drawers/BillDrawer/components.js index fb181da03..2a4d29f93 100644 --- a/client/src/containers/Drawers/BillDrawer/components.js +++ b/client/src/containers/Drawers/BillDrawer/components.js @@ -3,7 +3,6 @@ import intl from 'react-intl-universal'; import { Intent, MenuItem, Menu } from '@blueprintjs/core'; import { safeCallback } from 'utils'; import { Icon } from 'components'; - /** * Actions menu. */ @@ -20,22 +19,57 @@ export function ActionsMenu({ row: { original }, payload: { onDelete } }) { ); } -export function useLocatedLandedCostColumns() { - return React.useMemo(() => [ - { - Header: intl.get('name'), - accessor: 'description', - width: 150, - }, - { - Header: intl.get('amount'), - accessor: 'amount', - width: 100, - }, - { - Header: intl.get('allocation_method'), - accessor: 'allocation_method', - width: 100, - }, - ]); +/** + * From transaction table cell. + */ +export function FromTransactionCell({ + row: { original }, + payload: { onFromTranscationClick } +}) { + // Handle the link click + const handleAnchorClick = () => { + onFromTranscationClick && onFromTranscationClick(original); + }; + + return ( + + {original.from_transaction_type} → {original.from_transaction_id} + + ); +} + +/** + * Retrieve bill located landed cost table columns. + */ +export function useLocatedLandedCostColumns() { + return React.useMemo( + () => [ + { + Header: intl.get('name'), + accessor: 'description', + width: 150, + className: 'name', + }, + { + Header: intl.get('amount'), + accessor: 'formatted_amount', + width: 100, + className: 'amount', + }, + { + id: 'from_transaction', + Header: intl.get('From transaction'), + Cell: FromTransactionCell, + width: 100, + className: 'from-transaction', + }, + { + Header: intl.get('allocation_method'), + accessor: 'allocation_method_formatted', + width: 100, + className: 'allocation-method', + }, + ], + [], + ); } diff --git a/client/src/containers/Drawers/ExpenseDrawer/ExpenseDrawerContent.js b/client/src/containers/Drawers/ExpenseDrawer/ExpenseDrawerContent.js index 37f3bd8cb..fca141626 100644 --- a/client/src/containers/Drawers/ExpenseDrawer/ExpenseDrawerContent.js +++ b/client/src/containers/Drawers/ExpenseDrawer/ExpenseDrawerContent.js @@ -1,4 +1,7 @@ import React from 'react'; + +import 'style/components/Drawers/ViewDetails.scss'; + import { ExpenseDrawerProvider } from './ExpenseDrawerProvider'; import ExpenseDrawerDetails from './ExpenseDrawerDetails'; diff --git a/client/src/containers/Drawers/ExpenseDrawer/ExpenseDrawerDetails.js b/client/src/containers/Drawers/ExpenseDrawer/ExpenseDrawerDetails.js index 763d6304d..8ddbf6674 100644 --- a/client/src/containers/Drawers/ExpenseDrawer/ExpenseDrawerDetails.js +++ b/client/src/containers/Drawers/ExpenseDrawer/ExpenseDrawerDetails.js @@ -4,7 +4,6 @@ import ExpenseDrawerHeader from './ExpenseDrawerHeader'; import ExpenseDrawerTable from './ExpenseDrawerTable'; import ExpenseDrawerFooter from './ExpenseDrawerFooter'; import { useExpenseDrawerContext } from './ExpenseDrawerProvider'; -import 'style/components/Drawer/ViewDetails.scss'; /** * Expense view details. diff --git a/client/src/containers/Drawers/ManualJournalDrawer/ManualJournalDrawerContent.js b/client/src/containers/Drawers/ManualJournalDrawer/ManualJournalDrawerContent.js index c6ff9ccc0..905dd2174 100644 --- a/client/src/containers/Drawers/ManualJournalDrawer/ManualJournalDrawerContent.js +++ b/client/src/containers/Drawers/ManualJournalDrawer/ManualJournalDrawerContent.js @@ -1,4 +1,7 @@ import React from 'react'; + +import 'style/components/Drawers/ViewDetails.scss'; + import { ManualJournalDrawerProvider } from './ManualJournalDrawerProvider'; import ManualJournalDrawerDetails from './ManualJournalDrawerDetails'; diff --git a/client/src/containers/Drawers/ManualJournalDrawer/ManualJournalDrawerDetails.js b/client/src/containers/Drawers/ManualJournalDrawer/ManualJournalDrawerDetails.js index 804b47c8e..e5129cb5c 100644 --- a/client/src/containers/Drawers/ManualJournalDrawer/ManualJournalDrawerDetails.js +++ b/client/src/containers/Drawers/ManualJournalDrawer/ManualJournalDrawerDetails.js @@ -6,8 +6,6 @@ import ManualJournalDrawerFooter from './ManualJournalDrawerFooter'; import { useManualJournalDrawerContext } from 'containers/Drawers/ManualJournalDrawer/ManualJournalDrawerProvider'; -import 'style/components/Drawer/ViewDetails.scss'; - /** * Manual journal view details. */ diff --git a/client/src/containers/Drawers/ManualJournalDrawer/ManualJournalDrawerProvider.js b/client/src/containers/Drawers/ManualJournalDrawer/ManualJournalDrawerProvider.js index 966971e19..6b2b531f0 100644 --- a/client/src/containers/Drawers/ManualJournalDrawer/ManualJournalDrawerProvider.js +++ b/client/src/containers/Drawers/ManualJournalDrawer/ManualJournalDrawerProvider.js @@ -1,7 +1,7 @@ import React from 'react'; +import intl from 'react-intl-universal'; import { useJournal } from 'hooks/query'; import { DashboardInsider, DrawerHeaderContent } from 'components'; -import intl from 'react-intl-universal'; const ManualJournalDrawerContext = React.createContext(); diff --git a/client/src/containers/Drawers/ManualJournalDrawer/ManualJournalDrawerTable.js b/client/src/containers/Drawers/ManualJournalDrawer/ManualJournalDrawerTable.js index e0d59a46f..26798517b 100644 --- a/client/src/containers/Drawers/ManualJournalDrawer/ManualJournalDrawerTable.js +++ b/client/src/containers/Drawers/ManualJournalDrawer/ManualJournalDrawerTable.js @@ -69,6 +69,7 @@ export default function ManualJournalDrawerTable({ return (
+

Description: {description} diff --git a/client/src/containers/Drawers/PaperTemplate/PaperTemplate.js b/client/src/containers/Drawers/PaperTemplate/PaperTemplate.js index 9c12fc7fd..d55b83c97 100644 --- a/client/src/containers/Drawers/PaperTemplate/PaperTemplate.js +++ b/client/src/containers/Drawers/PaperTemplate/PaperTemplate.js @@ -1,11 +1,13 @@ import React from 'react'; + +import 'style/components/Drawers/DrawerTemplate.scss'; + import PaperTemplateHeader from './PaperTemplateHeader'; import PaperTemplateTable from './PaperTemplateTable'; import PaperTemplateFooter from './PaperTemplateFooter'; import { updateItemsEntriesTotal } from 'containers/Entries/utils'; import intl from 'react-intl-universal'; -import 'style/components/Drawer/DrawerTemplate.scss'; function PaperTemplate({ labels: propLabels, paperData, entries }) { const labels = { diff --git a/client/src/containers/Drawers/PaymentPaperTemplate/PaymentPaperTemplate.js b/client/src/containers/Drawers/PaymentPaperTemplate/PaymentPaperTemplate.js index b0fbb1ebb..12fb4e403 100644 --- a/client/src/containers/Drawers/PaymentPaperTemplate/PaymentPaperTemplate.js +++ b/client/src/containers/Drawers/PaymentPaperTemplate/PaymentPaperTemplate.js @@ -1,9 +1,11 @@ import React from 'react'; + +import 'style/components/Drawers/DrawerTemplate.scss'; + import PaymentPaperTemplateHeader from './PaymentPaperTemplateHeader'; import PaymentPaperTemplateTable from './PaymentPaperTemplateTable'; import intl from 'react-intl-universal'; -import 'style/components/Drawer/DrawerTemplate.scss'; export default function PaymentPaperTemplate({ labels: propLabels, diff --git a/client/src/containers/Entries/ItemsEntriesTable.js b/client/src/containers/Entries/ItemsEntriesTable.js index c4a482012..ce4e0ce9f 100644 --- a/client/src/containers/Entries/ItemsEntriesTable.js +++ b/client/src/containers/Entries/ItemsEntriesTable.js @@ -30,6 +30,7 @@ function ItemsEntriesTable({ linesNumber, currencyCode, itemType, // sellable or purchasable + landedCost = false }) { const [rows, setRows] = React.useState(initialEntries); const [rowItem, setRowItem] = React.useState(null); @@ -94,7 +95,7 @@ function ItemsEntriesTable({ }, [entries, rows]); // Editiable items entries columns. - const columns = useEditableItemsEntriesColumns(); + const columns = useEditableItemsEntriesColumns({ landedCost }); // Handles the editor data update. const handleUpdateData = useCallback( diff --git a/client/src/containers/Entries/components.js b/client/src/containers/Entries/components.js index a9c53596e..59db5385d 100644 --- a/client/src/containers/Entries/components.js +++ b/client/src/containers/Entries/components.js @@ -10,6 +10,7 @@ import { ItemsListCell, PercentFieldCell, NumericInputCell, + CheckBoxFieldCell, } from 'components/DataTableCells'; /** @@ -90,27 +91,18 @@ export function IndexTableCell({ row: { index } }) { return {index + 1}; } -/** - * Landed cost cell. - */ -const LandedCostCell = ({ - row: { index }, - column: { id }, - cell: { value: initialValue }, - data, - payload, -}) => { - return ; -}; - /** * Landed cost header cell. */ const LandedCostHeaderCell = () => { return ( <> - - + + ); }; @@ -118,7 +110,7 @@ const LandedCostHeaderCell = () => { /** * Retrieve editable items entries columns. */ -export function useEditableItemsEntriesColumns() { +export function useEditableItemsEntriesColumns({ landedCost }) { return React.useMemo( () => [ { @@ -182,14 +174,19 @@ export function useEditableItemsEntriesColumns() { width: 100, className: 'total', }, - { - Header: '', - accessor: 'landed_cost', - Cell: LandedCostCell, - width: 70, - disableSortBy: true, - disableResizing: true, - }, + ...(landedCost + ? [ + { + Header: LandedCostHeaderCell, + accessor: 'landed_cost', + Cell: CheckBoxFieldCell, + width: 100, + disableSortBy: true, + disableResizing: true, + className: 'landed-cost', + }, + ] + : []), { Header: '', accessor: 'action', diff --git a/client/src/containers/Expenses/ExpenseForm/ExpenseFormEntriesField.js b/client/src/containers/Expenses/ExpenseForm/ExpenseFormEntriesField.js index 437b10bb8..abec92e0f 100644 --- a/client/src/containers/Expenses/ExpenseForm/ExpenseFormEntriesField.js +++ b/client/src/containers/Expenses/ExpenseForm/ExpenseFormEntriesField.js @@ -1,14 +1,22 @@ import { FastField } from 'formik'; import React from 'react'; import ExpenseFormEntriesTable from './ExpenseFormEntriesTable'; -import { defaultExpenseEntry } from './utils'; +import { useExpenseFormContext } from './ExpenseFormPageProvider'; +import { defaultExpenseEntry, accountsFieldShouldUpdate } from './utils'; /** * Expense form entries field. */ export default function ExpenseFormEntriesField({ linesNumber = 4 }) { + // Expense form context. + const { accounts } = useExpenseFormContext(); + return ( - + {({ form: { values, setFieldValue }, field: { value }, diff --git a/client/src/containers/Expenses/ExpenseForm/ExpenseFormEntriesTable.js b/client/src/containers/Expenses/ExpenseForm/ExpenseFormEntriesTable.js index ab1c2cdbc..79ee363be 100644 --- a/client/src/containers/Expenses/ExpenseForm/ExpenseFormEntriesTable.js +++ b/client/src/containers/Expenses/ExpenseForm/ExpenseFormEntriesTable.js @@ -22,12 +22,13 @@ export default function ExpenseFormEntriesTable({ error, onChange, currencyCode, + landedCost = true, }) { // Expense form context. const { accounts } = useExpenseFormContext(); // Memorized data table columns. - const columns = useExpenseFormTableColumns(); + const columns = useExpenseFormTableColumns({ landedCost }); // Handles update datatable data. const handleUpdateData = useCallback( @@ -61,6 +62,7 @@ export default function ExpenseFormEntriesTable({ return ( - + {({ form, field: { value }, meta: { error, touched } }) => ( } @@ -118,7 +123,11 @@ export default function ExpenseFormHeader() { )} - + {({ form, field: { value }, meta: { error, touched } }) => ( } diff --git a/client/src/containers/Expenses/ExpenseForm/components.js b/client/src/containers/Expenses/ExpenseForm/components.js index 21d71cf20..64f275d00 100644 --- a/client/src/containers/Expenses/ExpenseForm/components.js +++ b/client/src/containers/Expenses/ExpenseForm/components.js @@ -56,8 +56,12 @@ const ActionsCellRenderer = ({ const LandedCostHeaderCell = () => { return ( <> - - + + ); }; @@ -87,7 +91,7 @@ function ExpenseAccountFooterCell() { /** * Retrieve expense form table entries columns. */ -export function useExpenseFormTableColumns() { +export function useExpenseFormTableColumns({ landedCost }) { return React.useMemo( () => [ { @@ -127,15 +131,19 @@ export function useExpenseFormTableColumns() { className: 'description', width: 100, }, - { - Header: LandedCostHeaderCell, - accessor: 'landed_cost', - Cell: CheckBoxFieldCell, - disableSortBy: true, - disableResizing: true, - width: 70, - className: 'landed_cost', - }, + ...(landedCost + ? [ + { + Header: LandedCostHeaderCell, + accessor: 'landed_cost', + Cell: CheckBoxFieldCell, + disableSortBy: true, + disableResizing: true, + width: 100, + className: 'landed-cost', + }, + ] + : []), { Header: '', accessor: 'action', diff --git a/client/src/containers/Expenses/ExpenseForm/utils.js b/client/src/containers/Expenses/ExpenseForm/utils.js index d418c42a3..f9d8392c3 100644 --- a/client/src/containers/Expenses/ExpenseForm/utils.js +++ b/client/src/containers/Expenses/ExpenseForm/utils.js @@ -1,7 +1,11 @@ import { AppToaster } from 'components'; import moment from 'moment'; import intl from 'react-intl-universal'; -import { transformToForm, repeatValue } from 'utils'; +import { + defaultFastFieldShouldUpdate, + transformToForm, + repeatValue, +} from 'utils'; const ERROR = { EXPENSE_ALREADY_PUBLISHED: 'EXPENSE.ALREADY.PUBLISHED', @@ -27,7 +31,7 @@ export const defaultExpenseEntry = { amount: '', expense_account_id: '', description: '', - landed_cost: 0, + landed_cost: false, }; export const defaultExpense = { @@ -62,3 +66,23 @@ export const transformToEditForm = ( ], }; }; + +/** + * Detarmine cusotmers fast-field should update. + */ +export const customersFieldShouldUpdate = (newProps, oldProps) => { + return ( + newProps.customers !== oldProps.customers || + defaultFastFieldShouldUpdate(newProps, oldProps) + ); +}; + +/** + * Detarmine accounts fast-field should update. + */ +export const accountsFieldShouldUpdate = (newProps, oldProps) => { + return ( + newProps.accounts !== oldProps.accounts || + defaultFastFieldShouldUpdate(newProps, oldProps) + ); +}; diff --git a/client/src/containers/Items/ItemFormBody.js b/client/src/containers/Items/ItemFormBody.js index 9123ebf4d..481e42db9 100644 --- a/client/src/containers/Items/ItemFormBody.js +++ b/client/src/containers/Items/ItemFormBody.js @@ -1,5 +1,5 @@ import React from 'react'; -import { FastField, Field, ErrorMessage } from 'formik'; +import { useFormikContext, FastField, Field, ErrorMessage } from 'formik'; import { FormGroup, Classes, @@ -23,12 +23,21 @@ import withSettings from 'containers/Settings/withSettings'; import { ACCOUNT_PARENT_TYPE } from 'common/accountTypes'; import { compose, inputIntent } from 'utils'; +import { + sellDescriptionFieldShouldUpdate, + sellAccountFieldShouldUpdate, + sellPriceFieldShouldUpdate, + costPriceFieldShouldUpdate, + costAccountFieldShouldUpdate, + purchaseDescFieldShouldUpdate, +} from './utils'; /** * Item form body. */ function ItemFormBody({ baseCurrency }) { const { accounts } = useItemFormContext(); + const { values } = useFormikContext(); return (

@@ -53,7 +62,11 @@ function ItemFormBody({ baseCurrency }) { {/*------------- Selling price ------------- */} - + {({ form, field: { value }, meta: { error, touched } }) => ( } @@ -78,7 +91,12 @@ function ItemFormBody({ baseCurrency }) { {/*------------- Selling account ------------- */} - + {({ form, field: { value }, meta: { error, touched } }) => ( } @@ -107,7 +125,11 @@ function ItemFormBody({ baseCurrency }) { )} - + {({ form: { values }, field, meta: { error, touched } }) => ( } @@ -146,7 +168,11 @@ function ItemFormBody({ baseCurrency }) { {/*------------- Cost price ------------- */} - + {({ field, form, field: { value }, meta: { error, touched } }) => ( } @@ -171,7 +197,12 @@ function ItemFormBody({ baseCurrency }) { {/*------------- Cost account ------------- */} - + {({ form, field: { value }, meta: { error, touched } }) => ( } @@ -200,7 +231,11 @@ function ItemFormBody({ baseCurrency }) { )} - + {({ form: { values }, field, meta: { error, touched } }) => ( } diff --git a/client/src/containers/Items/ItemFormInventorySection.js b/client/src/containers/Items/ItemFormInventorySection.js index c4684fcec..d5a28d7e3 100644 --- a/client/src/containers/Items/ItemFormInventorySection.js +++ b/client/src/containers/Items/ItemFormInventorySection.js @@ -8,6 +8,7 @@ import classNames from 'classnames'; import withSettings from 'containers/Settings/withSettings'; +import { accountsFieldShouldUpdate } from './utils'; import { compose, inputIntent } from 'utils'; import { ACCOUNT_TYPE } from 'common/accountTypes'; import { useItemFormContext } from './ItemFormProvider'; @@ -27,7 +28,11 @@ function ItemFormInventorySection({ baseCurrency }) { {/*------------- Inventory account ------------- */} - + {({ form, field: { value }, meta: { touched, error } }) => ( } diff --git a/client/src/containers/Items/ItemFormPrimarySection.js b/client/src/containers/Items/ItemFormPrimarySection.js index db6ae61d8..118601781 100644 --- a/client/src/containers/Items/ItemFormPrimarySection.js +++ b/client/src/containers/Items/ItemFormPrimarySection.js @@ -21,6 +21,7 @@ import { CLASSES } from 'common/classes'; import { useItemFormContext } from './ItemFormProvider'; import { handleStringChange, inputIntent } from 'utils'; +import { categoriesFieldShouldUpdate } from './utils'; /** * Item form primary section. @@ -130,7 +131,11 @@ export default function ItemFormPrimarySection() { {/*----------- Item category ----------*/} - + {({ form, field: { value }, meta: { error, touched } }) => ( } diff --git a/client/src/containers/Items/utils.js b/client/src/containers/Items/utils.js index 1a5abe31d..406f54fa1 100644 --- a/client/src/containers/Items/utils.js +++ b/client/src/containers/Items/utils.js @@ -1,6 +1,7 @@ import intl from 'react-intl-universal'; import { Intent } from '@blueprintjs/core'; import { AppToaster } from 'components'; +import { defaultFastFieldShouldUpdate } from 'utils'; export const transitionItemTypeKeyToLabel = (itemTypeKey) => { const table = { @@ -28,7 +29,9 @@ export const handleDeleteErrors = (errors) => { ) ) { AppToaster.show({ - message: intl.get('you_could_not_delete_item_that_has_associated_inventory_adjustments_transacions'), + message: intl.get( + 'you_could_not_delete_item_that_has_associated_inventory_adjustments_transacions', + ), intent: Intent.DANGER, }); } @@ -38,8 +41,83 @@ export const handleDeleteErrors = (errors) => { ) ) { AppToaster.show({ - message: intl.get('cannot_change_item_type_to_inventory_with_item_has_associated_transactions'), + message: intl.get( + 'cannot_change_item_type_to_inventory_with_item_has_associated_transactions', + ), intent: Intent.DANGER, }); } }; + +/** + * Detarmines accounts fast field should update. + */ +export const accountsFieldShouldUpdate = (newProps, oldProps) => { + return ( + newProps.accounts !== oldProps.accounts || + defaultFastFieldShouldUpdate(newProps, oldProps) + ); +}; + +/** + * Detarmines categories fast field should update. + */ +export const categoriesFieldShouldUpdate = (newProps, oldProps) => { + return ( + newProps.categories !== oldProps.categories || + defaultFastFieldShouldUpdate(newProps, oldProps) + ); +}; + +/** + * Sell price fast field should update. + */ +export const sellPriceFieldShouldUpdate = (newProps, oldProps) => { + return ( + newProps.sellable !== oldProps.sellable || + defaultFastFieldShouldUpdate(newProps, oldProps) + ); +}; + +/** + * Sell account fast field should update. + */ +export const sellAccountFieldShouldUpdate = (newProps, oldProps) => { + return ( + newProps.accounts !== oldProps.accounts || + newProps.sellable !== oldProps.sellable || + defaultFastFieldShouldUpdate(newProps, oldProps) + ); +}; + +/** + * Sell description fast field should update. + */ +export const sellDescriptionFieldShouldUpdate = (newProps, oldProps) => { + return ( + newProps.sellable !== oldProps.sellable || + defaultFastFieldShouldUpdate(newProps, oldProps) + ); +}; + +export const costAccountFieldShouldUpdate = (newProps, oldProps) => { + return ( + newProps.accounts !== oldProps.accounts || + newProps.purchasable !== oldProps.purchasable || + defaultFastFieldShouldUpdate(newProps, oldProps) + ); +}; + +export const costPriceFieldShouldUpdate = (newProps, oldProps) => { + return ( + newProps.purchasable !== oldProps.purchasable || + defaultFastFieldShouldUpdate(newProps, oldProps) + ); +}; + +export const purchaseDescFieldShouldUpdate = (newProps, oldProps) => { + return ( + newProps.purchasable !== oldProps.purchasable || + defaultFastFieldShouldUpdate(newProps, oldProps) + ); +}; diff --git a/client/src/containers/Purchases/Bills/BillForm/BillForm.js b/client/src/containers/Purchases/Bills/BillForm/BillForm.js index 02f6103ca..11dc49733 100644 --- a/client/src/containers/Purchases/Bills/BillForm/BillForm.js +++ b/client/src/containers/Purchases/Bills/BillForm/BillForm.js @@ -48,7 +48,7 @@ function BillForm({ currency_code: baseCurrency, }), }), - [bill], + [bill, baseCurrency], ); // Transform response error to fields. diff --git a/client/src/containers/Purchases/Bills/BillForm/BillFormHeaderFields.js b/client/src/containers/Purchases/Bills/BillForm/BillFormHeaderFields.js index 8f5086849..dfa2a5d28 100644 --- a/client/src/containers/Purchases/Bills/BillForm/BillFormHeaderFields.js +++ b/client/src/containers/Purchases/Bills/BillForm/BillFormHeaderFields.js @@ -7,6 +7,7 @@ import classNames from 'classnames'; import { CLASSES } from 'common/classes'; import { ContactSelecetList, FieldRequiredHint, Icon } from 'components'; +import { vendorsFieldShouldUpdate } from './utils'; import { useBillFormContext } from './BillFormProvider'; import withDialogActions from 'containers/Dialog/withDialogActions'; @@ -28,7 +29,11 @@ function BillFormHeader() { return (
{/* ------- Vendor name ------ */} - + {({ form, field: { value }, meta: { error, touched } }) => ( } diff --git a/client/src/containers/Purchases/Bills/BillForm/BillItemsEntriesEditor.js b/client/src/containers/Purchases/Bills/BillForm/BillItemsEntriesEditor.js index 25136485e..9ec38558d 100644 --- a/client/src/containers/Purchases/Bills/BillForm/BillItemsEntriesEditor.js +++ b/client/src/containers/Purchases/Bills/BillForm/BillItemsEntriesEditor.js @@ -4,13 +4,23 @@ import { FastField } from 'formik'; import { CLASSES } from 'common/classes'; import { useBillFormContext } from './BillFormProvider'; import ItemsEntriesTable from 'containers/Entries/ItemsEntriesTable'; +import { + entriesFieldShouldUpdate +} from './utils'; +/** + * Bill form body. + */ export default function BillFormBody({ defaultBill }) { const { items } = useBillFormContext(); return (
- + {({ form: { values, setFieldValue }, field: { value }, @@ -25,6 +35,7 @@ export default function BillFormBody({ defaultBill }) { errors={error} linesNumber={4} currencyCode={values.currency_code} + landedCost={true} /> )} diff --git a/client/src/containers/Purchases/Bills/BillForm/utils.js b/client/src/containers/Purchases/Bills/BillForm/utils.js index dba011cf7..9723b4f97 100644 --- a/client/src/containers/Purchases/Bills/BillForm/utils.js +++ b/client/src/containers/Purchases/Bills/BillForm/utils.js @@ -2,7 +2,11 @@ import moment from 'moment'; import intl from 'react-intl-universal'; import { Intent } from '@blueprintjs/core'; import { AppToaster } from 'components'; -import { transformToForm, repeatValue } from 'utils'; +import { + defaultFastFieldShouldUpdate, + transformToForm, + repeatValue, +} from 'utils'; export const MIN_LINES_NUMBER = 4; @@ -13,6 +17,7 @@ export const defaultBillEntry = { discount: '', quantity: '', description: '', + landed_cost: false, }; export const defaultBill = { @@ -62,3 +67,23 @@ export const handleDeleteErrors = (errors) => { }); } }; + +/** + * Detarmines vendors fast field should update + */ +export const vendorsFieldShouldUpdate = (newProps, oldProps) => { + return ( + newProps.vendors !== oldProps.vendors || + defaultFastFieldShouldUpdate(newProps, oldProps) + ); +}; + +/** + * Detarmines entries fast field should update. + */ +export const entriesFieldShouldUpdate = (newProps, oldProps) => { + return ( + newProps.items !== oldProps.items || + defaultFastFieldShouldUpdate(newProps, oldProps) + ); +}; diff --git a/client/src/containers/Purchases/Bills/BillsLanding/components.js b/client/src/containers/Purchases/Bills/BillsLanding/components.js index cf2b6c73a..de7271f8d 100644 --- a/client/src/containers/Purchases/Bills/BillsLanding/components.js +++ b/client/src/containers/Purchases/Bills/BillsLanding/components.js @@ -59,7 +59,7 @@ export function ActionsMenu({ /> } + icon={} text={intl.get('allocate_landed_coast')} onClick={safeCallback(onAllocateLandedCost, original)} /> diff --git a/client/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeFormHeaderFields.js b/client/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeFormHeaderFields.js index 88fdb5652..f22000973 100644 --- a/client/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeFormHeaderFields.js +++ b/client/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeFormHeaderFields.js @@ -36,6 +36,7 @@ import { fullAmountPaymentEntries, amountPaymentEntries, } from 'utils'; +import { accountsFieldShouldUpdate, vendorsFieldShouldUpdate } from './utils'; /** * Payment made form header fields. @@ -48,17 +49,14 @@ function PaymentMadeFormHeaderFields({ baseCurrency }) { } = useFormikContext(); // Payment made form context. - const { - vendors, - accounts, - isNewMode, - setPaymentVendorId, - } = usePaymentMadeFormContext(); + const { vendors, accounts, isNewMode, setPaymentVendorId } = + usePaymentMadeFormContext(); // Sumation of payable full-amount. - const payableFullAmount = useMemo(() => safeSumBy(entries, 'due_amount'), [ - entries, - ]); + const payableFullAmount = useMemo( + () => safeSumBy(entries, 'due_amount'), + [entries], + ); // Handle receive full-amount click. const handleReceiveFullAmountClick = () => { @@ -78,7 +76,11 @@ function PaymentMadeFormHeaderFields({ baseCurrency }) { return (
{/* ------------ Vendor name ------------ */} - + {({ form, field: { value }, meta: { error, touched } }) => ( } @@ -157,7 +159,7 @@ function PaymentMadeFormHeaderFields({ baseCurrency }) { small={true} minimal={true} > - ( + ( ) @@ -184,7 +186,11 @@ function PaymentMadeFormHeaderFields({ baseCurrency }) { {/* ------------ Payment account ------------ */} - + {({ form, field: { value }, meta: { error, touched } }) => ( } diff --git a/client/src/containers/Purchases/PaymentMades/PaymentForm/utils.js b/client/src/containers/Purchases/PaymentMades/PaymentForm/utils.js index 54aa39bca..0467c07c4 100644 --- a/client/src/containers/Purchases/PaymentMades/PaymentForm/utils.js +++ b/client/src/containers/Purchases/PaymentMades/PaymentForm/utils.js @@ -1,5 +1,9 @@ import moment from 'moment'; -import { safeSumBy, transformToForm } from 'utils'; +import { + defaultFastFieldShouldUpdate, + safeSumBy, + transformToForm, +} from 'utils'; export const ERRORS = { PAYMENT_NUMBER_NOT_UNIQUE: 'PAYMENT.NUMBER.NOT.UNIQUE', @@ -9,10 +13,10 @@ export const ERRORS = { export const defaultPaymentMadeEntry = { bill_id: '', payment_amount: '', - currency_code:'', + currency_code: '', id: null, due_amount: null, - amount:'' + amount: '', }; // Default initial values of payment made. @@ -48,7 +52,26 @@ export const transformToNewPageEntries = (entries) => { return entries.map((entry) => ({ ...transformToForm(entry, defaultPaymentMadeEntry), payment_amount: '', - currency_code:entry.currency_code, - + currency_code: entry.currency_code, })); -} \ No newline at end of file +}; + +/** + * Detarmines vendors fast field when update. + */ +export const vendorsFieldShouldUpdate = (newProps, oldProps) => { + return ( + newProps.vendors !== oldProps.vendors || + defaultFastFieldShouldUpdate(newProps, oldProps) + ); +}; + +/** + * Detarmines accounts fast field when update. + */ +export const accountsFieldShouldUpdate = (newProps, oldProps) => { + return ( + newProps.accounts !== oldProps.accounts || + defaultFastFieldShouldUpdate(newProps, oldProps) + ); +}; diff --git a/client/src/containers/Sales/Estimates/EstimateForm/EstimateFormHeader.js b/client/src/containers/Sales/Estimates/EstimateForm/EstimateFormHeader.js index 91da74c0a..9402d5d75 100644 --- a/client/src/containers/Sales/Estimates/EstimateForm/EstimateFormHeader.js +++ b/client/src/containers/Sales/Estimates/EstimateForm/EstimateFormHeader.js @@ -27,6 +27,7 @@ function EstimateFormHeader({ return (
+ {/* ----------- Customer name ----------- */} - + {({ form, field: { value }, meta: { error, touched } }) => ( } @@ -170,7 +175,9 @@ function EstimateFormHeader({ }} tooltip={true} tooltipProps={{ - content: , + content: ( + + ), position: Position.BOTTOM_LEFT, }} /> diff --git a/client/src/containers/Sales/Estimates/EstimateForm/EstimateItemsEntriesField.js b/client/src/containers/Sales/Estimates/EstimateForm/EstimateItemsEntriesField.js index 82125ccf2..e549f9342 100644 --- a/client/src/containers/Sales/Estimates/EstimateForm/EstimateItemsEntriesField.js +++ b/client/src/containers/Sales/Estimates/EstimateForm/EstimateItemsEntriesField.js @@ -4,6 +4,7 @@ import classNames from 'classnames'; import { CLASSES } from 'common/classes'; import ItemsEntriesTable from 'containers/Entries/ItemsEntriesTable'; import { useEstimateFormContext } from './EstimateFormProvider'; +import { entriesFieldShouldUpdate } from './utils'; /** * Estimate form items entries editor. @@ -13,7 +14,11 @@ export default function EstimateFormItemsEntriesField() { return (
- + {({ form: { values, setFieldValue }, field: { value }, diff --git a/client/src/containers/Sales/Estimates/EstimateForm/utils.js b/client/src/containers/Sales/Estimates/EstimateForm/utils.js index 42f820356..eba82d104 100644 --- a/client/src/containers/Sales/Estimates/EstimateForm/utils.js +++ b/client/src/containers/Sales/Estimates/EstimateForm/utils.js @@ -1,7 +1,12 @@ import React from 'react'; import { useFormikContext } from 'formik'; import moment from 'moment'; -import { transactionNumber, repeatValue, transformToForm } from 'utils'; +import { + defaultFastFieldShouldUpdate, + transactionNumber, + repeatValue, + transformToForm, +} from 'utils'; export const MIN_LINES_NUMBER = 4; @@ -49,4 +54,24 @@ export const useObserveEstimateNoSettings = (prefix, nextNumber) => { const estimateNo = transactionNumber(prefix, nextNumber); setFieldValue('estimate_number', estimateNo); }, [setFieldValue, prefix, nextNumber]); -} \ No newline at end of file +}; + +/** + * Detarmines customers fast field when update. + */ +export const customersFieldShouldUpdate = (newProps, oldProps) => { + return ( + newProps.customers !== oldProps.customers || + defaultFastFieldShouldUpdate(newProps, oldProps) + ); +}; + +/** + * Detarmines entries fast field should update. + */ +export const entriesFieldShouldUpdate = (newProps, oldProps) => { + return ( + newProps.items !== oldProps.items || + defaultFastFieldShouldUpdate(newProps, oldProps) + ); +}; diff --git a/client/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormHeaderFields.js b/client/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormHeaderFields.js index c28ccb1e6..96594a06c 100644 --- a/client/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormHeaderFields.js +++ b/client/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormHeaderFields.js @@ -10,7 +10,10 @@ import { FastField, Field, ErrorMessage } from 'formik'; import { FormattedMessage as T } from 'components'; import { momentFormatter, compose, tansformDateValue } from 'utils'; import classNames from 'classnames'; -import { useObserveInvoiceNoSettings } from './utils'; +import { + useObserveInvoiceNoSettings, + customerNameFieldShouldUpdate, +} from './utils'; import { CLASSES } from 'common/classes'; import { ContactSelecetList, @@ -58,15 +61,16 @@ function InvoiceFormHeaderFields({ }; // Syncs invoice number settings with form. - useObserveInvoiceNoSettings( - invoiceNumberPrefix, - invoiceNextNumber, - ); + useObserveInvoiceNoSettings(invoiceNumberPrefix, invoiceNextNumber); return (
{/* ----------- Customer name ----------- */} - + {({ form, field: { value }, meta: { error, touched } }) => ( } @@ -168,7 +172,9 @@ function InvoiceFormHeaderFields({ }} tooltip={true} tooltipProps={{ - content: , + content: ( + + ), position: Position.BOTTOM_LEFT, }} /> diff --git a/client/src/containers/Sales/Invoices/InvoiceForm/InvoiceItemsEntriesEditorField.js b/client/src/containers/Sales/Invoices/InvoiceForm/InvoiceItemsEntriesEditorField.js index 4adf2e44e..afd87d7dc 100644 --- a/client/src/containers/Sales/Invoices/InvoiceForm/InvoiceItemsEntriesEditorField.js +++ b/client/src/containers/Sales/Invoices/InvoiceForm/InvoiceItemsEntriesEditorField.js @@ -4,6 +4,7 @@ import classNames from 'classnames'; import { CLASSES } from 'common/classes'; import ItemsEntriesTable from 'containers/Entries/ItemsEntriesTable'; import { useInvoiceFormContext } from './InvoiceFormProvider'; +import { entriesFieldShouldUpdate } from './utils'; /** * Invoice items entries editor field. @@ -13,7 +14,11 @@ export default function InvoiceItemsEntriesEditorField() { return (
- + {({ form: { values, setFieldValue }, field: { value }, diff --git a/client/src/containers/Sales/Invoices/InvoiceForm/utils.js b/client/src/containers/Sales/Invoices/InvoiceForm/utils.js index 6dde8e128..70787c7fa 100644 --- a/client/src/containers/Sales/Invoices/InvoiceForm/utils.js +++ b/client/src/containers/Sales/Invoices/InvoiceForm/utils.js @@ -11,7 +11,7 @@ import { updateItemsEntriesTotal } from 'containers/Entries/utils'; import { useFormikContext } from 'formik'; import { Intent } from '@blueprintjs/core'; -import { orderingLinesIndexes } from 'utils'; +import { defaultFastFieldShouldUpdate } from 'utils'; import intl from 'react-intl-universal'; import { ERROR } from 'common/errors'; import { AppToaster } from 'components'; @@ -100,4 +100,18 @@ export const useObserveInvoiceNoSettings = (prefix, nextNumber) => { const invoiceNo = transactionNumber(prefix, nextNumber); setFieldValue('invoice_no', invoiceNo); }, [setFieldValue, prefix, nextNumber]); -}; \ No newline at end of file +}; + +export const customerNameFieldShouldUpdate = (newProps, oldProps) => { + return ( + newProps.customers !== oldProps.customers || + defaultFastFieldShouldUpdate(newProps, oldProps) + ); +}; + +export const entriesFieldShouldUpdate = (newProps, oldProps) => { + return ( + newProps.items !== oldProps.items || + defaultFastFieldShouldUpdate(newProps, oldProps) + ); +}; diff --git a/client/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveHeaderFields.js b/client/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveHeaderFields.js index 5fa0f1e11..1bccd3bca 100644 --- a/client/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveHeaderFields.js +++ b/client/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveHeaderFields.js @@ -34,6 +34,7 @@ import { } from 'components'; import { usePaymentReceiveFormContext } from './PaymentReceiveFormProvider'; import { ACCOUNT_TYPE } from 'common/accountTypes'; + import withDialogActions from 'containers/Dialog/withDialogActions'; import withSettings from 'containers/Settings/withSettings'; @@ -41,6 +42,8 @@ import { useObservePaymentNoSettings, amountPaymentEntries, fullAmountPaymentEntries, + customersFieldShouldUpdate, + accountsFieldShouldUpdate, } from './utils'; import { toSafeInteger } from 'lodash'; @@ -115,7 +118,11 @@ function PaymentReceiveHeaderFields({ return (
{/* ------------- Customer name ------------- */} - + {({ form, field: { value }, meta: { error, touched } }) => ( } @@ -247,7 +254,11 @@ function PaymentReceiveHeaderFields({ {/* ------------ Deposit account ------------ */} - + {({ form, field: { value }, meta: { error, touched } }) => ( } diff --git a/client/src/containers/Sales/PaymentReceives/PaymentReceiveForm/utils.js b/client/src/containers/Sales/PaymentReceives/PaymentReceiveForm/utils.js index 5513b6939..8309b2fc0 100644 --- a/client/src/containers/Sales/PaymentReceives/PaymentReceiveForm/utils.js +++ b/client/src/containers/Sales/PaymentReceives/PaymentReceiveForm/utils.js @@ -1,7 +1,12 @@ import React from 'react'; import { useFormikContext } from 'formik'; import moment from 'moment'; -import { transactionNumber, transformToForm, safeSumBy } from 'utils'; +import { + defaultFastFieldShouldUpdate, + transactionNumber, + transformToForm, + safeSumBy, +} from 'utils'; // Default payment receive entry. export const defaultPaymentReceiveEntry = { @@ -99,3 +104,23 @@ export const useObservePaymentNoSettings = (prefix, nextNumber) => { setFieldValue('payment_receive_no', invoiceNo); }, [setFieldValue, prefix, nextNumber]); }; + +/** + * Detarmines the customers fast-field should update. + */ +export const customersFieldShouldUpdate = (newProps, oldProps) => { + return ( + newProps.customers !== oldProps.customers || + defaultFastFieldShouldUpdate(newProps, oldProps) + ); +}; + +/** + * Detarmines the accounts fast-field should update. + */ +export const accountsFieldShouldUpdate = (newProps, oldProps) => { + return ( + newProps.accounts !== oldProps.accounts || + defaultFastFieldShouldUpdate(newProps, oldProps) + ); +}; diff --git a/client/src/containers/Sales/Receipts/ReceiptForm/ReceiptFormHeaderFields.js b/client/src/containers/Sales/Receipts/ReceiptForm/ReceiptFormHeaderFields.js index 7db0129d6..d14478e3e 100644 --- a/client/src/containers/Sales/Receipts/ReceiptForm/ReceiptFormHeaderFields.js +++ b/client/src/containers/Sales/Receipts/ReceiptForm/ReceiptFormHeaderFields.js @@ -28,7 +28,11 @@ import { inputIntent, } from 'utils'; import { useReceiptFormContext } from './ReceiptFormProvider'; -import { useObserveReceiptNoSettings } from './utils'; +import { + accountsFieldShouldUpdate, + customersFieldShouldUpdate, + useObserveReceiptNoSettings, +} from './utils'; /** * Receipt form header fields. @@ -70,7 +74,11 @@ function ReceiptFormHeader({ return (
{/* ----------- Customer name ----------- */} - + {({ form, field: { value }, meta: { error, touched } }) => ( } @@ -94,7 +102,11 @@ function ReceiptFormHeader({ {/* ----------- Deposit account ----------- */} - + {({ form, field: { value }, meta: { error, touched } }) => ( } diff --git a/client/src/containers/Sales/Receipts/ReceiptForm/ReceiptItemsEntriesEditor.js b/client/src/containers/Sales/Receipts/ReceiptForm/ReceiptItemsEntriesEditor.js index 581b7f4b0..b678bc7ba 100644 --- a/client/src/containers/Sales/Receipts/ReceiptForm/ReceiptItemsEntriesEditor.js +++ b/client/src/containers/Sales/Receipts/ReceiptForm/ReceiptItemsEntriesEditor.js @@ -4,13 +4,14 @@ import { FastField } from 'formik'; import ItemsEntriesTable from 'containers/Entries/ItemsEntriesTable'; import { CLASSES } from 'common/classes'; import { useReceiptFormContext } from './ReceiptFormProvider'; +import { entriesFieldShouldUpdate } from './utils'; export default function ReceiptItemsEntriesEditor({ defaultReceipt }) { const { items } = useReceiptFormContext(); return (
- + {({ form: { values, setFieldValue }, field: { value }, diff --git a/client/src/containers/Sales/Receipts/ReceiptForm/utils.js b/client/src/containers/Sales/Receipts/ReceiptForm/utils.js index 431500601..93cbb75cd 100644 --- a/client/src/containers/Sales/Receipts/ReceiptForm/utils.js +++ b/client/src/containers/Sales/Receipts/ReceiptForm/utils.js @@ -1,7 +1,12 @@ import React from 'react'; import { useFormikContext } from 'formik'; import moment from 'moment'; -import { transactionNumber, repeatValue, transformToForm } from 'utils'; +import { + defaultFastFieldShouldUpdate, + transactionNumber, + repeatValue, + transformToForm, +} from 'utils'; export const MIN_LINES_NUMBER = 4; @@ -42,7 +47,6 @@ export const transformToEditForm = (receipt) => ({ ], }); - export const useObserveReceiptNoSettings = (prefix, nextNumber) => { const { setFieldValue } = useFormikContext(); @@ -50,4 +54,34 @@ export const useObserveReceiptNoSettings = (prefix, nextNumber) => { const receiptNo = transactionNumber(prefix, nextNumber); setFieldValue('receipt_number', receiptNo); }, [setFieldValue, prefix, nextNumber]); -} \ No newline at end of file +}; + +/** + * Detarmines entries fast field should update. + */ +export const entriesFieldShouldUpdate = (newProps, oldProps) => { + return ( + newProps.items !== oldProps.items || + defaultFastFieldShouldUpdate(newProps, oldProps) + ); +}; + +/** + * Detarmines accounts fast field should update. + */ +export const accountsFieldShouldUpdate = (newProps, oldProps) => { + return ( + newProps.accounts !== oldProps.accounts || + defaultFastFieldShouldUpdate(newProps, oldProps) + ); +}; + +/** + * Detarmines customers fast field should update. + */ +export const customersFieldShouldUpdate = (newProps, oldProps) => { + return ( + newProps.customers !== oldProps.customers || + defaultFastFieldShouldUpdate(newProps, oldProps) + ); +}; diff --git a/client/src/lang/en/index.json b/client/src/lang/en/index.json index 6742cf866..081b54a77 100644 --- a/client/src/lang/en/index.json +++ b/client/src/lang/en/index.json @@ -1146,5 +1146,11 @@ "No items": "No items", "cannot_delete_bill_that_has_associated_landed_cost_transactions": "Cannot delete bill that has associated landed cost transactions.", "couldn_t_delete_expense_transaction_has_associated_located_landed_cost_transaction": "Couldn't delete expense transaction has associated located landed cost transaction", - "the_landed_cost_has_been_created_successfully": "The landed cost has been created successfully" + "the_landed_cost_has_been_created_successfully": "The landed cost has been created successfully", + "Select transaction": "Select transaction", + "Select transaction entry": "Select transaction entry", + "From transaction": "From transaction", + "Landed": "Landed", + "This options allows you to be able to add additional cost eg. freight then allocate cost to the items in your bills.": "This options allows you to be able to add additional cost eg. freight then allocate cost to the items in your bills.", + "Once your delete this located landed cost, you won't be able to restore it later, Are your sure you want to delete this transaction?": "Once your delete this located landed cost, you won't be able to restore it later, Are your sure you want to delete this transaction?" } \ No newline at end of file diff --git a/client/src/style/components/DataTable/DataTableEditable.scss b/client/src/style/components/DataTable/DataTableEditable.scss index e10c4f78f..703afb089 100644 --- a/client/src/style/components/DataTable/DataTableEditable.scss +++ b/client/src/style/components/DataTable/DataTableEditable.scss @@ -10,7 +10,7 @@ .th, .td { - border-left: 1px dashed #e2e2e2; + border-left: 1px solid #e2e2e2; &.index { text-align: center; @@ -55,6 +55,19 @@ margin-bottom: auto; } } + + &.landed-cost{ + + .bp3-control{ + margin-top: 0; + margin-left: 34px; + } + .bp3-control-indicator{ + height: 18px; + width: 18px; + border-color: #e0e0e0; + } + } } .tr { .bp3-form-group:not(.bp3-intent-danger) .bp3-input, diff --git a/client/src/style/components/Details.scss b/client/src/style/components/Details.scss new file mode 100644 index 000000000..d9a2134a1 --- /dev/null +++ b/client/src/style/components/Details.scss @@ -0,0 +1,21 @@ +.details-menu { + display: flex; + flex-wrap: wrap; + justify-content: flex-start; + + &.is-vertical {} + + .detail-item { + + + &__label { + color: #666666; + font-weight: 500; + } + + &__content { + text-transform: capitalize; + margin: 5px 0; + } + } +} \ No newline at end of file diff --git a/client/src/style/components/Drawer.scss b/client/src/style/components/Drawer.scss new file mode 100644 index 000000000..8d4fc9f4c --- /dev/null +++ b/client/src/style/components/Drawer.scss @@ -0,0 +1,17 @@ +.bp3-drawer { + + + .bp3-drawer-header { + margin-bottom: 2px; + background-color: #FFF; + + .bp3-heading { + font-weight: 500; + } + + .bp3-heading, + .bp3-icon { + color: #354152; + } + } +} \ No newline at end of file diff --git a/client/src/style/components/Drawer/AccountDrawer.scss b/client/src/style/components/Drawers/AccountDrawer.scss similarity index 72% rename from client/src/style/components/Drawer/AccountDrawer.scss rename to client/src/style/components/Drawers/AccountDrawer.scss index 318c66a44..2a90ded17 100644 --- a/client/src/style/components/Drawer/AccountDrawer.scss +++ b/client/src/style/components/Drawers/AccountDrawer.scss @@ -1,14 +1,4 @@ -.bp3-drawer-header { - box-shadow: 0 0 0; - .bp3-heading{ - font-size: 16px; - } - .bp3-button{ - min-height: 28px; - min-width: 28px; - } -} .account-drawer { background-color: #fbfbfb; @@ -94,29 +84,4 @@ } } } -} - -.bp3-drawer.bp3-position-right { - bottom: 0; - right: 0; - top: 0; - overflow: auto; - height: 100%; - - scrollbar-width: none; - - &::-webkit-scrollbar { - display: none; - } - - .bp3-drawer-header { - margin-bottom: 2px; - box-shadow: (0, 0, 0); - background-color: #6a7993; - - .bp3-heading, - .bp3-icon { - color: white; - } - } } \ No newline at end of file diff --git a/client/src/style/components/Drawer/BillDrawer.scss b/client/src/style/components/Drawers/BillDrawer.scss similarity index 60% rename from client/src/style/components/Drawer/BillDrawer.scss rename to client/src/style/components/Drawers/BillDrawer.scss index d3df06a54..3179bda99 100644 --- a/client/src/style/components/Drawer/BillDrawer.scss +++ b/client/src/style/components/Drawers/BillDrawer.scss @@ -4,14 +4,15 @@ .bp3-tabs { .bp3-tab-list { position: relative; - + background-color: #FFF; + &:before { content: ''; position: absolute; bottom: 0; width: 100%; height: 2px; - background: #f0f0f0; + background: #e1e2e8; } > *:not(:last-child) { @@ -29,14 +30,18 @@ } } } + + .bp3-tab-panel{ + margin-top: 0; + + .card{ + margin: 15px; + } + } } - .bigcapital-datatable { + .datatable--landed-cost-transactions { .table { - max-height: 500px; - border: 1px solid #d1dee2; - min-width: auto; - margin: 12px; .tbody, .tbody-inner { @@ -48,34 +53,13 @@ } .tbody { .tr .td { - padding: 0.8rem; + padding: 0.6rem; + + &.amount{ + font-weight: 600; + } } } } } -} - -.bp3-drawer.bp3-position-right { - bottom: 0; - right: 0; - top: 0; - overflow: auto; - height: 100%; - - scrollbar-width: none; - - &::-webkit-scrollbar { - display: none; - } - - .bp3-drawer-header { - margin-bottom: 2px; - box-shadow: (0, 0, 0); - background-color: #6a7993; - - .bp3-heading, - .bp3-icon { - color: white; - } - } -} +} \ No newline at end of file diff --git a/client/src/style/components/Drawer/DrawerTemplate.scss b/client/src/style/components/Drawers/DrawerTemplate.scss similarity index 99% rename from client/src/style/components/Drawer/DrawerTemplate.scss rename to client/src/style/components/Drawers/DrawerTemplate.scss index 4ad14b775..60e63bc8c 100644 --- a/client/src/style/components/Drawer/DrawerTemplate.scss +++ b/client/src/style/components/Drawers/DrawerTemplate.scss @@ -122,6 +122,7 @@ top: 0; overflow: auto; height: 100%; + .bp3-drawer-header .bp3-heading { overflow: hidden; text-overflow: ellipsis; diff --git a/client/src/style/components/Drawer/ViewDetails.scss b/client/src/style/components/Drawers/ViewDetails.scss similarity index 77% rename from client/src/style/components/Drawer/ViewDetails.scss rename to client/src/style/components/Drawers/ViewDetails.scss index 2a71ec9d8..f0d982727 100644 --- a/client/src/style/components/Drawer/ViewDetails.scss +++ b/client/src/style/components/Drawers/ViewDetails.scss @@ -1,6 +1,5 @@ .journal-drawer, .expense-drawer { - background: #f5f5f5; &__content { display: flex; @@ -18,8 +17,8 @@ justify-content: flex-start; margin: 15px 0 20px; font-size: 14px; - // color: #333333; color: #666666; + > div { flex-grow: 1; span { @@ -44,17 +43,17 @@ &--table { flex-grow: 1; flex-shrink: 0; + .table { color: #666666; font-size: 14px; - .thead .tr .th .resizer { - display: none; - } + .thead .th { + background: transparent; color: #222222; border-bottom: 1px solid #000000; + padding: 0.5rem; } - .thead .th, .tbody .tr .td { background: transparent; padding: 0.8rem 0.5rem; @@ -63,7 +62,6 @@ .desc { margin: 20px 0 60px; - // margin: 20px 0; > b { color: #2f2f2f; } @@ -93,25 +91,3 @@ } } } - -.bp3-drawer.bp3-position-right { - bottom: 0; - right: 0; - top: 0; - overflow: auto; - height: 100%; - scrollbar-width: none; - &::-webkit-scrollbar { - display: none; - } - - .bp3-drawer-header { - margin-bottom: 2px; - box-shadow: (0, 0, 0); - background-color: #6a7993; - .bp3-heading, - .bp3-icon { - color: white; - } - } -} diff --git a/client/src/style/pages/Bills/PageForm.scss b/client/src/style/pages/Bills/PageForm.scss index edecac102..df8a23c28 100644 --- a/client/src/style/pages/Bills/PageForm.scss +++ b/client/src/style/pages/Bills/PageForm.scss @@ -11,7 +11,6 @@ body.page-bill-edit{ padding-bottom: 64px; } - .page-form--bill{ $self: '.page-form'; @@ -36,7 +35,7 @@ body.page-bill-edit{ max-width: 440px; } - &.form-group{ + &.form-group{ &--expiration-date{ max-width: 340px; diff --git a/client/src/style/pages/Expense/PageForm.scss b/client/src/style/pages/Expense/PageForm.scss index 6a93637e5..102a3819e 100644 --- a/client/src/style/pages/Expense/PageForm.scss +++ b/client/src/style/pages/Expense/PageForm.scss @@ -1,11 +1,10 @@ +.dashboard__insider--expenses { -.dashboard__insider--expenses{ + .bigcapital-datatable { - .bigcapital-datatable{ - - .tbody{ - .tr .td.total_amount{ - span{ + .tbody { + .tr .td.total_amount { + span { font-weight: 600; } } @@ -13,36 +12,64 @@ } } -.page-form--expense{ +.page-form--expense { $self: '.page-form'; - #{$self}__header{ + #{$self}__header { display: flex; - &-fields{ + &-fields { flex: 1 0 0; } - .bp3-label{ + .bp3-label { min-width: 140px; } - .bp3-form-content{ + + .bp3-form-content { width: 100%; } - .bp3-form-group{ + .bp3-form-group { margin-bottom: 18px; - &.bp3-inline{ - max-width: 440px; + &.bp3-inline { + max-width: 440px; } } } - .form-group--description{ + .datatable-editor--expense-form { + + + .table { + + .tbody { + .tr .td { + + + &.landed-cost { + + .bp3-control { + margin-top: 0; + margin-left: 34px; + } + + .bp3-control-indicator { + height: 18px; + width: 18px; + border-color: #e0e0e0; + } + } + } + } + } + } + + .form-group--description { max-width: 500px; - textarea{ + textarea { min-height: 60px; width: 100%; } diff --git a/client/src/style/pages/fonts.scss b/client/src/style/pages/fonts.scss index 76e0d759f..8f5bb71b8 100644 --- a/client/src/style/pages/fonts.scss +++ b/client/src/style/pages/fonts.scss @@ -1,3 +1,7 @@ + + +// Noto Sans +// ------------------------------------- @font-face { font-family: Noto Sans; src: local('Noto Sans'), url('../fonts/NotoSans-SemiBold.woff') format('woff'); @@ -30,46 +34,8 @@ font-display: swap; } -// arabic regular -@font-face { - font-family: Noto Sans Arabic; - src: local('Noto Sans'), - url('../fonts/NotoSansArabicUI-SemiCondensed.woff') format('woff'); - font-style: normal; - font-weight: 400; - font-display: swap; -} - -// arabic black -@font-face { - font-family: Noto Sans Arabic; - src: local('Noto Sans'), - url('../fonts/NotoSansArabicUI-SemiCondensedBlack.woff') format('woff'); - font-style: normal; - font-weight: 900; - font-display: swap; -} - -//arabic Medium -@font-face { - font-family: Noto Sans Arabic; - src: local('Noto Sans'), - url('../fonts/NotoSansArabicUI-SemiCondensedMedium.woff') format('woff'); - font-style: normal; - font-weight: 500; - font-display: swap; -} - -//arabic SemiBold -@font-face { - font-family: Noto Sans Arabic; - src: local('Noto Sans'), - url('../fonts/NotoSansArabicUI-SemiCondensedSemiBold.woff') format('woff'); - font-style: normal; - font-weight: 600; - font-display: swap; -} - +// Segoe UI Arabic +// ------------------------------------- // Segoe UI Arabic - Regular @font-face { font-family: 'Segoe UI'; diff --git a/client/src/style/variables.scss b/client/src/style/variables.scss index 63c0400ee..02561ebe9 100644 --- a/client/src/style/variables.scss +++ b/client/src/style/variables.scss @@ -16,7 +16,7 @@ $menu-item-color-active: $light-gray3; $breadcrumbs-collapsed-icon: url("data:image/svg+xml,"); $sidebar-zindex: 15; -$pt-font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, +$pt-font-family: 'Noto Sans', -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Open Sans, Helvetica Neue, Icons16, sans-serif; diff --git a/client/src/utils.js b/client/src/utils.js index a6e3d07e0..32dadaaec 100644 --- a/client/src/utils.js +++ b/client/src/utils.js @@ -90,7 +90,9 @@ export const objectKeysTransform = (obj, transform) => { export const compose = (...funcs) => funcs.reduce( - (a, b) => (...args) => a(b(...args)), + (a, b) => + (...args) => + a(b(...args)), (arg) => arg, ); @@ -639,7 +641,32 @@ const getCurrenciesOptions = () => { currency_code: currencyCode, formatted_name: `${currencyCode} - ${currency.name}`, }; - }) + }); +}; + +export const currenciesOptions = getCurrenciesOptions(); + +/** + * Deeply get a value from an object via its path. + */ +function getIn(obj, key, def, p = 0) { + const path = _.toPath(key); + while (obj && p < path.length) { + obj = obj[path[p++]]; + } + return obj === undefined ? def : obj; } -export const currenciesOptions = getCurrenciesOptions(); \ No newline at end of file +export const defaultFastFieldShouldUpdate = (props, prevProps) => { + return ( + props.name !== prevProps.name || + getIn(props.formik.values, prevProps.name) !== + getIn(prevProps.formik.values, prevProps.name) || + getIn(props.formik.errors, prevProps.name) !== + getIn(prevProps.formik.errors, prevProps.name) || + getIn(props.formik.touched, prevProps.name) !== + getIn(prevProps.formik.touched, prevProps.name) || + Object.keys(prevProps).length !== Object.keys(props).length || + props.formik.isSubmitting !== prevProps.formik.isSubmitting + ); +}; diff --git a/server/src/api/controllers/FinancialStatements/CashFlow/CashFlow.ts b/server/src/api/controllers/FinancialStatements/CashFlow/CashFlow.ts index 87235b609..aa0943833 100644 --- a/server/src/api/controllers/FinancialStatements/CashFlow/CashFlow.ts +++ b/server/src/api/controllers/FinancialStatements/CashFlow/CashFlow.ts @@ -71,7 +71,6 @@ export default class CashFlowController extends BaseFinancialReportController { /** * Transformes the report statement to table rows. * @param {ITransactionsByVendorsStatement} statement - - * */ private transformToTableRows(cashFlowDOO: ICashFlowStatementDOO, tenantId: number) { const i18n = this.tenancy.i18n(tenantId); diff --git a/server/src/api/controllers/FinancialStatements/VendorBalanceSummary/index.ts b/server/src/api/controllers/FinancialStatements/VendorBalanceSummary/index.ts index 849a64eca..41b2e88c1 100644 --- a/server/src/api/controllers/FinancialStatements/VendorBalanceSummary/index.ts +++ b/server/src/api/controllers/FinancialStatements/VendorBalanceSummary/index.ts @@ -45,7 +45,7 @@ export default class VendorBalanceSummaryReportController extends BaseFinancialR * Transformes the report statement to table rows. * @param {IVendorBalanceSummaryStatement} statement - */ - transformToTableRows({ data }: IVendorBalanceSummaryStatement) { + private transformToTableRows({ data }: IVendorBalanceSummaryStatement) { return { table: { data: this.vendorBalanceSummaryTableRows.tableRowsTransformer(data), @@ -57,7 +57,10 @@ export default class VendorBalanceSummaryReportController extends BaseFinancialR * Transformes the report statement to raw json. * @param {IVendorBalanceSummaryStatement} statement - */ - transformToJsonResponse({ data, columns }: IVendorBalanceSummaryStatement) { + private transformToJsonResponse({ + data, + columns, + }: IVendorBalanceSummaryStatement) { return { data: this.transfromToResponse(data), columns: this.transfromToResponse(columns), @@ -76,10 +79,11 @@ export default class VendorBalanceSummaryReportController extends BaseFinancialR const filter = this.matchedQueryData(req); try { - const vendorBalanceSummary = await this.vendorBalanceSummaryService.vendorBalanceSummary( - tenantId, - filter - ); + const vendorBalanceSummary = + await this.vendorBalanceSummaryService.vendorBalanceSummary( + tenantId, + filter + ); const accept = this.accepts(req); const acceptType = accept.types(['json', 'application/json+table']); diff --git a/server/src/api/controllers/Purchases/LandedCost.ts b/server/src/api/controllers/Purchases/LandedCost.ts index 5471be78c..bf149c2ff 100644 --- a/server/src/api/controllers/Purchases/LandedCost.ts +++ b/server/src/api/controllers/Purchases/LandedCost.ts @@ -192,7 +192,10 @@ export default class BillAllocateLandedCost extends BaseController { billId ); - return res.status(200).send({ billId, transactions }); + return res.status(200).send({ + billId, + transactions: this.transfromToResponse(transactions) + }); } catch (error) { next(error); } diff --git a/server/src/services/Purchases/LandedCost/LandedCostListing.ts b/server/src/services/Purchases/LandedCost/LandedCostListing.ts index e476b9f93..afa02a5aa 100644 --- a/server/src/services/Purchases/LandedCost/LandedCostListing.ts +++ b/server/src/services/Purchases/LandedCost/LandedCostListing.ts @@ -1,5 +1,5 @@ import { Inject, Service } from 'typedi'; -import { ref } from 'objection'; +import { ref, transaction } from 'objection'; import { ILandedCostTransactionsQueryDTO, ILandedCostTransaction, @@ -8,6 +8,7 @@ import { import TransactionLandedCost from './TransctionLandedCost'; import BillsService from '../Bills'; import HasTenancyService from 'services/Tenancy/TenancyService'; +import { formatNumber } from 'utils'; @Service() export default class LandedCostListing { @@ -71,8 +72,15 @@ export default class LandedCostListing { const landedCostTransactions = await BillLandedCost.query() .where('bill_id', billId) - .withGraphFetched('allocateEntries'); + .withGraphFetched('allocateEntries') + .withGraphFetched('bill'); - return landedCostTransactions; + return landedCostTransactions.map((transaction) => ({ + ...transaction.toJSON(), + formattedAmount: formatNumber( + transaction.amount, + transaction.bill.currencyCode + ), + })); }; } From 7b071c6ef58bc9f513d54fb343eaa77203dd32ed Mon Sep 17 00:00:00 2001 From: "a.bouhuolia" Date: Tue, 27 Jul 2021 02:01:04 +0200 Subject: [PATCH 22/24] feat: Tweeks in data table rows style. --- client/src/style/components/DataTable/Pagination.scss | 2 +- client/src/style/pages/Bills/List.scss | 2 +- client/src/style/pages/Expense/List.scss | 2 +- client/src/style/pages/Items/List.scss | 2 +- client/src/style/pages/PaymentMade/List.scss | 2 +- client/src/style/pages/PaymentReceive/List.scss | 2 +- client/src/style/pages/SaleEstimate/List.scss | 3 +++ client/src/style/pages/SaleInvoice/List.scss | 2 +- client/src/style/pages/SaleReceipt/List.scss | 5 ++--- 9 files changed, 12 insertions(+), 10 deletions(-) diff --git a/client/src/style/components/DataTable/Pagination.scss b/client/src/style/components/DataTable/Pagination.scss index 52514fbfc..3e98c9534 100644 --- a/client/src/style/components/DataTable/Pagination.scss +++ b/client/src/style/components/DataTable/Pagination.scss @@ -1,7 +1,7 @@ .pagination{ display: flex; - padding: 28px 14px; + padding: 20px 14px; font-size: 13px; .bp3-button{ diff --git a/client/src/style/pages/Bills/List.scss b/client/src/style/pages/Bills/List.scss index 4fa38a7a9..ced4ceec0 100644 --- a/client/src/style/pages/Bills/List.scss +++ b/client/src/style/pages/Bills/List.scss @@ -2,7 +2,7 @@ .bigcapital-datatable { .tbody { .tr { - min-height: 50px; + min-height: 46px; } .td.amount { .cell-inner { diff --git a/client/src/style/pages/Expense/List.scss b/client/src/style/pages/Expense/List.scss index eb9900e9d..e6471c90b 100644 --- a/client/src/style/pages/Expense/List.scss +++ b/client/src/style/pages/Expense/List.scss @@ -7,7 +7,7 @@ .tbody { .tr{ - min-height: 50px; + min-height: 46px; } .td.amount { span { diff --git a/client/src/style/pages/Items/List.scss b/client/src/style/pages/Items/List.scss index e0fcf371a..f6da8d84e 100644 --- a/client/src/style/pages/Items/List.scss +++ b/client/src/style/pages/Items/List.scss @@ -7,7 +7,7 @@ .table { .tbody { .tr{ - min-height: 50px; + min-height: 46px; } .item_type.td { .bp3-tag { diff --git a/client/src/style/pages/PaymentMade/List.scss b/client/src/style/pages/PaymentMade/List.scss index aab384b28..e3e41df3c 100644 --- a/client/src/style/pages/PaymentMade/List.scss +++ b/client/src/style/pages/PaymentMade/List.scss @@ -5,7 +5,7 @@ .tbody{ .tr{ - min-height: 50px; + min-height: 46px; } .td.amount { diff --git a/client/src/style/pages/PaymentReceive/List.scss b/client/src/style/pages/PaymentReceive/List.scss index 491f47e0f..89d1bf4b3 100644 --- a/client/src/style/pages/PaymentReceive/List.scss +++ b/client/src/style/pages/PaymentReceive/List.scss @@ -5,7 +5,7 @@ .tbody{ .tr .td{ - min-height: 50px; + min-height: 46px; } .td.amount { diff --git a/client/src/style/pages/SaleEstimate/List.scss b/client/src/style/pages/SaleEstimate/List.scss index 5706c00b7..02e7d738b 100644 --- a/client/src/style/pages/SaleEstimate/List.scss +++ b/client/src/style/pages/SaleEstimate/List.scss @@ -4,6 +4,9 @@ .bigcapital-datatable{ .tbody{ + .tr{ + min-height: 46px; + } .tr .td{ padding-top: 0.88rem; diff --git a/client/src/style/pages/SaleInvoice/List.scss b/client/src/style/pages/SaleInvoice/List.scss index 2f3e85bff..24e706b2f 100644 --- a/client/src/style/pages/SaleInvoice/List.scss +++ b/client/src/style/pages/SaleInvoice/List.scss @@ -8,7 +8,7 @@ .tbody{ .tr{ - min-height: 50px; + min-height: 46px; } .balance.td{ diff --git a/client/src/style/pages/SaleReceipt/List.scss b/client/src/style/pages/SaleReceipt/List.scss index 06e8073f5..b246a5a9f 100644 --- a/client/src/style/pages/SaleReceipt/List.scss +++ b/client/src/style/pages/SaleReceipt/List.scss @@ -4,9 +4,8 @@ .bigcapital-datatable{ .tbody{ - - .tr .td{ - min-height: 50px; + .tr{ + min-height: 46px; } .td.amount { From 45ee031df167e4854d0957c787b52a6d6f170adc Mon Sep 17 00:00:00 2001 From: "a.bouhuolia" Date: Tue, 27 Jul 2021 02:11:13 +0200 Subject: [PATCH 23/24] feat: Tweeks in data table style. --- client/src/style/pages/Customers/List.scss | 4 ++-- client/src/style/pages/InventoryAdjustments/List.scss | 2 +- client/src/style/pages/ItemsCategories/List.scss | 2 +- client/src/style/pages/Vendors/List.scss | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/client/src/style/pages/Customers/List.scss b/client/src/style/pages/Customers/List.scss index b271d1a3a..a12f9e3b4 100644 --- a/client/src/style/pages/Customers/List.scss +++ b/client/src/style/pages/Customers/List.scss @@ -5,8 +5,8 @@ .bigcapital-datatable{ .tr .td{ - padding-top: 0.6rem; - padding-bottom: 0.6rem; + padding-top: 0.5rem; + padding-bottom: 0.5rem; } .avatar.td{ diff --git a/client/src/style/pages/InventoryAdjustments/List.scss b/client/src/style/pages/InventoryAdjustments/List.scss index d904555ef..87fb6158d 100644 --- a/client/src/style/pages/InventoryAdjustments/List.scss +++ b/client/src/style/pages/InventoryAdjustments/List.scss @@ -7,7 +7,7 @@ .table { .tbody { .tr{ - min-height: 50px; + min-height: 46px; } } } diff --git a/client/src/style/pages/ItemsCategories/List.scss b/client/src/style/pages/ItemsCategories/List.scss index a925a3f21..c6f4859b4 100644 --- a/client/src/style/pages/ItemsCategories/List.scss +++ b/client/src/style/pages/ItemsCategories/List.scss @@ -6,7 +6,7 @@ .table { .tbody { .tr{ - min-height: 50px; + min-height: 46px; } } } diff --git a/client/src/style/pages/Vendors/List.scss b/client/src/style/pages/Vendors/List.scss index 97764e88a..ef592ef98 100644 --- a/client/src/style/pages/Vendors/List.scss +++ b/client/src/style/pages/Vendors/List.scss @@ -4,8 +4,8 @@ tbody { .tr .td { - padding-top: 0.6rem; - padding-bottom: 0.6rem; + padding-top: 0.5rem; + padding-bottom: 0.5rem; } } From 3da7b53f5e5bb5f1d882e7d3fe8e32a9b7f92afe Mon Sep 17 00:00:00 2001 From: "a.bouhuolia" Date: Tue, 27 Jul 2021 02:40:45 +0200 Subject: [PATCH 24/24] feat: Application intl context provider. --- client/src/components/AppIntlLoader.js | 21 +++++++++-------- client/src/components/AppIntlProvider.js | 24 ++++++++++++++++++++ client/src/components/Datatable/TableCell.js | 6 +++-- 3 files changed, 40 insertions(+), 11 deletions(-) create mode 100644 client/src/components/AppIntlProvider.js diff --git a/client/src/components/AppIntlLoader.js b/client/src/components/AppIntlLoader.js index fdd533325..8b1b6690b 100644 --- a/client/src/components/AppIntlLoader.js +++ b/client/src/components/AppIntlLoader.js @@ -4,6 +4,7 @@ import { setLocale } from 'yup'; import intl from 'react-intl-universal'; import { find } from 'lodash'; import rtlDetect from 'rtl-detect'; +import { AppIntlProvider } from './AppIntlProvider'; import DashboardLoadingIndicator from 'components/Dashboard/DashboardLoadingIndicator'; const SUPPORTED_LOCALES = [ @@ -40,16 +41,14 @@ function loadYupLocales(currentLocale) { /** * Modifies the html document direction to RTl if it was rtl-language. */ -function useDocumentDirectionModifier(locale) { +function useDocumentDirectionModifier(locale, isRTL) { React.useEffect(() => { - const isRTL = rtlDetect.isRtlLang(locale); - if (isRTL) { const htmlDocument = document.querySelector('html'); htmlDocument.setAttribute('dir', 'rtl'); htmlDocument.setAttribute('lang', locale); } - }, []); + }, [isRTL, locale]); } /** @@ -59,8 +58,10 @@ export default function AppIntlLoader({ children }) { const [isLoading, setIsLoading] = React.useState(true); const currentLocale = getCurrentLocal(); + const isRTL = rtlDetect.isRtlLang(currentLocale); + // Modifies the html document direction - useDocumentDirectionModifier(currentLocale); + useDocumentDirectionModifier(currentLocale, isRTL); React.useEffect(() => { // Lodas the locales data file. @@ -86,10 +87,12 @@ export default function AppIntlLoader({ children }) { }) .then(() => {}); }, [currentLocale]); - + return ( - - {children} - + + + {children} + + ); } diff --git a/client/src/components/AppIntlProvider.js b/client/src/components/AppIntlProvider.js new file mode 100644 index 000000000..84e2f0638 --- /dev/null +++ b/client/src/components/AppIntlProvider.js @@ -0,0 +1,24 @@ +import React, { createContext } from 'react'; + +const AppIntlContext = createContext(); + +/** + * Application intl provider. + */ +function AppIntlProvider({ currentLocale, isRTL, children }) { + const provider = { + currentLocale, + isRTL, + isLTR: !isRTL, + }; + + return ( + + {children} + + ); +} + +const useAppIntlContext = () => React.useContext(AppIntlContext); + +export { AppIntlProvider, useAppIntlContext }; diff --git a/client/src/components/Datatable/TableCell.js b/client/src/components/Datatable/TableCell.js index 84a50a89a..4aa13b5b7 100644 --- a/client/src/components/Datatable/TableCell.js +++ b/client/src/components/Datatable/TableCell.js @@ -2,6 +2,7 @@ import React, { useContext } from 'react'; import classNames from 'classnames'; import { If } from 'components'; import { Skeleton } from 'components'; +import { useAppIntlContext } from 'components/AppIntlProvider'; import TableContext from './TableContext'; import { isCellLoading } from './utils'; @@ -26,6 +27,9 @@ export default function TableCell({ const isExpandColumn = expandToggleColumn === index; const { skeletonWidthMax = 100, skeletonWidthMin = 40 } = {}; + // Application intl context. + const { isRTL } = useAppIntlContext(); + // Detarmines whether the current cell is loading. const cellLoading = isCellLoading( cellsLoading, @@ -46,8 +50,6 @@ export default function TableCell({ ); } - const isRTL = true; - return (