feat: integrate tax rates to bills (#260)

This commit is contained in:
Ahmed Bouhuolia
2023-10-08 16:07:18 +02:00
committed by GitHub
parent ee62e3e1c2
commit d40de4d22b
34 changed files with 894 additions and 282 deletions

View File

@@ -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 = (

View File

@@ -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;
@@ -73,14 +77,24 @@ 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 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 +114,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);

View File

@@ -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 {
@@ -173,7 +185,7 @@ export class BillGLEntries {
return {
...commonJournalMeta,
credit: bill.localAmount,
credit: bill.totalLocal,
accountId: payableAccountId,
contactId: bill.vendorId,
accountNormal: AccountNormal.CREDIT,
@@ -182,15 +194,62 @@ 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,
indexGroup: 30,
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 +260,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);
};

View File

@@ -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);

View File

@@ -1,27 +1,42 @@
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[] => {
return [
'formattedBillDate',
'formattedDueDate',
'formattedAmount',
'formattedPaymentAmount',
'formattedBalance',
'formattedDueAmount',
'formattedExchangeRate',
'subtotalFormatted',
'subtotalLocalFormatted',
'subtotalExcludingTaxFormatted',
'taxAmountWithheldLocalFormatted',
'totalFormatted',
'totalLocalFormatted',
'taxes',
];
};
/**
* Retrieve formatted invoice date.
* @param {IBill} invoice
* Excluded attributes.
* @returns {string[]}
*/
public excludeAttributes = (): string[] => {
return ['amount', 'amountLocal', 'localAmount'];
};
/**
* Retrieve formatted bill date.
* @param {IBill} bill
* @returns {String}
*/
protected formattedBillDate = (bill: IBill): string => {
@@ -29,8 +44,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 +54,7 @@ export class PurchaseInvoiceTransformer extends Transformer {
/**
* Retrieve formatted bill amount.
* @param {IBill} invoice
* @param {IBill} bill
* @returns {string}
*/
protected formattedAmount = (bill): string => {
@@ -48,7 +63,7 @@ export class PurchaseInvoiceTransformer extends Transformer {
/**
* Retrieve formatted bill amount.
* @param {IBill} invoice
* @param {IBill} bill
* @returns {string}
*/
protected formattedPaymentAmount = (bill): string => {
@@ -59,7 +74,7 @@ export class PurchaseInvoiceTransformer extends Transformer {
/**
* Retrieve formatted bill amount.
* @param {IBill} invoice
* @param {IBill} bill
* @returns {string}
*/
protected formattedDueAmount = (bill): string => {
@@ -77,10 +92,90 @@ export class PurchaseInvoiceTransformer extends Transformer {
/**
* Retrieve the formatted exchange rate.
* @param {ISaleInvoice} invoice
* @param {IBill} bill
* @returns {string}
*/
protected formattedExchangeRate = (invoice): string => {
return formatNumber(invoice.exchangeRate, { money: false });
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 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(), {
subtotal: bill.subtotal,
isInclusiveTax: bill.isInclusiveTax,
currencyCode: bill.currencyCode,
});
};
}

View File

@@ -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,

View File

@@ -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);
};
/**

View File

@@ -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,
});

View File

@@ -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
);
};
}

View File

@@ -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
);
};
}