Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(server): integrate tax rates to bills #260

Merged
merged 10 commits into from
Oct 8, 2023
21 changes: 21 additions & 0 deletions packages/server/src/api/controllers/Purchases/Bills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ export default class BillsController extends BaseController {
check('note').optional().trim().escape(),
check('open').default(false).isBoolean().toBoolean(),

check('is_inclusive_tax').default(false).isBoolean().toBoolean(),

check('entries').isArray({ min: 1 }),

check('entries.*.index').exists().isNumeric().toInt(),
Expand All @@ -137,6 +139,15 @@ export default class BillsController extends BaseController {
.optional({ nullable: true })
.isNumeric()
.toInt(),
check('entries.*.tax_code')
.optional({ nullable: true })
.trim()
.escape()
.isString(),
check('entries.*.tax_rate_id')
.optional({ nullable: true })
.isNumeric()
.toInt(),
];
}

Expand Down Expand Up @@ -542,6 +553,16 @@ export default class BillsController extends BaseController {
],
});
}
if (error.errorType === 'ITEM_ENTRY_TAX_RATE_CODE_NOT_FOUND') {
return res.boom.badRequest(null, {
errors: [{ type: 'ITEM_ENTRY_TAX_RATE_CODE_NOT_FOUND', code: 1800 }],
});
}
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: 1900 }],
});
}
}
next(error);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
exports.up = (knex) => {
return knex.schema.table('bills', (table) => {
table.boolean('is_inclusive_tax').defaultTo(false);
table.decimal('tax_amount_withheld');
});
};

exports.down = (knex) => {
return knex.schema.table('bills', () => {});
};
12 changes: 11 additions & 1 deletion packages/server/src/interfaces/Bill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Knex } from 'knex';
import { IDynamicListFilterDTO } from './DynamicFilter';
import { IItemEntry, IItemEntryDTO } from './ItemEntry';
import { IBillLandedCost } from './LandedCost';

