From dc0899ebd1a338b42adc2b0b77ee4c2b72671733 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Wed, 4 Oct 2023 03:35:06 +0200 Subject: [PATCH 1/9] feat(server): integrate tax rates to bills --- .../src/api/controllers/Purchases/Bills.ts | 11 +++ ..._add_tax_amount_withheld_to_bills_table.js | 10 +++ packages/server/src/loaders/eventEmitter.ts | 4 + .../Purchases/Bills/BillDTOTransformer.ts | 17 +++- .../BillTaxRateValidateSubscriber.ts | 89 +++++++++++++++++++ .../WriteBillTaxTransactionsSubscriber.ts | 87 ++++++++++++++++++ 6 files changed, 217 insertions(+), 1 deletion(-) create mode 100644 packages/server/src/database/migrations/20231004012644_add_tax_amount_withheld_to_bills_table.js create mode 100644 packages/server/src/services/TaxRates/subscribers/BillTaxRateValidateSubscriber.ts create mode 100644 packages/server/src/services/TaxRates/subscribers/WriteBillTaxTransactionsSubscriber.ts diff --git a/packages/server/src/api/controllers/Purchases/Bills.ts b/packages/server/src/api/controllers/Purchases/Bills.ts index 2bfe230e36..221f96c65b 100644 --- a/packages/server/src/api/controllers/Purchases/Bills.ts +++ b/packages/server/src/api/controllers/Purchases/Bills.ts @@ -115,6 +115,8 @@ export default class BillsController extends BaseController { check('note').optional().trim().escape(), check('open').default(false).isBoolean().toBoolean(), + check('is_inclusive_tax').optional().isBoolean().toBoolean(), + check('entries').isArray({ min: 1 }), check('entries.*.index').exists().isNumeric().toInt(), @@ -137,6 +139,15 @@ export default class BillsController extends BaseController { .optional({ nullable: true }) .isNumeric() .toInt(), + check('entries.*.tax_code') + .optional({ nullable: true }) + .trim() + .escape() + .isString(), + check('entries.*.tax_rate_id') + .optional({ nullable: true }) + .isNumeric() + .toInt(), ]; } diff --git a/packages/server/src/database/migrations/20231004012644_add_tax_amount_withheld_to_bills_table.js b/packages/server/src/database/migrations/20231004012644_add_tax_amount_withheld_to_bills_table.js new file mode 100644 index 0000000000..f616558774 --- /dev/null +++ b/packages/server/src/database/migrations/20231004012644_add_tax_amount_withheld_to_bills_table.js @@ -0,0 +1,10 @@ +exports.up = (knex) => { + return knex.schema.table('bills', (table) => { + table.boolean('is_inclusive_tax').defaultTo(false); + table.decimal('tax_amount_withheld'); + }); +}; + +exports.down = (knex) => { + return knex.schema.table('bills', () => {}); +}; diff --git a/packages/server/src/loaders/eventEmitter.ts b/packages/server/src/loaders/eventEmitter.ts index df5724e83c..116542c297 100644 --- a/packages/server/src/loaders/eventEmitter.ts +++ b/packages/server/src/loaders/eventEmitter.ts @@ -81,6 +81,8 @@ import { ProjectBillableBillSubscriber } from '@/services/Projects/Projects/Proj import { SyncActualTimeTaskSubscriber } from '@/services/Projects/Times/SyncActualTimeTaskSubscriber'; import { SaleInvoiceTaxRateValidateSubscriber } from '@/services/TaxRates/subscribers/SaleInvoiceTaxRateValidateSubscriber'; import { WriteInvoiceTaxTransactionsSubscriber } from '@/services/TaxRates/subscribers/WriteInvoiceTaxTransactionsSubscriber'; +import { BillTaxRateValidateSubscriber } from '@/services/TaxRates/subscribers/BillTaxRateValidateSubscriber'; +import { WriteBillTaxTransactionsSubscriber } from '@/services/TaxRates/subscribers/WriteBillTaxTransactionsSubscriber'; export default () => { return new EventPublisher(); @@ -190,6 +192,8 @@ export const susbcribers = () => { // Tax Rates SaleInvoiceTaxRateValidateSubscriber, + BillTaxRateValidateSubscriber, WriteInvoiceTaxTransactionsSubscriber, + WriteBillTaxTransactionsSubscriber ]; }; diff --git a/packages/server/src/services/Purchases/Bills/BillDTOTransformer.ts b/packages/server/src/services/Purchases/Bills/BillDTOTransformer.ts index 046e0177e6..66499b13fd 100644 --- a/packages/server/src/services/Purchases/Bills/BillDTOTransformer.ts +++ b/packages/server/src/services/Purchases/Bills/BillDTOTransformer.ts @@ -14,6 +14,7 @@ import { import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform'; import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrations/WarehouseTransactionDTOTransform'; import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { ItemEntriesTaxTransactions } from '@/services/TaxRates/ItemEntriesTaxTransactions'; @Service() export class BillDTOTransformer { @@ -23,6 +24,9 @@ export class BillDTOTransformer { @Inject() private warehouseDTOTransform: WarehouseTransactionDTOTransform; + @Inject() + private taxDTOTransformer: ItemEntriesTaxTransactions; + @Inject() private tenancy: HasTenancyService; @@ -76,11 +80,20 @@ export class BillDTOTransformer { reference_type: 'Bill', ...omit(entry, ['amount']), })); - const entries = await composeAsync( + const asyncEntries = await composeAsync( + // Associate tax rate from tax id to entries. + this.taxDTOTransformer.assocTaxRateFromTaxIdToEntries(tenantId), + // Associate tax rate id from tax code to entries. + this.taxDTOTransformer.assocTaxRateIdFromCodeToEntries(tenantId), // Sets the default cost account to the bill entries. this.setBillEntriesDefaultAccounts(tenantId) )(initialEntries); + const entries = R.compose( + // Remove tax code from entries. + R.map(R.omit(['taxCode'])) + )(asyncEntries); + const initialDTO = { ...formatDateFields(omit(billDTO, ['open', 'entries']), [ 'billDate', @@ -100,6 +113,8 @@ export class BillDTOTransformer { userId: authorizedUser.id, }; return R.compose( + // Associates tax amount withheld to the model. + this.taxDTOTransformer.assocTaxAmountWithheldFromEntries, this.branchDTOTransform.transformDTO(tenantId), this.warehouseDTOTransform.transformDTO(tenantId) )(initialDTO); diff --git a/packages/server/src/services/TaxRates/subscribers/BillTaxRateValidateSubscriber.ts b/packages/server/src/services/TaxRates/subscribers/BillTaxRateValidateSubscriber.ts new file mode 100644 index 0000000000..9850e06482 --- /dev/null +++ b/packages/server/src/services/TaxRates/subscribers/BillTaxRateValidateSubscriber.ts @@ -0,0 +1,89 @@ +import { Inject, Service } from 'typedi'; +import { IBillCreatingPayload, IBillEditingPayload } from '@/interfaces'; +import events from '@/subscribers/events'; +import { CommandTaxRatesValidators } from '../CommandTaxRatesValidators'; + +@Service() +export class BillTaxRateValidateSubscriber { + @Inject() + private taxRateDTOValidator: CommandTaxRatesValidators; + + /** + * Attaches events with handlers. + */ + public attach(bus) { + bus.subscribe( + events.bill.onCreating, + this.validateBillEntriesTaxCodeExistanceOnCreating + ); + bus.subscribe( + events.bill.onCreating, + this.validateBillEntriesTaxIdExistanceOnCreating + ); + bus.subscribe( + events.bill.onEditing, + this.validateBillEntriesTaxCodeExistanceOnEditing + ); + bus.subscribe( + events.bill.onEditing, + this.validateBillEntriesTaxIdExistanceOnEditing + ); + return bus; + } + + /** + * Validate bill entries tax rate code existance when creating. + * @param {IBillCreatingPayload} + */ + private validateBillEntriesTaxCodeExistanceOnCreating = async ({ + billDTO, + tenantId, + }: IBillCreatingPayload) => { + await this.taxRateDTOValidator.validateItemEntriesTaxCode( + tenantId, + billDTO.entries + ); + }; + + /** + * Validate the tax rate id existance when creating. + * @param {IBillCreatingPayload} + */ + private validateBillEntriesTaxIdExistanceOnCreating = async ({ + billDTO, + tenantId, + }: IBillCreatingPayload) => { + await this.taxRateDTOValidator.validateItemEntriesTaxCodeId( + tenantId, + billDTO.entries + ); + }; + + /** + * Validate bill entries tax rate code existance when editing. + * @param {IBillEditingPayload} + */ + private validateBillEntriesTaxCodeExistanceOnEditing = async ({ + tenantId, + billDTO, + }: IBillEditingPayload) => { + await this.taxRateDTOValidator.validateItemEntriesTaxCode( + tenantId, + billDTO.entries + ); + }; + + /** + * Validates the bill entries tax rate id existance when editing. + * @param {ISaleInvoiceEditingPayload} payload - + */ + private validateBillEntriesTaxIdExistanceOnEditing = async ({ + tenantId, + billDTO, + }: IBillEditingPayload) => { + await this.taxRateDTOValidator.validateItemEntriesTaxCodeId( + tenantId, + billDTO.entries + ); + }; +} diff --git a/packages/server/src/services/TaxRates/subscribers/WriteBillTaxTransactionsSubscriber.ts b/packages/server/src/services/TaxRates/subscribers/WriteBillTaxTransactionsSubscriber.ts new file mode 100644 index 0000000000..fc74c06861 --- /dev/null +++ b/packages/server/src/services/TaxRates/subscribers/WriteBillTaxTransactionsSubscriber.ts @@ -0,0 +1,87 @@ +import { Inject, Service } from 'typedi'; +import { + IBIllEventDeletedPayload, + IBillCreatedPayload, + IBillEditedPayload, + ISaleInvoiceCreatedPayload, + ISaleInvoiceDeletedPayload, + ISaleInvoiceEditedPayload, +} from '@/interfaces'; +import events from '@/subscribers/events'; +import { WriteTaxTransactionsItemEntries } from '../WriteTaxTransactionsItemEntries'; + +@Service() +export class WriteBillTaxTransactionsSubscriber { + @Inject() + private writeTaxTransactions: WriteTaxTransactionsItemEntries; + + /** + * Attaches events with handlers. + */ + public attach(bus) { + bus.subscribe( + events.bill.onCreated, + this.writeInvoiceTaxTransactionsOnCreated + ); + bus.subscribe( + events.bill.onEdited, + this.rewriteInvoiceTaxTransactionsOnEdited + ); + bus.subscribe( + events.bill.onDeleted, + this.removeInvoiceTaxTransactionsOnDeleted + ); + return bus; + } + + /** + * Writes the bill tax transactions on invoice created. + * @param {ISaleInvoiceCreatingPaylaod} + */ + private writeInvoiceTaxTransactionsOnCreated = async ({ + tenantId, + bill, + trx, + }: IBillCreatedPayload) => { + await this.writeTaxTransactions.writeTaxTransactionsFromItemEntries( + tenantId, + bill.entries, + trx + ); + }; + + /** + * Rewrites the bill tax transactions on invoice edited. + * @param {IBillEditedPayload} payload - + */ + private rewriteInvoiceTaxTransactionsOnEdited = async ({ + tenantId, + bill, + trx, + }: IBillEditedPayload) => { + await this.writeTaxTransactions.rewriteTaxRateTransactionsFromItemEntries( + tenantId, + bill.entries, + 'Bill', + bill.id, + trx + ); + }; + + /** + * Removes the invoice tax transactions on invoice deleted. + * @param {IBIllEventDeletedPayload} + */ + private removeInvoiceTaxTransactionsOnDeleted = async ({ + tenantId, + oldBill, + trx, + }: IBIllEventDeletedPayload) => { + await this.writeTaxTransactions.removeTaxTransactionsFromItemEntries( + tenantId, + oldBill.id, + 'Bill', + trx + ); + }; +} From 372baf55e90595ca85933df656b00a75d0bfa694 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Thu, 5 Oct 2023 13:42:04 +0200 Subject: [PATCH 2/9] feat(webapp): add inclusive/exclusive tax option in bill form --- .../webapp/src/components/Forms/Select.tsx | 4 ++ .../src/constants/InclusiveTaxOptions.ts | 7 +++ .../containers/Entries/EntriesActionBar.tsx | 16 +++++ .../Purchases/Bills/BillForm/BillForm.tsx | 6 +- .../Bills/BillForm/BillFormEntriesActions.tsx | 58 +++++++++++++++++++ .../Bills/BillForm/BillItemsEntriesEditor.tsx | 56 ++++++++---------- .../Purchases/Bills/BillForm/utils.tsx | 19 ++++++ .../InvoiceForm/InvoiceFormActions.tsx | 27 ++------- .../Sales/Invoices/InvoiceForm/utils.tsx | 2 +- packages/webapp/src/style/objects/form.scss | 5 +- 10 files changed, 141 insertions(+), 59 deletions(-) create mode 100644 packages/webapp/src/constants/InclusiveTaxOptions.ts create mode 100644 packages/webapp/src/containers/Entries/EntriesActionBar.tsx create mode 100644 packages/webapp/src/containers/Purchases/Bills/BillForm/BillFormEntriesActions.tsx diff --git a/packages/webapp/src/components/Forms/Select.tsx b/packages/webapp/src/components/Forms/Select.tsx index 205f23f85b..cae52b1b80 100644 --- a/packages/webapp/src/components/Forms/Select.tsx +++ b/packages/webapp/src/components/Forms/Select.tsx @@ -26,6 +26,10 @@ const SelectButton = styled(Button)` position: relative; padding-right: 30px; + &.bp4-small{ + padding-right: 24px; + } + &:not(.is-selected):not([class*='bp4-intent-']):not(.bp4-minimal) { color: #5c7080; } diff --git a/packages/webapp/src/constants/InclusiveTaxOptions.ts b/packages/webapp/src/constants/InclusiveTaxOptions.ts new file mode 100644 index 0000000000..b5e16100af --- /dev/null +++ b/packages/webapp/src/constants/InclusiveTaxOptions.ts @@ -0,0 +1,7 @@ + +import { TaxType } from '@/interfaces/TaxRates'; + +export const InclusiveTaxOptions = [ + { key: TaxType.Inclusive, label: 'Inclusive of Tax' }, + { key: TaxType.Exclusive, label: 'Exclusive of Tax' }, +]; diff --git a/packages/webapp/src/containers/Entries/EntriesActionBar.tsx b/packages/webapp/src/containers/Entries/EntriesActionBar.tsx new file mode 100644 index 0000000000..921e63c10a --- /dev/null +++ b/packages/webapp/src/containers/Entries/EntriesActionBar.tsx @@ -0,0 +1,16 @@ +import { Box } from '@/components'; +import styled from 'styled-components'; + +export const EntriesActionsBar = styled(Box)` + padding-bottom: 12px; + display: flex; + + .bp4-form-group { + margin-bottom: 0; + + label.bp4-label { + opacity: 0.6; + margin-right: 8px; + } + } +`; diff --git a/packages/webapp/src/containers/Purchases/Bills/BillForm/BillForm.tsx b/packages/webapp/src/containers/Purchases/Bills/BillForm/BillForm.tsx index 38aa0fb5ed..57e9bda189 100644 --- a/packages/webapp/src/containers/Purchases/Bills/BillForm/BillForm.tsx +++ b/packages/webapp/src/containers/Purchases/Bills/BillForm/BillForm.tsx @@ -26,6 +26,7 @@ import { handleErrors, } from './utils'; import withCurrentOrganization from '@/containers/Organization/withCurrentOrganization'; +import { BillFormEntriesActions } from './BillFormEntriesActions'; /** * Bill form. @@ -126,7 +127,10 @@ function BillForm({
- +
+ + +
diff --git a/packages/webapp/src/containers/Purchases/Bills/BillForm/BillFormEntriesActions.tsx b/packages/webapp/src/containers/Purchases/Bills/BillForm/BillFormEntriesActions.tsx new file mode 100644 index 0000000000..acc7e99488 --- /dev/null +++ b/packages/webapp/src/containers/Purchases/Bills/BillForm/BillFormEntriesActions.tsx @@ -0,0 +1,58 @@ +// @ts-nocheck +import styled from 'styled-components'; +import { useFormikContext } from 'formik'; +import { FFormGroup, FSelect } from '@/components'; +import { InclusiveTaxOptions } from '@/constants/InclusiveTaxOptions'; + +import { composeEntriesOnEditInclusiveTax } from './utils'; +import { EntriesActionsBar } from '@/containers/Entries/EntriesActionBar'; + +export function BillFormEntriesActions() { + return ( + + + + ); +} + +/** + * Bill exclusive/inclusive select. + * @returns {React.ReactNode} + */ +export function BillExclusiveInclusiveSelect(props) { + const { values, setFieldValue } = useFormikContext(); + + const handleItemSelect = (item) => { + const newEntries = composeEntriesOnEditInclusiveTax( + item.key, + values.entries, + ); + setFieldValue('inclusive_exclusive_tax', item.key); + setFieldValue('entries', newEntries); + }; + + return ( + + ''} + valueAccessor={'key'} + popoverProps={{ minimal: true, usePortal: true, inline: false }} + buttonProps={{ small: true }} + onItemSelect={handleItemSelect} + filterable={false} + {...props} + /> + + ); +} + +const InclusiveFormGroup = styled(FFormGroup)` + margin-left: auto; +`; diff --git a/packages/webapp/src/containers/Purchases/Bills/BillForm/BillItemsEntriesEditor.tsx b/packages/webapp/src/containers/Purchases/Bills/BillForm/BillItemsEntriesEditor.tsx index 5d46a0e034..c2a022ae24 100644 --- a/packages/webapp/src/containers/Purchases/Bills/BillForm/BillItemsEntriesEditor.tsx +++ b/packages/webapp/src/containers/Purchases/Bills/BillForm/BillItemsEntriesEditor.tsx @@ -1,46 +1,40 @@ // @ts-nocheck -import React from 'react'; -import classNames from 'classnames'; import ItemsEntriesTable from '@/containers/Entries/ItemsEntriesTable'; import { FastField } from 'formik'; -import { CLASSES } from '@/constants/classes'; import { useBillFormContext } from './BillFormProvider'; import { entriesFieldShouldUpdate } from './utils'; import { ITEM_TYPE } from '@/containers/Entries/utils'; /** - * Bill form body. + * Bill form body. */ export default function BillFormBody({ defaultBill }) { const { items } = useBillFormContext(); return ( -
- - {({ - form: { values, setFieldValue }, - field: { value }, - meta: { error, touched }, - }) => ( - { - setFieldValue('entries', entries); - }} - items={items} - errors={error} - linesNumber={4} - currencyCode={values.currency_code} - itemType={ITEM_TYPE.PURCHASABLE} - landedCost={true} - enableTaxRates={false} - /> - )} - -
+ + {({ + form: { values, setFieldValue }, + field: { value }, + meta: { error, touched }, + }) => ( + { + setFieldValue('entries', entries); + }} + items={items} + errors={error} + linesNumber={4} + currencyCode={values.currency_code} + itemType={ITEM_TYPE.PURCHASABLE} + landedCost={true} + /> + )} + ); } diff --git a/packages/webapp/src/containers/Purchases/Bills/BillForm/utils.tsx b/packages/webapp/src/containers/Purchases/Bills/BillForm/utils.tsx index 7e7cce0176..ff56831fd1 100644 --- a/packages/webapp/src/containers/Purchases/Bills/BillForm/utils.tsx +++ b/packages/webapp/src/containers/Purchases/Bills/BillForm/utils.tsx @@ -17,6 +17,7 @@ import { import { updateItemsEntriesTotal, ensureEntriesHaveEmptyLine, + assignEntriesTaxAmount, } from '@/containers/Entries/utils'; import { useCurrentOrganization } from '@/hooks/state'; import { @@ -24,6 +25,7 @@ import { getEntriesTotal, } from '@/containers/Entries/utils'; import { useBillFormContext } from './BillFormProvider'; +import { TaxType } from '@/interfaces/TaxRates'; export const MIN_LINES_NUMBER = 1; @@ -46,6 +48,7 @@ export const defaultBill = { bill_date: moment(new Date()).format('YYYY-MM-DD'), due_date: moment(new Date()).format('YYYY-MM-DD'), reference_no: '', + inclusive_exclusive_tax: TaxType.Inclusive, note: '', open: '', branch_id: '', @@ -82,6 +85,9 @@ export const transformToEditForm = (bill) => { return { ...transformToForm(bill, defaultBill), + inclusive_exclusive_tax: invoice.is_inclusive_tax + ? TaxType.Inclusive + : TaxType.Exclusive, entries, }; }; @@ -288,3 +294,16 @@ export const useBillIsForeignCustomer = () => { ); return isForeignCustomer; }; + +/** + * Re-calcualte the entries tax amount when editing. + * @returns {string} + */ +export const composeEntriesOnEditInclusiveTax = ( + inclusiveExclusiveTax: string, + entries, +) => { + return R.compose( + assignEntriesTaxAmount(inclusiveExclusiveTax === 'inclusive'), + )(entries); +}; diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormActions.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormActions.tsx index bf21e0fada..7f8d74c397 100644 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormActions.tsx +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormActions.tsx @@ -3,7 +3,8 @@ import React from 'react'; import styled from 'styled-components'; import { useFormikContext } from 'formik'; import { InclusiveButtonOptions } from './constants'; -import { Box, FFormGroup, FSelect } from '@/components'; +import { FFormGroup, FSelect } from '@/components'; +import { EntriesActionsBar } from '@/containers/Entries/EntriesActionBar'; import { composeEntriesOnEditInclusiveTax } from './utils'; /** @@ -12,9 +13,9 @@ import { composeEntriesOnEditInclusiveTax } from './utils'; */ export function InvoiceFormActions() { return ( - + - + ); } @@ -40,7 +41,7 @@ export function InvoiceExclusiveInclusiveSelect(props) { label={'Amounts are'} inline={true} > - Date: Thu, 5 Oct 2023 18:35:04 +0200 Subject: [PATCH 3/9] feat(webapp): assign tax rates to bill entries --- .../webapp/src/containers/Entries/utils.tsx | 36 +++++++- .../Bills/BillForm/BillFormFooterRight.tsx | 35 ++++++-- .../Bills/BillForm/BillFormProvider.tsx | 11 ++- .../Bills/BillForm/BillItemsEntriesEditor.tsx | 3 +- .../Purchases/Bills/BillForm/utils.tsx | 86 +++++++++++++++++-- .../InvoiceForm/InvoiceFormFooterRight.tsx | 3 +- .../Sales/Invoices/InvoiceForm/utils.tsx | 43 ++++------ 7 files changed, 171 insertions(+), 46 deletions(-) diff --git a/packages/webapp/src/containers/Entries/utils.tsx b/packages/webapp/src/containers/Entries/utils.tsx index 51ff04412d..bfc62138ea 100644 --- a/packages/webapp/src/containers/Entries/utils.tsx +++ b/packages/webapp/src/containers/Entries/utils.tsx @@ -1,8 +1,7 @@ // @ts-nocheck import React, { useCallback } from 'react'; import * as R from 'ramda'; -import { sumBy, isEmpty, last, keyBy } from 'lodash'; - +import { sumBy, isEmpty, last, keyBy, groupBy } from 'lodash'; import { useItem } from '@/hooks/query'; import { toSafeNumber, @@ -12,6 +11,7 @@ import { updateAutoAddNewLine, orderingLinesIndexes, updateTableRow, + formattedAmount, } from '@/utils'; import { useItemEntriesTableContext } from './ItemEntriesTableProvider'; @@ -116,6 +116,11 @@ export function useFetchItemRow({ landedCost, itemType, notifyNewRow }) { ? item.purchase_description : item.sell_description; + const taxRateId = + itemType === ITEM_TYPE.PURCHASABLE + ? item.purchase_tax_rate_id + : item.sell_tax_rate_id; + // Detarmines whether the landed cost checkbox should be disabled. const landedCostDisabled = isLandedCostDisabled(item); @@ -130,6 +135,7 @@ export function useFetchItemRow({ landedCost, itemType, notifyNewRow }) { landed_cost_disabled: landedCostDisabled, } : {}), + taxRateId, }; setItemRow(null); saveInvoke(notifyNewRow, newRow, rowIndex); @@ -266,3 +272,29 @@ export const useComposeRowsOnRemoveTableRow = () => { [minLinesNumber, defaultEntry, localValue], ); }; + +/** + * Retrieves the aggregate tax rates from the given item entries. + */ +export const aggregateItemEntriesTaxRates = R.curry((taxRates, entries) => { + const taxRatesById = keyBy(taxRates, 'id'); + + // Calculate the total tax amount of invoice entries. + const filteredEntries = entries.filter((e) => e.tax_rate_id); + const groupedTaxRates = groupBy(filteredEntries, 'tax_rate_id'); + + return Object.keys(groupedTaxRates).map((taxRateId) => { + const taxRate = taxRatesById[taxRateId]; + const taxRates = groupedTaxRates[taxRateId]; + const totalTaxAmount = sumBy(taxRates, 'tax_amount'); + const taxAmountFormatted = formattedAmount(totalTaxAmount, 'USD'); + + return { + taxRateId, + taxRate: taxRate.rate, + label: `${taxRate.name} [${taxRate.rate}%]`, + taxAmount: totalTaxAmount, + taxAmountFormatted, + }; + }); +}); diff --git a/packages/webapp/src/containers/Purchases/Bills/BillForm/BillFormFooterRight.tsx b/packages/webapp/src/containers/Purchases/Bills/BillForm/BillFormFooterRight.tsx index 825cb8eff2..0657299d6c 100644 --- a/packages/webapp/src/containers/Purchases/Bills/BillForm/BillFormFooterRight.tsx +++ b/packages/webapp/src/containers/Purchases/Bills/BillForm/BillFormFooterRight.tsx @@ -1,15 +1,14 @@ // @ts-nocheck -import React from 'react'; import styled from 'styled-components'; - import { - T, TotalLines, TotalLine, TotalLineBorderStyle, TotalLineTextStyle, } from '@/components'; -import { useBillTotals } from './utils'; +import { useBillAggregatedTaxRates, useBillTotals } from './utils'; +import { useFormikContext } from 'formik'; +import { TaxType } from '@/interfaces/TaxRates'; export function BillFormFooterRight() { const { @@ -19,26 +18,46 @@ export function BillFormFooterRight() { formattedPaymentTotal, } = useBillTotals(); + const { + values: { inclusive_exclusive_tax, currency_code }, + } = useFormikContext(); + + const taxEntries = useBillAggregatedTaxRates(); + return ( } + title={ + <> + {inclusive_exclusive_tax === TaxType.Inclusive + ? 'Subtotal (Tax Inclusive)' + : 'Subtotal'} + + } value={formattedSubtotal} borderStyle={TotalLineBorderStyle.None} /> + {taxEntries.map((tax, index) => ( + + ))} } + title={`Total (${currency_code})`} value={formattedTotal} borderStyle={TotalLineBorderStyle.SingleDark} textStyle={TotalLineTextStyle.Bold} /> } + title={'Paid Amount'} value={formattedPaymentTotal} borderStyle={TotalLineBorderStyle.None} /> } + title={'Due Amount'} value={formattedDueTotal} textStyle={TotalLineTextStyle.Bold} /> diff --git a/packages/webapp/src/containers/Purchases/Bills/BillForm/BillFormProvider.tsx b/packages/webapp/src/containers/Purchases/Bills/BillForm/BillFormProvider.tsx index 7c717a9b1c..d960891f0b 100644 --- a/packages/webapp/src/containers/Purchases/Bills/BillForm/BillFormProvider.tsx +++ b/packages/webapp/src/containers/Purchases/Bills/BillForm/BillFormProvider.tsx @@ -15,6 +15,7 @@ import { useCreateBill, useEditBill, } from '@/hooks/query'; +import { useTaxRates } from '@/hooks/query/taxRates'; const BillFormContext = createContext(); @@ -83,6 +84,9 @@ function BillFormProvider({ billId, ...props }) { isSuccess: isBranchesSuccess, } = useBranches({}, { enabled: isBranchFeatureCan }); + // Fetch tax rates. + const { data: taxRates, isLoading: isTaxRatesLoading } = useTaxRates(); + // Fetches the projects list. const { data: { projects }, @@ -103,7 +107,10 @@ function BillFormProvider({ billId, ...props }) { // Determines whether the warehouse and branches are loading. const isFeatureLoading = - isWarehouesLoading || isBranchesLoading || isProjectsLoading; + isWarehouesLoading || + isBranchesLoading || + isProjectsLoading || + isTaxRatesLoading; const provider = { accounts, @@ -113,6 +120,7 @@ function BillFormProvider({ billId, ...props }) { warehouses, branches, projects, + taxRates, submitPayload, isNewMode, @@ -124,6 +132,7 @@ function BillFormProvider({ billId, ...props }) { isFeatureLoading, isBranchesSuccess, isWarehousesSuccess, + isTaxRatesLoading, createBillMutate, editBillMutate, diff --git a/packages/webapp/src/containers/Purchases/Bills/BillForm/BillItemsEntriesEditor.tsx b/packages/webapp/src/containers/Purchases/Bills/BillForm/BillItemsEntriesEditor.tsx index c2a022ae24..a07e43fcb4 100644 --- a/packages/webapp/src/containers/Purchases/Bills/BillForm/BillItemsEntriesEditor.tsx +++ b/packages/webapp/src/containers/Purchases/Bills/BillForm/BillItemsEntriesEditor.tsx @@ -9,7 +9,7 @@ import { ITEM_TYPE } from '@/containers/Entries/utils'; * Bill form body. */ export default function BillFormBody({ defaultBill }) { - const { items } = useBillFormContext(); + const { items, taxRates } = useBillFormContext(); return ( )} diff --git a/packages/webapp/src/containers/Purchases/Bills/BillForm/utils.tsx b/packages/webapp/src/containers/Purchases/Bills/BillForm/utils.tsx index ff56831fd1..9bc771198f 100644 --- a/packages/webapp/src/containers/Purchases/Bills/BillForm/utils.tsx +++ b/packages/webapp/src/containers/Purchases/Bills/BillForm/utils.tsx @@ -3,7 +3,7 @@ import React from 'react'; import moment from 'moment'; import intl from 'react-intl-universal'; import * as R from 'ramda'; -import { first } from 'lodash'; +import { first, chain } from 'lodash'; import { Intent } from '@blueprintjs/core'; import { useFormikContext } from 'formik'; import { AppToaster } from '@/components'; @@ -18,6 +18,7 @@ import { updateItemsEntriesTotal, ensureEntriesHaveEmptyLine, assignEntriesTaxAmount, + aggregateItemEntriesTaxRates, } from '@/containers/Entries/utils'; import { useCurrentOrganization } from '@/hooks/state'; import { @@ -85,7 +86,7 @@ export const transformToEditForm = (bill) => { return { ...transformToForm(bill, defaultBill), - inclusive_exclusive_tax: invoice.is_inclusive_tax + inclusive_exclusive_tax: bill.is_inclusive_tax ? TaxType.Inclusive : TaxType.Exclusive, entries, @@ -234,11 +235,12 @@ export const useSetPrimaryWarehouseToForm = () => { */ export const useBillTotals = () => { const { - values: { entries, currency_code: currencyCode }, + values: { currency_code: currencyCode }, } = useFormikContext(); - // Retrieves the bili entries total. - const total = React.useMemo(() => getEntriesTotal(entries), [entries]); + // Retrieves the bill subtotal. + const subtotal = useBillSubtotal(); + const total = useBillTotal(); // Retrieves the formatted total money. const formattedTotal = React.useMemo( @@ -247,8 +249,8 @@ export const useBillTotals = () => { ); // Retrieves the formatted subtotal. const formattedSubtotal = React.useMemo( - () => formattedAmount(total, currencyCode, { money: false }), - [total, currencyCode], + () => formattedAmount(subtotal, currencyCode, { money: false }), + [subtotal, currencyCode], ); // Retrieves the payment total. const paymentTotal = React.useMemo(() => 0, []); @@ -307,3 +309,73 @@ export const composeEntriesOnEditInclusiveTax = ( assignEntriesTaxAmount(inclusiveExclusiveTax === 'inclusive'), )(entries); }; + +/** + * Retreives the bill aggregated tax rates. + * @returns {Array} + */ +export const useBillAggregatedTaxRates = () => { + const { values } = useFormikContext(); + const { taxRates } = useBillFormContext(); + + const aggregateTaxRates = React.useMemo( + () => aggregateItemEntriesTaxRates(taxRates), + [taxRates], + ); + // Calculate the total tax amount of bill entries. + return React.useMemo(() => { + return aggregateTaxRates(values.entries); + }, [aggregateTaxRates, values.entries]); +}; + +/** + * Retrieves the bill subtotal. + * @returns {number} + */ +export const useBillSubtotal = () => { + const { + values: { entries }, + } = useFormikContext(); + + // Calculate the total due amount of bill entries. + return React.useMemo(() => getEntriesTotal(entries), [entries]); +}; + +/** + * Retreives the bill total tax amount. + * @returns {number} + */ +export const useBillTotalTaxAmount = () => { + const { values } = useFormikContext(); + + return React.useMemo(() => { + return chain(values.entries) + .filter((entry) => entry.tax_amount) + .sumBy('tax_amount') + .value(); + }, [values.entries]); +}; + +/** + * Detarmines whether the tax is exclusive. + * @returns {boolean} + */ +export const useIsBillTaxExclusive = () => { + const { values } = useFormikContext(); + + return values.inclusive_exclusive_tax === TaxType.Exclusive; +}; + +/** + * Retreives the bill total. + * @returns {number} + */ +export const useBillTotal = () => { + const subtotal = useBillSubtotal(); + const totalTaxAmount = useBillTotalTaxAmount(); + const isExclusiveTax = useIsBillTaxExclusive(); + + return R.compose(R.when(R.always(isExclusiveTax), R.add(totalTaxAmount)))( + subtotal, + ); +}; diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormFooterRight.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormFooterRight.tsx index a82db600eb..5c152e26ba 100644 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormFooterRight.tsx +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormFooterRight.tsx @@ -11,6 +11,7 @@ import { TotalLineTextStyle, } from '@/components'; import { useInvoiceAggregatedTaxRates, useInvoiceTotals } from './utils'; +import { TaxType } from '@/interfaces/TaxRates'; export function InvoiceFormFooterRight() { // Calculate the total due amount of invoice entries. @@ -32,7 +33,7 @@ export function InvoiceFormFooterRight() { - {inclusive_exclusive_tax === 'inclusive' + {inclusive_exclusive_tax === TaxType.Inclusive ? 'Subtotal (Tax Inclusive)' : 'Subtotal'} diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/utils.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/utils.tsx index 0e6e5dcbab..4186cbc63d 100644 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/utils.tsx +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceForm/utils.tsx @@ -1,18 +1,23 @@ // @ts-nocheck -import React, { useMemo } from 'react'; +import React from 'react'; +import { useFormikContext } from 'formik'; import intl from 'react-intl-universal'; import moment from 'moment'; import * as R from 'ramda'; import { Intent } from '@blueprintjs/core'; -import { omit, first, keyBy, sumBy, groupBy } from 'lodash'; -import { compose, transformToForm, repeatValue } from '@/utils'; -import { useFormikContext } from 'formik'; - -import { formattedAmount, defaultFastFieldShouldUpdate } from '@/utils'; +import { omit, first, sumBy } from 'lodash'; +import { + compose, + transformToForm, + repeatValue, + formattedAmount, + defaultFastFieldShouldUpdate, +} from '@/utils'; import { ERROR } from '@/constants/errors'; import { AppToaster } from '@/components'; import { useCurrentOrganization } from '@/hooks/state'; import { + aggregateItemEntriesTaxRates, assignEntriesTaxAmount, getEntriesTotal, } from '@/containers/Entries/utils'; @@ -327,28 +332,14 @@ export const useInvoiceAggregatedTaxRates = () => { const { values } = useFormikContext(); const { taxRates } = useInvoiceFormContext(); - const taxRatesById = useMemo(() => keyBy(taxRates, 'id'), [taxRates]); - + const aggregateTaxRates = React.useMemo( + () => aggregateItemEntriesTaxRates(taxRates), + [taxRates], + ); // Calculate the total tax amount of invoice entries. return React.useMemo(() => { - const filteredEntries = values.entries.filter((e) => e.tax_rate_id); - const groupedTaxRates = groupBy(filteredEntries, 'tax_rate_id'); - - return Object.keys(groupedTaxRates).map((taxRateId) => { - const taxRate = taxRatesById[taxRateId]; - const taxRates = groupedTaxRates[taxRateId]; - const totalTaxAmount = sumBy(taxRates, 'tax_amount'); - const taxAmountFormatted = formattedAmount(totalTaxAmount, 'USD'); - - return { - taxRateId, - taxRate: taxRate.rate, - label: `${taxRate.name} [${taxRate.rate}%]`, - taxAmount: totalTaxAmount, - taxAmountFormatted, - }; - }); - }, [values.entries]); + return aggregateTaxRates(values.entries); + }, [aggregateTaxRates, values.entries]); }; /** From a633e3a647cbdfd09ae24fca266b7fdaaa5e3991 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Fri, 6 Oct 2023 12:38:56 +0200 Subject: [PATCH 4/9] feat(server): add tax rates to bills service --- packages/server/src/interfaces/Bill.ts | 9 ++ packages/server/src/models/Bill.ts | 148 ++++++++++++++---- .../Purchases/Bills/BillDTOTransformer.ts | 3 +- .../src/services/Purchases/Bills/GetBill.ts | 3 +- .../Bills/PurchaseInvoiceTransformer.ts | 110 +++++++++++-- 5 files changed, 232 insertions(+), 41 deletions(-) diff --git a/packages/server/src/interfaces/Bill.ts b/packages/server/src/interfaces/Bill.ts index fa94c3e410..3919a28dbb 100644 --- a/packages/server/src/interfaces/Bill.ts +++ b/packages/server/src/interfaces/Bill.ts @@ -19,6 +19,7 @@ export interface IBillDTO { branchId?: number; warehouseId?: number; projectId?: number; + isInclusiveTax?: boolean; } export interface IBillEditDTO { @@ -80,6 +81,14 @@ export interface IBill { localAmount?: number; locatedLandedCosts?: IBillLandedCost[]; + + amountLocal: number; + subtotal: number; + subtotalLocal: number; + subtotalExcludingTax: number; + taxAmountWithheldLocal: number; + total: number; + totalLocal: number; } export interface IBillsFilter extends IDynamicListFilterDTO { diff --git a/packages/server/src/models/Bill.ts b/packages/server/src/models/Bill.ts index b651671c12..b29e32d237 100644 --- a/packages/server/src/models/Bill.ts +++ b/packages/server/src/models/Bill.ts @@ -13,6 +13,110 @@ export default class Bill extends mixin(TenantModel, [ CustomViewBaseModel, ModelSearchable, ]) { + public amount: number; + public paymentAmount: number; + public landedCostAmount: number; + public allocatedCostAmount: number; + public isInclusiveTax: boolean; + public taxAmountWithheld: number; + public exchangeRate: number; + + /** + * Timestamps columns. + */ + get timestamps() { + return ['createdAt', 'updatedAt']; + } + + /** + * Virtual attributes. + */ + static get virtualAttributes() { + return [ + 'balance', + 'dueAmount', + 'isOpen', + 'isPartiallyPaid', + 'isFullyPaid', + 'isPaid', + 'remainingDays', + 'overdueDays', + 'isOverdue', + 'unallocatedCostAmount', + 'localAmount', + 'localAllocatedCostAmount', + 'billableAmount', + + 'amountLocal', + 'subtotal', + 'subtotalLocal', + 'subtotalExludingTax', + 'taxAmountWithheldLocal', + 'total', + 'totalLocal', + ]; + } + + /** + * Invoice amount in base currency. + * @returns {number} + */ + get amountLocal() { + return this.amount * this.exchangeRate; + } + + /** + * Subtotal. (Tax inclusive) if the tax inclusive is enabled. + * @returns {number} + */ + get subtotal() { + return this.amount; + } + + /** + * Subtotal in base currency. (Tax inclusive) if the tax inclusive is enabled. + * @returns {number} + */ + get subtotalLocal() { + return this.amountLocal; + } + + /** + * Sale invoice amount excluding tax. + * @returns {number} + */ + get subtotalExcludingTax() { + return this.isInclusiveTax + ? this.subtotal - this.taxAmountWithheld + : this.subtotal; + } + + /** + * Tax amount withheld in base currency. + * @returns {number} + */ + get taxAmountWithheldLocal() { + return this.taxAmountWithheld * this.exchangeRate; + } + + /** + * Invoice total. (Tax included) + * @returns {number} + */ + get total() { + return this.isInclusiveTax + ? this.subtotal + : this.subtotal + this.taxAmountWithheld; + } + + /** + * Invoice total in local currency. (Tax included) + * @returns {number} + */ + get totalLocal() { + return this.total * this.exchangeRate; + } + /** * Table name */ @@ -158,34 +262,6 @@ export default class Bill extends mixin(TenantModel, [ }; } - /** - * Timestamps columns. - */ - get timestamps() { - return ['createdAt', 'updatedAt']; - } - - /** - * Virtual attributes. - */ - static get virtualAttributes() { - return [ - 'balance', - 'dueAmount', - 'isOpen', - 'isPartiallyPaid', - 'isFullyPaid', - 'isPaid', - 'remainingDays', - 'overdueDays', - 'isOverdue', - 'unallocatedCostAmount', - 'localAmount', - 'localAllocatedCostAmount', - 'billableAmount', - ]; - } - /** * Invoice amount in organization base currency. * @returns {number} @@ -326,6 +402,7 @@ export default class Bill extends mixin(TenantModel, [ const ItemEntry = require('models/ItemEntry'); const BillLandedCost = require('models/BillLandedCost'); const Branch = require('models/Branch'); + const TaxRateTransaction = require('models/TaxRateTransaction'); return { vendor: { @@ -373,6 +450,21 @@ export default class Bill extends mixin(TenantModel, [ to: 'branches.id', }, }, + + /** + * Bill may has associated tax rate transactions. + */ + taxes: { + relation: Model.HasManyRelation, + modelClass: TaxRateTransaction.default, + join: { + from: 'bills.id', + to: 'tax_rate_transactions.referenceId', + }, + filter(builder) { + builder.where('reference_type', 'Bill'); + }, + }, }; } diff --git a/packages/server/src/services/Purchases/Bills/BillDTOTransformer.ts b/packages/server/src/services/Purchases/Bills/BillDTOTransformer.ts index 66499b13fd..fed2c249e7 100644 --- a/packages/server/src/services/Purchases/Bills/BillDTOTransformer.ts +++ b/packages/server/src/services/Purchases/Bills/BillDTOTransformer.ts @@ -77,7 +77,8 @@ export class BillDTOTransformer { const billNumber = billDTO.billNumber || oldBill?.billNumber; const initialEntries = billDTO.entries.map((entry) => ({ - reference_type: 'Bill', + referenceType: 'Bill', + isInclusiveTax: billDTO.isInclusiveTax, ...omit(entry, ['amount']), })); const asyncEntries = await composeAsync( diff --git a/packages/server/src/services/Purchases/Bills/GetBill.ts b/packages/server/src/services/Purchases/Bills/GetBill.ts index fb653c25c1..03f6a83612 100644 --- a/packages/server/src/services/Purchases/Bills/GetBill.ts +++ b/packages/server/src/services/Purchases/Bills/GetBill.ts @@ -28,7 +28,8 @@ export class GetBill { .findById(billId) .withGraphFetched('vendor') .withGraphFetched('entries.item') - .withGraphFetched('branch'); + .withGraphFetched('branch') + .withGraphFetched('taxes.taxRate'); // Validates the bill existance. this.validators.validateBillExistance(bill); diff --git a/packages/server/src/services/Purchases/Bills/PurchaseInvoiceTransformer.ts b/packages/server/src/services/Purchases/Bills/PurchaseInvoiceTransformer.ts index 7918a36438..c280f51f76 100644 --- a/packages/server/src/services/Purchases/Bills/PurchaseInvoiceTransformer.ts +++ b/packages/server/src/services/Purchases/Bills/PurchaseInvoiceTransformer.ts @@ -1,10 +1,11 @@ import { IBill } from '@/interfaces'; import { Transformer } from '@/lib/Transformer/Transformer'; +import { SaleInvoiceTaxEntryTransformer } from '@/services/Sales/Invoices/SaleInvoiceTaxEntryTransformer'; import { formatNumber } from 'utils'; export class PurchaseInvoiceTransformer extends Transformer { /** - * Include these attributes to sale invoice object. + * Include these attributes to sale bill object. * @returns {Array} */ public includeAttributes = (): string[] => { @@ -16,12 +17,19 @@ export class PurchaseInvoiceTransformer extends Transformer { 'formattedBalance', 'formattedDueAmount', 'formattedExchangeRate', + 'subtotalFormatted', + 'subtotalLocalFormatted', + 'subtotalExcludingTaxFormatted', + 'taxAmountWithheldLocalFormatted', + 'totalFormatted', + 'totalLocalFormatted', + 'taxes', ]; }; /** - * Retrieve formatted invoice date. - * @param {IBill} invoice + * Retrieve formatted bill date. + * @param {IBill} bill * @returns {String} */ protected formattedBillDate = (bill: IBill): string => { @@ -29,8 +37,8 @@ export class PurchaseInvoiceTransformer extends Transformer { }; /** - * Retrieve formatted invoice date. - * @param {IBill} invoice + * Retrieve formatted bill date. + * @param {IBill} bill * @returns {String} */ protected formattedDueDate = (bill: IBill): string => { @@ -39,7 +47,7 @@ export class PurchaseInvoiceTransformer extends Transformer { /** * Retrieve formatted bill amount. - * @param {IBill} invoice + * @param {IBill} bill * @returns {string} */ protected formattedAmount = (bill): string => { @@ -48,7 +56,7 @@ export class PurchaseInvoiceTransformer extends Transformer { /** * Retrieve formatted bill amount. - * @param {IBill} invoice + * @param {IBill} bill * @returns {string} */ protected formattedPaymentAmount = (bill): string => { @@ -59,7 +67,7 @@ export class PurchaseInvoiceTransformer extends Transformer { /** * Retrieve formatted bill amount. - * @param {IBill} invoice + * @param {IBill} bill * @returns {string} */ protected formattedDueAmount = (bill): string => { @@ -77,10 +85,90 @@ export class PurchaseInvoiceTransformer extends Transformer { /** * Retrieve the formatted exchange rate. - * @param {ISaleInvoice} invoice + * @param {IBill} bill + * @returns {string} + */ + protected formattedExchangeRate = (bill): string => { + return formatNumber(bill.exchangeRate, { + currencyCode: this.context.organization.baseCurrency, + }); + }; + + /** + * Retrieves the formatted subtotal. + * @param {IBill} bill + * @returns {string} + */ + protected subtotalFormatted = (bill): string => { + return formatNumber(bill.subtotal, { + currencyCode: bill.currencyCode, + }); + }; + + /** + * Retrieves the local subtotal formatted. + * @param {IBill} bill + * @returns {string} + */ + protected subtotalLocalFormatted = (bill): string => { + return formatNumber(bill.subtotalLocal, { + currencyCode: this.context.organization.baseCurrency, + }); + }; + + /** + * Retrieves the formatted subtotal tax excluded. + * @param {IBill} bill + * @returns {string} + */ + protected subtotalExcludingTaxFormatted = (bill): string => { + return formatNumber(bill.subtotalExludingTax, { + currencyCode: bill.currencyCode, + }); + }; + + /** + * Retrieves the local formatted tax amount withheld + * @param {IBill} bill * @returns {string} */ - protected formattedExchangeRate = (invoice): string => { - return formatNumber(invoice.exchangeRate, { money: false }); + protected taxAmountWithheldLocalFormatted = (bill): string => { + return formatNumber(bill.taxAmountWithheldLocal, { + currencyCode: this.context.organization.baseCurrency, + }); + }; + + /** + * Retrieves the total formatted. + * @param {IBill} bill + * @returns {string} + */ + protected totalFormatted = (bill): string => { + return formatNumber(bill.total, { + currencyCode: bill.currencyCode, + }); + }; + + /** + * Retrieves the local total formatted. + * @param {IBill} bill + * @returns {string} + */ + protected totalLocalFormatted = (bill): string => { + return formatNumber(bill.totalLocal, { + currencyCode: this.context.organization.baseCurrency, + }); + }; + + /** + * Retrieve the taxes lines of bill. + * @param {Bill} bill + */ + protected taxes = (bill) => { + return this.item(bill.taxes, new SaleInvoiceTaxEntryTransformer(), { + amount: bill.amount, + isInclusiveTax: bill.isInclusiveTax, + currencyCode: bill.currencyCode, + }); }; } From 0bba521995f662e7705d13985031e3db22a6e6cf Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Fri, 6 Oct 2023 14:45:38 +0200 Subject: [PATCH 5/9] feat(server): bill taxes GL entries --- .../services/Purchases/Bills/BillGLEntries.ts | 92 ++++++++++++++++--- 1 file changed, 80 insertions(+), 12 deletions(-) diff --git a/packages/server/src/services/Purchases/Bills/BillGLEntries.ts b/packages/server/src/services/Purchases/Bills/BillGLEntries.ts index ebfbc8da82..dd4e3d241e 100644 --- a/packages/server/src/services/Purchases/Bills/BillGLEntries.ts +++ b/packages/server/src/services/Purchases/Bills/BillGLEntries.ts @@ -7,6 +7,7 @@ import { AccountNormal, IBill, IItemEntry, ILedgerEntry } from '@/interfaces'; import HasTenancyService from '@/services/Tenancy/TenancyService'; import Ledger from '@/services/Accounting/Ledger'; import LedgerStorageService from '@/services/Accounting/LedgerStorageService'; +import ItemsEntriesService from '@/services/Items/ItemsEntriesService'; @Service() export class BillGLEntries { @@ -16,6 +17,9 @@ export class BillGLEntries { @Inject() private ledgerStorage: LedgerStorageService; + @Inject() + private itemsEntriesService: ItemsEntriesService; + /** * Creates bill GL entries. * @param {number} tenantId - @@ -43,8 +47,16 @@ export class BillGLEntries { {}, trx ); - const billLedger = this.getBillLedger(bill, APAccount.id); - + // Find or create tax payable account. + const taxPayableAccount = await accountRepository.findOrCreateTaxPayable( + {}, + trx + ); + const billLedger = this.getBillLedger( + bill, + APAccount.id, + taxPayableAccount.id + ); // Commit the GL enties on the storage. await this.ledgerStorage.commit(tenantId, billLedger, trx); }; @@ -83,7 +95,7 @@ export class BillGLEntries { /** * Retrieves the bill common entry. - * @param {IBill} bill + * @param {IBill} bill * @returns {ILedgerEntry} */ private getBillCommonEntry = (bill: IBill) => { @@ -119,7 +131,7 @@ export class BillGLEntries { (bill: IBill, entry: IItemEntry, index: number): ILedgerEntry => { const commonJournalMeta = this.getBillCommonEntry(bill); - const localAmount = bill.exchangeRate * entry.amount; + const localAmount = bill.exchangeRate * entry.amountExludingTax; const landedCostAmount = sumBy(entry.allocatedCostEntries, 'cost'); return { @@ -182,15 +194,61 @@ export class BillGLEntries { }; }; + /** + * Retrieves the bill tax GL entry. + * @param {IBill} bill - + * @param {number} taxPayableAccountId - + * @param {IItemEntry} entry - + * @param {number} index - + * @returns {ILedgerEntry} + */ + private getBillTaxEntry = R.curry( + ( + bill: IBill, + taxPayableAccountId: number, + entry: IItemEntry, + index: number + ): ILedgerEntry => { + const commonJournalMeta = this.getBillCommonEntry(bill); + + return { + ...commonJournalMeta, + debit: entry.taxAmount, + index: index + 4, + accountId: taxPayableAccountId, + accountNormal: AccountNormal.CREDIT, + taxRateId: entry.taxRateId, + taxRate: entry.taxRate, + }; + } + ); + + /** + * Retrieves the bill tax GL entries. + * @param {IBill} bill + * @param {number} taxPayableAccountId + * @returns {ILedgerEntry[]} + */ + private getBillTaxEntries = (bill: IBill, taxPayableAccountId: number) => { + // Retrieves the non-zero tax entries. + const nonZeroTaxEntries = this.itemsEntriesService.getNonZeroEntries( + bill.entries + ); + const transformTaxEntry = this.getBillTaxEntry(bill, taxPayableAccountId); + + return nonZeroTaxEntries.map(transformTaxEntry); + }; + /** * Retrieves the given bill GL entries. - * @param {IBill} bill - * @param {number} payableAccountId + * @param {IBill} bill + * @param {number} payableAccountId * @returns {ILedgerEntry[]} */ private getBillGLEntries = ( bill: IBill, - payableAccountId: number + payableAccountId: number, + taxPayableAccountId: number ): ILedgerEntry[] => { const payableEntry = this.getBillPayableEntry(payableAccountId, bill); @@ -201,18 +259,28 @@ export class BillGLEntries { const landedCostEntries = bill.locatedLandedCosts.map( landedCostTransformer ); + const taxEntries = this.getBillTaxEntries(bill, taxPayableAccountId); + // Allocate cost entries journal entries. - return [payableEntry, ...itemsEntries, ...landedCostEntries]; + return [payableEntry, ...itemsEntries, ...landedCostEntries, ...taxEntries]; }; /** * Retrieves the given bill ledger. - * @param {IBill} bill - * @param {number} payableAccountId + * @param {IBill} bill + * @param {number} payableAccountId * @returns {Ledger} */ - private getBillLedger = (bill: IBill, payableAccountId: number) => { - const entries = this.getBillGLEntries(bill, payableAccountId); + private getBillLedger = ( + bill: IBill, + payableAccountId: number, + taxPayableAccountId: number + ) => { + const entries = this.getBillGLEntries( + bill, + payableAccountId, + taxPayableAccountId + ); return new Ledger(entries); }; From edbdb97bf73727cf695a5a02fe777fddc31db9b0 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Fri, 6 Oct 2023 14:52:13 +0200 Subject: [PATCH 6/9] feat(server): remove not used bill subscriber --- .../subscribers/Bills/WriteJournalEntries.ts | 87 ------------------- 1 file changed, 87 deletions(-) delete mode 100644 packages/server/src/subscribers/Bills/WriteJournalEntries.ts diff --git a/packages/server/src/subscribers/Bills/WriteJournalEntries.ts b/packages/server/src/subscribers/Bills/WriteJournalEntries.ts deleted file mode 100644 index 50e4ea1552..0000000000 --- a/packages/server/src/subscribers/Bills/WriteJournalEntries.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { Inject, Service } from 'typedi'; -import events from '@/subscribers/events'; -import TenancyService from '@/services/Tenancy/TenancyService'; -import BillsService from '@/services/Purchases/Bills'; -import { - IBillCreatedPayload, - IBillEditedPayload, - IBIllEventDeletedPayload, -} from '@/interfaces'; - -@Service() -export default class BillWriteGLEntriesSubscriber { - @Inject() - tenancy: TenancyService; - - @Inject() - billsService: BillsService; - - /** - * Attaches events with handles. - */ - public attach(bus) { - bus.subscribe( - events.bill.onCreated, - this.handlerWriteJournalEntriesOnCreate - ); - bus.subscribe( - events.bill.onEdited, - this.handleOverwriteJournalEntriesOnEdit - ); - bus.subscribe(events.bill.onDeleted, this.handlerDeleteJournalEntries); - } - - /** - * Handles writing journal entries once bill created. - * @param {IBillCreatedPayload} payload - - */ - private handlerWriteJournalEntriesOnCreate = async ({ - tenantId, - billId, - bill, - trx, - }: IBillCreatedPayload) => { - // Can't continue if the bill is not opened yet. - if (!bill.openedAt) return null; - - await this.billsService.recordJournalTransactions( - tenantId, - billId, - false, - trx - ); - }; - - /** - * Handles the overwriting journal entries once bill edited. - * @param {IBillEditedPayload} payload - - */ - private handleOverwriteJournalEntriesOnEdit = async ({ - tenantId, - billId, - bill, - trx, - }: IBillEditedPayload) => { - // Can't continue if the bill is not opened yet. - if (!bill.openedAt) return null; - - await this.billsService.recordJournalTransactions( - tenantId, - billId, - true, - trx - ); - }; - - /** - * Handles revert journal entries on bill deleted. - * @param {IBIllEventDeletedPayload} payload - - */ - private handlerDeleteJournalEntries = async ({ - tenantId, - billId, - trx, - }: IBIllEventDeletedPayload) => { - await this.billsService.revertJournalEntries(tenantId, billId, trx); - }; -} From 4c0e37008b43863ea2bac31a32213d8bb721eca9 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Fri, 6 Oct 2023 21:09:20 +0200 Subject: [PATCH 7/9] feat(webapp): add bill taxes on details drawer --- .../src/components/DrawersContainer.tsx | 2 -- .../Drawers/BillDrawer/BillDetailHeader.tsx | 2 +- .../BillDrawer/BillDetailTableFooter.tsx | 15 +++++++++----- .../Purchases/Bills/BillForm/utils.tsx | 1 + .../Bills/BillsLanding/components.tsx | 20 ++++--------------- 5 files changed, 16 insertions(+), 24 deletions(-) diff --git a/packages/webapp/src/components/DrawersContainer.tsx b/packages/webapp/src/components/DrawersContainer.tsx index ef96dc6084..c8c04f63ae 100644 --- a/packages/webapp/src/components/DrawersContainer.tsx +++ b/packages/webapp/src/components/DrawersContainer.tsx @@ -1,5 +1,3 @@ -import React from 'react'; - import AccountDrawer from '@/containers/Drawers/AccountDrawer'; import ManualJournalDrawer from '@/containers/Drawers/ManualJournalDrawer'; import ExpenseDrawer from '@/containers/Drawers/ExpenseDrawer'; diff --git a/packages/webapp/src/containers/Drawers/BillDrawer/BillDetailHeader.tsx b/packages/webapp/src/containers/Drawers/BillDrawer/BillDetailHeader.tsx index b8fa7a78f3..34e0418a38 100644 --- a/packages/webapp/src/containers/Drawers/BillDrawer/BillDetailHeader.tsx +++ b/packages/webapp/src/containers/Drawers/BillDrawer/BillDetailHeader.tsx @@ -30,7 +30,7 @@ export default function BillDetailHeader() { -

{bill.formatted_amount}

+

{bill.total_formatted}

diff --git a/packages/webapp/src/containers/Drawers/BillDrawer/BillDetailTableFooter.tsx b/packages/webapp/src/containers/Drawers/BillDrawer/BillDetailTableFooter.tsx index e3b44c1ce5..6233a9fae8 100644 --- a/packages/webapp/src/containers/Drawers/BillDrawer/BillDetailTableFooter.tsx +++ b/packages/webapp/src/containers/Drawers/BillDrawer/BillDetailTableFooter.tsx @@ -1,11 +1,8 @@ // @ts-nocheck -import React from 'react'; import styled from 'styled-components'; - import { TotalLineBorderStyle, TotalLineTextStyle, - FormatNumber, T, TotalLines, TotalLine, @@ -23,12 +20,20 @@ export function BillDetailTableFooter() { } - value={} + value={bill.subtotal_formatted} borderStyle={TotalLineBorderStyle.SingleDark} /> + {bill.taxes.map((taxRate) => ( + + ))} } - value={bill.formatted_amount} + value={bill.total_formatted} borderStyle={TotalLineBorderStyle.DoubleDark} textStyle={TotalLineTextStyle.Bold} /> diff --git a/packages/webapp/src/containers/Purchases/Bills/BillForm/utils.tsx b/packages/webapp/src/containers/Purchases/Bills/BillForm/utils.tsx index 9bc771198f..6f39477fa3 100644 --- a/packages/webapp/src/containers/Purchases/Bills/BillForm/utils.tsx +++ b/packages/webapp/src/containers/Purchases/Bills/BillForm/utils.tsx @@ -40,6 +40,7 @@ export const defaultBillEntry = { description: '', amount: '', landed_cost: false, + tax_rate_id: '', }; // Default bill. diff --git a/packages/webapp/src/containers/Purchases/Bills/BillsLanding/components.tsx b/packages/webapp/src/containers/Purchases/Bills/BillsLanding/components.tsx index 7facf19eea..167f145aa4 100644 --- a/packages/webapp/src/containers/Purchases/Bills/BillsLanding/components.tsx +++ b/packages/webapp/src/containers/Purchases/Bills/BillsLanding/components.tsx @@ -9,20 +9,18 @@ import { Tag, ProgressBar, } from '@blueprintjs/core'; - +import clsx from 'classnames'; import { FormatDateCell, FormattedMessage as T, Icon, If, Choose, - Money, Can, } from '@/components'; import { formattedAmount, safeCallback, - isBlank, calculateStatus, } from '@/utils'; import { @@ -30,6 +28,7 @@ import { PaymentMadeAction, AbilitySubject, } from '@/constants/abilityOption'; +import { CLASSES } from '@/constants'; /** * Actions menu. @@ -101,17 +100,6 @@ export function ActionsMenu({ ); } -/** - * Amount accessor. - */ -export function AmountAccessor(bill) { - return !isBlank(bill.amount) ? ( - - ) : ( - '' - ); -} - /** * Status accessor. */ @@ -198,11 +186,11 @@ export function useBillsTableColumns() { { id: 'amount', Header: intl.get('amount'), - accessor: AmountAccessor, + accessor: 'total_formatted', width: 120, - className: 'amount', align: 'right', clickable: true, + className: clsx(CLASSES.FONT_BOLD), }, { id: 'status', From dd789688587fde99266b2d15684f0415b68627f6 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Sat, 7 Oct 2023 03:25:20 +0200 Subject: [PATCH 8/9] fix: Bill virtual attributes with taxes --- packages/server/src/models/Bill.ts | 10 +++++----- .../Purchases/BillPayments/BillPaymentGLEntries.ts | 4 ++-- .../src/services/Purchases/Bills/BillGLEntries.ts | 2 +- .../Purchases/Bills/PurchaseInvoiceTransformer.ts | 9 ++++++++- .../Drawers/BillDrawer/BillDetailTableFooter.tsx | 1 + 5 files changed, 17 insertions(+), 9 deletions(-) diff --git a/packages/server/src/models/Bill.ts b/packages/server/src/models/Bill.ts index b29e32d237..17dd07fa40 100644 --- a/packages/server/src/models/Bill.ts +++ b/packages/server/src/models/Bill.ts @@ -46,7 +46,6 @@ export default class Bill extends mixin(TenantModel, [ 'localAmount', 'localAllocatedCostAmount', 'billableAmount', - 'amountLocal', 'subtotal', 'subtotalLocal', @@ -264,10 +263,11 @@ export default class Bill extends mixin(TenantModel, [ /** * Invoice amount in organization base currency. + * @deprecated * @returns {number} */ get localAmount() { - return this.amount * this.exchangeRate; + return this.amountLocal; } /** @@ -307,7 +307,7 @@ export default class Bill extends mixin(TenantModel, [ * @return {number} */ get dueAmount() { - return Math.max(this.amount - this.balance, 0); + return Math.max(this.total - this.balance, 0); } /** @@ -323,7 +323,7 @@ export default class Bill extends mixin(TenantModel, [ * @return {boolean} */ get isPartiallyPaid() { - return this.dueAmount !== this.amount && this.dueAmount > 0; + return this.dueAmount !== this.total && this.dueAmount > 0; } /** @@ -384,7 +384,7 @@ export default class Bill extends mixin(TenantModel, [ * Retrieves the calculated amount which have not been invoiced. */ get billableAmount() { - return Math.max(this.amount - this.invoicedAmount, 0); + return Math.max(this.total - this.invoicedAmount, 0); } /** diff --git a/packages/server/src/services/Purchases/BillPayments/BillPaymentGLEntries.ts b/packages/server/src/services/Purchases/BillPayments/BillPaymentGLEntries.ts index e2f51d309e..57f4966d7a 100644 --- a/packages/server/src/services/Purchases/BillPayments/BillPaymentGLEntries.ts +++ b/packages/server/src/services/Purchases/BillPayments/BillPaymentGLEntries.ts @@ -203,8 +203,8 @@ export class BillPaymentGLEntries { /** * Retrieves the payment GL payable entry. - * @param {IBillPayment} billPayment - * @param {number} APAccountId + * @param {IBillPayment} billPayment + * @param {number} APAccountId * @returns {ILedgerEntry} */ private getPaymentGLPayableEntry = ( diff --git a/packages/server/src/services/Purchases/Bills/BillGLEntries.ts b/packages/server/src/services/Purchases/Bills/BillGLEntries.ts index dd4e3d241e..383b5eebb2 100644 --- a/packages/server/src/services/Purchases/Bills/BillGLEntries.ts +++ b/packages/server/src/services/Purchases/Bills/BillGLEntries.ts @@ -185,7 +185,7 @@ export class BillGLEntries { return { ...commonJournalMeta, - credit: bill.localAmount, + credit: bill.totalLocal, accountId: payableAccountId, contactId: bill.vendorId, accountNormal: AccountNormal.CREDIT, diff --git a/packages/server/src/services/Purchases/Bills/PurchaseInvoiceTransformer.ts b/packages/server/src/services/Purchases/Bills/PurchaseInvoiceTransformer.ts index c280f51f76..6de17289bd 100644 --- a/packages/server/src/services/Purchases/Bills/PurchaseInvoiceTransformer.ts +++ b/packages/server/src/services/Purchases/Bills/PurchaseInvoiceTransformer.ts @@ -12,7 +12,6 @@ export class PurchaseInvoiceTransformer extends Transformer { return [ 'formattedBillDate', 'formattedDueDate', - 'formattedAmount', 'formattedPaymentAmount', 'formattedBalance', 'formattedDueAmount', @@ -27,6 +26,14 @@ export class PurchaseInvoiceTransformer extends Transformer { ]; }; + /** + * Excluded attributes. + * @returns {string[]} + */ + public excludeAttributes = (): string[] => { + return ['amount', 'amountLocal', 'localAmount']; + }; + /** * Retrieve formatted bill date. * @param {IBill} bill diff --git a/packages/webapp/src/containers/Drawers/BillDrawer/BillDetailTableFooter.tsx b/packages/webapp/src/containers/Drawers/BillDrawer/BillDetailTableFooter.tsx index 6233a9fae8..17c29c1a4d 100644 --- a/packages/webapp/src/containers/Drawers/BillDrawer/BillDetailTableFooter.tsx +++ b/packages/webapp/src/containers/Drawers/BillDrawer/BillDetailTableFooter.tsx @@ -44,6 +44,7 @@ export function BillDetailTableFooter() { } value={bill.formatted_due_amount} + textStyle={TotalLineTextStyle.Bold} /> From 538e0231baf191107b16f8c723897daaffff76f6 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Sun, 8 Oct 2023 16:01:16 +0200 Subject: [PATCH 9/9] fix bugs --- .../server/src/api/controllers/Purchases/Bills.ts | 12 +++++++++++- packages/server/src/interfaces/Bill.ts | 3 ++- packages/server/src/loaders/eventEmitter.ts | 8 +++++--- .../src/services/Purchases/Bills/BillGLEntries.ts | 3 ++- .../Purchases/Bills/PurchaseInvoiceTransformer.ts | 2 +- .../src/services/Sales/Invoices/InvoiceGLEntries.ts | 3 ++- .../Sales/Invoices/SaleInvoiceTaxEntryTransformer.ts | 4 ++-- .../Sales/Invoices/SaleInvoiceTransformer.ts | 2 +- .../containers/Purchases/Bills/BillForm/utils.tsx | 4 +++- 9 files changed, 29 insertions(+), 12 deletions(-) diff --git a/packages/server/src/api/controllers/Purchases/Bills.ts b/packages/server/src/api/controllers/Purchases/Bills.ts index 221f96c65b..61fc5cf70d 100644 --- a/packages/server/src/api/controllers/Purchases/Bills.ts +++ b/packages/server/src/api/controllers/Purchases/Bills.ts @@ -115,7 +115,7 @@ export default class BillsController extends BaseController { check('note').optional().trim().escape(), check('open').default(false).isBoolean().toBoolean(), - check('is_inclusive_tax').optional().isBoolean().toBoolean(), + check('is_inclusive_tax').default(false).isBoolean().toBoolean(), check('entries').isArray({ min: 1 }), @@ -553,6 +553,16 @@ export default class BillsController extends BaseController { ], }); } + if (error.errorType === 'ITEM_ENTRY_TAX_RATE_CODE_NOT_FOUND') { + return res.boom.badRequest(null, { + errors: [{ type: 'ITEM_ENTRY_TAX_RATE_CODE_NOT_FOUND', code: 1800 }], + }); + } + if (error.errorType === 'ITEM_ENTRY_TAX_RATE_ID_NOT_FOUND') { + return res.boom.badRequest(null, { + errors: [{ type: 'ITEM_ENTRY_TAX_RATE_ID_NOT_FOUND', code: 1900 }], + }); + } } next(error); } diff --git a/packages/server/src/interfaces/Bill.ts b/packages/server/src/interfaces/Bill.ts index 3919a28dbb..8276a4623f 100644 --- a/packages/server/src/interfaces/Bill.ts +++ b/packages/server/src/interfaces/Bill.ts @@ -2,6 +2,7 @@ import { Knex } from 'knex'; import { IDynamicListFilterDTO } from './DynamicFilter'; import { IItemEntry, IItemEntryDTO } from './ItemEntry'; import { IBillLandedCost } from './LandedCost'; + export interface IBillDTO { vendorId: number; billNumber: string; @@ -15,7 +16,6 @@ export interface IBillDTO { exchangeRate?: number; open: boolean; entries: IItemEntryDTO[]; - branchId?: number; warehouseId?: number; projectId?: number; @@ -86,6 +86,7 @@ export interface IBill { subtotal: number; subtotalLocal: number; subtotalExcludingTax: number; + taxAmountWithheld: number; taxAmountWithheldLocal: number; total: number; totalLocal: number; diff --git a/packages/server/src/loaders/eventEmitter.ts b/packages/server/src/loaders/eventEmitter.ts index 116542c297..47589de5f1 100644 --- a/packages/server/src/loaders/eventEmitter.ts +++ b/packages/server/src/loaders/eventEmitter.ts @@ -190,10 +190,12 @@ export const susbcribers = () => { ProjectBillableExpensesSubscriber, ProjectBillableBillSubscriber, - // Tax Rates + // Tax Rates - Sale Invoice SaleInvoiceTaxRateValidateSubscriber, - BillTaxRateValidateSubscriber, WriteInvoiceTaxTransactionsSubscriber, - WriteBillTaxTransactionsSubscriber + + // Tax Rates - Bills + BillTaxRateValidateSubscriber, + WriteBillTaxTransactionsSubscriber, ]; }; diff --git a/packages/server/src/services/Purchases/Bills/BillGLEntries.ts b/packages/server/src/services/Purchases/Bills/BillGLEntries.ts index 383b5eebb2..9c1352e9a6 100644 --- a/packages/server/src/services/Purchases/Bills/BillGLEntries.ts +++ b/packages/server/src/services/Purchases/Bills/BillGLEntries.ts @@ -214,7 +214,8 @@ export class BillGLEntries { return { ...commonJournalMeta, debit: entry.taxAmount, - index: index + 4, + index, + indexGroup: 30, accountId: taxPayableAccountId, accountNormal: AccountNormal.CREDIT, taxRateId: entry.taxRateId, diff --git a/packages/server/src/services/Purchases/Bills/PurchaseInvoiceTransformer.ts b/packages/server/src/services/Purchases/Bills/PurchaseInvoiceTransformer.ts index 6de17289bd..07d3a78d68 100644 --- a/packages/server/src/services/Purchases/Bills/PurchaseInvoiceTransformer.ts +++ b/packages/server/src/services/Purchases/Bills/PurchaseInvoiceTransformer.ts @@ -173,7 +173,7 @@ export class PurchaseInvoiceTransformer extends Transformer { */ protected taxes = (bill) => { return this.item(bill.taxes, new SaleInvoiceTaxEntryTransformer(), { - amount: bill.amount, + subtotal: bill.subtotal, isInclusiveTax: bill.isInclusiveTax, currencyCode: bill.currencyCode, }); diff --git a/packages/server/src/services/Sales/Invoices/InvoiceGLEntries.ts b/packages/server/src/services/Sales/Invoices/InvoiceGLEntries.ts index d816672c28..7b714e70ea 100644 --- a/packages/server/src/services/Sales/Invoices/InvoiceGLEntries.ts +++ b/packages/server/src/services/Sales/Invoices/InvoiceGLEntries.ts @@ -218,7 +218,8 @@ export class SaleInvoiceGLEntries { ...commonEntry, credit: entry.taxAmount, accountId: taxPayableAccountId, - index: index + 3, + index: index + 1, + indexGroup: 30, accountNormal: AccountNormal.CREDIT, taxRateId: entry.taxRateId, taxRate: entry.taxRate, diff --git a/packages/server/src/services/Sales/Invoices/SaleInvoiceTaxEntryTransformer.ts b/packages/server/src/services/Sales/Invoices/SaleInvoiceTaxEntryTransformer.ts index 6f028423dc..ae4e9c87ee 100644 --- a/packages/server/src/services/Sales/Invoices/SaleInvoiceTaxEntryTransformer.ts +++ b/packages/server/src/services/Sales/Invoices/SaleInvoiceTaxEntryTransformer.ts @@ -62,8 +62,8 @@ export class SaleInvoiceTaxEntryTransformer extends Transformer { const taxRate = this.taxRate(taxEntry); return this.options.isInclusiveTax - ? getInclusiveTaxAmount(this.options.amount, taxRate) - : getExlusiveTaxAmount(this.options.amount, taxRate); + ? getInclusiveTaxAmount(this.options.subtotal, taxRate) + : getExlusiveTaxAmount(this.options.subtotal, taxRate); }; /** diff --git a/packages/server/src/services/Sales/Invoices/SaleInvoiceTransformer.ts b/packages/server/src/services/Sales/Invoices/SaleInvoiceTransformer.ts index ffb2d8391a..979b312a9f 100644 --- a/packages/server/src/services/Sales/Invoices/SaleInvoiceTransformer.ts +++ b/packages/server/src/services/Sales/Invoices/SaleInvoiceTransformer.ts @@ -171,7 +171,7 @@ export class SaleInvoiceTransformer extends Transformer { */ protected taxes = (invoice) => { return this.item(invoice.taxes, new SaleInvoiceTaxEntryTransformer(), { - amount: invoice.amount, + subtotal: invoice.subtotal, isInclusiveTax: invoice.isInclusiveTax, currencyCode: invoice.currencyCode, }); diff --git a/packages/webapp/src/containers/Purchases/Bills/BillForm/utils.tsx b/packages/webapp/src/containers/Purchases/Bills/BillForm/utils.tsx index 6f39477fa3..2d82bfea5e 100644 --- a/packages/webapp/src/containers/Purchases/Bills/BillForm/utils.tsx +++ b/packages/webapp/src/containers/Purchases/Bills/BillForm/utils.tsx @@ -41,6 +41,8 @@ export const defaultBillEntry = { amount: '', landed_cost: false, tax_rate_id: '', + tax_rate: '', + tax_amount: '', }; // Default bill. @@ -299,7 +301,7 @@ export const useBillIsForeignCustomer = () => { }; /** - * Re-calcualte the entries tax amount when editing. + * Re-calculates the entries tax amount when editing. * @returns {string} */ export const composeEntriesOnEditInclusiveTax = (