diff --git a/packages/server/src/api/controllers/Sales/SalesInvoices.ts b/packages/server/src/api/controllers/Sales/SalesInvoices.ts index 044ddc153..e55aeeb7d 100644 --- a/packages/server/src/api/controllers/Sales/SalesInvoices.ts +++ b/packages/server/src/api/controllers/Sales/SalesInvoices.ts @@ -764,6 +764,11 @@ export default class SaleInvoicesController extends BaseController { errors: [{ type: 'ITEM_ENTRY_TAX_RATE_CODE_NOT_FOUND', code: 5000 }], }); } + 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: 5100 }], + }); + } } next(error); } diff --git a/packages/server/src/database/migrations/20230810191606_create_tax_rates.js b/packages/server/src/database/migrations/20230810191606_create_tax_rates.js index 472f6f533..e5279151c 100644 --- a/packages/server/src/database/migrations/20230810191606_create_tax_rates.js +++ b/packages/server/src/database/migrations/20230810191606_create_tax_rates.js @@ -12,6 +12,11 @@ exports.up = (knex) => { }) .table('items_entries', (table) => { table.boolean('is_tax_exclusive'); + table + .integer('tax_rate_id') + .unsigned() + .references('id') + .inTable('tax_rates'); table.string('tax_code'); table.decimal('tax_rate'); }) @@ -21,13 +26,13 @@ exports.up = (knex) => { }) .createTable('tax_rate_transactions', (table) => { table.increments('id'); - - table.string('tax_name'); - table.string('tax_code'); - + table + .integer('tax_rate_id') + .unsigned() + .references('id') + .inTable('tax_rates'); table.string('reference_type'); table.integer('reference_id'); - table.decimal('tax_amount'); table.integer('tax_account_id').unsigned(); }); diff --git a/packages/server/src/interfaces/ItemEntry.ts b/packages/server/src/interfaces/ItemEntry.ts index f6899777f..bb67df9e8 100644 --- a/packages/server/src/interfaces/ItemEntry.ts +++ b/packages/server/src/interfaces/ItemEntry.ts @@ -32,6 +32,7 @@ export interface IItemEntry { projectRefType?: ProjectLinkRefType; projectRefInvoicedAmount?: number; + taxRateId: number | null; taxCode: string; taxRate: number; taxAmount: number; @@ -51,8 +52,9 @@ export interface IItemEntryDTO { projectRefType?: ProjectLinkRefType; projectRefInvoicedAmount?: number; - taxCode: string; - taxRate: number; + taxCodeId?: number; + taxCode?: string; + taxRate?: number; } export enum ProjectLinkRefType { diff --git a/packages/server/src/interfaces/TaxRate.ts b/packages/server/src/interfaces/TaxRate.ts index 16a38e9bb..074e46501 100644 --- a/packages/server/src/interfaces/TaxRate.ts +++ b/packages/server/src/interfaces/TaxRate.ts @@ -48,9 +48,11 @@ export interface ITaxRateDeletedPayload { trx: Knex.Transaction; } - export interface ITaxTransaction { + id?: number; + taxRateId: number; + referenceType: string; + referenceId: number; taxAmount: number; - taxName: string; - taxCode: string; -} \ No newline at end of file + taxAccountId: number; +} diff --git a/packages/server/src/models/TaxRateTransaction.ts b/packages/server/src/models/TaxRateTransaction.ts index b26c26903..3cbca88a0 100644 --- a/packages/server/src/models/TaxRateTransaction.ts +++ b/packages/server/src/models/TaxRateTransaction.ts @@ -37,6 +37,20 @@ export default class TaxRateTransaction extends mixin(TenantModel, [ * Relationship mapping. */ static get relationMappings() { - return {}; + const TaxRate = require('models/TaxRate'); + + return { + /** + * Belongs to the tax rate. + */ + taxRate: { + relation: Model.BelongsToOneRelation, + modelClass: TaxRate.default, + join: { + from: 'tax_rate_transactions.taxRateId', + to: 'tax_rates.id', + }, + }, + }; } } diff --git a/packages/server/src/services/Sales/Invoices/CommandSaleInvoiceDTOTransformer.ts b/packages/server/src/services/Sales/Invoices/CommandSaleInvoiceDTOTransformer.ts index dc728f56c..867e7c660 100644 --- a/packages/server/src/services/Sales/Invoices/CommandSaleInvoiceDTOTransformer.ts +++ b/packages/server/src/services/Sales/Invoices/CommandSaleInvoiceDTOTransformer.ts @@ -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); diff --git a/packages/server/src/services/Sales/Invoices/GetSaleInvoice.ts b/packages/server/src/services/Sales/Invoices/GetSaleInvoice.ts index 7fb5a4407..b7eaa4440 100644 --- a/packages/server/src/services/Sales/Invoices/GetSaleInvoice.ts +++ b/packages/server/src/services/Sales/Invoices/GetSaleInvoice.ts @@ -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); diff --git a/packages/server/src/services/Sales/Invoices/SaleInvoiceTaxEntryTransformer.ts b/packages/server/src/services/Sales/Invoices/SaleInvoiceTaxEntryTransformer.ts new file mode 100644 index 000000000..b61242ca5 --- /dev/null +++ b/packages/server/src/services/Sales/Invoices/SaleInvoiceTaxEntryTransformer.ts @@ -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; + }; +} diff --git a/packages/server/src/services/Sales/Invoices/SaleInvoiceTransformer.ts b/packages/server/src/services/Sales/Invoices/SaleInvoiceTransformer.ts index dfbd704fa..c40320c33 100644 --- a/packages/server/src/services/Sales/Invoices/SaleInvoiceTransformer.ts +++ b/packages/server/src/services/Sales/Invoices/SaleInvoiceTransformer.ts @@ -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()); + }; } diff --git a/packages/server/src/services/TaxRates/CommandTaxRatesValidators.ts b/packages/server/src/services/TaxRates/CommandTaxRatesValidators.ts index dcd444e0a..74ea0ce14 100644 --- a/packages/server/src/services/TaxRates/CommandTaxRatesValidators.ts +++ b/packages/server/src/services/TaxRates/CommandTaxRatesValidators.ts @@ -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); + } + } } diff --git a/packages/server/src/services/TaxRates/ItemEntriesTaxTransactions.ts b/packages/server/src/services/TaxRates/ItemEntriesTaxTransactions.ts index 9a1e72d80..a85d1c998 100644 --- a/packages/server/src/services/TaxRates/ItemEntriesTaxTransactions.ts +++ b/packages/server/src/services/TaxRates/ItemEntriesTaxTransactions.ts @@ -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; } -} \ No newline at end of file + + /** + * 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; + }); + }; +} diff --git a/packages/server/src/services/TaxRates/WriteTaxTransactionsItemEntries.ts b/packages/server/src/services/TaxRates/WriteTaxTransactionsItemEntries.ts index 40304a0ab..d5c654c4d 100644 --- a/packages/server/src/services/TaxRates/WriteTaxTransactionsItemEntries.ts +++ b/packages/server/src/services/TaxRates/WriteTaxTransactionsItemEntries.ts @@ -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(); } } diff --git a/packages/server/src/services/TaxRates/constants.ts b/packages/server/src/services/TaxRates/constants.ts index 5822d6a5d..a38968d93 100644 --- a/packages/server/src/services/TaxRates/constants.ts +++ b/packages/server/src/services/TaxRates/constants.ts @@ -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', }; diff --git a/packages/server/src/services/TaxRates/subscribers/SaleInvoiceTaxRateValidateSubscriber.ts b/packages/server/src/services/TaxRates/subscribers/SaleInvoiceTaxRateValidateSubscriber.ts index afe085e2a..ab8de770a 100644 --- a/packages/server/src/services/TaxRates/subscribers/SaleInvoiceTaxRateValidateSubscriber.ts +++ b/packages/server/src/services/TaxRates/subscribers/SaleInvoiceTaxRateValidateSubscriber.ts @@ -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 ({ diff --git a/packages/server/src/services/TaxRates/subscribers/WriteInvoiceTaxTransactionsSubscriber.ts b/packages/server/src/services/TaxRates/subscribers/WriteInvoiceTaxTransactionsSubscriber.ts index 75720d3af..eb0e1c9b3 100644 --- a/packages/server/src/services/TaxRates/subscribers/WriteInvoiceTaxTransactionsSubscriber.ts +++ b/packages/server/src/services/TaxRates/subscribers/WriteInvoiceTaxTransactionsSubscriber.ts @@ -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' ); }; }