mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-19 22:30:31 +00:00
feat: integrate tax rates to bills (#260)
This commit is contained in:
@@ -115,6 +115,8 @@ export default class BillsController extends BaseController {
|
|||||||
check('note').optional().trim().escape(),
|
check('note').optional().trim().escape(),
|
||||||
check('open').default(false).isBoolean().toBoolean(),
|
check('open').default(false).isBoolean().toBoolean(),
|
||||||
|
|
||||||
|
check('is_inclusive_tax').default(false).isBoolean().toBoolean(),
|
||||||
|
|
||||||
check('entries').isArray({ min: 1 }),
|
check('entries').isArray({ min: 1 }),
|
||||||
|
|
||||||
check('entries.*.index').exists().isNumeric().toInt(),
|
check('entries.*.index').exists().isNumeric().toInt(),
|
||||||
@@ -137,6 +139,15 @@ export default class BillsController extends BaseController {
|
|||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isNumeric()
|
.isNumeric()
|
||||||
.toInt(),
|
.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);
|
next(error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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', () => {});
|
||||||
|
};
|
||||||
@@ -2,6 +2,7 @@ import { Knex } from 'knex';
|
|||||||
import { IDynamicListFilterDTO } from './DynamicFilter';
|
import { IDynamicListFilterDTO } from './DynamicFilter';
|
||||||
import { IItemEntry, IItemEntryDTO } from './ItemEntry';
|
import { IItemEntry, IItemEntryDTO } from './ItemEntry';
|
||||||
import { IBillLandedCost } from './LandedCost';
|
import { IBillLandedCost } from './LandedCost';
|
||||||
|
|
||||||
export interface IBillDTO {
|
export interface IBillDTO {
|
||||||
vendorId: number;
|
vendorId: number;
|
||||||
billNumber: string;
|
billNumber: string;
|
||||||
@@ -15,10 +16,10 @@ export interface IBillDTO {
|
|||||||
exchangeRate?: number;
|
exchangeRate?: number;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
entries: IItemEntryDTO[];
|
entries: IItemEntryDTO[];
|
||||||
|
|
||||||
branchId?: number;
|
branchId?: number;
|
||||||
warehouseId?: number;
|
warehouseId?: number;
|
||||||
projectId?: number;
|
projectId?: number;
|
||||||
|
isInclusiveTax?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IBillEditDTO {
|
export interface IBillEditDTO {
|
||||||
@@ -80,6 +81,15 @@ export interface IBill {
|
|||||||
|
|
||||||
localAmount?: number;
|
localAmount?: number;
|
||||||
locatedLandedCosts?: IBillLandedCost[];
|
locatedLandedCosts?: IBillLandedCost[];
|
||||||
|
|
||||||
|
amountLocal: number;
|
||||||
|
subtotal: number;
|
||||||
|
subtotalLocal: number;
|
||||||
|
subtotalExcludingTax: number;
|
||||||
|
taxAmountWithheld: number;
|
||||||
|
taxAmountWithheldLocal: number;
|
||||||
|
total: number;
|
||||||
|
totalLocal: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IBillsFilter extends IDynamicListFilterDTO {
|
export interface IBillsFilter extends IDynamicListFilterDTO {
|
||||||
|
|||||||
@@ -81,6 +81,8 @@ import { ProjectBillableBillSubscriber } from '@/services/Projects/Projects/Proj
|
|||||||
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 { WriteInvoiceTaxTransactionsSubscriber } from '@/services/TaxRates/subscribers/WriteInvoiceTaxTransactionsSubscriber';
|
import { WriteInvoiceTaxTransactionsSubscriber } from '@/services/TaxRates/subscribers/WriteInvoiceTaxTransactionsSubscriber';
|
||||||
|
import { BillTaxRateValidateSubscriber } from '@/services/TaxRates/subscribers/BillTaxRateValidateSubscriber';
|
||||||
|
import { WriteBillTaxTransactionsSubscriber } from '@/services/TaxRates/subscribers/WriteBillTaxTransactionsSubscriber';
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
return new EventPublisher();
|
return new EventPublisher();
|
||||||
@@ -188,8 +190,12 @@ export const susbcribers = () => {
|
|||||||
ProjectBillableExpensesSubscriber,
|
ProjectBillableExpensesSubscriber,
|
||||||
ProjectBillableBillSubscriber,
|
ProjectBillableBillSubscriber,
|
||||||
|
|
||||||
// Tax Rates
|
// Tax Rates - Sale Invoice
|
||||||
SaleInvoiceTaxRateValidateSubscriber,
|
SaleInvoiceTaxRateValidateSubscriber,
|
||||||
WriteInvoiceTaxTransactionsSubscriber,
|
WriteInvoiceTaxTransactionsSubscriber,
|
||||||
|
|
||||||
|
// Tax Rates - Bills
|
||||||
|
BillTaxRateValidateSubscriber,
|
||||||
|
WriteBillTaxTransactionsSubscriber,
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,6 +13,109 @@ export default class Bill extends mixin(TenantModel, [
|
|||||||
CustomViewBaseModel,
|
CustomViewBaseModel,
|
||||||
ModelSearchable,
|
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
|
* 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.
|
* Invoice amount in organization base currency.
|
||||||
|
* @deprecated
|
||||||
* @returns {number}
|
* @returns {number}
|
||||||
*/
|
*/
|
||||||
get localAmount() {
|
get localAmount() {
|
||||||
return this.amount * this.exchangeRate;
|
return this.amountLocal;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -231,7 +307,7 @@ export default class Bill extends mixin(TenantModel, [
|
|||||||
* @return {number}
|
* @return {number}
|
||||||
*/
|
*/
|
||||||
get dueAmount() {
|
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}
|
* @return {boolean}
|
||||||
*/
|
*/
|
||||||
get isPartiallyPaid() {
|
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.
|
* Retrieves the calculated amount which have not been invoiced.
|
||||||
*/
|
*/
|
||||||
get billableAmount() {
|
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 ItemEntry = require('models/ItemEntry');
|
||||||
const BillLandedCost = require('models/BillLandedCost');
|
const BillLandedCost = require('models/BillLandedCost');
|
||||||
const Branch = require('models/Branch');
|
const Branch = require('models/Branch');
|
||||||
|
const TaxRateTransaction = require('models/TaxRateTransaction');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
vendor: {
|
vendor: {
|
||||||
@@ -373,6 +450,21 @@ export default class Bill extends mixin(TenantModel, [
|
|||||||
to: 'branches.id',
|
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');
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -203,8 +203,8 @@ export class BillPaymentGLEntries {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the payment GL payable entry.
|
* Retrieves the payment GL payable entry.
|
||||||
* @param {IBillPayment} billPayment
|
* @param {IBillPayment} billPayment
|
||||||
* @param {number} APAccountId
|
* @param {number} APAccountId
|
||||||
* @returns {ILedgerEntry}
|
* @returns {ILedgerEntry}
|
||||||
*/
|
*/
|
||||||
private getPaymentGLPayableEntry = (
|
private getPaymentGLPayableEntry = (
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
|
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
|
||||||
import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrations/WarehouseTransactionDTOTransform';
|
import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrations/WarehouseTransactionDTOTransform';
|
||||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
|
import { ItemEntriesTaxTransactions } from '@/services/TaxRates/ItemEntriesTaxTransactions';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class BillDTOTransformer {
|
export class BillDTOTransformer {
|
||||||
@@ -23,6 +24,9 @@ export class BillDTOTransformer {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private warehouseDTOTransform: WarehouseTransactionDTOTransform;
|
private warehouseDTOTransform: WarehouseTransactionDTOTransform;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private taxDTOTransformer: ItemEntriesTaxTransactions;
|
||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
private tenancy: HasTenancyService;
|
private tenancy: HasTenancyService;
|
||||||
|
|
||||||
@@ -73,14 +77,24 @@ export class BillDTOTransformer {
|
|||||||
const billNumber = billDTO.billNumber || oldBill?.billNumber;
|
const billNumber = billDTO.billNumber || oldBill?.billNumber;
|
||||||
|
|
||||||
const initialEntries = billDTO.entries.map((entry) => ({
|
const initialEntries = billDTO.entries.map((entry) => ({
|
||||||
reference_type: 'Bill',
|
referenceType: 'Bill',
|
||||||
|
isInclusiveTax: billDTO.isInclusiveTax,
|
||||||
...omit(entry, ['amount']),
|
...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.
|
// Sets the default cost account to the bill entries.
|
||||||
this.setBillEntriesDefaultAccounts(tenantId)
|
this.setBillEntriesDefaultAccounts(tenantId)
|
||||||
)(initialEntries);
|
)(initialEntries);
|
||||||
|
|
||||||
|
const entries = R.compose(
|
||||||
|
// Remove tax code from entries.
|
||||||
|
R.map(R.omit(['taxCode']))
|
||||||
|
)(asyncEntries);
|
||||||
|
|
||||||
const initialDTO = {
|
const initialDTO = {
|
||||||
...formatDateFields(omit(billDTO, ['open', 'entries']), [
|
...formatDateFields(omit(billDTO, ['open', 'entries']), [
|
||||||
'billDate',
|
'billDate',
|
||||||
@@ -100,6 +114,8 @@ export class BillDTOTransformer {
|
|||||||
userId: authorizedUser.id,
|
userId: authorizedUser.id,
|
||||||
};
|
};
|
||||||
return R.compose(
|
return R.compose(
|
||||||
|
// Associates tax amount withheld to the model.
|
||||||
|
this.taxDTOTransformer.assocTaxAmountWithheldFromEntries,
|
||||||
this.branchDTOTransform.transformDTO(tenantId),
|
this.branchDTOTransform.transformDTO(tenantId),
|
||||||
this.warehouseDTOTransform.transformDTO(tenantId)
|
this.warehouseDTOTransform.transformDTO(tenantId)
|
||||||
)(initialDTO);
|
)(initialDTO);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { AccountNormal, IBill, IItemEntry, ILedgerEntry } from '@/interfaces';
|
|||||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
import Ledger from '@/services/Accounting/Ledger';
|
import Ledger from '@/services/Accounting/Ledger';
|
||||||
import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
|
import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
|
||||||
|
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class BillGLEntries {
|
export class BillGLEntries {
|
||||||
@@ -16,6 +17,9 @@ export class BillGLEntries {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private ledgerStorage: LedgerStorageService;
|
private ledgerStorage: LedgerStorageService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private itemsEntriesService: ItemsEntriesService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates bill GL entries.
|
* Creates bill GL entries.
|
||||||
* @param {number} tenantId -
|
* @param {number} tenantId -
|
||||||
@@ -43,8 +47,16 @@ export class BillGLEntries {
|
|||||||
{},
|
{},
|
||||||
trx
|
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.
|
// Commit the GL enties on the storage.
|
||||||
await this.ledgerStorage.commit(tenantId, billLedger, trx);
|
await this.ledgerStorage.commit(tenantId, billLedger, trx);
|
||||||
};
|
};
|
||||||
@@ -83,7 +95,7 @@ export class BillGLEntries {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the bill common entry.
|
* Retrieves the bill common entry.
|
||||||
* @param {IBill} bill
|
* @param {IBill} bill
|
||||||
* @returns {ILedgerEntry}
|
* @returns {ILedgerEntry}
|
||||||
*/
|
*/
|
||||||
private getBillCommonEntry = (bill: IBill) => {
|
private getBillCommonEntry = (bill: IBill) => {
|
||||||
@@ -119,7 +131,7 @@ export class BillGLEntries {
|
|||||||
(bill: IBill, entry: IItemEntry, index: number): ILedgerEntry => {
|
(bill: IBill, entry: IItemEntry, index: number): ILedgerEntry => {
|
||||||
const commonJournalMeta = this.getBillCommonEntry(bill);
|
const commonJournalMeta = this.getBillCommonEntry(bill);
|
||||||
|
|
||||||
const localAmount = bill.exchangeRate * entry.amount;
|
const localAmount = bill.exchangeRate * entry.amountExludingTax;
|
||||||
const landedCostAmount = sumBy(entry.allocatedCostEntries, 'cost');
|
const landedCostAmount = sumBy(entry.allocatedCostEntries, 'cost');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -173,7 +185,7 @@ export class BillGLEntries {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...commonJournalMeta,
|
...commonJournalMeta,
|
||||||
credit: bill.localAmount,
|
credit: bill.totalLocal,
|
||||||
accountId: payableAccountId,
|
accountId: payableAccountId,
|
||||||
contactId: bill.vendorId,
|
contactId: bill.vendorId,
|
||||||
accountNormal: AccountNormal.CREDIT,
|
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.
|
* Retrieves the given bill GL entries.
|
||||||
* @param {IBill} bill
|
* @param {IBill} bill
|
||||||
* @param {number} payableAccountId
|
* @param {number} payableAccountId
|
||||||
* @returns {ILedgerEntry[]}
|
* @returns {ILedgerEntry[]}
|
||||||
*/
|
*/
|
||||||
private getBillGLEntries = (
|
private getBillGLEntries = (
|
||||||
bill: IBill,
|
bill: IBill,
|
||||||
payableAccountId: number
|
payableAccountId: number,
|
||||||
|
taxPayableAccountId: number
|
||||||
): ILedgerEntry[] => {
|
): ILedgerEntry[] => {
|
||||||
const payableEntry = this.getBillPayableEntry(payableAccountId, bill);
|
const payableEntry = this.getBillPayableEntry(payableAccountId, bill);
|
||||||
|
|
||||||
@@ -201,18 +260,28 @@ export class BillGLEntries {
|
|||||||
const landedCostEntries = bill.locatedLandedCosts.map(
|
const landedCostEntries = bill.locatedLandedCosts.map(
|
||||||
landedCostTransformer
|
landedCostTransformer
|
||||||
);
|
);
|
||||||
|
const taxEntries = this.getBillTaxEntries(bill, taxPayableAccountId);
|
||||||
|
|
||||||
// Allocate cost entries journal entries.
|
// Allocate cost entries journal entries.
|
||||||
return [payableEntry, ...itemsEntries, ...landedCostEntries];
|
return [payableEntry, ...itemsEntries, ...landedCostEntries, ...taxEntries];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the given bill ledger.
|
* Retrieves the given bill ledger.
|
||||||
* @param {IBill} bill
|
* @param {IBill} bill
|
||||||
* @param {number} payableAccountId
|
* @param {number} payableAccountId
|
||||||
* @returns {Ledger}
|
* @returns {Ledger}
|
||||||
*/
|
*/
|
||||||
private getBillLedger = (bill: IBill, payableAccountId: number) => {
|
private getBillLedger = (
|
||||||
const entries = this.getBillGLEntries(bill, payableAccountId);
|
bill: IBill,
|
||||||
|
payableAccountId: number,
|
||||||
|
taxPayableAccountId: number
|
||||||
|
) => {
|
||||||
|
const entries = this.getBillGLEntries(
|
||||||
|
bill,
|
||||||
|
payableAccountId,
|
||||||
|
taxPayableAccountId
|
||||||
|
);
|
||||||
|
|
||||||
return new Ledger(entries);
|
return new Ledger(entries);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ export class GetBill {
|
|||||||
.findById(billId)
|
.findById(billId)
|
||||||
.withGraphFetched('vendor')
|
.withGraphFetched('vendor')
|
||||||
.withGraphFetched('entries.item')
|
.withGraphFetched('entries.item')
|
||||||
.withGraphFetched('branch');
|
.withGraphFetched('branch')
|
||||||
|
.withGraphFetched('taxes.taxRate');
|
||||||
|
|
||||||
// Validates the bill existance.
|
// Validates the bill existance.
|
||||||
this.validators.validateBillExistance(bill);
|
this.validators.validateBillExistance(bill);
|
||||||
|
|||||||
@@ -1,27 +1,42 @@
|
|||||||
import { IBill } from '@/interfaces';
|
import { IBill } from '@/interfaces';
|
||||||
import { Transformer } from '@/lib/Transformer/Transformer';
|
import { Transformer } from '@/lib/Transformer/Transformer';
|
||||||
|
import { SaleInvoiceTaxEntryTransformer } from '@/services/Sales/Invoices/SaleInvoiceTaxEntryTransformer';
|
||||||
import { formatNumber } from 'utils';
|
import { formatNumber } from 'utils';
|
||||||
|
|
||||||
export class PurchaseInvoiceTransformer extends Transformer {
|
export class PurchaseInvoiceTransformer extends Transformer {
|
||||||
/**
|
/**
|
||||||
* Include these attributes to sale invoice object.
|
* Include these attributes to sale bill object.
|
||||||
* @returns {Array}
|
* @returns {Array}
|
||||||
*/
|
*/
|
||||||
public includeAttributes = (): string[] => {
|
public includeAttributes = (): string[] => {
|
||||||
return [
|
return [
|
||||||
'formattedBillDate',
|
'formattedBillDate',
|
||||||
'formattedDueDate',
|
'formattedDueDate',
|
||||||
'formattedAmount',
|
|
||||||
'formattedPaymentAmount',
|
'formattedPaymentAmount',
|
||||||
'formattedBalance',
|
'formattedBalance',
|
||||||
'formattedDueAmount',
|
'formattedDueAmount',
|
||||||
'formattedExchangeRate',
|
'formattedExchangeRate',
|
||||||
|
'subtotalFormatted',
|
||||||
|
'subtotalLocalFormatted',
|
||||||
|
'subtotalExcludingTaxFormatted',
|
||||||
|
'taxAmountWithheldLocalFormatted',
|
||||||
|
'totalFormatted',
|
||||||
|
'totalLocalFormatted',
|
||||||
|
'taxes',
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve formatted invoice date.
|
* Excluded attributes.
|
||||||
* @param {IBill} invoice
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
public excludeAttributes = (): string[] => {
|
||||||
|
return ['amount', 'amountLocal', 'localAmount'];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve formatted bill date.
|
||||||
|
* @param {IBill} bill
|
||||||
* @returns {String}
|
* @returns {String}
|
||||||
*/
|
*/
|
||||||
protected formattedBillDate = (bill: IBill): string => {
|
protected formattedBillDate = (bill: IBill): string => {
|
||||||
@@ -29,8 +44,8 @@ export class PurchaseInvoiceTransformer extends Transformer {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve formatted invoice date.
|
* Retrieve formatted bill date.
|
||||||
* @param {IBill} invoice
|
* @param {IBill} bill
|
||||||
* @returns {String}
|
* @returns {String}
|
||||||
*/
|
*/
|
||||||
protected formattedDueDate = (bill: IBill): string => {
|
protected formattedDueDate = (bill: IBill): string => {
|
||||||
@@ -39,7 +54,7 @@ export class PurchaseInvoiceTransformer extends Transformer {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve formatted bill amount.
|
* Retrieve formatted bill amount.
|
||||||
* @param {IBill} invoice
|
* @param {IBill} bill
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
protected formattedAmount = (bill): string => {
|
protected formattedAmount = (bill): string => {
|
||||||
@@ -48,7 +63,7 @@ export class PurchaseInvoiceTransformer extends Transformer {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve formatted bill amount.
|
* Retrieve formatted bill amount.
|
||||||
* @param {IBill} invoice
|
* @param {IBill} bill
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
protected formattedPaymentAmount = (bill): string => {
|
protected formattedPaymentAmount = (bill): string => {
|
||||||
@@ -59,7 +74,7 @@ export class PurchaseInvoiceTransformer extends Transformer {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve formatted bill amount.
|
* Retrieve formatted bill amount.
|
||||||
* @param {IBill} invoice
|
* @param {IBill} bill
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
protected formattedDueAmount = (bill): string => {
|
protected formattedDueAmount = (bill): string => {
|
||||||
@@ -77,10 +92,90 @@ export class PurchaseInvoiceTransformer extends Transformer {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the formatted exchange rate.
|
* Retrieve the formatted exchange rate.
|
||||||
* @param {ISaleInvoice} invoice
|
* @param {IBill} bill
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
protected formattedExchangeRate = (invoice): string => {
|
protected formattedExchangeRate = (bill): string => {
|
||||||
return formatNumber(invoice.exchangeRate, { money: false });
|
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,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -218,7 +218,8 @@ export class SaleInvoiceGLEntries {
|
|||||||
...commonEntry,
|
...commonEntry,
|
||||||
credit: entry.taxAmount,
|
credit: entry.taxAmount,
|
||||||
accountId: taxPayableAccountId,
|
accountId: taxPayableAccountId,
|
||||||
index: index + 3,
|
index: index + 1,
|
||||||
|
indexGroup: 30,
|
||||||
accountNormal: AccountNormal.CREDIT,
|
accountNormal: AccountNormal.CREDIT,
|
||||||
taxRateId: entry.taxRateId,
|
taxRateId: entry.taxRateId,
|
||||||
taxRate: entry.taxRate,
|
taxRate: entry.taxRate,
|
||||||
|
|||||||
@@ -62,8 +62,8 @@ export class SaleInvoiceTaxEntryTransformer extends Transformer {
|
|||||||
const taxRate = this.taxRate(taxEntry);
|
const taxRate = this.taxRate(taxEntry);
|
||||||
|
|
||||||
return this.options.isInclusiveTax
|
return this.options.isInclusiveTax
|
||||||
? getInclusiveTaxAmount(this.options.amount, taxRate)
|
? getInclusiveTaxAmount(this.options.subtotal, taxRate)
|
||||||
: getExlusiveTaxAmount(this.options.amount, taxRate);
|
: getExlusiveTaxAmount(this.options.subtotal, taxRate);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ export class SaleInvoiceTransformer extends Transformer {
|
|||||||
*/
|
*/
|
||||||
protected taxes = (invoice) => {
|
protected taxes = (invoice) => {
|
||||||
return this.item(invoice.taxes, new SaleInvoiceTaxEntryTransformer(), {
|
return this.item(invoice.taxes, new SaleInvoiceTaxEntryTransformer(), {
|
||||||
amount: invoice.amount,
|
subtotal: invoice.subtotal,
|
||||||
isInclusiveTax: invoice.isInclusiveTax,
|
isInclusiveTax: invoice.isInclusiveTax,
|
||||||
currencyCode: invoice.currencyCode,
|
currencyCode: invoice.currencyCode,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import AccountDrawer from '@/containers/Drawers/AccountDrawer';
|
import AccountDrawer from '@/containers/Drawers/AccountDrawer';
|
||||||
import ManualJournalDrawer from '@/containers/Drawers/ManualJournalDrawer';
|
import ManualJournalDrawer from '@/containers/Drawers/ManualJournalDrawer';
|
||||||
import ExpenseDrawer from '@/containers/Drawers/ExpenseDrawer';
|
import ExpenseDrawer from '@/containers/Drawers/ExpenseDrawer';
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ const SelectButton = styled(Button)`
|
|||||||
position: relative;
|
position: relative;
|
||||||
padding-right: 30px;
|
padding-right: 30px;
|
||||||
|
|
||||||
|
&.bp4-small{
|
||||||
|
padding-right: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
&:not(.is-selected):not([class*='bp4-intent-']):not(.bp4-minimal) {
|
&:not(.is-selected):not([class*='bp4-intent-']):not(.bp4-minimal) {
|
||||||
color: #5c7080;
|
color: #5c7080;
|
||||||
}
|
}
|
||||||
|
|||||||
7
packages/webapp/src/constants/InclusiveTaxOptions.ts
Normal file
7
packages/webapp/src/constants/InclusiveTaxOptions.ts
Normal 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' },
|
||||||
|
];
|
||||||
@@ -30,7 +30,7 @@ export default function BillDetailHeader() {
|
|||||||
<CommercialDocTopHeader>
|
<CommercialDocTopHeader>
|
||||||
<DetailsMenu>
|
<DetailsMenu>
|
||||||
<AmountDetailItem label={intl.get('amount')}>
|
<AmountDetailItem label={intl.get('amount')}>
|
||||||
<h3 class="big-number">{bill.formatted_amount}</h3>
|
<h3 class="big-number">{bill.total_formatted}</h3>
|
||||||
</AmountDetailItem>
|
</AmountDetailItem>
|
||||||
<StatusDetailItem>
|
<StatusDetailItem>
|
||||||
<BillDetailsStatus bill={bill} />
|
<BillDetailsStatus bill={bill} />
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import React from 'react';
|
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
TotalLineBorderStyle,
|
TotalLineBorderStyle,
|
||||||
TotalLineTextStyle,
|
TotalLineTextStyle,
|
||||||
FormatNumber,
|
|
||||||
T,
|
T,
|
||||||
TotalLines,
|
TotalLines,
|
||||||
TotalLine,
|
TotalLine,
|
||||||
@@ -23,12 +20,20 @@ export function BillDetailTableFooter() {
|
|||||||
<BillTotalLines labelColWidth={'180px'} amountColWidth={'180px'}>
|
<BillTotalLines labelColWidth={'180px'} amountColWidth={'180px'}>
|
||||||
<TotalLine
|
<TotalLine
|
||||||
title={<T id={'bill.details.subtotal'} />}
|
title={<T id={'bill.details.subtotal'} />}
|
||||||
value={<FormatNumber value={bill.amount} />}
|
value={bill.subtotal_formatted}
|
||||||
borderStyle={TotalLineBorderStyle.SingleDark}
|
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
|
<TotalLine
|
||||||
title={<T id={'bill.details.total'} />}
|
title={<T id={'bill.details.total'} />}
|
||||||
value={bill.formatted_amount}
|
value={bill.total_formatted}
|
||||||
borderStyle={TotalLineBorderStyle.DoubleDark}
|
borderStyle={TotalLineBorderStyle.DoubleDark}
|
||||||
textStyle={TotalLineTextStyle.Bold}
|
textStyle={TotalLineTextStyle.Bold}
|
||||||
/>
|
/>
|
||||||
@@ -39,6 +44,7 @@ export function BillDetailTableFooter() {
|
|||||||
<TotalLine
|
<TotalLine
|
||||||
title={<T id={'bill.details.due_amount'} />}
|
title={<T id={'bill.details.due_amount'} />}
|
||||||
value={bill.formatted_due_amount}
|
value={bill.formatted_due_amount}
|
||||||
|
textStyle={TotalLineTextStyle.Bold}
|
||||||
/>
|
/>
|
||||||
</BillTotalLines>
|
</BillTotalLines>
|
||||||
</BillDetailsFooterRoot>
|
</BillDetailsFooterRoot>
|
||||||
|
|||||||
16
packages/webapp/src/containers/Entries/EntriesActionBar.tsx
Normal file
16
packages/webapp/src/containers/Entries/EntriesActionBar.tsx
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import * as R from 'ramda';
|
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 { useItem } from '@/hooks/query';
|
||||||
import {
|
import {
|
||||||
toSafeNumber,
|
toSafeNumber,
|
||||||
@@ -12,6 +11,7 @@ import {
|
|||||||
updateAutoAddNewLine,
|
updateAutoAddNewLine,
|
||||||
orderingLinesIndexes,
|
orderingLinesIndexes,
|
||||||
updateTableRow,
|
updateTableRow,
|
||||||
|
formattedAmount,
|
||||||
} from '@/utils';
|
} from '@/utils';
|
||||||
import { useItemEntriesTableContext } from './ItemEntriesTableProvider';
|
import { useItemEntriesTableContext } from './ItemEntriesTableProvider';
|
||||||
|
|
||||||
@@ -116,6 +116,11 @@ export function useFetchItemRow({ landedCost, itemType, notifyNewRow }) {
|
|||||||
? item.purchase_description
|
? item.purchase_description
|
||||||
: item.sell_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.
|
// Detarmines whether the landed cost checkbox should be disabled.
|
||||||
const landedCostDisabled = isLandedCostDisabled(item);
|
const landedCostDisabled = isLandedCostDisabled(item);
|
||||||
|
|
||||||
@@ -130,6 +135,7 @@ export function useFetchItemRow({ landedCost, itemType, notifyNewRow }) {
|
|||||||
landed_cost_disabled: landedCostDisabled,
|
landed_cost_disabled: landedCostDisabled,
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
|
taxRateId,
|
||||||
};
|
};
|
||||||
setItemRow(null);
|
setItemRow(null);
|
||||||
saveInvoke(notifyNewRow, newRow, rowIndex);
|
saveInvoke(notifyNewRow, newRow, rowIndex);
|
||||||
@@ -266,3 +272,29 @@ export const useComposeRowsOnRemoveTableRow = () => {
|
|||||||
[minLinesNumber, defaultEntry, localValue],
|
[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,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
handleErrors,
|
handleErrors,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
import withCurrentOrganization from '@/containers/Organization/withCurrentOrganization';
|
import withCurrentOrganization from '@/containers/Organization/withCurrentOrganization';
|
||||||
|
import { BillFormEntriesActions } from './BillFormEntriesActions';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bill form.
|
* Bill form.
|
||||||
@@ -126,7 +127,10 @@ function BillForm({
|
|||||||
<Form>
|
<Form>
|
||||||
<BillFormTopBar />
|
<BillFormTopBar />
|
||||||
<BillFormHeader />
|
<BillFormHeader />
|
||||||
<BillItemsEntriesEditor />
|
<div className={classNames(CLASSES.PAGE_FORM_BODY)}>
|
||||||
|
<BillFormEntriesActions />
|
||||||
|
<BillItemsEntriesEditor />
|
||||||
|
</div>
|
||||||
<BillFormFooter />
|
<BillFormFooter />
|
||||||
<BillFloatingActions />
|
<BillFloatingActions />
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
`;
|
||||||
@@ -1,15 +1,14 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import React from 'react';
|
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
T,
|
|
||||||
TotalLines,
|
TotalLines,
|
||||||
TotalLine,
|
TotalLine,
|
||||||
TotalLineBorderStyle,
|
TotalLineBorderStyle,
|
||||||
TotalLineTextStyle,
|
TotalLineTextStyle,
|
||||||
} from '@/components';
|
} from '@/components';
|
||||||
import { useBillTotals } from './utils';
|
import { useBillAggregatedTaxRates, useBillTotals } from './utils';
|
||||||
|
import { useFormikContext } from 'formik';
|
||||||
|
import { TaxType } from '@/interfaces/TaxRates';
|
||||||
|
|
||||||
export function BillFormFooterRight() {
|
export function BillFormFooterRight() {
|
||||||
const {
|
const {
|
||||||
@@ -19,26 +18,46 @@ export function BillFormFooterRight() {
|
|||||||
formattedPaymentTotal,
|
formattedPaymentTotal,
|
||||||
} = useBillTotals();
|
} = useBillTotals();
|
||||||
|
|
||||||
|
const {
|
||||||
|
values: { inclusive_exclusive_tax, currency_code },
|
||||||
|
} = useFormikContext();
|
||||||
|
|
||||||
|
const taxEntries = useBillAggregatedTaxRates();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BillTotalLines labelColWidth={'180px'} amountColWidth={'180px'}>
|
<BillTotalLines labelColWidth={'180px'} amountColWidth={'180px'}>
|
||||||
<TotalLine
|
<TotalLine
|
||||||
title={<T id={'bill_form.label.subtotal'} />}
|
title={
|
||||||
|
<>
|
||||||
|
{inclusive_exclusive_tax === TaxType.Inclusive
|
||||||
|
? 'Subtotal (Tax Inclusive)'
|
||||||
|
: 'Subtotal'}
|
||||||
|
</>
|
||||||
|
}
|
||||||
value={formattedSubtotal}
|
value={formattedSubtotal}
|
||||||
borderStyle={TotalLineBorderStyle.None}
|
borderStyle={TotalLineBorderStyle.None}
|
||||||
/>
|
/>
|
||||||
|
{taxEntries.map((tax, index) => (
|
||||||
|
<TotalLine
|
||||||
|
key={index}
|
||||||
|
title={tax.label}
|
||||||
|
value={tax.taxAmountFormatted}
|
||||||
|
borderStyle={TotalLineBorderStyle.None}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
<TotalLine
|
<TotalLine
|
||||||
title={<T id={'bill_form.label.total'} />}
|
title={`Total (${currency_code})`}
|
||||||
value={formattedTotal}
|
value={formattedTotal}
|
||||||
borderStyle={TotalLineBorderStyle.SingleDark}
|
borderStyle={TotalLineBorderStyle.SingleDark}
|
||||||
textStyle={TotalLineTextStyle.Bold}
|
textStyle={TotalLineTextStyle.Bold}
|
||||||
/>
|
/>
|
||||||
<TotalLine
|
<TotalLine
|
||||||
title={<T id={'bill_form.label.total'} />}
|
title={'Paid Amount'}
|
||||||
value={formattedPaymentTotal}
|
value={formattedPaymentTotal}
|
||||||
borderStyle={TotalLineBorderStyle.None}
|
borderStyle={TotalLineBorderStyle.None}
|
||||||
/>
|
/>
|
||||||
<TotalLine
|
<TotalLine
|
||||||
title={<T id={'bill_form.label.total'} />}
|
title={'Due Amount'}
|
||||||
value={formattedDueTotal}
|
value={formattedDueTotal}
|
||||||
textStyle={TotalLineTextStyle.Bold}
|
textStyle={TotalLineTextStyle.Bold}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
useCreateBill,
|
useCreateBill,
|
||||||
useEditBill,
|
useEditBill,
|
||||||
} from '@/hooks/query';
|
} from '@/hooks/query';
|
||||||
|
import { useTaxRates } from '@/hooks/query/taxRates';
|
||||||
|
|
||||||
const BillFormContext = createContext();
|
const BillFormContext = createContext();
|
||||||
|
|
||||||
@@ -83,6 +84,9 @@ function BillFormProvider({ billId, ...props }) {
|
|||||||
isSuccess: isBranchesSuccess,
|
isSuccess: isBranchesSuccess,
|
||||||
} = useBranches({}, { enabled: isBranchFeatureCan });
|
} = useBranches({}, { enabled: isBranchFeatureCan });
|
||||||
|
|
||||||
|
// Fetch tax rates.
|
||||||
|
const { data: taxRates, isLoading: isTaxRatesLoading } = useTaxRates();
|
||||||
|
|
||||||
// Fetches the projects list.
|
// Fetches the projects list.
|
||||||
const {
|
const {
|
||||||
data: { projects },
|
data: { projects },
|
||||||
@@ -103,7 +107,10 @@ function BillFormProvider({ billId, ...props }) {
|
|||||||
|
|
||||||
// Determines whether the warehouse and branches are loading.
|
// Determines whether the warehouse and branches are loading.
|
||||||
const isFeatureLoading =
|
const isFeatureLoading =
|
||||||
isWarehouesLoading || isBranchesLoading || isProjectsLoading;
|
isWarehouesLoading ||
|
||||||
|
isBranchesLoading ||
|
||||||
|
isProjectsLoading ||
|
||||||
|
isTaxRatesLoading;
|
||||||
|
|
||||||
const provider = {
|
const provider = {
|
||||||
accounts,
|
accounts,
|
||||||
@@ -113,6 +120,7 @@ function BillFormProvider({ billId, ...props }) {
|
|||||||
warehouses,
|
warehouses,
|
||||||
branches,
|
branches,
|
||||||
projects,
|
projects,
|
||||||
|
taxRates,
|
||||||
submitPayload,
|
submitPayload,
|
||||||
isNewMode,
|
isNewMode,
|
||||||
|
|
||||||
@@ -124,6 +132,7 @@ function BillFormProvider({ billId, ...props }) {
|
|||||||
isFeatureLoading,
|
isFeatureLoading,
|
||||||
isBranchesSuccess,
|
isBranchesSuccess,
|
||||||
isWarehousesSuccess,
|
isWarehousesSuccess,
|
||||||
|
isTaxRatesLoading,
|
||||||
|
|
||||||
createBillMutate,
|
createBillMutate,
|
||||||
editBillMutate,
|
editBillMutate,
|
||||||
|
|||||||
@@ -1,46 +1,41 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import React from 'react';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import ItemsEntriesTable from '@/containers/Entries/ItemsEntriesTable';
|
import ItemsEntriesTable from '@/containers/Entries/ItemsEntriesTable';
|
||||||
import { FastField } from 'formik';
|
import { FastField } from 'formik';
|
||||||
import { CLASSES } from '@/constants/classes';
|
|
||||||
import { useBillFormContext } from './BillFormProvider';
|
import { useBillFormContext } from './BillFormProvider';
|
||||||
import { entriesFieldShouldUpdate } from './utils';
|
import { entriesFieldShouldUpdate } from './utils';
|
||||||
import { ITEM_TYPE } from '@/containers/Entries/utils';
|
import { ITEM_TYPE } from '@/containers/Entries/utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bill form body.
|
* Bill form body.
|
||||||
*/
|
*/
|
||||||
export default function BillFormBody({ defaultBill }) {
|
export default function BillFormBody({ defaultBill }) {
|
||||||
const { items } = useBillFormContext();
|
const { items, taxRates } = useBillFormContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames(CLASSES.PAGE_FORM_BODY)}>
|
<FastField
|
||||||
<FastField
|
name={'entries'}
|
||||||
name={'entries'}
|
items={items}
|
||||||
items={items}
|
shouldUpdate={entriesFieldShouldUpdate}
|
||||||
shouldUpdate={entriesFieldShouldUpdate}
|
>
|
||||||
>
|
{({
|
||||||
{({
|
form: { values, setFieldValue },
|
||||||
form: { values, setFieldValue },
|
field: { value },
|
||||||
field: { value },
|
meta: { error, touched },
|
||||||
meta: { error, touched },
|
}) => (
|
||||||
}) => (
|
<ItemsEntriesTable
|
||||||
<ItemsEntriesTable
|
value={value}
|
||||||
value={value}
|
onChange={(entries) => {
|
||||||
onChange={(entries) => {
|
setFieldValue('entries', entries);
|
||||||
setFieldValue('entries', entries);
|
}}
|
||||||
}}
|
items={items}
|
||||||
items={items}
|
errors={error}
|
||||||
errors={error}
|
linesNumber={4}
|
||||||
linesNumber={4}
|
currencyCode={values.currency_code}
|
||||||
currencyCode={values.currency_code}
|
itemType={ITEM_TYPE.PURCHASABLE}
|
||||||
itemType={ITEM_TYPE.PURCHASABLE}
|
taxRates={taxRates}
|
||||||
landedCost={true}
|
landedCost={true}
|
||||||
enableTaxRates={false}
|
/>
|
||||||
/>
|
)}
|
||||||
)}
|
</FastField>
|
||||||
</FastField>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import React from 'react';
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import intl from 'react-intl-universal';
|
import intl from 'react-intl-universal';
|
||||||
import * as R from 'ramda';
|
import * as R from 'ramda';
|
||||||
import { first } from 'lodash';
|
import { first, chain } from 'lodash';
|
||||||
import { Intent } from '@blueprintjs/core';
|
import { Intent } from '@blueprintjs/core';
|
||||||
import { useFormikContext } from 'formik';
|
import { useFormikContext } from 'formik';
|
||||||
import { AppToaster } from '@/components';
|
import { AppToaster } from '@/components';
|
||||||
@@ -17,6 +17,8 @@ import {
|
|||||||
import {
|
import {
|
||||||
updateItemsEntriesTotal,
|
updateItemsEntriesTotal,
|
||||||
ensureEntriesHaveEmptyLine,
|
ensureEntriesHaveEmptyLine,
|
||||||
|
assignEntriesTaxAmount,
|
||||||
|
aggregateItemEntriesTaxRates,
|
||||||
} from '@/containers/Entries/utils';
|
} from '@/containers/Entries/utils';
|
||||||
import { useCurrentOrganization } from '@/hooks/state';
|
import { useCurrentOrganization } from '@/hooks/state';
|
||||||
import {
|
import {
|
||||||
@@ -24,6 +26,7 @@ import {
|
|||||||
getEntriesTotal,
|
getEntriesTotal,
|
||||||
} from '@/containers/Entries/utils';
|
} from '@/containers/Entries/utils';
|
||||||
import { useBillFormContext } from './BillFormProvider';
|
import { useBillFormContext } from './BillFormProvider';
|
||||||
|
import { TaxType } from '@/interfaces/TaxRates';
|
||||||
|
|
||||||
export const MIN_LINES_NUMBER = 1;
|
export const MIN_LINES_NUMBER = 1;
|
||||||
|
|
||||||
@@ -37,6 +40,9 @@ export const defaultBillEntry = {
|
|||||||
description: '',
|
description: '',
|
||||||
amount: '',
|
amount: '',
|
||||||
landed_cost: false,
|
landed_cost: false,
|
||||||
|
tax_rate_id: '',
|
||||||
|
tax_rate: '',
|
||||||
|
tax_amount: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Default bill.
|
// Default bill.
|
||||||
@@ -46,6 +52,7 @@ export const defaultBill = {
|
|||||||
bill_date: moment(new Date()).format('YYYY-MM-DD'),
|
bill_date: moment(new Date()).format('YYYY-MM-DD'),
|
||||||
due_date: moment(new Date()).format('YYYY-MM-DD'),
|
due_date: moment(new Date()).format('YYYY-MM-DD'),
|
||||||
reference_no: '',
|
reference_no: '',
|
||||||
|
inclusive_exclusive_tax: TaxType.Inclusive,
|
||||||
note: '',
|
note: '',
|
||||||
open: '',
|
open: '',
|
||||||
branch_id: '',
|
branch_id: '',
|
||||||
@@ -82,6 +89,9 @@ export const transformToEditForm = (bill) => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...transformToForm(bill, defaultBill),
|
...transformToForm(bill, defaultBill),
|
||||||
|
inclusive_exclusive_tax: bill.is_inclusive_tax
|
||||||
|
? TaxType.Inclusive
|
||||||
|
: TaxType.Exclusive,
|
||||||
entries,
|
entries,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -228,11 +238,12 @@ export const useSetPrimaryWarehouseToForm = () => {
|
|||||||
*/
|
*/
|
||||||
export const useBillTotals = () => {
|
export const useBillTotals = () => {
|
||||||
const {
|
const {
|
||||||
values: { entries, currency_code: currencyCode },
|
values: { currency_code: currencyCode },
|
||||||
} = useFormikContext();
|
} = useFormikContext();
|
||||||
|
|
||||||
// Retrieves the bili entries total.
|
// Retrieves the bill subtotal.
|
||||||
const total = React.useMemo(() => getEntriesTotal(entries), [entries]);
|
const subtotal = useBillSubtotal();
|
||||||
|
const total = useBillTotal();
|
||||||
|
|
||||||
// Retrieves the formatted total money.
|
// Retrieves the formatted total money.
|
||||||
const formattedTotal = React.useMemo(
|
const formattedTotal = React.useMemo(
|
||||||
@@ -241,8 +252,8 @@ export const useBillTotals = () => {
|
|||||||
);
|
);
|
||||||
// Retrieves the formatted subtotal.
|
// Retrieves the formatted subtotal.
|
||||||
const formattedSubtotal = React.useMemo(
|
const formattedSubtotal = React.useMemo(
|
||||||
() => formattedAmount(total, currencyCode, { money: false }),
|
() => formattedAmount(subtotal, currencyCode, { money: false }),
|
||||||
[total, currencyCode],
|
[subtotal, currencyCode],
|
||||||
);
|
);
|
||||||
// Retrieves the payment total.
|
// Retrieves the payment total.
|
||||||
const paymentTotal = React.useMemo(() => 0, []);
|
const paymentTotal = React.useMemo(() => 0, []);
|
||||||
@@ -288,3 +299,86 @@ export const useBillIsForeignCustomer = () => {
|
|||||||
);
|
);
|
||||||
return isForeignCustomer;
|
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,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -9,20 +9,18 @@ import {
|
|||||||
Tag,
|
Tag,
|
||||||
ProgressBar,
|
ProgressBar,
|
||||||
} from '@blueprintjs/core';
|
} from '@blueprintjs/core';
|
||||||
|
import clsx from 'classnames';
|
||||||
import {
|
import {
|
||||||
FormatDateCell,
|
FormatDateCell,
|
||||||
FormattedMessage as T,
|
FormattedMessage as T,
|
||||||
Icon,
|
Icon,
|
||||||
If,
|
If,
|
||||||
Choose,
|
Choose,
|
||||||
Money,
|
|
||||||
Can,
|
Can,
|
||||||
} from '@/components';
|
} from '@/components';
|
||||||
import {
|
import {
|
||||||
formattedAmount,
|
formattedAmount,
|
||||||
safeCallback,
|
safeCallback,
|
||||||
isBlank,
|
|
||||||
calculateStatus,
|
calculateStatus,
|
||||||
} from '@/utils';
|
} from '@/utils';
|
||||||
import {
|
import {
|
||||||
@@ -30,6 +28,7 @@ import {
|
|||||||
PaymentMadeAction,
|
PaymentMadeAction,
|
||||||
AbilitySubject,
|
AbilitySubject,
|
||||||
} from '@/constants/abilityOption';
|
} from '@/constants/abilityOption';
|
||||||
|
import { CLASSES } from '@/constants';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Actions menu.
|
* 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.
|
* Status accessor.
|
||||||
*/
|
*/
|
||||||
@@ -198,11 +186,11 @@ export function useBillsTableColumns() {
|
|||||||
{
|
{
|
||||||
id: 'amount',
|
id: 'amount',
|
||||||
Header: intl.get('amount'),
|
Header: intl.get('amount'),
|
||||||
accessor: AmountAccessor,
|
accessor: 'total_formatted',
|
||||||
width: 120,
|
width: 120,
|
||||||
className: 'amount',
|
|
||||||
align: 'right',
|
align: 'right',
|
||||||
clickable: true,
|
clickable: true,
|
||||||
|
className: clsx(CLASSES.FONT_BOLD),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'status',
|
id: 'status',
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import React from 'react';
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { useFormikContext } from 'formik';
|
import { useFormikContext } from 'formik';
|
||||||
import { InclusiveButtonOptions } from './constants';
|
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';
|
import { composeEntriesOnEditInclusiveTax } from './utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -12,9 +13,9 @@ import { composeEntriesOnEditInclusiveTax } from './utils';
|
|||||||
*/
|
*/
|
||||||
export function InvoiceFormActions() {
|
export function InvoiceFormActions() {
|
||||||
return (
|
return (
|
||||||
<InvoiceFormActionsRoot>
|
<EntriesActionsBar>
|
||||||
<InvoiceExclusiveInclusiveSelect />
|
<InvoiceExclusiveInclusiveSelect />
|
||||||
</InvoiceFormActionsRoot>
|
</EntriesActionsBar>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,7 +41,7 @@ export function InvoiceExclusiveInclusiveSelect(props) {
|
|||||||
label={'Amounts are'}
|
label={'Amounts are'}
|
||||||
inline={true}
|
inline={true}
|
||||||
>
|
>
|
||||||
<InclusiveSelect
|
<FSelect
|
||||||
name={'inclusive_exclusive_tax'}
|
name={'inclusive_exclusive_tax'}
|
||||||
items={InclusiveButtonOptions}
|
items={InclusiveButtonOptions}
|
||||||
textAccessor={'label'}
|
textAccessor={'label'}
|
||||||
@@ -57,23 +58,5 @@ export function InvoiceExclusiveInclusiveSelect(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const InclusiveFormGroup = styled(FFormGroup)`
|
const InclusiveFormGroup = styled(FFormGroup)`
|
||||||
margin-bottom: 0;
|
|
||||||
margin-left: auto;
|
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;
|
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
TotalLineTextStyle,
|
TotalLineTextStyle,
|
||||||
} from '@/components';
|
} from '@/components';
|
||||||
import { useInvoiceAggregatedTaxRates, useInvoiceTotals } from './utils';
|
import { useInvoiceAggregatedTaxRates, useInvoiceTotals } from './utils';
|
||||||
|
import { TaxType } from '@/interfaces/TaxRates';
|
||||||
|
|
||||||
export function InvoiceFormFooterRight() {
|
export function InvoiceFormFooterRight() {
|
||||||
// Calculate the total due amount of invoice entries.
|
// Calculate the total due amount of invoice entries.
|
||||||
@@ -32,7 +33,7 @@ export function InvoiceFormFooterRight() {
|
|||||||
<TotalLine
|
<TotalLine
|
||||||
title={
|
title={
|
||||||
<>
|
<>
|
||||||
{inclusive_exclusive_tax === 'inclusive'
|
{inclusive_exclusive_tax === TaxType.Inclusive
|
||||||
? 'Subtotal (Tax Inclusive)'
|
? 'Subtotal (Tax Inclusive)'
|
||||||
: 'Subtotal'}
|
: 'Subtotal'}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,18 +1,23 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import React, { useCallback, useMemo } from 'react';
|
import React from 'react';
|
||||||
|
import { useFormikContext } from 'formik';
|
||||||
import intl from 'react-intl-universal';
|
import intl from 'react-intl-universal';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import * as R from 'ramda';
|
import * as R from 'ramda';
|
||||||
import { Intent } from '@blueprintjs/core';
|
import { Intent } from '@blueprintjs/core';
|
||||||
import { omit, first, keyBy, sumBy, groupBy } from 'lodash';
|
import { omit, first, sumBy } from 'lodash';
|
||||||
import { compose, transformToForm, repeatValue } from '@/utils';
|
import {
|
||||||
import { useFormikContext } from 'formik';
|
compose,
|
||||||
|
transformToForm,
|
||||||
import { formattedAmount, defaultFastFieldShouldUpdate } from '@/utils';
|
repeatValue,
|
||||||
|
formattedAmount,
|
||||||
|
defaultFastFieldShouldUpdate,
|
||||||
|
} from '@/utils';
|
||||||
import { ERROR } from '@/constants/errors';
|
import { ERROR } from '@/constants/errors';
|
||||||
import { AppToaster } from '@/components';
|
import { AppToaster } from '@/components';
|
||||||
import { useCurrentOrganization } from '@/hooks/state';
|
import { useCurrentOrganization } from '@/hooks/state';
|
||||||
import {
|
import {
|
||||||
|
aggregateItemEntriesTaxRates,
|
||||||
assignEntriesTaxAmount,
|
assignEntriesTaxAmount,
|
||||||
getEntriesTotal,
|
getEntriesTotal,
|
||||||
} from '@/containers/Entries/utils';
|
} from '@/containers/Entries/utils';
|
||||||
@@ -327,28 +332,14 @@ export const useInvoiceAggregatedTaxRates = () => {
|
|||||||
const { values } = useFormikContext();
|
const { values } = useFormikContext();
|
||||||
const { taxRates } = useInvoiceFormContext();
|
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.
|
// Calculate the total tax amount of invoice entries.
|
||||||
return React.useMemo(() => {
|
return React.useMemo(() => {
|
||||||
const filteredEntries = values.entries.filter((e) => e.tax_rate_id);
|
return aggregateTaxRates(values.entries);
|
||||||
const groupedTaxRates = groupBy(filteredEntries, 'tax_rate_id');
|
}, [aggregateTaxRates, values.entries]);
|
||||||
|
|
||||||
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]);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -21,12 +21,9 @@ label.bp4-label {
|
|||||||
.required {
|
.required {
|
||||||
color: red;
|
color: red;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bp4-form-group.bp4-inline & {
|
.bp4-form-group.bp4-inline & {
|
||||||
margin: 0 10px 0 0;
|
margin: 0 10px 0 0;
|
||||||
line-height: 1.6;
|
line-height: 30px;
|
||||||
padding-top: calc(0.3rem + 1px);
|
|
||||||
padding-bottom: calc(0.3rem + 1px);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user