export interface IBillDTO {
vendorId: number;
billNumber: string;
Expand All @@ -15,10 +16,10 @@ export interface IBillDTO {
exchangeRate?: number;
open: boolean;
entries: IItemEntryDTO[];

branchId?: number;
warehouseId?: number;
projectId?: number;
isInclusiveTax?: boolean;
}

export interface IBillEditDTO {
Expand Down Expand Up @@ -80,6 +81,15 @@ export interface IBill {

localAmount?: number;
locatedLandedCosts?: IBillLandedCost[];

amountLocal: number;
subtotal: number;
subtotalLocal: number;
subtotalExcludingTax: number;
taxAmountWithheld: number;
taxAmountWithheldLocal: number;
total: number;
totalLocal: number;
}

export interface IBillsFilter extends IDynamicListFilterDTO {
Expand Down
8 changes: 7 additions & 1 deletion packages/server/src/loaders/eventEmitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ import { ProjectBillableBillSubscriber } from '@/services/Projects/Projects/Proj
import { SyncActualTimeTaskSubscriber } from '@/services/Projects/Times/SyncActualTimeTaskSubscriber';
import { SaleInvoiceTaxRateValidateSubscriber } from '@/services/TaxRates/subscribers/SaleInvoiceTaxRateValidateSubscriber';
import { WriteInvoiceTaxTransactionsSubscriber } from '@/services/TaxRates/subscribers/WriteInvoiceTaxTransactionsSubscriber';
import { BillTaxRateValidateSubscriber } from '@/services/TaxRates/subscribers/BillTaxRateValidateSubscriber';
import { WriteBillTaxTransactionsSubscriber } from '@/services/TaxRates/subscribers/WriteBillTaxTransactionsSubscriber';

export default () => {
return new EventPublisher();
Expand Down Expand Up @@ -188,8 +190,12 @@ export const susbcribers = () => {
ProjectBillableExpensesSubscriber,
ProjectBillableBillSubscriber,

// Tax Rates
// Tax Rates - Sale Invoice
SaleInvoiceTaxRateValidateSubscriber,
WriteInvoiceTaxTransactionsSubscriber,

// Tax Rates - Bills
BillTaxRateValidateSubscriber,
WriteBillTaxTransactionsSubscriber,
];
};
156 changes: 124 additions & 32 deletions packages/server/src/models/Bill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,109 @@ export default class Bill extends mixin(TenantModel, [
CustomViewBaseModel,
ModelSearchable,
]) {
public amount: number;
public paymentAmount: number;
public landedCostAmount: number;
public allocatedCostAmount: number;
public isInclusiveTax: boolean;
public taxAmountWithheld: number;
public exchangeRate: number;

/**
* Timestamps columns.
*/
get timestamps() {
return ['createdAt', 'updatedAt'];
}

/**
* Virtual attributes.
*/
static get virtualAttributes() {
return [
'balance',
'dueAmount',
'isOpen',
'isPartiallyPaid',
'isFullyPaid',
'isPaid',
'remainingDays',
'overdueDays',
'isOverdue',
'unallocatedCostAmount',
'localAmount',
'localAllocatedCostAmount',
'billableAmount',
'amountLocal',
'subtotal',
'subtotalLocal',
'subtotalExludingTax',
'taxAmountWithheldLocal',
'total',
'totalLocal',
];
}

/**
* Invoice amount in base currency.
* @returns {number}
*/
get amountLocal() {
return this.amount * this.exchangeRate;
}

/**
* Subtotal. (Tax inclusive) if the tax inclusive is enabled.
* @returns {number}
*/
get subtotal() {
return this.amount;
}

/**
* Subtotal in base currency. (Tax inclusive) if the tax inclusive is enabled.
* @returns {number}
*/
get subtotalLocal() {
return this.amountLocal;
}

/**
* Sale invoice amount excluding tax.
* @returns {number}
*/
get subtotalExcludingTax() {
return this.isInclusiveTax
? this.subtotal - this.taxAmountWithheld
: this.subtotal;
}

/**
* Tax amount withheld in base currency.
* @returns {number}
*/
get taxAmountWithheldLocal() {
return this.taxAmountWithheld * this.exchangeRate;
}

/**
* Invoice total. (Tax included)
* @returns {number}
*/
get total() {
return this.isInclusiveTax
? this.subtotal
: this.subtotal + this.taxAmountWithheld;
}

/**
* Invoice total in local currency. (Tax included)
* @returns {number}
*/
get totalLocal() {
return this.total * this.exchangeRate;
}

/**
* Table name
*/
Expand Down Expand Up @@ -158,40 +261,13 @@ export default class Bill extends mixin(TenantModel, [
};
}

/**
* Timestamps columns.
*/
get timestamps() {
return ['createdAt', 'updatedAt'];
}

/**
* Virtual attributes.
*/
static get virtualAttributes() {
return [
'balance',
'dueAmount',
'isOpen',
'isPartiallyPaid',
'isFullyPaid',
'isPaid',
'remainingDays',
'overdueDays',
'isOverdue',
'unallocatedCostAmount',
'localAmount',
'localAllocatedCostAmount',
'billableAmount',
];
}

/**
* Invoice amount in organization base currency.
* @deprecated
* @returns {number}
*/
get localAmount() {
return this.amount * this.exchangeRate;
return this.amountLocal;
}

/**
Expand Down Expand Up @@ -231,7 +307,7 @@ export default class Bill extends mixin(TenantModel, [
* @return {number}
*/
get dueAmount() {
return Math.max(this.amount - this.balance, 0);
return Math.max(this.total - this.balance, 0);
}

/**
Expand All @@ -247,7 +323,7 @@ export default class Bill extends mixin(TenantModel, [
* @return {boolean}
*/
get isPartiallyPaid() {
return this.dueAmount !== this.amount && this.dueAmount > 0;
return this.dueAmount !== this.total && this.dueAmount > 0;
}

/**
Expand Down Expand Up @@ -308,7 +384,7 @@ export default class Bill extends mixin(TenantModel, [
* Retrieves the calculated amount which have not been invoiced.
*/
get billableAmount() {
return Math.max(this.amount - this.invoicedAmount, 0);
return Math.max(this.total - this.invoicedAmount, 0);
}

/**
Expand All @@ -326,6 +402,7 @@ export default class Bill extends mixin(TenantModel, [
const ItemEntry = require('models/ItemEntry');
const BillLandedCost = require('models/BillLandedCost');
const Branch = require('models/Branch');
const TaxRateTransaction = require('models/TaxRateTransaction');

return {
vendor: {
Expand Down Expand Up @@ -373,6 +450,21 @@ export default class Bill extends mixin(TenantModel, [
to: 'branches.id',
},
},

/**
* Bill may has associated tax rate transactions.
*/
taxes: {
relation: Model.HasManyRelation,
modelClass: TaxRateTransaction.default,
join: {
from: 'bills.id',
to: 'tax_rate_transactions.referenceId',
},
filter(builder) {
builder.where('reference_type', 'Bill');
},
},
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
Expand Down
20 changes: 18 additions & 2 deletions packages/server/src/services/Purchases/Bills/BillDTOTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -23,6 +24,9 @@ export class BillDTOTransformer {
@Inject()
private warehouseDTOTransform: WarehouseTransactionDTOTransform;

@Inject()
private taxDTOTransformer: ItemEntriesTaxTransactions;

@Inject()
private tenancy: HasTenancyService;

Expand Down Expand Up @@ -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',
Expand All @@ -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);
Expand Down
Loading