feat: integrate tax rates to bills (#260)

This commit is contained in:
Ahmed Bouhuolia
2023-10-08 16:07:18 +02:00
committed by GitHub
parent ee62e3e1c2
commit d40de4d22b
34 changed files with 894 additions and 282 deletions

View File

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

View File

@@ -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', () => {});
};

View File

@@ -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;
@@ -15,10 +16,10 @@ export interface IBillDTO {
exchangeRate?: number;
open: boolean;
entries: IItemEntryDTO[];
branchId?: number;
warehouseId?: number;
projectId?: number;
isInclusiveTax?: boolean;
}
export interface IBillEditDTO {
@@ -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 {

View File

@@ -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();
@@ -188,8 +190,12 @@ export const susbcribers = () => {
ProjectBillableExpensesSubscriber,
ProjectBillableBillSubscriber,
// Tax Rates
// Tax Rates - Sale Invoice
SaleInvoiceTaxRateValidateSubscriber,
WriteInvoiceTaxTransactionsSubscriber,
// Tax Rates - Bills
BillTaxRateValidateSubscriber,
WriteBillTaxTransactionsSubscriber,
];
};

View File

@@ -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
*/
@@ -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;
}
/**
@@ -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);
}
/**
@@ -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;
}
/**
@@ -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);
}
/**
@@ -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: {
@@ -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');
},
},
};
}

View File

@@ -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 = (

View File

@@ -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 {
@@ -23,6 +24,9 @@ export class BillDTOTransformer {
@Inject()
private warehouseDTOTransform: WarehouseTransactionDTOTransform;
@Inject()
private taxDTOTransformer: ItemEntriesTaxTransactions;
@Inject()
private tenancy: HasTenancyService;
@@ -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',
@@ -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);

View File

@@ -7,6 +7,7 @@ import { AccountNormal, IBill, IItemEntry, ILedgerEntry } from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import Ledger from '@/services/Accounting/Ledger';
import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
@Service()
export class BillGLEntries {
@@ -16,6 +17,9 @@ export class BillGLEntries {
@Inject()
private ledgerStorage: LedgerStorageService;
@Inject()
private itemsEntriesService: ItemsEntriesService;
/**
* Creates bill GL entries.
* @param {number} tenantId -
@@ -43,8 +47,16 @@ export class BillGLEntries {
{},
trx
);
const billLedger = this.getBillLedger(bill, APAccount.id);
// Find or create tax payable account.
const taxPayableAccount = await accountRepository.findOrCreateTaxPayable(
{},
trx
);
const billLedger = this.getBillLedger(
bill,
APAccount.id,
taxPayableAccount.id
);
// Commit the GL enties on the storage.
await this.ledgerStorage.commit(tenantId, billLedger, trx);
};
@@ -83,7 +95,7 @@ export class BillGLEntries {
/**
* Retrieves the bill common entry.
* @param {IBill} bill
* @param {IBill} bill
* @returns {ILedgerEntry}
*/
private getBillCommonEntry = (bill: IBill) => {
@@ -119,7 +131,7 @@ export class BillGLEntries {
(bill: IBill, entry: IItemEntry, index: number): ILedgerEntry => {
const commonJournalMeta = this.getBillCommonEntry(bill);
const localAmount = bill.exchangeRate * entry.amount;
const localAmount = bill.exchangeRate * entry.amountExludingTax;
const landedCostAmount = sumBy(entry.allocatedCostEntries, 'cost');
return {
@@ -173,7 +185,7 @@ export class BillGLEntries {
return {
...commonJournalMeta,
credit: bill.localAmount,
credit: bill.totalLocal,
accountId: payableAccountId,
contactId: bill.vendorId,
accountNormal: AccountNormal.CREDIT,
@@ -182,15 +194,62 @@ export class BillGLEntries {
};
};
/**
* Retrieves the bill tax GL entry.
* @param {IBill} bill -
* @param {number} taxPayableAccountId -
* @param {IItemEntry} entry -
* @param {number} index -
* @returns {ILedgerEntry}
*/
private getBillTaxEntry = R.curry(
(
bill: IBill,
taxPayableAccountId: number,
entry: IItemEntry,
index: number
): ILedgerEntry => {
const commonJournalMeta = this.getBillCommonEntry(bill);
return {
...commonJournalMeta,
debit: entry.taxAmount,
index,
indexGroup: 30,
accountId: taxPayableAccountId,
accountNormal: AccountNormal.CREDIT,
taxRateId: entry.taxRateId,
taxRate: entry.taxRate,
};
}
);
/**
* Retrieves the bill tax GL entries.
* @param {IBill} bill
* @param {number} taxPayableAccountId
* @returns {ILedgerEntry[]}
*/
private getBillTaxEntries = (bill: IBill, taxPayableAccountId: number) => {
// Retrieves the non-zero tax entries.
const nonZeroTaxEntries = this.itemsEntriesService.getNonZeroEntries(
bill.entries
);
const transformTaxEntry = this.getBillTaxEntry(bill, taxPayableAccountId);
return nonZeroTaxEntries.map(transformTaxEntry);
};
/**
* Retrieves the given bill GL entries.
* @param {IBill} bill
* @param {number} payableAccountId
* @param {IBill} bill
* @param {number} payableAccountId
* @returns {ILedgerEntry[]}
*/
private getBillGLEntries = (
bill: IBill,
payableAccountId: number
payableAccountId: number,
taxPayableAccountId: number
): ILedgerEntry[] => {
const payableEntry = this.getBillPayableEntry(payableAccountId, bill);
@@ -201,18 +260,28 @@ export class BillGLEntries {
const landedCostEntries = bill.locatedLandedCosts.map(
landedCostTransformer
);
const taxEntries = this.getBillTaxEntries(bill, taxPayableAccountId);
// Allocate cost entries journal entries.
return [payableEntry, ...itemsEntries, ...landedCostEntries];
return [payableEntry, ...itemsEntries, ...landedCostEntries, ...taxEntries];
};
/**
* Retrieves the given bill ledger.
* @param {IBill} bill
* @param {number} payableAccountId
* @param {IBill} bill
* @param {number} payableAccountId
* @returns {Ledger}
*/
private getBillLedger = (bill: IBill, payableAccountId: number) => {
const entries = this.getBillGLEntries(bill, payableAccountId);
private getBillLedger = (
bill: IBill,
payableAccountId: number,
taxPayableAccountId: number
) => {
const entries = this.getBillGLEntries(
bill,
payableAccountId,
taxPayableAccountId
);
return new Ledger(entries);
};

View File

@@ -28,7 +28,8 @@ export class GetBill {
.findById(billId)
.withGraphFetched('vendor')
.withGraphFetched('entries.item')
.withGraphFetched('branch');
.withGraphFetched('branch')
.withGraphFetched('taxes.taxRate');
// Validates the bill existance.
this.validators.validateBillExistance(bill);

View File

@@ -1,27 +1,42 @@
import { IBill } from '@/interfaces';
import { Transformer } from '@/lib/Transformer/Transformer';
import { SaleInvoiceTaxEntryTransformer } from '@/services/Sales/Invoices/SaleInvoiceTaxEntryTransformer';
import { formatNumber } from 'utils';
export class PurchaseInvoiceTransformer extends Transformer {
/**
* Include these attributes to sale invoice object.
* Include these attributes to sale bill object.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return [
'formattedBillDate',
'formattedDueDate',
'formattedAmount',
'formattedPaymentAmount',
'formattedBalance',
'formattedDueAmount',
'formattedExchangeRate',
'subtotalFormatted',
'subtotalLocalFormatted',
'subtotalExcludingTaxFormatted',
'taxAmountWithheldLocalFormatted',
'totalFormatted',
'totalLocalFormatted',
'taxes',
];
};
/**
* Retrieve formatted invoice date.
* @param {IBill} invoice
* Excluded attributes.
* @returns {string[]}
*/
public excludeAttributes = (): string[] => {
return ['amount', 'amountLocal', 'localAmount'];
};
/**
* Retrieve formatted bill date.
* @param {IBill} bill
* @returns {String}
*/
protected formattedBillDate = (bill: IBill): string => {
@@ -29,8 +44,8 @@ export class PurchaseInvoiceTransformer extends Transformer {
};
/**
* Retrieve formatted invoice date.
* @param {IBill} invoice
* Retrieve formatted bill date.
* @param {IBill} bill
* @returns {String}
*/
protected formattedDueDate = (bill: IBill): string => {
@@ -39,7 +54,7 @@ export class PurchaseInvoiceTransformer extends Transformer {
/**
* Retrieve formatted bill amount.
* @param {IBill} invoice
* @param {IBill} bill
* @returns {string}
*/
protected formattedAmount = (bill): string => {
@@ -48,7 +63,7 @@ export class PurchaseInvoiceTransformer extends Transformer {
/**
* Retrieve formatted bill amount.
* @param {IBill} invoice
* @param {IBill} bill
* @returns {string}
*/
protected formattedPaymentAmount = (bill): string => {
@@ -59,7 +74,7 @@ export class PurchaseInvoiceTransformer extends Transformer {
/**
* Retrieve formatted bill amount.
* @param {IBill} invoice
* @param {IBill} bill
* @returns {string}
*/
protected formattedDueAmount = (bill): string => {
@@ -77,10 +92,90 @@ export class PurchaseInvoiceTransformer extends Transformer {
/**
* Retrieve the formatted exchange rate.
* @param {ISaleInvoice} invoice
* @param {IBill} bill
* @returns {string}
*/
protected formattedExchangeRate = (invoice): string => {
return formatNumber(invoice.exchangeRate, { money: false });
protected formattedExchangeRate = (bill): string => {
return formatNumber(bill.exchangeRate, {
currencyCode: this.context.organization.baseCurrency,
});
};
/**
* Retrieves the formatted subtotal.
* @param {IBill} bill
* @returns {string}
*/
protected subtotalFormatted = (bill): string => {
return formatNumber(bill.subtotal, {
currencyCode: bill.currencyCode,
});
};
/**
* Retrieves the local subtotal formatted.
* @param {IBill} bill
* @returns {string}
*/
protected subtotalLocalFormatted = (bill): string => {
return formatNumber(bill.subtotalLocal, {
currencyCode: this.context.organization.baseCurrency,
});
};
/**
* Retrieves the formatted subtotal tax excluded.
* @param {IBill} bill
* @returns {string}
*/
protected subtotalExcludingTaxFormatted = (bill): string => {
return formatNumber(bill.subtotalExludingTax, {
currencyCode: bill.currencyCode,
});
};
/**
* Retrieves the local formatted tax amount withheld
* @param {IBill} bill
* @returns {string}
*/
protected taxAmountWithheldLocalFormatted = (bill): string => {
return formatNumber(bill.taxAmountWithheldLocal, {
currencyCode: this.context.organization.baseCurrency,
});
};
/**
* Retrieves the total formatted.
* @param {IBill} bill
* @returns {string}
*/
protected totalFormatted = (bill): string => {
return formatNumber(bill.total, {
currencyCode: bill.currencyCode,
});
};
/**
* Retrieves the local total formatted.
* @param {IBill} bill
* @returns {string}
*/
protected totalLocalFormatted = (bill): string => {
return formatNumber(bill.totalLocal, {
currencyCode: this.context.organization.baseCurrency,
});
};
/**
* Retrieve the taxes lines of bill.
* @param {Bill} bill
*/
protected taxes = (bill) => {
return this.item(bill.taxes, new SaleInvoiceTaxEntryTransformer(), {
subtotal: bill.subtotal,
isInclusiveTax: bill.isInclusiveTax,
currencyCode: bill.currencyCode,
});
};
}

View File

@@ -218,7 +218,8 @@ export class SaleInvoiceGLEntries {
...commonEntry,
credit: entry.taxAmount,
accountId: taxPayableAccountId,
index: index + 3,
index: index + 1,
indexGroup: 30,
accountNormal: AccountNormal.CREDIT,
taxRateId: entry.taxRateId,
taxRate: entry.taxRate,

View File

@@ -62,8 +62,8 @@ export class SaleInvoiceTaxEntryTransformer extends Transformer {
const taxRate = this.taxRate(taxEntry);
return this.options.isInclusiveTax
? getInclusiveTaxAmount(this.options.amount, taxRate)
: getExlusiveTaxAmount(this.options.amount, taxRate);
? getInclusiveTaxAmount(this.options.subtotal, taxRate)
: getExlusiveTaxAmount(this.options.subtotal, taxRate);
};
/**

View File

@@ -171,7 +171,7 @@ export class SaleInvoiceTransformer extends Transformer {
*/
protected taxes = (invoice) => {
return this.item(invoice.taxes, new SaleInvoiceTaxEntryTransformer(), {
amount: invoice.amount,
subtotal: invoice.subtotal,
isInclusiveTax: invoice.isInclusiveTax,
currencyCode: invoice.currencyCode,
});

View File

@@ -0,0 +1,89 @@
import { Inject, Service } from 'typedi';
import { IBillCreatingPayload, IBillEditingPayload } from '@/interfaces';
import events from '@/subscribers/events';
import { CommandTaxRatesValidators } from '../CommandTaxRatesValidators';
@Service()
export class BillTaxRateValidateSubscriber {
@Inject()
private taxRateDTOValidator: CommandTaxRatesValidators;
/**
* Attaches events with handlers.
*/
public attach(bus) {
bus.subscribe(
events.bill.onCreating,
this.validateBillEntriesTaxCodeExistanceOnCreating
);
bus.subscribe(
events.bill.onCreating,
this.validateBillEntriesTaxIdExistanceOnCreating
);
bus.subscribe(
events.bill.onEditing,
this.validateBillEntriesTaxCodeExistanceOnEditing
);
bus.subscribe(
events.bill.onEditing,
this.validateBillEntriesTaxIdExistanceOnEditing
);
return bus;
}
/**
* Validate bill entries tax rate code existance when creating.
* @param {IBillCreatingPayload}
*/
private validateBillEntriesTaxCodeExistanceOnCreating = async ({
billDTO,
tenantId,
}: IBillCreatingPayload) => {
await this.taxRateDTOValidator.validateItemEntriesTaxCode(
tenantId,
billDTO.entries
);
};
/**
* Validate the tax rate id existance when creating.
* @param {IBillCreatingPayload}
*/
private validateBillEntriesTaxIdExistanceOnCreating = async ({
billDTO,
tenantId,
}: IBillCreatingPayload) => {
await this.taxRateDTOValidator.validateItemEntriesTaxCodeId(
tenantId,
billDTO.entries
);
};
/**
* Validate bill entries tax rate code existance when editing.
* @param {IBillEditingPayload}
*/
private validateBillEntriesTaxCodeExistanceOnEditing = async ({
tenantId,
billDTO,
}: IBillEditingPayload) => {
await this.taxRateDTOValidator.validateItemEntriesTaxCode(
tenantId,
billDTO.entries
);
};
/**
* Validates the bill entries tax rate id existance when editing.
* @param {ISaleInvoiceEditingPayload} payload -
*/
private validateBillEntriesTaxIdExistanceOnEditing = async ({
tenantId,
billDTO,
}: IBillEditingPayload) => {
await this.taxRateDTOValidator.validateItemEntriesTaxCodeId(
tenantId,
billDTO.entries
);
};
}

View File

@@ -0,0 +1,87 @@
import { Inject, Service } from 'typedi';
import {
IBIllEventDeletedPayload,
IBillCreatedPayload,
IBillEditedPayload,
ISaleInvoiceCreatedPayload,
ISaleInvoiceDeletedPayload,
ISaleInvoiceEditedPayload,
} from '@/interfaces';
import events from '@/subscribers/events';
import { WriteTaxTransactionsItemEntries } from '../WriteTaxTransactionsItemEntries';
@Service()
export class WriteBillTaxTransactionsSubscriber {
@Inject()
private writeTaxTransactions: WriteTaxTransactionsItemEntries;
/**
* Attaches events with handlers.
*/
public attach(bus) {
bus.subscribe(
events.bill.onCreated,
this.writeInvoiceTaxTransactionsOnCreated
);
bus.subscribe(
events.bill.onEdited,
this.rewriteInvoiceTaxTransactionsOnEdited
);
bus.subscribe(
events.bill.onDeleted,
this.removeInvoiceTaxTransactionsOnDeleted
);
return bus;
}
/**
* Writes the bill tax transactions on invoice created.
* @param {ISaleInvoiceCreatingPaylaod}
*/
private writeInvoiceTaxTransactionsOnCreated = async ({
tenantId,
bill,
trx,
}: IBillCreatedPayload) => {
await this.writeTaxTransactions.writeTaxTransactionsFromItemEntries(
tenantId,
bill.entries,
trx
);
};
/**
* Rewrites the bill tax transactions on invoice edited.
* @param {IBillEditedPayload} payload -
*/
private rewriteInvoiceTaxTransactionsOnEdited = async ({
tenantId,
bill,
trx,
}: IBillEditedPayload) => {
await this.writeTaxTransactions.rewriteTaxRateTransactionsFromItemEntries(
tenantId,
bill.entries,
'Bill',
bill.id,
trx
);
};
/**
* Removes the invoice tax transactions on invoice deleted.
* @param {IBIllEventDeletedPayload}
*/
private removeInvoiceTaxTransactionsOnDeleted = async ({
tenantId,
oldBill,
trx,
}: IBIllEventDeletedPayload) => {
await this.writeTaxTransactions.removeTaxTransactionsFromItemEntries(
tenantId,
oldBill.id,
'Bill',
trx
);
};
}

View File

@@ -1,87 +0,0 @@
import { Inject, Service } from 'typedi';
import events from '@/subscribers/events';
import TenancyService from '@/services/Tenancy/TenancyService';
import BillsService from '@/services/Purchases/Bills';
import {
IBillCreatedPayload,
IBillEditedPayload,
IBIllEventDeletedPayload,
} from '@/interfaces';
@Service()
export default class BillWriteGLEntriesSubscriber {
@Inject()
tenancy: TenancyService;
@Inject()
billsService: BillsService;
/**
* Attaches events with handles.
*/
public attach(bus) {
bus.subscribe(
events.bill.onCreated,
this.handlerWriteJournalEntriesOnCreate
);
bus.subscribe(
events.bill.onEdited,
this.handleOverwriteJournalEntriesOnEdit
);
bus.subscribe(events.bill.onDeleted, this.handlerDeleteJournalEntries);
}
/**
* Handles writing journal entries once bill created.
* @param {IBillCreatedPayload} payload -
*/
private handlerWriteJournalEntriesOnCreate = async ({
tenantId,
billId,
bill,
trx,
}: IBillCreatedPayload) => {
// Can't continue if the bill is not opened yet.
if (!bill.openedAt) return null;
await this.billsService.recordJournalTransactions(
tenantId,
billId,
false,
trx
);
};
/**
* Handles the overwriting journal entries once bill edited.
* @param {IBillEditedPayload} payload -
*/
private handleOverwriteJournalEntriesOnEdit = async ({
tenantId,
billId,
bill,
trx,
}: IBillEditedPayload) => {
// Can't continue if the bill is not opened yet.
if (!bill.openedAt) return null;
await this.billsService.recordJournalTransactions(
tenantId,
billId,
true,
trx
);
};
/**
* Handles revert journal entries on bill deleted.
* @param {IBIllEventDeletedPayload} payload -
*/
private handlerDeleteJournalEntries = async ({
tenantId,
billId,
trx,
}: IBIllEventDeletedPayload) => {
await this.billsService.revertJournalEntries(tenantId, billId, trx);
};
}

View File

@@ -1,5 +1,3 @@
import React from 'react';
import AccountDrawer from '@/containers/Drawers/AccountDrawer';
import ManualJournalDrawer from '@/containers/Drawers/ManualJournalDrawer';
import ExpenseDrawer from '@/containers/Drawers/ExpenseDrawer';

View File

@@ -26,6 +26,10 @@ const SelectButton = styled(Button)`
position: relative;
padding-right: 30px;
&.bp4-small{
padding-right: 24px;
}
&:not(.is-selected):not([class*='bp4-intent-']):not(.bp4-minimal) {
color: #5c7080;
}

View File

@@ -0,0 +1,7 @@
import { TaxType } from '@/interfaces/TaxRates';
export const InclusiveTaxOptions = [
{ key: TaxType.Inclusive, label: 'Inclusive of Tax' },
{ key: TaxType.Exclusive, label: 'Exclusive of Tax' },
];

View File

@@ -30,7 +30,7 @@ export default function BillDetailHeader() {
<CommercialDocTopHeader>
<DetailsMenu>
<AmountDetailItem label={intl.get('amount')}>
<h3 class="big-number">{bill.formatted_amount}</h3>
<h3 class="big-number">{bill.total_formatted}</h3>
</AmountDetailItem>
<StatusDetailItem>
<BillDetailsStatus bill={bill} />

View File

@@ -1,11 +1,8 @@
// @ts-nocheck
import React from 'react';
import styled from 'styled-components';
import {
TotalLineBorderStyle,
TotalLineTextStyle,
FormatNumber,
T,
TotalLines,
TotalLine,
@@ -23,12 +20,20 @@ export function BillDetailTableFooter() {
<BillTotalLines labelColWidth={'180px'} amountColWidth={'180px'}>
<TotalLine
title={<T id={'bill.details.subtotal'} />}
value={<FormatNumber value={bill.amount} />}
value={bill.subtotal_formatted}
borderStyle={TotalLineBorderStyle.SingleDark}
/>
{bill.taxes.map((taxRate) => (
<TotalLine
key={taxRate.id}
title={`${taxRate.name} [${taxRate.tax_rate}%]`}
value={taxRate.tax_rate_amount_formatted}
textStyle={TotalLineTextStyle.Regular}
/>
))}
<TotalLine
title={<T id={'bill.details.total'} />}
value={bill.formatted_amount}
value={bill.total_formatted}
borderStyle={TotalLineBorderStyle.DoubleDark}
textStyle={TotalLineTextStyle.Bold}
/>
@@ -39,6 +44,7 @@ export function BillDetailTableFooter() {
<TotalLine
title={<T id={'bill.details.due_amount'} />}
value={bill.formatted_due_amount}
textStyle={TotalLineTextStyle.Bold}
/>
</BillTotalLines>
</BillDetailsFooterRoot>

View File

@@ -0,0 +1,16 @@
import { Box } from '@/components';
import styled from 'styled-components';
export const EntriesActionsBar = styled(Box)`
padding-bottom: 12px;
display: flex;
.bp4-form-group {
margin-bottom: 0;
label.bp4-label {
opacity: 0.6;
margin-right: 8px;
}
}
`;

View File

@@ -1,8 +1,7 @@
// @ts-nocheck
import React, { useCallback } from 'react';
import * as R from 'ramda';
import { sumBy, isEmpty, last, keyBy } from 'lodash';
import { sumBy, isEmpty, last, keyBy, groupBy } from 'lodash';
import { useItem } from '@/hooks/query';
import {
toSafeNumber,
@@ -12,6 +11,7 @@ import {
updateAutoAddNewLine,
orderingLinesIndexes,
updateTableRow,
formattedAmount,
} from '@/utils';
import { useItemEntriesTableContext } from './ItemEntriesTableProvider';
@@ -116,6 +116,11 @@ export function useFetchItemRow({ landedCost, itemType, notifyNewRow }) {
? item.purchase_description
: item.sell_description;
const taxRateId =
itemType === ITEM_TYPE.PURCHASABLE
? item.purchase_tax_rate_id
: item.sell_tax_rate_id;
// Detarmines whether the landed cost checkbox should be disabled.
const landedCostDisabled = isLandedCostDisabled(item);
@@ -130,6 +135,7 @@ export function useFetchItemRow({ landedCost, itemType, notifyNewRow }) {
landed_cost_disabled: landedCostDisabled,
}
: {}),
taxRateId,
};
setItemRow(null);
saveInvoke(notifyNewRow, newRow, rowIndex);
@@ -266,3 +272,29 @@ export const useComposeRowsOnRemoveTableRow = () => {
[minLinesNumber, defaultEntry, localValue],
);
};
/**
* Retrieves the aggregate tax rates from the given item entries.
*/
export const aggregateItemEntriesTaxRates = R.curry((taxRates, entries) => {
const taxRatesById = keyBy(taxRates, 'id');
// Calculate the total tax amount of invoice entries.
const filteredEntries = entries.filter((e) => e.tax_rate_id);
const groupedTaxRates = groupBy(filteredEntries, 'tax_rate_id');
return Object.keys(groupedTaxRates).map((taxRateId) => {
const taxRate = taxRatesById[taxRateId];
const taxRates = groupedTaxRates[taxRateId];
const totalTaxAmount = sumBy(taxRates, 'tax_amount');
const taxAmountFormatted = formattedAmount(totalTaxAmount, 'USD');
return {
taxRateId,
taxRate: taxRate.rate,
label: `${taxRate.name} [${taxRate.rate}%]`,
taxAmount: totalTaxAmount,
taxAmountFormatted,
};
});
});

View File

@@ -26,6 +26,7 @@ import {
handleErrors,
} from './utils';
import withCurrentOrganization from '@/containers/Organization/withCurrentOrganization';
import { BillFormEntriesActions } from './BillFormEntriesActions';
/**
* Bill form.
@@ -126,7 +127,10 @@ function BillForm({
<Form>
<BillFormTopBar />
<BillFormHeader />
<BillItemsEntriesEditor />
<div className={classNames(CLASSES.PAGE_FORM_BODY)}>
<BillFormEntriesActions />
<BillItemsEntriesEditor />
</div>
<BillFormFooter />
<BillFloatingActions />
</Form>

View File

@@ -0,0 +1,58 @@
// @ts-nocheck
import styled from 'styled-components';
import { useFormikContext } from 'formik';
import { FFormGroup, FSelect } from '@/components';
import { InclusiveTaxOptions } from '@/constants/InclusiveTaxOptions';
import { composeEntriesOnEditInclusiveTax } from './utils';
import { EntriesActionsBar } from '@/containers/Entries/EntriesActionBar';
export function BillFormEntriesActions() {
return (
<EntriesActionsBar>
<BillExclusiveInclusiveSelect />
</EntriesActionsBar>
);
}
/**
* Bill exclusive/inclusive select.
* @returns {React.ReactNode}
*/
export function BillExclusiveInclusiveSelect(props) {
const { values, setFieldValue } = useFormikContext();
const handleItemSelect = (item) => {
const newEntries = composeEntriesOnEditInclusiveTax(
item.key,
values.entries,
);
setFieldValue('inclusive_exclusive_tax', item.key);
setFieldValue('entries', newEntries);
};
return (
<InclusiveFormGroup
name={'inclusive_exclusive_tax'}
label={'Amounts are'}
inline={true}
>
<FSelect
name={'inclusive_exclusive_tax'}
items={InclusiveTaxOptions}
textAccessor={'label'}
labelAccessor={() => ''}
valueAccessor={'key'}
popoverProps={{ minimal: true, usePortal: true, inline: false }}
buttonProps={{ small: true }}
onItemSelect={handleItemSelect}
filterable={false}
{...props}
/>
</InclusiveFormGroup>
);
}
const InclusiveFormGroup = styled(FFormGroup)`
margin-left: auto;
`;

View File

@@ -1,15 +1,14 @@
// @ts-nocheck
import React from 'react';
import styled from 'styled-components';
import {
T,
TotalLines,
TotalLine,
TotalLineBorderStyle,
TotalLineTextStyle,
} from '@/components';
import { useBillTotals } from './utils';
import { useBillAggregatedTaxRates, useBillTotals } from './utils';
import { useFormikContext } from 'formik';
import { TaxType } from '@/interfaces/TaxRates';
export function BillFormFooterRight() {
const {
@@ -19,26 +18,46 @@ export function BillFormFooterRight() {
formattedPaymentTotal,
} = useBillTotals();
const {
values: { inclusive_exclusive_tax, currency_code },
} = useFormikContext();
const taxEntries = useBillAggregatedTaxRates();
return (
<BillTotalLines labelColWidth={'180px'} amountColWidth={'180px'}>
<TotalLine
title={<T id={'bill_form.label.subtotal'} />}
title={
<>
{inclusive_exclusive_tax === TaxType.Inclusive
? 'Subtotal (Tax Inclusive)'
: 'Subtotal'}
</>
}
value={formattedSubtotal}
borderStyle={TotalLineBorderStyle.None}
/>
{taxEntries.map((tax, index) => (
<TotalLine
key={index}
title={tax.label}
value={tax.taxAmountFormatted}
borderStyle={TotalLineBorderStyle.None}
/>
))}
<TotalLine
title={<T id={'bill_form.label.total'} />}
title={`Total (${currency_code})`}
value={formattedTotal}
borderStyle={TotalLineBorderStyle.SingleDark}
textStyle={TotalLineTextStyle.Bold}
/>
<TotalLine
title={<T id={'bill_form.label.total'} />}
title={'Paid Amount'}
value={formattedPaymentTotal}
borderStyle={TotalLineBorderStyle.None}
/>
<TotalLine
title={<T id={'bill_form.label.total'} />}
title={'Due Amount'}
value={formattedDueTotal}
textStyle={TotalLineTextStyle.Bold}
/>

View File

@@ -15,6 +15,7 @@ import {
useCreateBill,
useEditBill,
} from '@/hooks/query';
import { useTaxRates } from '@/hooks/query/taxRates';
const BillFormContext = createContext();
@@ -83,6 +84,9 @@ function BillFormProvider({ billId, ...props }) {
isSuccess: isBranchesSuccess,
} = useBranches({}, { enabled: isBranchFeatureCan });
// Fetch tax rates.
const { data: taxRates, isLoading: isTaxRatesLoading } = useTaxRates();
// Fetches the projects list.
const {
data: { projects },
@@ -103,7 +107,10 @@ function BillFormProvider({ billId, ...props }) {
// Determines whether the warehouse and branches are loading.
const isFeatureLoading =
isWarehouesLoading || isBranchesLoading || isProjectsLoading;
isWarehouesLoading ||
isBranchesLoading ||
isProjectsLoading ||
isTaxRatesLoading;
const provider = {
accounts,
@@ -113,6 +120,7 @@ function BillFormProvider({ billId, ...props }) {
warehouses,
branches,
projects,
taxRates,
submitPayload,
isNewMode,
@@ -124,6 +132,7 @@ function BillFormProvider({ billId, ...props }) {
isFeatureLoading,
isBranchesSuccess,
isWarehousesSuccess,
isTaxRatesLoading,
createBillMutate,
editBillMutate,

View File

@@ -1,46 +1,41 @@
// @ts-nocheck
import React from 'react';
import classNames from 'classnames';
import ItemsEntriesTable from '@/containers/Entries/ItemsEntriesTable';
import { FastField } from 'formik';
import { CLASSES } from '@/constants/classes';
import { useBillFormContext } from './BillFormProvider';
import { entriesFieldShouldUpdate } from './utils';
import { ITEM_TYPE } from '@/containers/Entries/utils';
/**
* Bill form body.
* Bill form body.
*/
export default function BillFormBody({ defaultBill }) {
const { items } = useBillFormContext();
const { items, taxRates } = useBillFormContext();
return (
<div className={classNames(CLASSES.PAGE_FORM_BODY)}>
<FastField
name={'entries'}
items={items}
shouldUpdate={entriesFieldShouldUpdate}
>
{({
form: { values, setFieldValue },
field: { value },
meta: { error, touched },
}) => (
<ItemsEntriesTable
value={value}
onChange={(entries) => {
setFieldValue('entries', entries);
}}
items={items}
errors={error}
linesNumber={4}
currencyCode={values.currency_code}
itemType={ITEM_TYPE.PURCHASABLE}
landedCost={true}
enableTaxRates={false}
/>
)}
</FastField>
</div>
<FastField
name={'entries'}
items={items}
shouldUpdate={entriesFieldShouldUpdate}
>
{({
form: { values, setFieldValue },
field: { value },
meta: { error, touched },
}) => (
<ItemsEntriesTable
value={value}
onChange={(entries) => {
setFieldValue('entries', entries);
}}
items={items}
errors={error}
linesNumber={4}
currencyCode={values.currency_code}
itemType={ITEM_TYPE.PURCHASABLE}
taxRates={taxRates}
landedCost={true}
/>
)}
</FastField>
);
}

View File

@@ -3,7 +3,7 @@ import React from 'react';
import moment from 'moment';
import intl from 'react-intl-universal';
import * as R from 'ramda';
import { first } from 'lodash';
import { first, chain } from 'lodash';
import { Intent } from '@blueprintjs/core';
import { useFormikContext } from 'formik';
import { AppToaster } from '@/components';
@@ -17,6 +17,8 @@ import {
import {
updateItemsEntriesTotal,
ensureEntriesHaveEmptyLine,
assignEntriesTaxAmount,
aggregateItemEntriesTaxRates,
} from '@/containers/Entries/utils';
import { useCurrentOrganization } from '@/hooks/state';
import {
@@ -24,6 +26,7 @@ import {
getEntriesTotal,
} from '@/containers/Entries/utils';
import { useBillFormContext } from './BillFormProvider';
import { TaxType } from '@/interfaces/TaxRates';
export const MIN_LINES_NUMBER = 1;
@@ -37,6 +40,9 @@ export const defaultBillEntry = {
description: '',
amount: '',
landed_cost: false,
tax_rate_id: '',
tax_rate: '',
tax_amount: '',
};
// Default bill.
@@ -46,6 +52,7 @@ export const defaultBill = {
bill_date: moment(new Date()).format('YYYY-MM-DD'),
due_date: moment(new Date()).format('YYYY-MM-DD'),
reference_no: '',
inclusive_exclusive_tax: TaxType.Inclusive,
note: '',
open: '',
branch_id: '',
@@ -82,6 +89,9 @@ export const transformToEditForm = (bill) => {
return {
...transformToForm(bill, defaultBill),
inclusive_exclusive_tax: bill.is_inclusive_tax
? TaxType.Inclusive
: TaxType.Exclusive,
entries,
};
};
@@ -228,11 +238,12 @@ export const useSetPrimaryWarehouseToForm = () => {
*/
export const useBillTotals = () => {
const {
values: { entries, currency_code: currencyCode },
values: { currency_code: currencyCode },
} = useFormikContext();
// Retrieves the bili entries total.
const total = React.useMemo(() => getEntriesTotal(entries), [entries]);
// Retrieves the bill subtotal.
const subtotal = useBillSubtotal();
const total = useBillTotal();
// Retrieves the formatted total money.
const formattedTotal = React.useMemo(
@@ -241,8 +252,8 @@ export const useBillTotals = () => {
);
// Retrieves the formatted subtotal.
const formattedSubtotal = React.useMemo(
() => formattedAmount(total, currencyCode, { money: false }),
[total, currencyCode],
() => formattedAmount(subtotal, currencyCode, { money: false }),
[subtotal, currencyCode],
);
// Retrieves the payment total.
const paymentTotal = React.useMemo(() => 0, []);
@@ -288,3 +299,86 @@ export const useBillIsForeignCustomer = () => {
);
return isForeignCustomer;
};
/**
* Re-calculates the entries tax amount when editing.
* @returns {string}
*/
export const composeEntriesOnEditInclusiveTax = (
inclusiveExclusiveTax: string,
entries,
) => {
return R.compose(
assignEntriesTaxAmount(inclusiveExclusiveTax === 'inclusive'),
)(entries);
};
/**
* Retreives the bill aggregated tax rates.
* @returns {Array}
*/
export const useBillAggregatedTaxRates = () => {
const { values } = useFormikContext();
const { taxRates } = useBillFormContext();
const aggregateTaxRates = React.useMemo(
() => aggregateItemEntriesTaxRates(taxRates),
[taxRates],
);
// Calculate the total tax amount of bill entries.
return React.useMemo(() => {
return aggregateTaxRates(values.entries);
}, [aggregateTaxRates, values.entries]);
};
/**
* Retrieves the bill subtotal.
* @returns {number}
*/
export const useBillSubtotal = () => {
const {
values: { entries },
} = useFormikContext();
// Calculate the total due amount of bill entries.
return React.useMemo(() => getEntriesTotal(entries), [entries]);
};
/**
* Retreives the bill total tax amount.
* @returns {number}
*/
export const useBillTotalTaxAmount = () => {
const { values } = useFormikContext();
return React.useMemo(() => {
return chain(values.entries)
.filter((entry) => entry.tax_amount)
.sumBy('tax_amount')
.value();
}, [values.entries]);
};
/**
* Detarmines whether the tax is exclusive.
* @returns {boolean}
*/
export const useIsBillTaxExclusive = () => {
const { values } = useFormikContext();
return values.inclusive_exclusive_tax === TaxType.Exclusive;
};
/**
* Retreives the bill total.
* @returns {number}
*/
export const useBillTotal = () => {
const subtotal = useBillSubtotal();
const totalTaxAmount = useBillTotalTaxAmount();
const isExclusiveTax = useIsBillTaxExclusive();
return R.compose(R.when(R.always(isExclusiveTax), R.add(totalTaxAmount)))(
subtotal,
);
};

View File

@@ -9,20 +9,18 @@ import {
Tag,
ProgressBar,
} from '@blueprintjs/core';
import clsx from 'classnames';
import {
FormatDateCell,
FormattedMessage as T,
Icon,
If,
Choose,
Money,
Can,
} from '@/components';
import {
formattedAmount,
safeCallback,
isBlank,
calculateStatus,
} from '@/utils';
import {
@@ -30,6 +28,7 @@ import {
PaymentMadeAction,
AbilitySubject,
} from '@/constants/abilityOption';
import { CLASSES } from '@/constants';
/**
* Actions menu.
@@ -101,17 +100,6 @@ export function ActionsMenu({
);
}
/**
* Amount accessor.
*/
export function AmountAccessor(bill) {
return !isBlank(bill.amount) ? (
<Money amount={bill.amount} currency={bill.currency_code} />
) : (
''
);
}
/**
* Status accessor.
*/
@@ -198,11 +186,11 @@ export function useBillsTableColumns() {
{
id: 'amount',
Header: intl.get('amount'),
accessor: AmountAccessor,
accessor: 'total_formatted',
width: 120,
className: 'amount',
align: 'right',
clickable: true,
className: clsx(CLASSES.FONT_BOLD),
},
{
id: 'status',

View File

@@ -3,7 +3,8 @@ import React from 'react';
import styled from 'styled-components';
import { useFormikContext } from 'formik';
import { InclusiveButtonOptions } from './constants';
import { Box, FFormGroup, FSelect } from '@/components';
import { FFormGroup, FSelect } from '@/components';
import { EntriesActionsBar } from '@/containers/Entries/EntriesActionBar';
import { composeEntriesOnEditInclusiveTax } from './utils';
/**
@@ -12,9 +13,9 @@ import { composeEntriesOnEditInclusiveTax } from './utils';
*/
export function InvoiceFormActions() {
return (
<InvoiceFormActionsRoot>
<EntriesActionsBar>
<InvoiceExclusiveInclusiveSelect />
</InvoiceFormActionsRoot>
</EntriesActionsBar>
);
}
@@ -40,7 +41,7 @@ export function InvoiceExclusiveInclusiveSelect(props) {
label={'Amounts are'}
inline={true}
>
<InclusiveSelect
<FSelect
name={'inclusive_exclusive_tax'}
items={InclusiveButtonOptions}
textAccessor={'label'}
@@ -57,23 +58,5 @@ export function InvoiceExclusiveInclusiveSelect(props) {
}
const InclusiveFormGroup = styled(FFormGroup)`
margin-bottom: 0;
margin-left: auto;
&.bp4-form-group.bp4-inline label.bp4-label {
line-height: 1.25;
opacity: 0.6;
margin-right: 8px;
}
`;
const InclusiveSelect = styled(FSelect)`
.bp4-button {
padding-right: 24px;
}
`;
const InvoiceFormActionsRoot = styled(Box)`
padding-bottom: 12px;
display: flex;
`;

View File

@@ -11,6 +11,7 @@ import {
TotalLineTextStyle,
} from '@/components';
import { useInvoiceAggregatedTaxRates, useInvoiceTotals } from './utils';
import { TaxType } from '@/interfaces/TaxRates';
export function InvoiceFormFooterRight() {
// Calculate the total due amount of invoice entries.
@@ -32,7 +33,7 @@ export function InvoiceFormFooterRight() {
<TotalLine
title={
<>
{inclusive_exclusive_tax === 'inclusive'
{inclusive_exclusive_tax === TaxType.Inclusive
? 'Subtotal (Tax Inclusive)'
: 'Subtotal'}
</>

View File

@@ -1,18 +1,23 @@
// @ts-nocheck
import React, { useCallback, useMemo } from 'react';
import React from 'react';
import { useFormikContext } from 'formik';
import intl from 'react-intl-universal';
import moment from 'moment';
import * as R from 'ramda';
import { Intent } from '@blueprintjs/core';
import { omit, first, keyBy, sumBy, groupBy } from 'lodash';
import { compose, transformToForm, repeatValue } from '@/utils';
import { useFormikContext } from 'formik';
import { formattedAmount, defaultFastFieldShouldUpdate } from '@/utils';
import { omit, first, sumBy } from 'lodash';
import {
compose,
transformToForm,
repeatValue,
formattedAmount,
defaultFastFieldShouldUpdate,
} from '@/utils';
import { ERROR } from '@/constants/errors';
import { AppToaster } from '@/components';
import { useCurrentOrganization } from '@/hooks/state';
import {
aggregateItemEntriesTaxRates,
assignEntriesTaxAmount,
getEntriesTotal,
} from '@/containers/Entries/utils';
@@ -327,28 +332,14 @@ export const useInvoiceAggregatedTaxRates = () => {
const { values } = useFormikContext();
const { taxRates } = useInvoiceFormContext();
const taxRatesById = useMemo(() => keyBy(taxRates, 'id'), [taxRates]);
const aggregateTaxRates = React.useMemo(
() => aggregateItemEntriesTaxRates(taxRates),
[taxRates],
);
// Calculate the total tax amount of invoice entries.
return React.useMemo(() => {
const filteredEntries = values.entries.filter((e) => e.tax_rate_id);
const groupedTaxRates = groupBy(filteredEntries, 'tax_rate_id');
return Object.keys(groupedTaxRates).map((taxRateId) => {
const taxRate = taxRatesById[taxRateId];
const taxRates = groupedTaxRates[taxRateId];
const totalTaxAmount = sumBy(taxRates, 'tax_amount');
const taxAmountFormatted = formattedAmount(totalTaxAmount, 'USD');
return {
taxRateId,
taxRate: taxRate.rate,
label: `${taxRate.name} [${taxRate.rate}%]`,
taxAmount: totalTaxAmount,
taxAmountFormatted,
};
});
}, [values.entries]);
return aggregateTaxRates(values.entries);
}, [aggregateTaxRates, values.entries]);
};
/**

View File

@@ -21,12 +21,9 @@ label.bp4-label {
.required {
color: red;
}
.bp4-form-group.bp4-inline & {
margin: 0 10px 0 0;
line-height: 1.6;
padding-top: calc(0.3rem + 1px);
padding-bottom: calc(0.3rem + 1px);
line-height: 30px;
}
}