mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 21:00:31 +00:00
feat(server): wip sale invoice tax rates
This commit is contained in:
@@ -764,6 +764,11 @@ export default class SaleInvoicesController extends BaseController {
|
||||
errors: [{ type: 'ITEM_ENTRY_TAX_RATE_CODE_NOT_FOUND', code: 5000 }],
|
||||
});
|
||||
}
|
||||
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: 5100 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,11 @@ exports.up = (knex) => {
|
||||
})
|
||||
.table('items_entries', (table) => {
|
||||
table.boolean('is_tax_exclusive');
|
||||
table
|
||||
.integer('tax_rate_id')
|
||||
.unsigned()
|
||||
.references('id')
|
||||
.inTable('tax_rates');
|
||||
table.string('tax_code');
|
||||
table.decimal('tax_rate');
|
||||
})
|
||||
@@ -21,13 +26,13 @@ exports.up = (knex) => {
|
||||
})
|
||||
.createTable('tax_rate_transactions', (table) => {
|
||||
table.increments('id');
|
||||
|
||||
table.string('tax_name');
|
||||
table.string('tax_code');
|
||||
|
||||
table
|
||||
.integer('tax_rate_id')
|
||||
.unsigned()
|
||||
.references('id')
|
||||
.inTable('tax_rates');
|
||||
table.string('reference_type');
|
||||
table.integer('reference_id');
|
||||
|
||||
table.decimal('tax_amount');
|
||||
table.integer('tax_account_id').unsigned();
|
||||
});
|
||||
|
||||
@@ -32,6 +32,7 @@ export interface IItemEntry {
|
||||
projectRefType?: ProjectLinkRefType;
|
||||
projectRefInvoicedAmount?: number;
|
||||
|
||||
taxRateId: number | null;
|
||||
taxCode: string;
|
||||
taxRate: number;
|
||||
taxAmount: number;
|
||||
@@ -51,8 +52,9 @@ export interface IItemEntryDTO {
|
||||
projectRefType?: ProjectLinkRefType;
|
||||
projectRefInvoicedAmount?: number;
|
||||
|
||||
taxCode: string;
|
||||
taxRate: number;
|
||||
taxCodeId?: number;
|
||||
taxCode?: string;
|
||||
taxRate?: number;
|
||||
}
|
||||
|
||||
export enum ProjectLinkRefType {
|
||||
|
||||
@@ -48,9 +48,11 @@ export interface ITaxRateDeletedPayload {
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
|
||||
export interface ITaxTransaction {
|
||||
id?: number;
|
||||
taxRateId: number;
|
||||
referenceType: string;
|
||||
referenceId: number;
|
||||
taxAmount: number;
|
||||
taxName: string;
|
||||
taxCode: string;
|
||||
}
|
||||
taxAccountId: number;
|
||||
}
|
||||
|
||||
@@ -37,6 +37,20 @@ export default class TaxRateTransaction extends mixin(TenantModel, [
|
||||
* Relationship mapping.
|
||||
*/
|
||||
static get relationMappings() {
|
||||
return {};
|
||||
const TaxRate = require('models/TaxRate');
|
||||
|
||||
return {
|
||||
/**
|
||||
* Belongs to the tax rate.
|
||||
*/
|
||||
taxRate: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: TaxRate.default,
|
||||
join: {
|
||||
from: 'tax_rate_transactions.taxRateId',
|
||||
to: 'tax_rates.id',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +75,8 @@ export class CommandSaleInvoiceDTOTransformer {
|
||||
...entry,
|
||||
}));
|
||||
const entries = await composeAsync(
|
||||
// Associate tax rate id from tax code to entries.
|
||||
this.taxDTOTransformer.assocTaxRateIdFromCodeToEntries(tenantId),
|
||||
// Sets default cost and sell account to invoice items entries.
|
||||
this.itemsEntriesService.setItemsEntriesDefaultAccounts(tenantId)
|
||||
)(initialEntries);
|
||||
|
||||
@@ -33,7 +33,8 @@ export class GetSaleInvoice {
|
||||
.findById(saleInvoiceId)
|
||||
.withGraphFetched('entries.item')
|
||||
.withGraphFetched('customer')
|
||||
.withGraphFetched('branch');
|
||||
.withGraphFetched('branch')
|
||||
.withGraphFetched('taxes.taxRate');
|
||||
|
||||
// Validates the given sale invoice existance.
|
||||
this.validators.validateInvoiceExistance(saleInvoice);
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Transformer } from '@/lib/Transformer/Transformer';
|
||||
|
||||
export class SaleInvoiceTaxEntryTransformer extends Transformer {
|
||||
/**
|
||||
* Included attributes.
|
||||
* @returns {Array}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return ['name', 'taxRateCode', 'raxRate', 'taxRateId'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Exclude attributes.
|
||||
* @returns {string[]}
|
||||
*/
|
||||
public excludeAttributes = (): string[] => {
|
||||
return ['*'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve tax rate code.
|
||||
* @param taxEntry
|
||||
* @returns {string}
|
||||
*/
|
||||
protected taxRateCode = (taxEntry) => {
|
||||
return taxEntry.taxRate.code;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve tax rate id.
|
||||
* @param taxEntry
|
||||
* @returns {number}
|
||||
*/
|
||||
protected raxRate = (taxEntry) => {
|
||||
return taxEntry.taxAmount || taxEntry.taxRate.rate;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve tax rate name.
|
||||
* @param taxEntry
|
||||
* @returns {string}
|
||||
*/
|
||||
protected name = (taxEntry) => {
|
||||
return taxEntry.taxRate.name;
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Transformer } from '@/lib/Transformer/Transformer';
|
||||
import { formatNumber } from 'utils';
|
||||
import { SaleInvoiceTaxEntryTransformer } from './SaleInvoiceTaxEntryTransformer';
|
||||
|
||||
export class SaleInvoiceTransformer extends Transformer {
|
||||
/**
|
||||
@@ -15,6 +16,7 @@ export class SaleInvoiceTransformer extends Transformer {
|
||||
'formattedPaymentAmount',
|
||||
'formattedBalanceAmount',
|
||||
'formattedExchangeRate',
|
||||
'taxes',
|
||||
];
|
||||
};
|
||||
|
||||
@@ -88,4 +90,12 @@ export class SaleInvoiceTransformer extends Transformer {
|
||||
protected formattedExchangeRate = (invoice): string => {
|
||||
return formatNumber(invoice.exchangeRate, { money: false });
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the taxes lines of sale invoice.
|
||||
* @param {ISaleInvoice} invoice
|
||||
*/
|
||||
protected taxes = (invoice) => {
|
||||
return this.item(invoice.taxes, new SaleInvoiceTaxEntryTransformer());
|
||||
};
|
||||
}
|
||||
|
||||
@@ -45,9 +45,13 @@ export class CommandTaxRatesValidators {
|
||||
itemEntriesDTO: IItemEntryDTO[]
|
||||
) {
|
||||
const { TaxRate } = this.tenancy.models(tenantId);
|
||||
|
||||
const filteredTaxEntries = itemEntriesDTO.filter((e) => e.taxCode);
|
||||
const taxCodes = filteredTaxEntries.map((e) => e.taxCode);
|
||||
|
||||
// Can't validate if there is no tax codes.
|
||||
if (taxCodes.length === 0) return;
|
||||
|
||||
const foundTaxCodes = await TaxRate.query().whereIn('code', taxCodes);
|
||||
const foundCodes = foundTaxCodes.map((tax) => tax.code);
|
||||
|
||||
@@ -57,4 +61,31 @@ export class CommandTaxRatesValidators {
|
||||
throw new ServiceError(ERRORS.ITEM_ENTRY_TAX_RATE_CODE_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the tax rate id of the given item entries DTO.
|
||||
* @param {number} tenantId
|
||||
* @param {IItemEntryDTO[]} itemEntriesDTO
|
||||
* @throws {ServiceError}
|
||||
*/
|
||||
public async validateItemEntriesTaxCodeId(
|
||||
tenantId: number,
|
||||
itemEntriesDTO: IItemEntryDTO[]
|
||||
) {
|
||||
const filteredTaxEntries = itemEntriesDTO.filter((e) => e.taxCodeId);
|
||||
const taxCodes = filteredTaxEntries.map((e) => e.taxCodeId);
|
||||
|
||||
// Can't validate if there is no tax codes.
|
||||
if (taxCodes.length === 0) return;
|
||||
|
||||
const { TaxRate } = this.tenancy.models(tenantId);
|
||||
const foundTaxCodes = await TaxRate.query().whereIn('id', taxCodes);
|
||||
const foundCodes = foundTaxCodes.map((tax) => tax.id);
|
||||
|
||||
const notFoundTaxCodes = difference(taxCodes, foundCodes);
|
||||
|
||||
if (notFoundTaxCodes.length > 0) {
|
||||
throw new ServiceError(ERRORS.ITEM_ENTRY_TAX_RATE_ID_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import { ItemEntry } from "@/models";
|
||||
import { sumBy } from "lodash";
|
||||
import { Service } from "typedi";
|
||||
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { keyBy, sumBy } from 'lodash';
|
||||
import * as R from 'ramda';
|
||||
import { ItemEntry } from '@/models';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
|
||||
@Service()
|
||||
export class ItemEntriesTaxTransactions {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param model
|
||||
* @returns
|
||||
* Associates tax amount withheld to the model.
|
||||
* @param model
|
||||
* @returns
|
||||
*/
|
||||
public assocTaxAmountWithheldFromEntries(model: any) {
|
||||
const entries = model.entries.map((entry) => ItemEntry.fromJson(entry));
|
||||
@@ -19,4 +23,27 @@ export class ItemEntriesTaxTransactions {
|
||||
}
|
||||
return model;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Associates tax rate id from tax code to entries.
|
||||
* @param {number} tenantId
|
||||
* @param {} model
|
||||
*/
|
||||
public assocTaxRateIdFromCodeToEntries =
|
||||
(tenantId: number) => async (entries: any) => {
|
||||
const entriesWithCode = entries.filter((entry) => entry.taxCode);
|
||||
const taxCodes = entriesWithCode.map((entry) => entry.taxCode);
|
||||
|
||||
const { TaxRate } = this.tenancy.models(tenantId);
|
||||
const foundTaxCodes = await TaxRate.query().whereIn('code', taxCodes);
|
||||
|
||||
const taxCodesMap = keyBy(foundTaxCodes, 'code');
|
||||
|
||||
return entries.map((entry) => {
|
||||
if (entry.taxCode) {
|
||||
entry.taxRateId = taxCodesMap[entry.taxCode]?.id;
|
||||
}
|
||||
return entry;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { sumBy, chain } from 'lodash';
|
||||
import { IItemEntry } from '@/interfaces';
|
||||
import { sumBy, chain, keyBy } from 'lodash';
|
||||
import { IItemEntry, ITaxTransaction } from '@/interfaces';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import { Inject, Service } from 'typedi';
|
||||
|
||||
@@ -17,49 +17,54 @@ export class WriteTaxTransactionsItemEntries {
|
||||
tenantId: number,
|
||||
itemEntries: IItemEntry[]
|
||||
) {
|
||||
const { TaxRateTransaction } = this.tenancy.models(tenantId);
|
||||
const { TaxRateTransaction, TaxRate } = this.tenancy.models(tenantId);
|
||||
const aggregatedEntries = this.aggregateItemEntriesByTaxCode(itemEntries);
|
||||
|
||||
const entriesTaxRateIds = aggregatedEntries.map((entry) => entry.taxRateId);
|
||||
|
||||
const taxRates = await TaxRate.query().whereIn('id', entriesTaxRateIds);
|
||||
const taxRatesById = keyBy(taxRates, 'id');
|
||||
|
||||
const taxTransactions = aggregatedEntries.map((entry) => ({
|
||||
taxName: 'TAX NAME',
|
||||
taxCode: 'TAG_CODE',
|
||||
taxRateId: entry.taxRateId,
|
||||
referenceType: entry.referenceType,
|
||||
referenceId: entry.referenceId,
|
||||
taxAmount: entry.taxAmount,
|
||||
}));
|
||||
taxAmount: entry.taxRate || taxRatesById[entry.taxRateId]?.rate,
|
||||
})) as ITaxTransaction[];
|
||||
|
||||
await TaxRateTransaction.query().upsertGraph(taxTransactions);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Aggregates by tax code id and sums the amount.
|
||||
* @param {IItemEntry[]} itemEntries
|
||||
* @returns {}
|
||||
* @returns {IItemEntry[]}
|
||||
*/
|
||||
private aggregateItemEntriesByTaxCode(itemEntries: IItemEntry[]) {
|
||||
return chain(itemEntries.filter((item) => item.taxCode))
|
||||
.groupBy((item) => item.taxCode)
|
||||
private aggregateItemEntriesByTaxCode = (
|
||||
itemEntries: IItemEntry[]
|
||||
): IItemEntry[] => {
|
||||
return chain(itemEntries.filter((item) => item.taxRateId))
|
||||
.groupBy((item) => item.taxRateId)
|
||||
.values()
|
||||
.map((group) => ({ ...group[0], amount: sumBy(group, 'amount') }))
|
||||
.value();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param itemEntries
|
||||
*/
|
||||
private aggregateItemEntriesByReferenceTypeId(itemEntries: IItemEntry) {}
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes the tax transactions from the given item entries.
|
||||
* @param tenantId
|
||||
* @param itemEntries
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {string} referenceType - Reference type.
|
||||
* @param {number} referenceId - Reference id.
|
||||
*/
|
||||
public removeTaxTransactionsFromItemEntries(
|
||||
public async removeTaxTransactionsFromItemEntries(
|
||||
tenantId: number,
|
||||
itemEntries: IItemEntry[]
|
||||
referenceId: number,
|
||||
referenceType: string
|
||||
) {
|
||||
const { TaxRateTransaction } = this.tenancy.models(tenantId);
|
||||
|
||||
const filteredEntries = itemEntries.filter((item) => item.taxCode);
|
||||
await TaxRateTransaction.query()
|
||||
.where({ referenceType, referenceId })
|
||||
.delete();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,4 +2,5 @@ export const ERRORS = {
|
||||
TAX_RATE_NOT_FOUND: 'TAX_RATE_NOT_FOUND',
|
||||
TAX_CODE_NOT_UNIQUE: 'TAX_CODE_NOT_UNIQUE',
|
||||
ITEM_ENTRY_TAX_RATE_CODE_NOT_FOUND: 'ITEM_ENTRY_TAX_RATE_CODE_NOT_FOUND',
|
||||
ITEM_ENTRY_TAX_RATE_ID_NOT_FOUND: 'ITEM_ENTRY_TAX_RATE_ID_NOT_FOUND',
|
||||
};
|
||||
|
||||
@@ -19,6 +19,10 @@ export class SaleInvoiceTaxRateValidateSubscriber {
|
||||
events.saleInvoice.onCreating,
|
||||
this.validateSaleInvoiceEntriesTaxCodeExistanceOnCreating
|
||||
);
|
||||
bus.subscribe(
|
||||
events.saleInvoice.onCreating,
|
||||
this.validateSaleInvoiceEntriesTaxIdExistanceOnCreating
|
||||
);
|
||||
bus.subscribe(
|
||||
events.saleInvoice.onEditing,
|
||||
this.validateSaleInvoiceEntriesTaxCodeExistanceOnEditing
|
||||
@@ -27,7 +31,7 @@ export class SaleInvoiceTaxRateValidateSubscriber {
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate invoice entries tax rate code existance.
|
||||
* Validate invoice entries tax rate code existance when creating.
|
||||
* @param {ISaleInvoiceCreatingPaylaod}
|
||||
*/
|
||||
private validateSaleInvoiceEntriesTaxCodeExistanceOnCreating = async ({
|
||||
@@ -41,7 +45,21 @@ export class SaleInvoiceTaxRateValidateSubscriber {
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* Validate the tax rate id existance when creating.
|
||||
* @param {ISaleInvoiceCreatingPaylaod}
|
||||
*/
|
||||
private validateSaleInvoiceEntriesTaxIdExistanceOnCreating = async ({
|
||||
saleInvoiceDTO,
|
||||
tenantId,
|
||||
}: ISaleInvoiceCreatingPaylaod) => {
|
||||
await this.taxRateDTOValidator.validateItemEntriesTaxCodeId(
|
||||
tenantId,
|
||||
saleInvoiceDTO.entries
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate invoice entries tax rate code existance when editing.
|
||||
* @param {ISaleInvoiceEditingPayload}
|
||||
*/
|
||||
private validateSaleInvoiceEntriesTaxCodeExistanceOnEditing = async ({
|
||||
|
||||
@@ -20,7 +20,7 @@ export class WriteInvoiceTaxTransactionsSubscriber {
|
||||
this.writeInvoiceTaxTransactionsOnCreated
|
||||
);
|
||||
bus.subscribe(
|
||||
events.saleInvoice.onDeleted,
|
||||
events.saleInvoice.onDelete,
|
||||
this.removeInvoiceTaxTransactionsOnDeleted
|
||||
);
|
||||
return bus;
|
||||
@@ -50,7 +50,8 @@ export class WriteInvoiceTaxTransactionsSubscriber {
|
||||
}: ISaleInvoiceDeletedPayload) => {
|
||||
await this.writeTaxTransactions.removeTaxTransactionsFromItemEntries(
|
||||
tenantId,
|
||||
oldSaleInvoice.entries
|
||||
oldSaleInvoice.id,
|
||||
'SaleInvoice'
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user