mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-19 22:30:31 +00:00
feat: tax rates on sale invoice service
This commit is contained in:
@@ -169,8 +169,9 @@ export default class SaleInvoicesController extends BaseController {
|
|||||||
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
|
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||||
check('project_id').optional({ nullable: true }).isNumeric().toInt(),
|
check('project_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||||
|
|
||||||
check('entries').exists().isArray({ min: 1 }),
|
check('is_tax_exclusive').optional().isBoolean().toBoolean(),
|
||||||
|
|
||||||
|
check('entries').exists().isArray({ min: 1 }),
|
||||||
check('entries.*.index').exists().isNumeric().toInt(),
|
check('entries.*.index').exists().isNumeric().toInt(),
|
||||||
check('entries.*.item_id').exists().isNumeric().toInt(),
|
check('entries.*.item_id').exists().isNumeric().toInt(),
|
||||||
check('entries.*.rate').exists().isNumeric().toFloat(),
|
check('entries.*.rate').exists().isNumeric().toFloat(),
|
||||||
@@ -183,6 +184,8 @@ export default class SaleInvoicesController extends BaseController {
|
|||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.trim()
|
.trim()
|
||||||
.escape(),
|
.escape(),
|
||||||
|
check('entries.*.tax_code').optional({ nullable: true }).trim().escape(),
|
||||||
|
check('entries.*.tax_rate').optional().isNumeric().toFloat(),
|
||||||
check('entries.*.warehouse_id')
|
check('entries.*.warehouse_id')
|
||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isNumeric()
|
.isNumeric()
|
||||||
@@ -756,6 +759,11 @@ export default class SaleInvoicesController 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: 5000 }],
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,25 @@
|
|||||||
exports.up = (knex) => {
|
exports.up = (knex) => {
|
||||||
return knex.schema.createTable('tax_rates', (table) => {
|
return knex.schema
|
||||||
table.increments();
|
.createTable('tax_rates', (table) => {
|
||||||
table.string('name');
|
table.increments();
|
||||||
table.string('code');
|
table.string('name');
|
||||||
table.decimal('rate');
|
table.string('code');
|
||||||
table.boolean('is_non_recoverable');
|
table.decimal('rate');
|
||||||
table.boolean('is_compound');
|
table.boolean('is_non_recoverable');
|
||||||
table.integer('status');
|
table.boolean('is_compound');
|
||||||
table.timestamps();
|
table.integer('status');
|
||||||
});
|
table.timestamps();
|
||||||
|
})
|
||||||
|
.table('items_entries', (table) => {
|
||||||
|
table.boolean(['is_tax_exclusive']);
|
||||||
|
table.string('tax_code');
|
||||||
|
table.decimal('tax_rate');
|
||||||
|
table.decimal('tax_amount_withheld')
|
||||||
|
})
|
||||||
|
.table('sales_invoices', (table) => {
|
||||||
|
table.boolean(['is_tax_exclusive']);
|
||||||
|
table.decimal('tax_amount_withheld')
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.down = (knex) => {
|
exports.down = (knex) => {
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ export interface IItemEntry {
|
|||||||
projectRefType?: ProjectLinkRefType;
|
projectRefType?: ProjectLinkRefType;
|
||||||
projectRefInvoicedAmount?: number;
|
projectRefInvoicedAmount?: number;
|
||||||
|
|
||||||
|
taxCode: string;
|
||||||
|
taxRate: number;
|
||||||
|
|
||||||
item?: IItem;
|
item?: IItem;
|
||||||
|
|
||||||
allocatedCostEntries?: IBillLandedCostEntry[];
|
allocatedCostEntries?: IBillLandedCostEntry[];
|
||||||
@@ -46,6 +49,9 @@ export interface IItemEntryDTO {
|
|||||||
projectRefId?: number;
|
projectRefId?: number;
|
||||||
projectRefType?: ProjectLinkRefType;
|
projectRefType?: ProjectLinkRefType;
|
||||||
projectRefInvoicedAmount?: number;
|
projectRefInvoicedAmount?: number;
|
||||||
|
|
||||||
|
taxCode: string;
|
||||||
|
taxRate: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ProjectLinkRefType {
|
export enum ProjectLinkRefType {
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export interface ISaleInvoiceDTO {
|
|||||||
exchangeRate?: number;
|
exchangeRate?: number;
|
||||||
invoiceMessage: string;
|
invoiceMessage: string;
|
||||||
termsConditions: string;
|
termsConditions: string;
|
||||||
|
isTaxExclusive: boolean;
|
||||||
entries: IItemEntryDTO[];
|
entries: IItemEntryDTO[];
|
||||||
delivered: boolean;
|
delivered: boolean;
|
||||||
|
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ import { ProjectBillableTasksSubscriber } from '@/services/Projects/Projects/Pro
|
|||||||
import { ProjectBillableExpensesSubscriber } from '@/services/Projects/Projects/ProjectBillableExpenseSubscriber';
|
import { ProjectBillableExpensesSubscriber } from '@/services/Projects/Projects/ProjectBillableExpenseSubscriber';
|
||||||
import { ProjectBillableBillSubscriber } from '@/services/Projects/Projects/ProjectBillableBillSubscriber';
|
import { ProjectBillableBillSubscriber } from '@/services/Projects/Projects/ProjectBillableBillSubscriber';
|
||||||
import { SyncActualTimeTaskSubscriber } from '@/services/Projects/Times/SyncActualTimeTaskSubscriber';
|
import { SyncActualTimeTaskSubscriber } from '@/services/Projects/Times/SyncActualTimeTaskSubscriber';
|
||||||
|
import { SaleInvoiceTaxRateValidateSubscriber } from '@/services/TaxRates/subscribers/SaleInvoiceTaxRateValidateSubscriber';
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
return new EventPublisher();
|
return new EventPublisher();
|
||||||
@@ -187,5 +188,6 @@ export const susbcribers = () => {
|
|||||||
ProjectBillableTasksSubscriber,
|
ProjectBillableTasksSubscriber,
|
||||||
ProjectBillableExpensesSubscriber,
|
ProjectBillableExpensesSubscriber,
|
||||||
ProjectBillableBillSubscriber,
|
ProjectBillableBillSubscriber,
|
||||||
|
SaleInvoiceTaxRateValidateSubscriber
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { ServiceError } from '@/exceptions';
|
import { ServiceError } from '@/exceptions';
|
||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import HasTenancyService from '../Tenancy/TenancyService';
|
import HasTenancyService from '../Tenancy/TenancyService';
|
||||||
import { ITaxRate } from '@/interfaces';
|
import { IItemEntryDTO, ITaxRate } from '@/interfaces';
|
||||||
import { ERRORS } from './constants';
|
import { ERRORS } from './constants';
|
||||||
|
import { difference } from 'lodash';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class CommandTaxRatesValidators {
|
export class CommandTaxRatesValidators {
|
||||||
@@ -33,4 +34,27 @@ export class CommandTaxRatesValidators {
|
|||||||
throw new ServiceError(ERRORS.TAX_CODE_NOT_UNIQUE);
|
throw new ServiceError(ERRORS.TAX_CODE_NOT_UNIQUE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the tax codes of the given item entries DTO.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {IItemEntryDTO[]} itemEntriesDTO
|
||||||
|
*/
|
||||||
|
public async validateItemEntriesTaxCode(
|
||||||
|
tenantId: number,
|
||||||
|
itemEntriesDTO: IItemEntryDTO[]
|
||||||
|
) {
|
||||||
|
const { TaxRate } = this.tenancy.models(tenantId);
|
||||||
|
const filteredTaxEntries = itemEntriesDTO.filter((e) => e.taxCode);
|
||||||
|
const taxCodes = filteredTaxEntries.map((e) => e.taxCode);
|
||||||
|
|
||||||
|
const foundTaxCodes = await TaxRate.query().whereIn('code', taxCodes);
|
||||||
|
const foundCodes = foundTaxCodes.map((tax) => tax.code);
|
||||||
|
|
||||||
|
const notFoundTaxCodes = difference(taxCodes, foundCodes);
|
||||||
|
|
||||||
|
if (notFoundTaxCodes.length > 0) {
|
||||||
|
throw new ServiceError(ERRORS.ITEM_ENTRY_TAX_RATE_CODE_NOT_FOUND);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export const ERRORS = {
|
export const ERRORS = {
|
||||||
TAX_RATE_NOT_FOUND: 'TAX_RATE_NOT_FOUND',
|
TAX_RATE_NOT_FOUND: 'TAX_RATE_NOT_FOUND',
|
||||||
TAX_CODE_NOT_UNIQUE: 'TAX_CODE_NOT_UNIQUE',
|
TAX_CODE_NOT_UNIQUE: 'TAX_CODE_NOT_UNIQUE',
|
||||||
|
ITEM_ENTRY_TAX_RATE_CODE_NOT_FOUND: 'ITEM_ENTRY_TAX_RATE_CODE_NOT_FOUND',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import {
|
||||||
|
ISaleEstimateCreatingPayload,
|
||||||
|
ISaleEstimateEditingPayload,
|
||||||
|
ISaleInvoiceCreatingPaylaod,
|
||||||
|
ISaleInvoiceEditingPayload,
|
||||||
|
} from '@/interfaces';
|
||||||
|
import events from '@/subscribers/events';
|
||||||
|
import { CommandTaxRatesValidators } from '../CommandTaxRatesValidators';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class SaleEstimateTaxRateValidateSubscriber {
|
||||||
|
@Inject()
|
||||||
|
private taxRateDTOValidator: CommandTaxRatesValidators;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attaches events with handlers.
|
||||||
|
*/
|
||||||
|
public attach(bus) {
|
||||||
|
bus.subscribe(
|
||||||
|
events.saleEstimate.onCreating,
|
||||||
|
this.validateSaleEstimateEntriesTaxCodeExistanceOnCreating
|
||||||
|
);
|
||||||
|
bus.subscribe(
|
||||||
|
events.saleEstimate.onEditing,
|
||||||
|
this.validateSaleEstimateEntriesTaxCodeExistanceOnEditing
|
||||||
|
);
|
||||||
|
return bus;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate invoice entries tax rate code existance.
|
||||||
|
* @param {ISaleInvoiceCreatingPaylaod}
|
||||||
|
*/
|
||||||
|
private validateSaleEstimateEntriesTaxCodeExistanceOnCreating = async ({
|
||||||
|
estimateDTO,
|
||||||
|
tenantId,
|
||||||
|
}: ISaleEstimateCreatingPayload) => {
|
||||||
|
await this.taxRateDTOValidator.validateItemEntriesTaxCode(
|
||||||
|
tenantId,
|
||||||
|
estimateDTO.entries
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {ISaleInvoiceEditingPayload}
|
||||||
|
*/
|
||||||
|
private validateSaleEstimateEntriesTaxCodeExistanceOnEditing = async ({
|
||||||
|
tenantId,
|
||||||
|
estimateDTO,
|
||||||
|
}: ISaleEstimateEditingPayload) => {
|
||||||
|
await this.taxRateDTOValidator.validateItemEntriesTaxCode(
|
||||||
|
tenantId,
|
||||||
|
estimateDTO.entries
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import {
|
||||||
|
ISaleInvoiceCreatingPaylaod,
|
||||||
|
ISaleInvoiceEditingPayload,
|
||||||
|
} from '@/interfaces';
|
||||||
|
import events from '@/subscribers/events';
|
||||||
|
import { CommandTaxRatesValidators } from '../CommandTaxRatesValidators';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class SaleInvoiceTaxRateValidateSubscriber {
|
||||||
|
@Inject()
|
||||||
|
private taxRateDTOValidator: CommandTaxRatesValidators;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attaches events with handlers.
|
||||||
|
*/
|
||||||
|
public attach(bus) {
|
||||||
|
bus.subscribe(
|
||||||
|
events.saleInvoice.onCreating,
|
||||||
|
this.validateSaleInvoiceEntriesTaxCodeExistanceOnCreating
|
||||||
|
);
|
||||||
|
bus.subscribe(
|
||||||
|
events.saleInvoice.onEditing,
|
||||||
|
this.validateSaleInvoiceEntriesTaxCodeExistanceOnEditing
|
||||||
|
);
|
||||||
|
return bus;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate invoice entries tax rate code existance.
|
||||||
|
* @param {ISaleInvoiceCreatingPaylaod}
|
||||||
|
*/
|
||||||
|
private validateSaleInvoiceEntriesTaxCodeExistanceOnCreating = async ({
|
||||||
|
saleInvoiceDTO,
|
||||||
|
tenantId,
|
||||||
|
}: ISaleInvoiceCreatingPaylaod) => {
|
||||||
|
await this.taxRateDTOValidator.validateItemEntriesTaxCode(
|
||||||
|
tenantId,
|
||||||
|
saleInvoiceDTO.entries
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {ISaleInvoiceEditingPayload}
|
||||||
|
*/
|
||||||
|
private validateSaleInvoiceEntriesTaxCodeExistanceOnEditing = async ({
|
||||||
|
tenantId,
|
||||||
|
saleInvoiceDTO,
|
||||||
|
}: ISaleInvoiceEditingPayload) => {
|
||||||
|
await this.taxRateDTOValidator.validateItemEntriesTaxCode(
|
||||||
|
tenantId,
|
||||||
|
saleInvoiceDTO.entries
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import {
|
||||||
|
ISaleReceiptCreatingPayload,
|
||||||
|
ISaleReceiptEditingPayload,
|
||||||
|
} from '@/interfaces';
|
||||||
|
import events from '@/subscribers/events';
|
||||||
|
import { CommandTaxRatesValidators } from '../CommandTaxRatesValidators';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class SaleReceiptTaxRateValidateSubscriber {
|
||||||
|
@Inject()
|
||||||
|
private taxRateDTOValidator: CommandTaxRatesValidators;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attaches events with handlers.
|
||||||
|
*/
|
||||||
|
public attach(bus) {
|
||||||
|
bus.subscribe(
|
||||||
|
events.saleReceipt.onCreating,
|
||||||
|
this.validateSaleReceiptEntriesTaxCodeExistanceOnCreating
|
||||||
|
);
|
||||||
|
bus.subscribe(
|
||||||
|
events.saleReceipt.onEditing,
|
||||||
|
this.validateSaleReceiptEntriesTaxCodeExistanceOnEditing
|
||||||
|
);
|
||||||
|
return bus;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate receipt entries tax rate code existance.
|
||||||
|
* @param {ISaleInvoiceCreatingPaylaod}
|
||||||
|
*/
|
||||||
|
private validateSaleReceiptEntriesTaxCodeExistanceOnCreating = async ({
|
||||||
|
tenantId,
|
||||||
|
saleReceiptDTO,
|
||||||
|
}: ISaleReceiptCreatingPayload) => {
|
||||||
|
await this.taxRateDTOValidator.validateItemEntriesTaxCode(
|
||||||
|
tenantId,
|
||||||
|
saleReceiptDTO.entries
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {ISaleInvoiceEditingPayload}
|
||||||
|
*/
|
||||||
|
private validateSaleReceiptEntriesTaxCodeExistanceOnEditing = async ({
|
||||||
|
tenantId,
|
||||||
|
saleReceiptDTO,
|
||||||
|
}: ISaleReceiptEditingPayload) => {
|
||||||
|
await this.taxRateDTOValidator.validateItemEntriesTaxCode(
|
||||||
|
tenantId,
|
||||||
|
saleReceiptDTO.entries
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user