feat(server): wip sale invoice tax rates

This commit is contained in:
Ahmed Bouhuolia
2023-08-29 19:12:19 +02:00
parent 09d73db20f
commit 6535424d0f
15 changed files with 219 additions and 49 deletions

View File

@@ -75,6 +75,8 @@ export class CommandSaleInvoiceDTOTransformer {
...entry,
}));
const entries = await composeAsync(
// Associate tax rate id from tax code to entries.
this.taxDTOTransformer.assocTaxRateIdFromCodeToEntries(tenantId),
// Sets default cost and sell account to invoice items entries.
this.itemsEntriesService.setItemsEntriesDefaultAccounts(tenantId)
)(initialEntries);

View File

@@ -33,7 +33,8 @@ export class GetSaleInvoice {
.findById(saleInvoiceId)
.withGraphFetched('entries.item')
.withGraphFetched('customer')
.withGraphFetched('branch');
.withGraphFetched('branch')
.withGraphFetched('taxes.taxRate');
// Validates the given sale invoice existance.
this.validators.validateInvoiceExistance(saleInvoice);

View File

@@ -0,0 +1,46 @@
import { Transformer } from '@/lib/Transformer/Transformer';
export class SaleInvoiceTaxEntryTransformer extends Transformer {
/**
* Included attributes.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return ['name', 'taxRateCode', 'raxRate', 'taxRateId'];
};
/**
* Exclude attributes.
* @returns {string[]}
*/
public excludeAttributes = (): string[] => {
return ['*'];
};
/**
* Retrieve tax rate code.
* @param taxEntry
* @returns {string}
*/
protected taxRateCode = (taxEntry) => {
return taxEntry.taxRate.code;
};
/**
* Retrieve tax rate id.
* @param taxEntry
* @returns {number}
*/
protected raxRate = (taxEntry) => {
return taxEntry.taxAmount || taxEntry.taxRate.rate;
};
/**
* Retrieve tax rate name.
* @param taxEntry
* @returns {string}
*/
protected name = (taxEntry) => {
return taxEntry.taxRate.name;
};
}

View File

@@ -1,5 +1,6 @@
import { Transformer } from '@/lib/Transformer/Transformer';
import { formatNumber } from 'utils';
import { SaleInvoiceTaxEntryTransformer } from './SaleInvoiceTaxEntryTransformer';
export class SaleInvoiceTransformer extends Transformer {
/**
@@ -15,6 +16,7 @@ export class SaleInvoiceTransformer extends Transformer {
'formattedPaymentAmount',
'formattedBalanceAmount',
'formattedExchangeRate',
'taxes',
];
};
@@ -88,4 +90,12 @@ export class SaleInvoiceTransformer extends Transformer {
protected formattedExchangeRate = (invoice): string => {
return formatNumber(invoice.exchangeRate, { money: false });
};
/**
* Retrieve the taxes lines of sale invoice.
* @param {ISaleInvoice} invoice
*/
protected taxes = (invoice) => {
return this.item(invoice.taxes, new SaleInvoiceTaxEntryTransformer());
};
}

View File

@@ -45,9 +45,13 @@ export class CommandTaxRatesValidators {
itemEntriesDTO: IItemEntryDTO[]
) {
const { TaxRate } = this.tenancy.models(tenantId);
const filteredTaxEntries = itemEntriesDTO.filter((e) => e.taxCode);
const taxCodes = filteredTaxEntries.map((e) => e.taxCode);
// Can't validate if there is no tax codes.
if (taxCodes.length === 0) return;
const foundTaxCodes = await TaxRate.query().whereIn('code', taxCodes);
const foundCodes = foundTaxCodes.map((tax) => tax.code);
@@ -57,4 +61,31 @@ export class CommandTaxRatesValidators {
throw new ServiceError(ERRORS.ITEM_ENTRY_TAX_RATE_CODE_NOT_FOUND);
}
}
/**
* Validates the tax rate id of the given item entries DTO.
* @param {number} tenantId
* @param {IItemEntryDTO[]} itemEntriesDTO
* @throws {ServiceError}
*/
public async validateItemEntriesTaxCodeId(
tenantId: number,
itemEntriesDTO: IItemEntryDTO[]
) {
const filteredTaxEntries = itemEntriesDTO.filter((e) => e.taxCodeId);
const taxCodes = filteredTaxEntries.map((e) => e.taxCodeId);
// Can't validate if there is no tax codes.
if (taxCodes.length === 0) return;
const { TaxRate } = this.tenancy.models(tenantId);
const foundTaxCodes = await TaxRate.query().whereIn('id', taxCodes);
const foundCodes = foundTaxCodes.map((tax) => tax.id);
const notFoundTaxCodes = difference(taxCodes, foundCodes);
if (notFoundTaxCodes.length > 0) {
throw new ServiceError(ERRORS.ITEM_ENTRY_TAX_RATE_ID_NOT_FOUND);
}
}
}

