feat(server): wip tax rate on sale invoice service

This commit is contained in:
Ahmed Bouhuolia
2023-08-14 14:59:10 +02:00
parent a7644e6481
commit d1121f0b81
18 changed files with 514 additions and 74 deletions

View File

@@ -11,14 +11,25 @@ exports.up = (knex) => {
table.timestamps(); table.timestamps();
}) })
.table('items_entries', (table) => { .table('items_entries', (table) => {
table.boolean(['is_tax_exclusive']); table.boolean('is_tax_exclusive');
table.string('tax_code'); table.string('tax_code');
table.decimal('tax_rate'); table.decimal('tax_rate');
table.decimal('tax_amount_withheld')
}) })
.table('sales_invoices', (table) => { .table('sales_invoices', (table) => {
table.boolean(['is_tax_exclusive']); table.boolean('is_tax_exclusive');
table.decimal('tax_amount_withheld') 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();
}); });
}; };

View File

@@ -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 [ export default [
{ {
name:'Bank Account', name: 'Bank Account',
slug: 'bank-account', slug: 'bank-account',
account_type: 'bank', account_type: 'bank',
code: '10001', code: '10001',
@@ -11,7 +21,7 @@ export default [
predefined: 1, predefined: 1,
}, },
{ {
name:'Saving Bank Account', name: 'Saving Bank Account',
slug: 'saving-bank-account', slug: 'saving-bank-account',
account_type: 'bank', account_type: 'bank',
code: '10002', code: '10002',
@@ -21,7 +31,7 @@ export default [
predefined: 0, predefined: 0,
}, },
{ {
name:'Undeposited Funds', name: 'Undeposited Funds',
slug: 'undeposited-funds', slug: 'undeposited-funds',
account_type: 'cash', account_type: 'cash',
code: '10003', code: '10003',
@@ -31,7 +41,7 @@ export default [
predefined: 1, predefined: 1,
}, },
{ {
name:'Petty Cash', name: 'Petty Cash',
slug: 'petty-cash', slug: 'petty-cash',
account_type: 'cash', account_type: 'cash',
code: '10004', code: '10004',
@@ -41,7 +51,7 @@ export default [
predefined: 1, predefined: 1,
}, },
{ {
name:'Computer Equipment', name: 'Computer Equipment',
slug: 'computer-equipment', slug: 'computer-equipment',
code: '10005', code: '10005',
account_type: 'fixed-asset', account_type: 'fixed-asset',
@@ -52,7 +62,7 @@ export default [
description: '', description: '',
}, },
{ {
name:'Office Equipment', name: 'Office Equipment',
slug: 'office-equipment', slug: 'office-equipment',
code: '10006', code: '10006',
account_type: 'fixed-asset', account_type: 'fixed-asset',
@@ -63,7 +73,7 @@ export default [
description: '', description: '',
}, },
{ {
name:'Accounts Receivable (A/R)', name: 'Accounts Receivable (A/R)',
slug: 'accounts-receivable', slug: 'accounts-receivable',
account_type: 'accounts-receivable', account_type: 'accounts-receivable',
code: '10007', code: '10007',
@@ -73,7 +83,7 @@ export default [
predefined: 1, predefined: 1,
}, },
{ {
name:'Inventory Asset', name: 'Inventory Asset',
slug: 'inventory-asset', slug: 'inventory-asset',
code: '10008', code: '10008',
account_type: 'inventory', account_type: 'inventory',
@@ -81,12 +91,13 @@ export default [
parent_account_id: null, parent_account_id: null,
index: 1, index: 1,
active: 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 // Libilities
{ {
name:'Accounts Payable (A/P)', name: 'Accounts Payable (A/P)',
slug: 'accounts-payable', slug: 'accounts-payable',
account_type: 'accounts-payable', account_type: 'accounts-payable',
parent_account_id: null, parent_account_id: null,
@@ -97,38 +108,39 @@ export default [
predefined: 1, predefined: 1,
}, },
{ {
name:'Owner A Drawings', name: 'Owner A Drawings',
slug: 'owner-drawings', slug: 'owner-drawings',
account_type: 'other-current-liability', account_type: 'other-current-liability',
parent_account_id: null, parent_account_id: null,
code: '20002', code: '20002',
description:'Withdrawals by the owners.', description: 'Withdrawals by the owners.',
active: 1, active: 1,
index: 1, index: 1,
predefined: 0, predefined: 0,
}, },
{ {
name:'Loan', name: 'Loan',
slug: 'owner-drawings', slug: 'owner-drawings',
account_type: 'other-current-liability', account_type: 'other-current-liability',
code: '20003', code: '20003',
description:'Money that has been borrowed from a creditor.', description: 'Money that has been borrowed from a creditor.',
active: 1, active: 1,
index: 1, index: 1,
predefined: 0, predefined: 0,
}, },
{ {
name:'Opening Balance Liabilities', name: 'Opening Balance Liabilities',
slug: 'opening-balance-liabilities', slug: 'opening-balance-liabilities',
account_type: 'other-current-liability', account_type: 'other-current-liability',
code: '20004', 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, active: 1,
index: 1, index: 1,
predefined: 0, predefined: 0,
}, },
{ {
name:'Revenue Received in Advance', name: 'Revenue Received in Advance',
slug: 'revenue-received-in-advance', slug: 'revenue-received-in-advance',
account_type: 'other-current-liability', account_type: 'other-current-liability',
parent_account_id: null, parent_account_id: null,
@@ -138,34 +150,27 @@ export default [
index: 1, index: 1,
predefined: 0, predefined: 0,
}, },
{ TaxPayableAccount,
name:'Sales Tax Payable',
slug: 'owner-drawings',
account_type: 'other-current-liability',
code: '20006',
description: '',
active: 1,
index: 1,
predefined: 1,
},
// Equity // Equity
{ {
name:'Retained Earnings', name: 'Retained Earnings',
slug: 'retained-earnings', slug: 'retained-earnings',
account_type: 'equity', account_type: 'equity',
code: '30001', code: '30001',
description:'Retained earnings tracks net income from previous fiscal years.', description:
'Retained earnings tracks net income from previous fiscal years.',
active: 1, active: 1,
index: 1, index: 1,
predefined: 1, predefined: 1,
}, },
{ {
name:'Opening Balance Equity', name: 'Opening Balance Equity',
slug: 'opening-balance-equity', slug: 'opening-balance-equity',
account_type: 'equity', account_type: 'equity',
code: '30002', 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, active: 1,
index: 1, index: 1,
predefined: 1, predefined: 1,
@@ -181,11 +186,12 @@ export default [
predefined: 1, predefined: 1,
}, },
{ {
name:`Drawings`, name: `Drawings`,
slug: 'drawings', slug: 'drawings',
account_type: 'equity', account_type: 'equity',
code: '30003', 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, active: 1,
index: 1, index: 1,
predefined: 1, predefined: 1,
@@ -193,7 +199,7 @@ export default [
// Expenses // Expenses
{ {
name:'Other Expenses', name: 'Other Expenses',
slug: 'other-expenses', slug: 'other-expenses',
account_type: 'other-expense', account_type: 'other-expense',
parent_account_id: null, parent_account_id: null,
@@ -204,18 +210,18 @@ export default [
predefined: 1, predefined: 1,
}, },
{ {
name:'Cost of Goods Sold', name: 'Cost of Goods Sold',
slug: 'cost-of-goods-sold', slug: 'cost-of-goods-sold',
account_type: 'cost-of-goods-sold', account_type: 'cost-of-goods-sold',
parent_account_id: null, parent_account_id: null,
code: '40002', code: '40002',
description:'Tracks the direct cost of the goods sold.', description: 'Tracks the direct cost of the goods sold.',
active: 1, active: 1,
index: 1, index: 1,
predefined: 1, predefined: 1,
}, },
{ {
name:'Office expenses', name: 'Office expenses',
slug: 'office-expenses', slug: 'office-expenses',
account_type: 'expense', account_type: 'expense',
parent_account_id: null, parent_account_id: null,
@@ -226,7 +232,7 @@ export default [
predefined: 0, predefined: 0,
}, },
{ {
name:'Rent', name: 'Rent',
slug: 'rent', slug: 'rent',
account_type: 'expense', account_type: 'expense',
parent_account_id: null, parent_account_id: null,
@@ -237,29 +243,30 @@ export default [
predefined: 0, predefined: 0,
}, },
{ {
name:'Exchange Gain or Loss', name: 'Exchange Gain or Loss',
slug: 'exchange-grain-loss', slug: 'exchange-grain-loss',
account_type: 'other-expense', account_type: 'other-expense',
parent_account_id: null, parent_account_id: null,
code: '40005', code: '40005',
description:'Tracks the gain and losses of the exchange differences.', description: 'Tracks the gain and losses of the exchange differences.',
active: 1, active: 1,
index: 1, index: 1,
predefined: 1, predefined: 1,
}, },
{ {
name:'Bank Fees and Charges', name: 'Bank Fees and Charges',
slug: 'bank-fees-and-charges', slug: 'bank-fees-and-charges',
account_type: 'expense', account_type: 'expense',
parent_account_id: null, parent_account_id: null,
code: '40006', 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, active: 1,
index: 1, index: 1,
predefined: 0, predefined: 0,
}, },
{ {
name:'Depreciation Expense', name: 'Depreciation Expense',
slug: 'depreciation-expense', slug: 'depreciation-expense',
account_type: 'expense', account_type: 'expense',
parent_account_id: null, parent_account_id: null,
@@ -272,7 +279,7 @@ export default [
// Income // Income
{ {
name:'Sales of Product Income', name: 'Sales of Product Income',
slug: 'sales-of-product-income', slug: 'sales-of-product-income',
account_type: 'income', account_type: 'income',
predefined: 1, predefined: 1,
@@ -283,7 +290,7 @@ export default [
description: '', description: '',
}, },
{ {
name:'Sales of Service Income', name: 'Sales of Service Income',
slug: 'sales-of-service-income', slug: 'sales-of-service-income',
account_type: 'income', account_type: 'income',
predefined: 0, predefined: 0,
@@ -294,7 +301,7 @@ export default [
description: '', description: '',
}, },
{ {
name:'Uncategorized Income', name: 'Uncategorized Income',
slug: 'uncategorized-income', slug: 'uncategorized-income',
account_type: 'income', account_type: 'income',
parent_account_id: null, parent_account_id: null,
@@ -305,14 +312,15 @@ export default [
predefined: 1, predefined: 1,
}, },
{ {
name:'Other Income', name: 'Other Income',
slug: 'other-income', slug: 'other-income',
account_type: 'other-income', account_type: 'other-income',
parent_account_id: null, parent_account_id: null,
code: '50004', 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, active: 1,
index: 1, index: 1,
predefined: 0, predefined: 0,
} },
]; ];

View File

@@ -54,6 +54,14 @@ export interface IItemDTO {
sellDescription: string; sellDescription: string;
purchaseDescription: 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; quantityOnHand: number;
note: string; note: string;

View File

@@ -34,6 +34,7 @@ export interface IItemEntry {
taxCode: string; taxCode: string;
taxRate: number; taxRate: number;
taxAmount: number;
item?: IItem; item?: IItem;

View File

@@ -1,5 +1,5 @@
import { Knex } from 'knex'; import { Knex } from 'knex';
import { IItemEntry } from './ItemEntry'; import { IItemEntry, IItemEntryDTO } from './ItemEntry';
import { IDynamicListFilterDTO } from '@/interfaces/DynamicFilter'; import { IDynamicListFilterDTO } from '@/interfaces/DynamicFilter';
export interface ISaleEstimate { export interface ISaleEstimate {
@@ -29,7 +29,7 @@ export interface ISaleEstimateDTO {
estimateDate?: Date; estimateDate?: Date;
reference?: string; reference?: string;
estimateNumber?: string; estimateNumber?: string;
entries: IItemEntry[]; entries: IItemEntryDTO[];
note: string; note: string;
termsConditions: string; termsConditions: string;
sendToEmail: string; sendToEmail: string;

View File

@@ -1,5 +1,5 @@
import { Knex } from 'knex'; import { Knex } from 'knex';
import { ISystemUser, IAccount } from '@/interfaces'; import { ISystemUser, IAccount, ITaxTransaction } from '@/interfaces';
import { IDynamicListFilter } from '@/interfaces/DynamicFilter'; import { IDynamicListFilter } from '@/interfaces/DynamicFilter';
import { IItemEntry, IItemEntryDTO } from './ItemEntry'; import { IItemEntry, IItemEntryDTO } from './ItemEntry';
@@ -33,6 +33,9 @@ export interface ISaleInvoice {
writtenoffExpenseAccountId?: number; writtenoffExpenseAccountId?: number;
writtenoffExpenseAccount?: IAccount; writtenoffExpenseAccount?: IAccount;
taxAmountWithheld: number;
taxes: ITaxTransaction[]
} }
export interface ISaleInvoiceDTO { export interface ISaleInvoiceDTO {

View File

@@ -47,3 +47,10 @@ export interface ITaxRateDeletedPayload {
tenantId: number; tenantId: number;
trx: Knex.Transaction; trx: Knex.Transaction;
} }
export interface ITaxTransaction {
taxAmount: number;
taxName: string;
taxCode: string;
}

View File

@@ -81,6 +81,9 @@ import { ProjectBillableExpensesSubscriber } from '@/services/Projects/Projects/
import { ProjectBillableBillSubscriber } from '@/services/Projects/Projects/ProjectBillableBillSubscriber'; import { ProjectBillableBillSubscriber } from '@/services/Projects/Projects/ProjectBillableBillSubscriber';
import { SyncActualTimeTaskSubscriber } from '@/services/Projects/Times/SyncActualTimeTaskSubscriber'; import { SyncActualTimeTaskSubscriber } from '@/services/Projects/Times/SyncActualTimeTaskSubscriber';
import { SaleInvoiceTaxRateValidateSubscriber } from '@/services/TaxRates/subscribers/SaleInvoiceTaxRateValidateSubscriber'; 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 () => { export default () => {
return new EventPublisher(); return new EventPublisher();
@@ -188,6 +191,11 @@ export const susbcribers = () => {
ProjectBillableTasksSubscriber, ProjectBillableTasksSubscriber,
ProjectBillableExpensesSubscriber, ProjectBillableExpensesSubscriber,
ProjectBillableBillSubscriber, ProjectBillableBillSubscriber,
SaleInvoiceTaxRateValidateSubscriber
// Tax Rates
SaleInvoiceTaxRateValidateSubscriber,
SaleEstimateTaxRateValidateSubscriber,
SaleReceiptTaxRateValidateSubscriber,
WriteInvoiceTaxTransactionsSubscriber
]; ];
}; };

View File

@@ -59,6 +59,7 @@ import Project from 'models/Project';
import Time from 'models/Time'; import Time from 'models/Time';
import Task from 'models/Task'; import Task from 'models/Task';
import TaxRate from 'models/TaxRate'; import TaxRate from 'models/TaxRate';
import TaxRateTransaction from 'models/TaxRateTransaction';
export default (knex) => { export default (knex) => {
const models = { const models = {
@@ -121,6 +122,7 @@ export default (knex) => {
Time, Time,
Task, Task,
TaxRate, TaxRate,
TaxRateTransaction,
}; };
return mapValues(models, (model) => model.bindKnex(knex)); return mapValues(models, (model) => model.bindKnex(knex));
}; };

View File

@@ -2,6 +2,8 @@ import { Model } from 'objection';
import TenantModel from 'models/TenantModel'; import TenantModel from 'models/TenantModel';
export default class ItemEntry extends TenantModel { export default class ItemEntry extends TenantModel {
public taxRate: number;
/** /**
* Table name. * Table name.
*/ */
@@ -17,7 +19,7 @@ export default class ItemEntry extends TenantModel {
} }
static get virtualAttributes() { static get virtualAttributes() {
return ['amount']; return ['amount', 'taxAmount'];
} }
get amount() { get amount() {
@@ -31,6 +33,22 @@ export default class ItemEntry extends TenantModel {
return discount ? total - total * discount * 0.01 : total; 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() { static get relationMappings() {
const Item = require('models/Item'); const Item = require('models/Item');
const BillLandedCostEntry = require('models/BillLandedCostEntry'); const BillLandedCostEntry = require('models/BillLandedCostEntry');

View File

@@ -13,6 +13,11 @@ export default class SaleInvoice extends mixin(TenantModel, [
CustomViewBaseModel, CustomViewBaseModel,
ModelSearchable, ModelSearchable,
]) { ]) {
taxAmountWithheld: number;
balance: number;
paymentAmount: number;
exchangeRate: number;
/** /**
* Table name * 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. * Invoice amount in local currency.
* @returns {number} * @returns {number}
*/ */
get localAmount() { 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 PaymentReceiveEntry = require('models/PaymentReceiveEntry');
const Branch = require('models/Branch'); const Branch = require('models/Branch');
const Account = require('models/Account'); const Account = require('models/Account');
const TaxRateTransaction = require('models/TaxRateTransaction');
return { return {
/** /**
@@ -428,6 +537,21 @@ export default class SaleInvoice extends mixin(TenantModel, [
to: 'accounts.id', 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');
},
},
}; };
} }

View File

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

View File

@@ -2,6 +2,7 @@ import { Account } from 'models';
import TenantRepository from '@/repositories/TenantRepository'; import TenantRepository from '@/repositories/TenantRepository';
import { IAccount } from '@/interfaces'; import { IAccount } from '@/interfaces';
import { Knex } from 'knex'; import { Knex } from 'knex';
import { TaxPayableAccount } from '@/database/seeds/data/accounts';
export default class AccountRepository extends TenantRepository { export default class AccountRepository extends TenantRepository {
/** /**
@@ -116,7 +117,7 @@ export default class AccountRepository extends TenantRepository {
if (!result) { if (!result) {
result = await this.model.query(trx).insertAndFetch({ result = await this.model.query(trx).insertAndFetch({
name: this.i18n.__('account.accounts_receivable.currency', { name: this.i18n.__('account.accounts_receivable.currency', {
currency: currencyCode currency: currencyCode,
}), }),
accountType: 'accounts-receivable', accountType: 'accounts-receivable',
currencyCode, currencyCode,
@@ -127,6 +128,29 @@ export default class AccountRepository extends TenantRepository {
return result; return result;
}; };
/**
* Find or create tax payable account.
* @param {Record<string, string>}extraAttrs
* @param {Knex.Transaction} trx
* @returns
*/
async findOrCreateTaxPayable(
extraAttrs: Record<string, string> = {},
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 ( findOrCreateAccountsPayable = async (
currencyCode: string = '', currencyCode: string = '',
extraAttrs = {}, extraAttrs = {},

View File

@@ -17,6 +17,7 @@ import HasTenancyService from '@/services/Tenancy/TenancyService';
import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators'; import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators';
import { SaleInvoiceIncrement } from './SaleInvoiceIncrement'; import { SaleInvoiceIncrement } from './SaleInvoiceIncrement';
import { formatDateFields } from 'utils'; import { formatDateFields } from 'utils';
import { ItemEntriesTaxTransactions } from '@/services/TaxRates/ItemEntriesTaxTransactions';
@Service() @Service()
export class CommandSaleInvoiceDTOTransformer { export class CommandSaleInvoiceDTOTransformer {
@@ -38,6 +39,9 @@ export class CommandSaleInvoiceDTOTransformer {
@Inject() @Inject()
private invoiceIncrement: SaleInvoiceIncrement; private invoiceIncrement: SaleInvoiceIncrement;
@Inject()
private taxDTOTransformer: ItemEntriesTaxTransactions;
/** /**
* Transformes the create DTO to invoice object model. * Transformes the create DTO to invoice object model.
* @param {ISaleInvoiceCreateDTO} saleInvoiceDTO - Sale invoice DTO. * @param {ISaleInvoiceCreateDTO} saleInvoiceDTO - Sale invoice DTO.
@@ -96,6 +100,7 @@ export class CommandSaleInvoiceDTOTransformer {
} as ISaleInvoice; } as ISaleInvoice;
return R.compose( return R.compose(
this.taxDTOTransformer.assocTaxAmountWithheldFromEntries,
this.branchDTOTransform.transformDTO<ISaleInvoice>(tenantId), this.branchDTOTransform.transformDTO<ISaleInvoice>(tenantId),
this.warehouseDTOTransform.transformDTO<ISaleInvoice>(tenantId) this.warehouseDTOTransform.transformDTO<ISaleInvoice>(tenantId)
)(initialDTO); )(initialDTO);

View File

@@ -1,12 +1,13 @@
import * as R from 'ramda'; import * as R from 'ramda';
import { Knex } from 'knex';
import { import {
ISaleInvoice, ISaleInvoice,
IItemEntry, IItemEntry,
ILedgerEntry, ILedgerEntry,
AccountNormal, AccountNormal,
ILedger, ILedger,
ITaxTransaction,
} from '@/interfaces'; } from '@/interfaces';
import { Knex } from 'knex';
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import Ledger from '@/services/Accounting/Ledger'; import Ledger from '@/services/Accounting/Ledger';
import LedgerStorageService from '@/services/Accounting/LedgerStorageService'; import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
@@ -22,8 +23,8 @@ export class SaleInvoiceGLEntries {
/** /**
* Writes a sale invoice GL entries. * Writes a sale invoice GL entries.
* @param {number} tenantId * @param {number} tenantId - Tenant id.
* @param {number} saleInvoiceId * @param {number} saleInvoiceId - Sale invoice id.
* @param {Knex.Transaction} trx * @param {Knex.Transaction} trx
*/ */
public writeInvoiceGLEntries = async ( public writeInvoiceGLEntries = async (
@@ -42,9 +43,17 @@ export class SaleInvoiceGLEntries {
const ARAccount = await accountRepository.findOrCreateAccountReceivable( const ARAccount = await accountRepository.findOrCreateAccountReceivable(
saleInvoice.currencyCode saleInvoice.currencyCode
); );
// Find or create tax payable account.
const taxPayableAccount = await accountRepository.findOrCreateTaxPayable(
{},
trx
);
// Retrieves the ledger of the invoice. // 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. // Commits the ledger entries to the storage as UOW.
await this.ledegrRepository.commit(tenantId, ledger, trx); await this.ledegrRepository.commit(tenantId, ledger, trx);
}; };
@@ -94,10 +103,14 @@ export class SaleInvoiceGLEntries {
*/ */
public getInvoiceGLedger = ( public getInvoiceGLedger = (
saleInvoice: ISaleInvoice, saleInvoice: ISaleInvoice,
ARAccountId: number ARAccountId: number,
taxPayableAccountId: number
): ILedger => { ): ILedger => {
const entries = this.getInvoiceGLEntries(saleInvoice, ARAccountId); const entries = this.getInvoiceGLEntries(
saleInvoice,
ARAccountId,
taxPayableAccountId
);
return new Ledger(entries); return new Ledger(entries);
}; };
@@ -143,7 +156,7 @@ export class SaleInvoiceGLEntries {
return { return {
...commonEntry, ...commonEntry,
debit: saleInvoice.localAmount, debit: saleInvoice.totalBcy,
accountId: ARAccountId, accountId: ARAccountId,
contactId: saleInvoice.customerId, contactId: saleInvoice.customerId,
accountNormal: AccountNormal.DEBIT, accountNormal: AccountNormal.DEBIT,
@@ -176,7 +189,27 @@ export class SaleInvoiceGLEntries {
itemId: entry.itemId, itemId: entry.itemId,
itemQuantity: entry.quantity, itemQuantity: entry.quantity,
accountNormal: AccountNormal.CREDIT, 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 = ( public getInvoiceGLEntries = (
saleInvoice: ISaleInvoice, saleInvoice: ISaleInvoice,
ARAccountId: number ARAccountId: number,
taxPayableAccountId: number
): ILedgerEntry[] => { ): ILedgerEntry[] => {
const receivableEntry = this.getInvoiceReceivableEntry( const receivableEntry = this.getInvoiceReceivableEntry(
saleInvoice, saleInvoice,
ARAccountId ARAccountId
); );
const transformItemEntry = this.getInvoiceItemEntry(saleInvoice); 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];
}; };
} }

View File

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

View File

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

View File

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