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 63b486d2a..472f6f533 100644 --- a/packages/server/src/database/migrations/20230810191606_create_tax_rates.js +++ b/packages/server/src/database/migrations/20230810191606_create_tax_rates.js @@ -11,14 +11,25 @@ exports.up = (knex) => { table.timestamps(); }) .table('items_entries', (table) => { - table.boolean(['is_tax_exclusive']); + table.boolean('is_tax_exclusive'); table.string('tax_code'); table.decimal('tax_rate'); - table.decimal('tax_amount_withheld') }) .table('sales_invoices', (table) => { - table.boolean(['is_tax_exclusive']); - table.decimal('tax_amount_withheld') + table.boolean('is_tax_exclusive'); + table.decimal('tax_amount_withheld'); + }) + .createTable('tax_rate_transactions', (table) => { + table.increments('id'); + + table.string('tax_name'); + table.string('tax_code'); + + table.string('reference_type'); + table.integer('reference_id'); + + table.decimal('tax_amount'); + table.integer('tax_account_id').unsigned(); }); }; diff --git a/packages/server/src/database/seeds/data/accounts.js b/packages/server/src/database/seeds/data/accounts.js index fb6bf6c07..fa9ee0d0c 100644 --- a/packages/server/src/database/seeds/data/accounts.js +++ b/packages/server/src/database/seeds/data/accounts.js @@ -1,7 +1,17 @@ +export const TaxPayableAccount = { + name: 'Tax Payable', + slug: 'tax-payable', + account_type: 'other-current-liability', + code: '20006', + description: '', + active: 1, + index: 1, + predefined: 1, +}; export default [ { - name:'Bank Account', + name: 'Bank Account', slug: 'bank-account', account_type: 'bank', code: '10001', @@ -11,7 +21,7 @@ export default [ predefined: 1, }, { - name:'Saving Bank Account', + name: 'Saving Bank Account', slug: 'saving-bank-account', account_type: 'bank', code: '10002', @@ -21,7 +31,7 @@ export default [ predefined: 0, }, { - name:'Undeposited Funds', + name: 'Undeposited Funds', slug: 'undeposited-funds', account_type: 'cash', code: '10003', @@ -31,7 +41,7 @@ export default [ predefined: 1, }, { - name:'Petty Cash', + name: 'Petty Cash', slug: 'petty-cash', account_type: 'cash', code: '10004', @@ -41,7 +51,7 @@ export default [ predefined: 1, }, { - name:'Computer Equipment', + name: 'Computer Equipment', slug: 'computer-equipment', code: '10005', account_type: 'fixed-asset', @@ -52,7 +62,7 @@ export default [ description: '', }, { - name:'Office Equipment', + name: 'Office Equipment', slug: 'office-equipment', code: '10006', account_type: 'fixed-asset', @@ -63,7 +73,7 @@ export default [ description: '', }, { - name:'Accounts Receivable (A/R)', + name: 'Accounts Receivable (A/R)', slug: 'accounts-receivable', account_type: 'accounts-receivable', code: '10007', @@ -73,7 +83,7 @@ export default [ predefined: 1, }, { - name:'Inventory Asset', + name: 'Inventory Asset', slug: 'inventory-asset', code: '10008', account_type: 'inventory', @@ -81,12 +91,13 @@ export default [ parent_account_id: null, index: 1, active: 1, - description:'An account that holds valuation of products or goods that availiable for sale.', + description: + 'An account that holds valuation of products or goods that availiable for sale.', }, // Libilities { - name:'Accounts Payable (A/P)', + name: 'Accounts Payable (A/P)', slug: 'accounts-payable', account_type: 'accounts-payable', parent_account_id: null, @@ -97,38 +108,39 @@ export default [ predefined: 1, }, { - name:'Owner A Drawings', + name: 'Owner A Drawings', slug: 'owner-drawings', account_type: 'other-current-liability', parent_account_id: null, code: '20002', - description:'Withdrawals by the owners.', + description: 'Withdrawals by the owners.', active: 1, index: 1, predefined: 0, }, { - name:'Loan', + name: 'Loan', slug: 'owner-drawings', account_type: 'other-current-liability', code: '20003', - description:'Money that has been borrowed from a creditor.', + description: 'Money that has been borrowed from a creditor.', active: 1, index: 1, predefined: 0, }, { - name:'Opening Balance Liabilities', + name: 'Opening Balance Liabilities', slug: 'opening-balance-liabilities', account_type: 'other-current-liability', code: '20004', - description:'This account will hold the difference in the debits and credits entered during the opening balance..', + description: + 'This account will hold the difference in the debits and credits entered during the opening balance..', active: 1, index: 1, predefined: 0, }, { - name:'Revenue Received in Advance', + name: 'Revenue Received in Advance', slug: 'revenue-received-in-advance', account_type: 'other-current-liability', parent_account_id: null, @@ -138,34 +150,27 @@ export default [ index: 1, predefined: 0, }, - { - name:'Sales Tax Payable', - slug: 'owner-drawings', - account_type: 'other-current-liability', - code: '20006', - description: '', - active: 1, - index: 1, - predefined: 1, - }, + TaxPayableAccount, // Equity { - name:'Retained Earnings', + name: 'Retained Earnings', slug: 'retained-earnings', account_type: 'equity', code: '30001', - description:'Retained earnings tracks net income from previous fiscal years.', + description: + 'Retained earnings tracks net income from previous fiscal years.', active: 1, index: 1, predefined: 1, }, { - name:'Opening Balance Equity', + name: 'Opening Balance Equity', slug: 'opening-balance-equity', account_type: 'equity', code: '30002', - description:'When you enter opening balances to the accounts, the amounts enter in Opening balance equity. This ensures that you have a correct trial balance sheet for your company, without even specific the second credit or debit entry.', + description: + 'When you enter opening balances to the accounts, the amounts enter in Opening balance equity. This ensures that you have a correct trial balance sheet for your company, without even specific the second credit or debit entry.', active: 1, index: 1, predefined: 1, @@ -181,11 +186,12 @@ export default [ predefined: 1, }, { - name:`Drawings`, + name: `Drawings`, slug: 'drawings', account_type: 'equity', code: '30003', - description:'Goods purchased with the intention of selling these to customers', + description: + 'Goods purchased with the intention of selling these to customers', active: 1, index: 1, predefined: 1, @@ -193,7 +199,7 @@ export default [ // Expenses { - name:'Other Expenses', + name: 'Other Expenses', slug: 'other-expenses', account_type: 'other-expense', parent_account_id: null, @@ -204,18 +210,18 @@ export default [ predefined: 1, }, { - name:'Cost of Goods Sold', + name: 'Cost of Goods Sold', slug: 'cost-of-goods-sold', account_type: 'cost-of-goods-sold', parent_account_id: null, code: '40002', - description:'Tracks the direct cost of the goods sold.', + description: 'Tracks the direct cost of the goods sold.', active: 1, index: 1, predefined: 1, }, { - name:'Office expenses', + name: 'Office expenses', slug: 'office-expenses', account_type: 'expense', parent_account_id: null, @@ -226,7 +232,7 @@ export default [ predefined: 0, }, { - name:'Rent', + name: 'Rent', slug: 'rent', account_type: 'expense', parent_account_id: null, @@ -237,29 +243,30 @@ export default [ predefined: 0, }, { - name:'Exchange Gain or Loss', + name: 'Exchange Gain or Loss', slug: 'exchange-grain-loss', account_type: 'other-expense', parent_account_id: null, code: '40005', - description:'Tracks the gain and losses of the exchange differences.', + description: 'Tracks the gain and losses of the exchange differences.', active: 1, index: 1, predefined: 1, }, { - name:'Bank Fees and Charges', + name: 'Bank Fees and Charges', slug: 'bank-fees-and-charges', account_type: 'expense', parent_account_id: null, code: '40006', - description: 'Any bank fees levied is recorded into the bank fees and charges account. A bank account maintenance fee, transaction charges, a late payment fee are some examples.', + description: + 'Any bank fees levied is recorded into the bank fees and charges account. A bank account maintenance fee, transaction charges, a late payment fee are some examples.', active: 1, index: 1, predefined: 0, }, { - name:'Depreciation Expense', + name: 'Depreciation Expense', slug: 'depreciation-expense', account_type: 'expense', parent_account_id: null, @@ -272,7 +279,7 @@ export default [ // Income { - name:'Sales of Product Income', + name: 'Sales of Product Income', slug: 'sales-of-product-income', account_type: 'income', predefined: 1, @@ -283,7 +290,7 @@ export default [ description: '', }, { - name:'Sales of Service Income', + name: 'Sales of Service Income', slug: 'sales-of-service-income', account_type: 'income', predefined: 0, @@ -294,7 +301,7 @@ export default [ description: '', }, { - name:'Uncategorized Income', + name: 'Uncategorized Income', slug: 'uncategorized-income', account_type: 'income', parent_account_id: null, @@ -305,14 +312,15 @@ export default [ predefined: 1, }, { - name:'Other Income', + name: 'Other Income', slug: 'other-income', account_type: 'other-income', parent_account_id: null, code: '50004', - description:'The income activities are not associated to the core business.', + description: + 'The income activities are not associated to the core business.', active: 1, index: 1, predefined: 0, - } -]; \ No newline at end of file + }, +]; diff --git a/packages/server/src/interfaces/Item.ts b/packages/server/src/interfaces/Item.ts index 748fccefb..6e942fd81 100644 --- a/packages/server/src/interfaces/Item.ts +++ b/packages/server/src/interfaces/Item.ts @@ -54,6 +54,14 @@ export interface IItemDTO { sellDescription: string; purchaseDescription: string; + // Used as an override if the default Tax Code for the selected `costAccountId` is not correct + purchaseTaxCode: string; + purchaseTaxId: string; + + // Used as an override if the default Tax Code for the selected `sellAccountId` is not correct + saleTaxCode: string; + saleTaxId: string; + quantityOnHand: number; note: string; diff --git a/packages/server/src/interfaces/ItemEntry.ts b/packages/server/src/interfaces/ItemEntry.ts index 4b905668b..f6899777f 100644 --- a/packages/server/src/interfaces/ItemEntry.ts +++ b/packages/server/src/interfaces/ItemEntry.ts @@ -34,6 +34,7 @@ export interface IItemEntry { taxCode: string; taxRate: number; + taxAmount: number; item?: IItem; diff --git a/packages/server/src/interfaces/SaleEstimate.ts b/packages/server/src/interfaces/SaleEstimate.ts index 92385c046..f2a820e98 100644 --- a/packages/server/src/interfaces/SaleEstimate.ts +++ b/packages/server/src/interfaces/SaleEstimate.ts @@ -1,5 +1,5 @@ import { Knex } from 'knex'; -import { IItemEntry } from './ItemEntry'; +import { IItemEntry, IItemEntryDTO } from './ItemEntry'; import { IDynamicListFilterDTO } from '@/interfaces/DynamicFilter'; export interface ISaleEstimate { @@ -29,7 +29,7 @@ export interface ISaleEstimateDTO { estimateDate?: Date; reference?: string; estimateNumber?: string; - entries: IItemEntry[]; + entries: IItemEntryDTO[]; note: string; termsConditions: string; sendToEmail: string; diff --git a/packages/server/src/interfaces/SaleInvoice.ts b/packages/server/src/interfaces/SaleInvoice.ts index f2283de43..874d1b6c9 100644 --- a/packages/server/src/interfaces/SaleInvoice.ts +++ b/packages/server/src/interfaces/SaleInvoice.ts @@ -1,5 +1,5 @@ import { Knex } from 'knex'; -import { ISystemUser, IAccount } from '@/interfaces'; +import { ISystemUser, IAccount, ITaxTransaction } from '@/interfaces'; import { IDynamicListFilter } from '@/interfaces/DynamicFilter'; import { IItemEntry, IItemEntryDTO } from './ItemEntry'; @@ -33,6 +33,9 @@ export interface ISaleInvoice { writtenoffExpenseAccountId?: number; writtenoffExpenseAccount?: IAccount; + + taxAmountWithheld: number; + taxes: ITaxTransaction[] } export interface ISaleInvoiceDTO { diff --git a/packages/server/src/interfaces/TaxRate.ts b/packages/server/src/interfaces/TaxRate.ts index b65a03851..16a38e9bb 100644 --- a/packages/server/src/interfaces/TaxRate.ts +++ b/packages/server/src/interfaces/TaxRate.ts @@ -47,3 +47,10 @@ export interface ITaxRateDeletedPayload { tenantId: number; trx: Knex.Transaction; } + + +export interface ITaxTransaction { + taxAmount: number; + taxName: string; + taxCode: string; +} \ No newline at end of file diff --git a/packages/server/src/loaders/eventEmitter.ts b/packages/server/src/loaders/eventEmitter.ts index f42144cb0..ea2b97677 100644 --- a/packages/server/src/loaders/eventEmitter.ts +++ b/packages/server/src/loaders/eventEmitter.ts @@ -81,6 +81,9 @@ import { ProjectBillableExpensesSubscriber } from '@/services/Projects/Projects/ import { ProjectBillableBillSubscriber } from '@/services/Projects/Projects/ProjectBillableBillSubscriber'; import { SyncActualTimeTaskSubscriber } from '@/services/Projects/Times/SyncActualTimeTaskSubscriber'; import { SaleInvoiceTaxRateValidateSubscriber } from '@/services/TaxRates/subscribers/SaleInvoiceTaxRateValidateSubscriber'; +import { SaleEstimateTaxRateValidateSubscriber } from '@/services/TaxRates/subscribers/SaleEstimateTaxRateValidateSubscriber'; +import { SaleReceiptTaxRateValidateSubscriber } from '@/services/TaxRates/subscribers/SaleReceiptTaxRateValidateSubscriber'; +import { WriteInvoiceTaxTransactionsSubscriber } from '@/services/TaxRates/subscribers/WriteInvoiceTaxTransactionsSubscriber'; export default () => { return new EventPublisher(); @@ -188,6 +191,11 @@ export const susbcribers = () => { ProjectBillableTasksSubscriber, ProjectBillableExpensesSubscriber, ProjectBillableBillSubscriber, - SaleInvoiceTaxRateValidateSubscriber + + // Tax Rates + SaleInvoiceTaxRateValidateSubscriber, + SaleEstimateTaxRateValidateSubscriber, + SaleReceiptTaxRateValidateSubscriber, + WriteInvoiceTaxTransactionsSubscriber ]; }; diff --git a/packages/server/src/loaders/tenantModels.ts b/packages/server/src/loaders/tenantModels.ts index 3c7865922..d76c7e618 100644 --- a/packages/server/src/loaders/tenantModels.ts +++ b/packages/server/src/loaders/tenantModels.ts @@ -59,6 +59,7 @@ import Project from 'models/Project'; import Time from 'models/Time'; import Task from 'models/Task'; import TaxRate from 'models/TaxRate'; +import TaxRateTransaction from 'models/TaxRateTransaction'; export default (knex) => { const models = { @@ -121,6 +122,7 @@ export default (knex) => { Time, Task, TaxRate, + TaxRateTransaction, }; return mapValues(models, (model) => model.bindKnex(knex)); }; diff --git a/packages/server/src/models/ItemEntry.ts b/packages/server/src/models/ItemEntry.ts index cae1c9cf2..2d7e5de95 100644 --- a/packages/server/src/models/ItemEntry.ts +++ b/packages/server/src/models/ItemEntry.ts @@ -2,6 +2,8 @@ import { Model } from 'objection'; import TenantModel from 'models/TenantModel'; export default class ItemEntry extends TenantModel { + public taxRate: number; + /** * Table name. */ @@ -17,7 +19,7 @@ export default class ItemEntry extends TenantModel { } static get virtualAttributes() { - return ['amount']; + return ['amount', 'taxAmount']; } get amount() { @@ -31,6 +33,22 @@ export default class ItemEntry extends TenantModel { return discount ? total - total * discount * 0.01 : total; } + /** + * Tag rate fraction. + * @returns {number} + */ + get tagRateFraction() { + return this.taxRate / 100; + } + + /** + * Tax amount withheld. + * @returns {number} + */ + get taxAmount() { + return this.amount * this.tagRateFraction; + } + static get relationMappings() { const Item = require('models/Item'); const BillLandedCostEntry = require('models/BillLandedCostEntry'); diff --git a/packages/server/src/models/SaleInvoice.ts b/packages/server/src/models/SaleInvoice.ts index 38f21f285..ca2612397 100644 --- a/packages/server/src/models/SaleInvoice.ts +++ b/packages/server/src/models/SaleInvoice.ts @@ -13,6 +13,11 @@ export default class SaleInvoice extends mixin(TenantModel, [ CustomViewBaseModel, ModelSearchable, ]) { + taxAmountWithheld: number; + balance: number; + paymentAmount: number; + exchangeRate: number; + /** * Table name */ @@ -51,12 +56,115 @@ export default class SaleInvoice extends mixin(TenantModel, [ ]; } + /** + * Invoice total FCY. + * @returns {number} + */ + get totalFcy() { + return this.amountFcy + this.taxAmountWithheldFcy; + } + + /** + * Invoice total BCY. + * @returns {number} + */ + get totalBcy() { + return this.amountBcy + this.taxAmountWithheldBcy; + } + + /** + * Tax amount withheld FCY. + * @returns {number} + */ + get taxAmountWithheldFcy() { + return this.taxAmountWithheld; + } + + /** + * Tax amount withheld BCY. + * @returns {number} + */ + get taxAmountWithheldBcy() { + return this.taxAmountWithheld; + } + + /** + * Subtotal FCY. + * @returns {number} + */ + get subtotalFcy() { + return this.amountFcy; + } + + /** + * Subtotal BCY. + * @returns {number} + */ + get subtotalBcy() { + return this.amountBcy; + } + + /** + * Invoice due amount FCY. + * @returns {number} + */ + get dueAmountFcy() { + return this.amountFcy - this.paymentAmountFcy; + } + + /** + * Invoice due amount BCY. + * @returns {number} + */ + get dueAmountBcy() { + return this.amountBcy - this.paymentAmountBcy; + } + + /** + * Invoice amount FCY. + * @returns {number} + */ + get amountFcy() { + return this.balance; + } + + /** + * Invoice amount BCY. + * @returns {number} + */ + get amountBcy() { + return this.balance * this.exchangeRate; + } + + /** + * Invoice payment amount FCY. + * @returns {number} + */ + get paymentAmountFcy() { + return this.paymentAmount; + } + + /** + * Invoice payment amount BCY. + * @returns {number} + */ + get paymentAmountBcy() { + return this.paymentAmount * this.exchangeRate; + } + + /** + * + */ + get total() { + return this.balance + this.taxAmountWithheld; + } + /** * Invoice amount in local currency. * @returns {number} */ get localAmount() { - return this.balance * this.exchangeRate; + return this.total * this.exchangeRate; } /** @@ -333,6 +441,7 @@ export default class SaleInvoice extends mixin(TenantModel, [ const PaymentReceiveEntry = require('models/PaymentReceiveEntry'); const Branch = require('models/Branch'); const Account = require('models/Account'); + const TaxRateTransaction = require('models/TaxRateTransaction'); return { /** @@ -428,6 +537,21 @@ export default class SaleInvoice extends mixin(TenantModel, [ to: 'accounts.id', }, }, + + /** + * + */ + taxes: { + relation: Model.HasManyRelation, + modelClass: TaxRateTransaction.default, + join: { + from: 'sales_invoices.id', + to: 'tax_rate_transactions.referenceId', + }, + filter(builder) { + builder.where('reference_type', 'SaleInvoice'); + }, + }, }; } diff --git a/packages/server/src/models/TaxRateTransaction.ts b/packages/server/src/models/TaxRateTransaction.ts new file mode 100644 index 000000000..b26c26903 --- /dev/null +++ b/packages/server/src/models/TaxRateTransaction.ts @@ -0,0 +1,42 @@ +import { mixin, Model, raw } from 'objection'; +import TenantModel from 'models/TenantModel'; +import ModelSearchable from './ModelSearchable'; + +export default class TaxRateTransaction extends mixin(TenantModel, [ + ModelSearchable, +]) { + /** + * Table name + */ + static get tableName() { + return 'tax_rate_transactions'; + } + + /** + * Timestamps columns. + */ + get timestamps() { + return []; + } + + /** + * Virtual attributes. + */ + static get virtualAttributes() { + return []; + } + + /** + * Model modifiers. + */ + static get modifiers() { + return {}; + } + + /** + * Relationship mapping. + */ + static get relationMappings() { + return {}; + } +} diff --git a/packages/server/src/repositories/AccountRepository.ts b/packages/server/src/repositories/AccountRepository.ts index decde91f5..8bc6bf7d1 100644 --- a/packages/server/src/repositories/AccountRepository.ts +++ b/packages/server/src/repositories/AccountRepository.ts @@ -2,6 +2,7 @@ import { Account } from 'models'; import TenantRepository from '@/repositories/TenantRepository'; import { IAccount } from '@/interfaces'; import { Knex } from 'knex'; +import { TaxPayableAccount } from '@/database/seeds/data/accounts'; export default class AccountRepository extends TenantRepository { /** @@ -116,7 +117,7 @@ export default class AccountRepository extends TenantRepository { if (!result) { result = await this.model.query(trx).insertAndFetch({ name: this.i18n.__('account.accounts_receivable.currency', { - currency: currencyCode + currency: currencyCode, }), accountType: 'accounts-receivable', currencyCode, @@ -127,6 +128,29 @@ export default class AccountRepository extends TenantRepository { return result; }; + /** + * Find or create tax payable account. + * @param {Record}extraAttrs + * @param {Knex.Transaction} trx + * @returns + */ + async findOrCreateTaxPayable( + extraAttrs: Record = {}, + trx?: Knex.Transaction + ) { + let result = await this.model + .query(trx) + .findOne({ slug: TaxPayableAccount.slug, ...extraAttrs }); + + if (!result) { + result = await this.model.query(trx).insertAndFetch({ + ...TaxPayableAccount, + ...extraAttrs, + }); + } + return result; + } + findOrCreateAccountsPayable = async ( currencyCode: string = '', extraAttrs = {}, diff --git a/packages/server/src/services/Sales/Invoices/CommandSaleInvoiceDTOTransformer.ts b/packages/server/src/services/Sales/Invoices/CommandSaleInvoiceDTOTransformer.ts index c4743350e..dc728f56c 100644 --- a/packages/server/src/services/Sales/Invoices/CommandSaleInvoiceDTOTransformer.ts +++ b/packages/server/src/services/Sales/Invoices/CommandSaleInvoiceDTOTransformer.ts @@ -17,6 +17,7 @@ import HasTenancyService from '@/services/Tenancy/TenancyService'; import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators'; import { SaleInvoiceIncrement } from './SaleInvoiceIncrement'; import { formatDateFields } from 'utils'; +import { ItemEntriesTaxTransactions } from '@/services/TaxRates/ItemEntriesTaxTransactions'; @Service() export class CommandSaleInvoiceDTOTransformer { @@ -38,6 +39,9 @@ export class CommandSaleInvoiceDTOTransformer { @Inject() private invoiceIncrement: SaleInvoiceIncrement; + @Inject() + private taxDTOTransformer: ItemEntriesTaxTransactions; + /** * Transformes the create DTO to invoice object model. * @param {ISaleInvoiceCreateDTO} saleInvoiceDTO - Sale invoice DTO. @@ -96,6 +100,7 @@ export class CommandSaleInvoiceDTOTransformer { } as ISaleInvoice; return R.compose( + this.taxDTOTransformer.assocTaxAmountWithheldFromEntries, this.branchDTOTransform.transformDTO(tenantId), this.warehouseDTOTransform.transformDTO(tenantId) )(initialDTO); diff --git a/packages/server/src/services/Sales/Invoices/InvoiceGLEntries.ts b/packages/server/src/services/Sales/Invoices/InvoiceGLEntries.ts index c47291fe0..8371aed86 100644 --- a/packages/server/src/services/Sales/Invoices/InvoiceGLEntries.ts +++ b/packages/server/src/services/Sales/Invoices/InvoiceGLEntries.ts @@ -1,12 +1,13 @@ import * as R from 'ramda'; +import { Knex } from 'knex'; import { ISaleInvoice, IItemEntry, ILedgerEntry, AccountNormal, ILedger, + ITaxTransaction, } from '@/interfaces'; -import { Knex } from 'knex'; import { Service, Inject } from 'typedi'; import Ledger from '@/services/Accounting/Ledger'; import LedgerStorageService from '@/services/Accounting/LedgerStorageService'; @@ -22,8 +23,8 @@ export class SaleInvoiceGLEntries { /** * Writes a sale invoice GL entries. - * @param {number} tenantId - * @param {number} saleInvoiceId + * @param {number} tenantId - Tenant id. + * @param {number} saleInvoiceId - Sale invoice id. * @param {Knex.Transaction} trx */ public writeInvoiceGLEntries = async ( @@ -42,9 +43,17 @@ export class SaleInvoiceGLEntries { const ARAccount = await accountRepository.findOrCreateAccountReceivable( saleInvoice.currencyCode ); + // Find or create tax payable account. + const taxPayableAccount = await accountRepository.findOrCreateTaxPayable( + {}, + trx + ); // Retrieves the ledger of the invoice. - const ledger = this.getInvoiceGLedger(saleInvoice, ARAccount.id); - + const ledger = this.getInvoiceGLedger( + saleInvoice, + ARAccount.id, + taxPayableAccount.id + ); // Commits the ledger entries to the storage as UOW. await this.ledegrRepository.commit(tenantId, ledger, trx); }; @@ -94,10 +103,14 @@ export class SaleInvoiceGLEntries { */ public getInvoiceGLedger = ( saleInvoice: ISaleInvoice, - ARAccountId: number + ARAccountId: number, + taxPayableAccountId: number ): ILedger => { - const entries = this.getInvoiceGLEntries(saleInvoice, ARAccountId); - + const entries = this.getInvoiceGLEntries( + saleInvoice, + ARAccountId, + taxPayableAccountId + ); return new Ledger(entries); }; @@ -143,7 +156,7 @@ export class SaleInvoiceGLEntries { return { ...commonEntry, - debit: saleInvoice.localAmount, + debit: saleInvoice.totalBcy, accountId: ARAccountId, contactId: saleInvoice.customerId, accountNormal: AccountNormal.DEBIT, @@ -176,7 +189,27 @@ export class SaleInvoiceGLEntries { itemId: entry.itemId, itemQuantity: entry.quantity, accountNormal: AccountNormal.CREDIT, - projectId: entry.projectId || saleInvoice.projectId + projectId: entry.projectId || saleInvoice.projectId, + }; + } + ); + + /** + * Retreives the GL entry of tax payable. + * @param {ISaleInvoice} saleInvoice - + * @param {number} taxPayableAccountId - + * @returns {ILedgerEntry} + */ + private getInvoiceTaxEntry = R.curry( + (saleInvoice: ISaleInvoice, taxPayableAccountId: number): ILedgerEntry => { + const commonEntry = this.getInvoiceGLCommonEntry(saleInvoice); + + return { + ...commonEntry, + credit: saleInvoice.taxAmountWithheld, + accountId: taxPayableAccountId, + index: saleInvoice.entries.length + 3, + accountNormal: AccountNormal.CREDIT, }; } ); @@ -189,15 +222,18 @@ export class SaleInvoiceGLEntries { */ public getInvoiceGLEntries = ( saleInvoice: ISaleInvoice, - ARAccountId: number + ARAccountId: number, + taxPayableAccountId: number ): ILedgerEntry[] => { const receivableEntry = this.getInvoiceReceivableEntry( saleInvoice, ARAccountId ); const transformItemEntry = this.getInvoiceItemEntry(saleInvoice); - const creditEntries = saleInvoice.entries.map(transformItemEntry); - return [receivableEntry, ...creditEntries]; + const creditEntries = saleInvoice.entries.map(transformItemEntry); + const taxEntry = this.getInvoiceTaxEntry(saleInvoice, taxPayableAccountId); + + return [receivableEntry, ...creditEntries, taxEntry]; }; } diff --git a/packages/server/src/services/TaxRates/ItemEntriesTaxTransactions.ts b/packages/server/src/services/TaxRates/ItemEntriesTaxTransactions.ts new file mode 100644 index 000000000..9a1e72d80 --- /dev/null +++ b/packages/server/src/services/TaxRates/ItemEntriesTaxTransactions.ts @@ -0,0 +1,22 @@ +import { ItemEntry } from "@/models"; +import { sumBy } from "lodash"; +import { Service } from "typedi"; + + +@Service() +export class ItemEntriesTaxTransactions { + /** + * + * @param model + * @returns + */ + public assocTaxAmountWithheldFromEntries(model: any) { + const entries = model.entries.map((entry) => ItemEntry.fromJson(entry)); + const taxAmountWithheld = sumBy(entries, 'taxAmount'); + + if (taxAmountWithheld) { + model.taxAmountWithheld = taxAmountWithheld; + } + return model; + } +} \ No newline at end of file diff --git a/packages/server/src/services/TaxRates/WriteTaxTransactionsItemEntries.ts b/packages/server/src/services/TaxRates/WriteTaxTransactionsItemEntries.ts new file mode 100644 index 000000000..40304a0ab --- /dev/null +++ b/packages/server/src/services/TaxRates/WriteTaxTransactionsItemEntries.ts @@ -0,0 +1,65 @@ +import { sumBy, chain } from 'lodash'; +import { IItemEntry } from '@/interfaces'; +import HasTenancyService from '../Tenancy/TenancyService'; +import { Inject, Service } from 'typedi'; + +@Service() +export class WriteTaxTransactionsItemEntries { + @Inject() + private tenancy: HasTenancyService; + + /** + * Writes the tax transactions from the given item entries. + * @param {number} tenantId + * @param {IItemEntry[]} itemEntries + */ + public async writeTaxTransactionsFromItemEntries( + tenantId: number, + itemEntries: IItemEntry[] + ) { + const { TaxRateTransaction } = this.tenancy.models(tenantId); + const aggregatedEntries = this.aggregateItemEntriesByTaxCode(itemEntries); + + const taxTransactions = aggregatedEntries.map((entry) => ({ + taxName: 'TAX NAME', + taxCode: 'TAG_CODE', + referenceType: entry.referenceType, + referenceId: entry.referenceId, + taxAmount: entry.taxAmount, + })); + await TaxRateTransaction.query().upsertGraph(taxTransactions); + } + + /** + * + * @param {IItemEntry[]} itemEntries + * @returns {} + */ + private aggregateItemEntriesByTaxCode(itemEntries: IItemEntry[]) { + return chain(itemEntries.filter((item) => item.taxCode)) + .groupBy((item) => item.taxCode) + .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 + */ + public removeTaxTransactionsFromItemEntries( + tenantId: number, + itemEntries: IItemEntry[] + ) { + const { TaxRateTransaction } = this.tenancy.models(tenantId); + + const filteredEntries = itemEntries.filter((item) => item.taxCode); + } +} diff --git a/packages/server/src/services/TaxRates/subscribers/WriteInvoiceTaxTransactionsSubscriber.ts b/packages/server/src/services/TaxRates/subscribers/WriteInvoiceTaxTransactionsSubscriber.ts new file mode 100644 index 000000000..75720d3af --- /dev/null +++ b/packages/server/src/services/TaxRates/subscribers/WriteInvoiceTaxTransactionsSubscriber.ts @@ -0,0 +1,56 @@ +import { Inject, Service } from 'typedi'; +import { + ISaleInvoiceCreatedPayload, + ISaleInvoiceDeletedPayload, +} from '@/interfaces'; +import events from '@/subscribers/events'; +import { WriteTaxTransactionsItemEntries } from '../WriteTaxTransactionsItemEntries'; + +@Service() +export class WriteInvoiceTaxTransactionsSubscriber { + @Inject() + private writeTaxTransactions: WriteTaxTransactionsItemEntries; + + /** + * Attaches events with handlers. + */ + public attach(bus) { + bus.subscribe( + events.saleInvoice.onCreated, + this.writeInvoiceTaxTransactionsOnCreated + ); + bus.subscribe( + events.saleInvoice.onDeleted, + this.removeInvoiceTaxTransactionsOnDeleted + ); + return bus; + } + + /** + * Validate receipt entries tax rate code existance. + * @param {ISaleInvoiceCreatingPaylaod} + */ + private writeInvoiceTaxTransactionsOnCreated = async ({ + tenantId, + saleInvoice, + }: ISaleInvoiceCreatedPayload) => { + await this.writeTaxTransactions.writeTaxTransactionsFromItemEntries( + tenantId, + saleInvoice.entries + ); + }; + + /** + * Removes the invoice tax transactions on invoice deleted. + * @param {ISaleInvoiceEditingPayload} + */ + private removeInvoiceTaxTransactionsOnDeleted = async ({ + tenantId, + oldSaleInvoice, + }: ISaleInvoiceDeletedPayload) => { + await this.writeTaxTransactions.removeTaxTransactionsFromItemEntries( + tenantId, + oldSaleInvoice.entries + ); + }; +}