View File

@@ -1,14 +1,18 @@
import { ItemEntry } from "@/models";
import { sumBy } from "lodash";
import { Service } from "typedi";
import { Inject, Service } from 'typedi';
import { keyBy, sumBy } from 'lodash';
import * as R from 'ramda';
import { ItemEntry } from '@/models';
import HasTenancyService from '../Tenancy/TenancyService';
@Service()
export class ItemEntriesTaxTransactions {
@Inject()
private tenancy: HasTenancyService;
/**
*
* @param model
* @returns
* Associates tax amount withheld to the model.
* @param model
* @returns
*/
public assocTaxAmountWithheldFromEntries(model: any) {
const entries = model.entries.map((entry) => ItemEntry.fromJson(entry));
@@ -19,4 +23,27 @@ export class ItemEntriesTaxTransactions {
}
return model;
}
}
/**
* Associates tax rate id from tax code to entries.
* @param {number} tenantId
* @param {} model
*/
public assocTaxRateIdFromCodeToEntries =
(tenantId: number) => async (entries: any) => {
const entriesWithCode = entries.filter((entry) => entry.taxCode);
const taxCodes = entriesWithCode.map((entry) => entry.taxCode);
const { TaxRate } = this.tenancy.models(tenantId);
const foundTaxCodes = await TaxRate.query().whereIn('code', taxCodes);
const taxCodesMap = keyBy(foundTaxCodes, 'code');
return entries.map((entry) => {
if (entry.taxCode) {
entry.taxRateId = taxCodesMap[entry.taxCode]?.id;
}
return entry;
});
};
}

View File

@@ -1,5 +1,5 @@
import { sumBy, chain } from 'lodash';
import { IItemEntry } from '@/interfaces';
import { sumBy, chain, keyBy } from 'lodash';
import { IItemEntry, ITaxTransaction } from '@/interfaces';
import HasTenancyService from '../Tenancy/TenancyService';
import { Inject, Service } from 'typedi';
@@ -17,49 +17,54 @@ export class WriteTaxTransactionsItemEntries {
tenantId: number,
itemEntries: IItemEntry[]
) {
const { TaxRateTransaction } = this.tenancy.models(tenantId);
const { TaxRateTransaction, TaxRate } = this.tenancy.models(tenantId);
const aggregatedEntries = this.aggregateItemEntriesByTaxCode(itemEntries);
const entriesTaxRateIds = aggregatedEntries.map((entry) => entry.taxRateId);
const taxRates = await TaxRate.query().whereIn('id', entriesTaxRateIds);
const taxRatesById = keyBy(taxRates, 'id');
const taxTransactions = aggregatedEntries.map((entry) => ({
taxName: 'TAX NAME',
taxCode: 'TAG_CODE',
taxRateId: entry.taxRateId,
referenceType: entry.referenceType,
referenceId: entry.referenceId,
taxAmount: entry.taxAmount,
}));
taxAmount: entry.taxRate || taxRatesById[entry.taxRateId]?.rate,
})) as ITaxTransaction[];
await TaxRateTransaction.query().upsertGraph(taxTransactions);
}
/**
*
* Aggregates by tax code id and sums the amount.
* @param {IItemEntry[]} itemEntries
* @returns {}
* @returns {IItemEntry[]}
*/
private aggregateItemEntriesByTaxCode(itemEntries: IItemEntry[]) {
return chain(itemEntries.filter((item) => item.taxCode))
.groupBy((item) => item.taxCode)
private aggregateItemEntriesByTaxCode = (
itemEntries: IItemEntry[]
): IItemEntry[] => {
return chain(itemEntries.filter((item) => item.taxRateId))
.groupBy((item) => item.taxRateId)
.values()
.map((group) => ({ ...group[0], amount: sumBy(group, 'amount') }))
.value();
}
/**
*
* @param itemEntries
*/
private aggregateItemEntriesByReferenceTypeId(itemEntries: IItemEntry) {}
};
/**
* Removes the tax transactions from the given item entries.
* @param tenantId
* @param itemEntries
* @param {number} tenantId - Tenant id.
* @param {string} referenceType - Reference type.
* @param {number} referenceId - Reference id.
*/
public removeTaxTransactionsFromItemEntries(
public async removeTaxTransactionsFromItemEntries(
tenantId: number,
itemEntries: IItemEntry[]
referenceId: number,
referenceType: string
) {
const { TaxRateTransaction } = this.tenancy.models(tenantId);
const filteredEntries = itemEntries.filter((item) => item.taxCode);
await TaxRateTransaction.query()
.where({ referenceType, referenceId })
.delete();
}
}

