Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: assign default sell/purchase tax rates to items #261

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions packages/server/src/api/controllers/Items/Items.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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');
};
6 changes: 6 additions & 0 deletions packages/server/src/interfaces/Item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ export interface IItem {
sellDescription: string;
purchaseDescription: string;

sellTaxRateId: number;
purchaseTaxRateId: number;

quantityOnHand: number;

note: string;
Expand Down Expand Up @@ -54,6 +57,9 @@ export interface IItemDTO {
sellDescription: string;
purchaseDescription: string;

sellTaxRateId: number;
purchaseTaxRateId: number;

quantityOnHand: number;

note: string;
Expand Down
1 change: 1 addition & 0 deletions packages/server/src/interfaces/TaxRate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export interface ITaxRateCreatedPayload {
}

export interface ITaxRateEditingPayload {
oldTaxRate: ITaxRate;
editTaxRateDTO: IEditTaxRateDTO;
tenantId: number;
trx: Knex.Transaction;
Expand Down
3 changes: 3 additions & 0 deletions packages/server/src/loaders/eventEmitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -197,5 +198,7 @@ export const susbcribers = () => {
// Tax Rates - Bills
BillTaxRateValidateSubscriber,
WriteBillTaxTransactionsSubscriber,

SyncItemTaxRateOnEditTaxSubscriber
];
};
27 changes: 26 additions & 1 deletion packages/server/src/models/Item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
/**
Expand Down Expand Up @@ -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 [
Expand Down
12 changes: 12 additions & 0 deletions packages/server/src/services/Items/CreateItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
}
}

/**
Expand Down
14 changes: 14 additions & 0 deletions packages/server/src/services/Items/EditItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
2 changes: 2 additions & 0 deletions packages/server/src/services/Items/GetItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
36 changes: 36 additions & 0 deletions packages/server/src/services/Items/ItemValidators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
5 changes: 4 additions & 1 deletion packages/server/src/services/Items/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [];
Expand Down
1 change: 1 addition & 0 deletions packages/server/src/services/TaxRates/EditTaxRate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ export class EditTaxRateService {
// Triggers `onTaxRateEdited` event.
await this.eventPublisher.emitAsync(events.taxRates.onEdited, {
editTaxRateDTO,
oldTaxRate,
taxRate,
tenantId,
trx,
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
});
};
}
Original file line number Diff line number Diff line change
@@ -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
);
});
};
}
Loading
Loading