mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 13:20:31 +00:00
feat: assign default sell/purchase tax rates to items (#261)
This commit is contained in:
@@ -149,6 +149,11 @@ export default class ItemsController extends BaseController {
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.TEXT }),
|
||||
check('sell_tax_rate_id').optional({ nullable: true }).isInt().toInt(),
|
||||
check('purchase_tax_rate_id')
|
||||
.optional({ nullable: true })
|
||||
.isInt()
|
||||
.toInt(),
|
||||
check('category_id')
|
||||
.optional({ nullable: true })
|
||||
.isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 })
|
||||
@@ -508,6 +513,28 @@ export default class ItemsController extends BaseController {
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'PURCHASE_TAX_RATE_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'PURCHASE_TAX_RATE_NOT_FOUND',
|
||||
message: 'Purchase tax rate has not found.',
|
||||
code: 410,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'SELL_TAX_RATE_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'SELL_TAX_RATE_NOT_FOUND',
|
||||
message: 'Sell tax rate is not found.',
|
||||
code: 420,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
exports.up = (knex) => {
|
||||
return knex.schema.table('items', (table) => {
|
||||
table
|
||||
.integer('sell_tax_rate_id')
|
||||
.unsigned()
|
||||
.references('id')
|
||||
.inTable('tax_rates');
|
||||
table
|
||||
.integer('purchase_tax_rate_id')
|
||||
.unsigned()
|
||||
.references('id')
|
||||
.inTable('tax_rates');
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = (knex) => {
|
||||
return knex.schema.dropTableIfExists('tax_rates');
|
||||
};
|
||||
@@ -22,6 +22,9 @@ export interface IItem {
|
||||
sellDescription: string;
|
||||
purchaseDescription: string;
|
||||
|
||||
sellTaxRateId: number;
|
||||
purchaseTaxRateId: number;
|
||||
|
||||
quantityOnHand: number;
|
||||
|
||||
note: string;
|
||||
@@ -54,6 +57,9 @@ export interface IItemDTO {
|
||||
sellDescription: string;
|
||||
purchaseDescription: string;
|
||||
|
||||
sellTaxRateId: number;
|
||||
purchaseTaxRateId: number;
|
||||
|
||||
quantityOnHand: number;
|
||||
|
||||
note: string;
|
||||
|
||||
@@ -36,6 +36,7 @@ export interface ITaxRateCreatedPayload {
|
||||
}
|
||||
|
||||
export interface ITaxRateEditingPayload {
|
||||
oldTaxRate: ITaxRate;
|
||||
editTaxRateDTO: IEditTaxRateDTO;
|
||||
tenantId: number;
|
||||
trx: Knex.Transaction;
|
||||
|
||||
@@ -83,6 +83,7 @@ import { SaleInvoiceTaxRateValidateSubscriber } from '@/services/TaxRates/subscr
|
||||
import { WriteInvoiceTaxTransactionsSubscriber } from '@/services/TaxRates/subscribers/WriteInvoiceTaxTransactionsSubscriber';
|
||||
import { BillTaxRateValidateSubscriber } from '@/services/TaxRates/subscribers/BillTaxRateValidateSubscriber';
|
||||
import { WriteBillTaxTransactionsSubscriber } from '@/services/TaxRates/subscribers/WriteBillTaxTransactionsSubscriber';
|
||||
import { SyncItemTaxRateOnEditTaxSubscriber } from '@/services/TaxRates/SyncItemTaxRateOnEditTaxSubscriber';
|
||||
|
||||
export default () => {
|
||||
return new EventPublisher();
|
||||
@@ -197,5 +198,7 @@ export const susbcribers = () => {
|
||||
// Tax Rates - Bills
|
||||
BillTaxRateValidateSubscriber,
|
||||
WriteBillTaxTransactionsSubscriber,
|
||||
|
||||
SyncItemTaxRateOnEditTaxSubscriber
|
||||
];
|
||||
};
|
||||
|
||||
@@ -65,6 +65,7 @@ export default class Item extends mixin(TenantModel, [
|
||||
const ItemEntry = require('models/ItemEntry');
|
||||
const WarehouseTransferEntry = require('models/WarehouseTransferEntry');
|
||||
const InventoryAdjustmentEntry = require('models/InventoryAdjustmentEntry');
|
||||
const TaxRate = require('models/TaxRate');
|
||||
|
||||
return {
|
||||
/**
|
||||
@@ -178,11 +179,35 @@ export default class Item extends mixin(TenantModel, [
|
||||
to: 'media.id',
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Item may has sell tax rate.
|
||||
*/
|
||||
sellTaxRate: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: TaxRate.default,
|
||||
join: {
|
||||
from: 'items.sellTaxRateId',
|
||||
to: 'tax_rates.id',
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Item may has purchase tax rate.
|
||||
*/
|
||||
purchaseTaxRate: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: TaxRate.default,
|
||||
join: {
|
||||
from: 'items.purchaseTaxRateId',
|
||||
to: 'tax_rates.id',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
*/
|
||||
static get secureDeleteRelations() {
|
||||
return [
|
||||
|
||||
@@ -55,6 +55,18 @@ export class CreateItem {
|
||||
itemDTO.inventoryAccountId
|
||||
);
|
||||
}
|
||||
if (itemDTO.purchaseTaxRateId) {
|
||||
await this.validators.validatePurchaseTaxRateExistance(
|
||||
tenantId,
|
||||
itemDTO.purchaseTaxRateId
|
||||
);
|
||||
}
|
||||
if (itemDTO.sellTaxRateId) {
|
||||
await this.validators.validateSellTaxRateExistance(
|
||||
tenantId,
|
||||
itemDTO.sellTaxRateId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -76,6 +76,20 @@ export class EditItem {
|
||||
itemDTO.inventoryAccountId
|
||||
);
|
||||
}
|
||||
// Validate the purchase tax rate id existance.
|
||||
if (itemDTO.purchaseTaxRateId) {
|
||||
await this.validators.validatePurchaseTaxRateExistance(
|
||||
tenantId,
|
||||
itemDTO.purchaseTaxRateId
|
||||
);
|
||||
}
|
||||
// Validate the sell tax rate id existance.
|
||||
if (itemDTO.sellTaxRateId) {
|
||||
await this.validators.validateSellTaxRateExistance(
|
||||
tenantId,
|
||||
itemDTO.sellTaxRateId
|
||||
);
|
||||
}
|
||||
// Validate inventory account should be modified in inventory item
|
||||
// has inventory transactions.
|
||||
await this.validators.validateItemInvnetoryAccountModified(
|
||||
|
||||
@@ -27,6 +27,8 @@ export class GetItem {
|
||||
.withGraphFetched('category')
|
||||
.withGraphFetched('costAccount')
|
||||
.withGraphFetched('itemWarehouses.warehouse')
|
||||
.withGraphFetched('sellTaxRate')
|
||||
.withGraphFetched('purchaseTaxRate')
|
||||
.throwIfNotFound();
|
||||
|
||||
return this.transformer.transform(tenantId, item, new ItemTransformer());
|
||||
|
||||
@@ -241,4 +241,40 @@ export class ItemsValidators {
|
||||
throw new ServiceError(ERRORS.ITEM_CANNOT_CHANGE_INVENTORY_TYPE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the purchase tax rate id existance.
|
||||
* @param {number} tenantId -
|
||||
* @param {number} taxRateId -
|
||||
*/
|
||||
public async validatePurchaseTaxRateExistance(
|
||||
tenantId: number,
|
||||
taxRateId: number
|
||||
) {
|
||||
const { TaxRate } = this.tenancy.models(tenantId);
|
||||
|
||||
const foundTaxRate = await TaxRate.query().findById(taxRateId);
|
||||
|
||||
if (!foundTaxRate) {
|
||||
throw new ServiceError(ERRORS.PURCHASE_TAX_RATE_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the sell tax rate id existance.
|
||||
* @param {number} tenantId
|
||||
* @param {number} taxRateId
|
||||
*/
|
||||
public async validateSellTaxRateExistance(
|
||||
tenantId: number,
|
||||
taxRateId: number
|
||||
) {
|
||||
const { TaxRate } = this.tenancy.models(tenantId);
|
||||
|
||||
const foundTaxRate = await TaxRate.query().findById(taxRateId);
|
||||
|
||||
if (!foundTaxRate) {
|
||||
throw new ServiceError(ERRORS.SELL_TAX_RATE_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,10 @@ export const ERRORS = {
|
||||
TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS: 'TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS',
|
||||
INVENTORY_ACCOUNT_CANNOT_MODIFIED: 'INVENTORY_ACCOUNT_CANNOT_MODIFIED',
|
||||
|
||||
ITEM_HAS_ASSOCIATED_TRANSACTIONS: 'ITEM_HAS_ASSOCIATED_TRANSACTIONS'
|
||||
ITEM_HAS_ASSOCIATED_TRANSACTIONS: 'ITEM_HAS_ASSOCIATED_TRANSACTIONS',
|
||||
|
||||
PURCHASE_TAX_RATE_NOT_FOUND: 'PURCHASE_TAX_RATE_NOT_FOUND',
|
||||
SELL_TAX_RATE_NOT_FOUND: 'SELL_TAX_RATE_NOT_FOUND',
|
||||
};
|
||||
|
||||
export const DEFAULT_VIEW_COLUMNS = [];
|
||||
|
||||
@@ -115,6 +115,7 @@ export class EditTaxRateService {
|
||||
// Triggers `onTaxRateEdited` event.
|
||||
await this.eventPublisher.emitAsync(events.taxRates.onEdited, {
|
||||
editTaxRateDTO,
|
||||
oldTaxRate,
|
||||
taxRate,
|
||||
tenantId,
|
||||
trx,
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Knex } from 'knex';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
|
||||
@Service()
|
||||
export class SyncItemTaxRateOnEditTaxRate {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Syncs the new tax rate created to item default sell tax rate.
|
||||
* @param {number} tenantId
|
||||
* @param {number} itemId
|
||||
* @param {number} sellTaxRateId
|
||||
*/
|
||||
public updateItemSellTaxRate = async (
|
||||
tenantId: number,
|
||||
oldSellTaxRateId: number,
|
||||
sellTaxRateId: number,
|
||||
trx?: Knex.Transaction
|
||||
) => {
|
||||
const { Item } = this.tenancy.models(tenantId);
|
||||
|
||||
// Can't continue if the old and new sell tax rate id are equal.
|
||||
if (oldSellTaxRateId === sellTaxRateId) return;
|
||||
|
||||
await Item.query().where('sellTaxRateId', oldSellTaxRateId).update({
|
||||
sellTaxRateId,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Syncs the new tax rate created to item default purchase tax rate.
|
||||
* @param {number} tenantId
|
||||
* @param {number} itemId
|
||||
* @param {number} purchaseTaxRateId
|
||||
*/
|
||||
public updateItemPurchaseTaxRate = async (
|
||||
tenantId: number,
|
||||
oldPurchaseTaxRateId: number,
|
||||
purchaseTaxRateId: number,
|
||||
trx?: Knex.Transaction
|
||||
) => {
|
||||
const { Item } = this.tenancy.models(tenantId);
|
||||
|
||||
// Can't continue if the old and new sell tax rate id are equal.
|
||||
if (oldPurchaseTaxRateId === purchaseTaxRateId) return;
|
||||
|
||||
await Item.query(trx)
|
||||
.where('purchaseTaxRateId', oldPurchaseTaxRateId)
|
||||
.update({
|
||||
purchaseTaxRateId,
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { SyncItemTaxRateOnEditTaxRate } from './SyncItemTaxRateOnEditTaxRate';
|
||||
import events from '@/subscribers/events';
|
||||
import { ITaxRateEditedPayload } from '@/interfaces';
|
||||
import { runAfterTransaction } from '../UnitOfWork/TransactionsHooks';
|
||||
|
||||
@Service()
|
||||
export class SyncItemTaxRateOnEditTaxSubscriber {
|
||||
@Inject()
|
||||
private syncItemRateOnEdit: SyncItemTaxRateOnEditTaxRate;
|
||||
|
||||
/**
|
||||
* Attaches events with handles.
|
||||
*/
|
||||
public attach(bus) {
|
||||
bus.subscribe(
|
||||
events.taxRates.onEdited,
|
||||
this.handleSyncNewTaxRateToItemTaxRate
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Syncs the new tax rate created to default item tax rates.
|
||||
* @param {ITaxRateEditedPayload} payload -
|
||||
*/
|
||||
private handleSyncNewTaxRateToItemTaxRate = async ({
|
||||
taxRate,
|
||||
tenantId,
|
||||
oldTaxRate,
|
||||
trx,
|
||||
}: ITaxRateEditedPayload) => {
|
||||
runAfterTransaction(trx, async () => {
|
||||
await this.syncItemRateOnEdit.updateItemPurchaseTaxRate(
|
||||
tenantId,
|
||||
oldTaxRate.id,
|
||||
taxRate.id
|
||||
);
|
||||
await this.syncItemRateOnEdit.updateItemSellTaxRate(
|
||||
tenantId,
|
||||
oldTaxRate.id,
|
||||
taxRate.id
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user