View File

@@ -2,4 +2,5 @@ export const ERRORS = {
TAX_RATE_NOT_FOUND: 'TAX_RATE_NOT_FOUND',
TAX_CODE_NOT_UNIQUE: 'TAX_CODE_NOT_UNIQUE',
ITEM_ENTRY_TAX_RATE_CODE_NOT_FOUND: 'ITEM_ENTRY_TAX_RATE_CODE_NOT_FOUND',
ITEM_ENTRY_TAX_RATE_ID_NOT_FOUND: 'ITEM_ENTRY_TAX_RATE_ID_NOT_FOUND',
};

View File

@@ -19,6 +19,10 @@ export class SaleInvoiceTaxRateValidateSubscriber {
events.saleInvoice.onCreating,
this.validateSaleInvoiceEntriesTaxCodeExistanceOnCreating
);
bus.subscribe(
events.saleInvoice.onCreating,
this.validateSaleInvoiceEntriesTaxIdExistanceOnCreating
);
bus.subscribe(
events.saleInvoice.onEditing,
this.validateSaleInvoiceEntriesTaxCodeExistanceOnEditing
@@ -27,7 +31,7 @@ export class SaleInvoiceTaxRateValidateSubscriber {
}
/**
* Validate invoice entries tax rate code existance.
* Validate invoice entries tax rate code existance when creating.
* @param {ISaleInvoiceCreatingPaylaod}
*/
private validateSaleInvoiceEntriesTaxCodeExistanceOnCreating = async ({
@@ -41,7 +45,21 @@ export class SaleInvoiceTaxRateValidateSubscriber {
};
/**
*
* Validate the tax rate id existance when creating.
* @param {ISaleInvoiceCreatingPaylaod}
*/
private validateSaleInvoiceEntriesTaxIdExistanceOnCreating = async ({
saleInvoiceDTO,
tenantId,
}: ISaleInvoiceCreatingPaylaod) => {
await this.taxRateDTOValidator.validateItemEntriesTaxCodeId(
tenantId,
saleInvoiceDTO.entries
);
};
/**
* Validate invoice entries tax rate code existance when editing.
* @param {ISaleInvoiceEditingPayload}
*/
private validateSaleInvoiceEntriesTaxCodeExistanceOnEditing = async ({

View File

@@ -20,7 +20,7 @@ export class WriteInvoiceTaxTransactionsSubscriber {
this.writeInvoiceTaxTransactionsOnCreated
);
bus.subscribe(
events.saleInvoice.onDeleted,
events.saleInvoice.onDelete,
this.removeInvoiceTaxTransactionsOnDeleted
);
return bus;
@@ -50,7 +50,8 @@ export class WriteInvoiceTaxTransactionsSubscriber {
}: ISaleInvoiceDeletedPayload) => {
await this.writeTaxTransactions.removeTaxTransactionsFromItemEntries(
tenantId,
oldSaleInvoice.entries
oldSaleInvoice.id,
'SaleInvoice'
);
};
}