mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-19 14:20:31 +00:00
WIP: Allocate landed cost.
This commit is contained in:
@@ -111,6 +111,7 @@ export default class ExpensesController extends BaseController {
|
|||||||
.trim()
|
.trim()
|
||||||
.escape()
|
.escape()
|
||||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||||
|
check('categories.*.landed_cost').optional().isBoolean().toBoolean(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,11 +252,8 @@ export default class ExpensesController extends BaseController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const {
|
const { expenses, pagination, filterMeta } =
|
||||||
expenses,
|
await this.expensesService.getExpensesList(tenantId, filter);
|
||||||
pagination,
|
|
||||||
filterMeta,
|
|
||||||
} = await this.expensesService.getExpensesList(tenantId, filter);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
expenses,
|
expenses,
|
||||||
@@ -345,6 +343,11 @@ export default class ExpensesController extends BaseController {
|
|||||||
errors: [{ type: 'CONTACT_NOT_FOUND', code: 800 }],
|
errors: [{ type: 'CONTACT_NOT_FOUND', code: 800 }],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (error.errorType === 'EXPENSE_HAS_ASSOCIATED_LANDED_COST') {
|
||||||
|
return res.status(400).send({
|
||||||
|
errors: [{ type: 'EXPENSE_HAS_ASSOCIATED_LANDED_COST', code: 900 }],
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,6 +110,10 @@ export default class BillsController extends BaseController {
|
|||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.trim()
|
.trim()
|
||||||
.escape(),
|
.escape(),
|
||||||
|
check('entries.*.landed_cost')
|
||||||
|
.optional({ nullable: true })
|
||||||
|
.isBoolean()
|
||||||
|
.toBoolean(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,6 +145,10 @@ export default class BillsController extends BaseController {
|
|||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.trim()
|
.trim()
|
||||||
.escape(),
|
.escape(),
|
||||||
|
check('entries.*.landedCost')
|
||||||
|
.optional({ nullable: true })
|
||||||
|
.isBoolean()
|
||||||
|
.toBoolean(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,11 +309,8 @@ export default class BillsController extends BaseController {
|
|||||||
filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles);
|
filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const {
|
const { bills, pagination, filterMeta } =
|
||||||
bills,
|
await this.billsService.getBills(tenantId, filter);
|
||||||
pagination,
|
|
||||||
filterMeta,
|
|
||||||
} = await this.billsService.getBills(tenantId, filter);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
bills,
|
bills,
|
||||||
@@ -397,17 +402,24 @@ export default class BillsController extends BaseController {
|
|||||||
if (error.errorType === 'contact_not_found') {
|
if (error.errorType === 'contact_not_found') {
|
||||||
return res.boom.badRequest(null, {
|
return res.boom.badRequest(null, {
|
||||||
errors: [
|
errors: [
|
||||||
{ type: 'VENDOR_NOT_FOUND', message: 'Vendor not found.', code: 1200 },
|
{
|
||||||
|
type: 'VENDOR_NOT_FOUND',
|
||||||
|
message: 'Vendor not found.',
|
||||||
|
code: 1200,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (error.errorType === 'BILL_HAS_ASSOCIATED_PAYMENT_ENTRIES') {
|
if (error.errorType === 'BILL_HAS_ASSOCIATED_PAYMENT_ENTRIES') {
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
errors: [{
|
errors: [
|
||||||
type: 'BILL_HAS_ASSOCIATED_PAYMENT_ENTRIES',
|
{
|
||||||
message: 'Cannot delete bill that has associated payment transactions.',
|
type: 'BILL_HAS_ASSOCIATED_PAYMENT_ENTRIES',
|
||||||
code: 1200
|
message:
|
||||||
}],
|
'Cannot delete bill that has associated payment transactions.',
|
||||||
|
code: 1200,
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
289
server/src/api/controllers/Purchases/LandedCost.ts
Normal file
289
server/src/api/controllers/Purchases/LandedCost.ts
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
import { Router, Request, Response, NextFunction } from 'express';
|
||||||
|
import { check, param, query } from 'express-validator';
|
||||||
|
import { Service, Inject } from 'typedi';
|
||||||
|
import { ServiceError } from 'exceptions';
|
||||||
|
import AllocateLandedCostService from 'services/Purchases/LandedCost';
|
||||||
|
import LandedCostListing from 'services/Purchases/LandedCost/LandedCostListing';
|
||||||
|
import BaseController from '../BaseController';
|
||||||
|
import { ResultSetDependencies } from 'mathjs';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export default class BillAllocateLandedCost extends BaseController {
|
||||||
|
@Inject()
|
||||||
|
allocateLandedCost: AllocateLandedCostService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
landedCostListing: LandedCostListing;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Router constructor.
|
||||||
|
*/
|
||||||
|
public router() {
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/bills/:billId/allocate',
|
||||||
|
[
|
||||||
|
check('transaction_id').exists().isInt(),
|
||||||
|
check('transaction_type').exists().isIn(['Expense', 'Bill']),
|
||||||
|
check('transaction_entry_id').exists().isInt(),
|
||||||
|
|
||||||
|
check('allocation_method').exists().isIn(['value', 'quantity']),
|
||||||
|
check('description').optional({ nullable: true }),
|
||||||
|
|
||||||
|
check('items').isArray({ min: 1 }),
|
||||||
|
check('items.*.entry_id').isInt(),
|
||||||
|
check('items.*.cost').isDecimal(),
|
||||||
|
],
|
||||||
|
this.validationResult,
|
||||||
|
this.calculateLandedCost.bind(this),
|
||||||
|
this.handleServiceErrors
|
||||||
|
);
|
||||||
|
router.delete(
|
||||||
|
'/:allocatedLandedCostId',
|
||||||
|
[param('allocatedLandedCostId').exists().isInt()],
|
||||||
|
this.validationResult,
|
||||||
|
this.deleteAllocatedLandedCost.bind(this),
|
||||||
|
this.handleServiceErrors
|
||||||
|
);
|
||||||
|
router.get(
|
||||||
|
'/transactions',
|
||||||
|
[query('transaction_type').exists().isIn(['Expense', 'Bill'])],
|
||||||
|
this.validationResult,
|
||||||
|
this.getLandedCostTransactions.bind(this),
|
||||||
|
this.handleServiceErrors
|
||||||
|
);
|
||||||
|
router.get(
|
||||||
|
'/bills/:billId/transactions',
|
||||||
|
[param('billId').exists()],
|
||||||
|
this.validationResult,
|
||||||
|
this.getBillLandedCostTransactions.bind(this),
|
||||||
|
this.handleServiceErrors
|
||||||
|
);
|
||||||
|
return router;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the landed cost transactions of the given query.
|
||||||
|
* @param {Request} req
|
||||||
|
* @param {Response} res
|
||||||
|
* @param {NextFunction} next
|
||||||
|
*/
|
||||||
|
private async getLandedCostTransactions(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) {
|
||||||
|
const { tenantId } = req;
|
||||||
|
const query = this.matchedQueryData(req);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const transactions =
|
||||||
|
await this.landedCostListing.getLandedCostTransactions(tenantId, query);
|
||||||
|
return res.status(200).send({ transactions });
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allocate landed cost.
|
||||||
|
* @param {Request} req
|
||||||
|
* @param {Response} res
|
||||||
|
* @param {NextFunction} next
|
||||||
|
* @returns {Response}
|
||||||
|
*/
|
||||||
|
public async calculateLandedCost(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) {
|
||||||
|
const { tenantId } = req;
|
||||||
|
const { billId: purchaseInvoiceId } = req.params;
|
||||||
|
const landedCostDTO = this.matchedBodyData(req);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { billLandedCost } =
|
||||||
|
await this.allocateLandedCost.allocateLandedCost(
|
||||||
|
tenantId,
|
||||||
|
landedCostDTO,
|
||||||
|
purchaseInvoiceId
|
||||||
|
);
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
id: billLandedCost.id,
|
||||||
|
message: 'The items cost are located successfully.',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the allocated landed cost.
|
||||||
|
* @param {Request} req
|
||||||
|
* @param {Response} res
|
||||||
|
* @param {NextFunction} next
|
||||||
|
* @returns {Response}
|
||||||
|
*/
|
||||||
|
public async deleteAllocatedLandedCost(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): Promise<Response> {
|
||||||
|
const { tenantId } = req;
|
||||||
|
const { allocatedLandedCostId } = req.params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.allocateLandedCost.deleteAllocatedLandedCost(
|
||||||
|
tenantId,
|
||||||
|
allocatedLandedCostId
|
||||||
|
);
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
id: allocatedLandedCostId,
|
||||||
|
message: 'The allocated landed cost are delete successfully.',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the list unlocated landed costs.
|
||||||
|
* @param {Request} req
|
||||||
|
* @param {Response} res
|
||||||
|
* @param {NextFunction} next
|
||||||
|
*/
|
||||||
|
public async listLandedCosts(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) {
|
||||||
|
const query = this.matchedQueryData(req);
|
||||||
|
const { tenantId } = req;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const transactions =
|
||||||
|
await this.landedCostListing.getLandedCostTransactions(tenantId, query);
|
||||||
|
return res.status(200).send({ transactions });
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the bill landed cost transactions.
|
||||||
|
* @param {Request} req
|
||||||
|
* @param {Response} res
|
||||||
|
* @param {NextFunction} next
|
||||||
|
*/
|
||||||
|
public async getBillLandedCostTransactions(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): Promise<Response> {
|
||||||
|
const { tenantId } = req;
|
||||||
|
const { billId } = req.params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const transactions =
|
||||||
|
await this.landedCostListing.getBillLandedCostTransactions(
|
||||||
|
tenantId,
|
||||||
|
billId
|
||||||
|
);
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
billId,
|
||||||
|
transactions,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle service errors.
|
||||||
|
* @param {Request} req
|
||||||
|
* @param {Response} res
|
||||||
|
* @param {NextFunction} next
|
||||||
|
* @param {Error} error
|
||||||
|
*/
|
||||||
|
public handleServiceErrors(
|
||||||
|
error: Error,
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) {
|
||||||
|
if (error instanceof ServiceError) {
|
||||||
|
if (error.errorType === 'BILL_NOT_FOUND') {
|
||||||
|
return res.status(400).send({
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
type: 'BILL_NOT_FOUND',
|
||||||
|
code: 400,
|
||||||
|
message: 'The give bill id not found.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (error.errorType === 'LANDED_COST_TRANSACTION_NOT_FOUND') {
|
||||||
|
return res.status(400).send({
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
type: 'LANDED_COST_TRANSACTION_NOT_FOUND',
|
||||||
|
code: 200,
|
||||||
|
message: 'The given landed cost transaction id not found.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (error.errorType === 'LANDED_COST_ENTRY_NOT_FOUND') {
|
||||||
|
return res.status(400).send({
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
type: 'LANDED_COST_ENTRY_NOT_FOUND',
|
||||||
|
code: 300,
|
||||||
|
message: 'The given landed cost tranasction entry id not found.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (error.errorType === 'COST_AMOUNT_BIGGER_THAN_UNALLOCATED_AMOUNT') {
|
||||||
|
return res.status(400).send({
|
||||||
|
errors: [
|
||||||
|
{ type: 'COST_AMOUNT_BIGGER_THAN_UNALLOCATED_AMOUNT', code: 300 },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (error.errorType === 'LANDED_COST_ITEMS_IDS_NOT_FOUND') {
|
||||||
|
return res.status(400).send({
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
type: 'LANDED_COST_ITEMS_IDS_NOT_FOUND',
|
||||||
|
code: 200,
|
||||||
|
message: 'The given entries ids of purchase invoice not found.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (error.errorType === 'BILL_LANDED_COST_NOT_FOUND') {
|
||||||
|
return res.status(400).send({
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
type: 'BILL_LANDED_COST_NOT_FOUND',
|
||||||
|
code: 200,
|
||||||
|
message: 'The given bill located landed cost not found.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (error.errorType === 'COST_TRASNACTION_NOT_FOUND') {
|
||||||
|
return res.status(400).send({
|
||||||
|
errors: [{ type: 'COST_TRASNACTION_NOT_FOUND', code: 500 }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import { Router } from 'express';
|
|||||||
import { Container, Service } from 'typedi';
|
import { Container, Service } from 'typedi';
|
||||||
import Bills from 'api/controllers/Purchases/Bills'
|
import Bills from 'api/controllers/Purchases/Bills'
|
||||||
import BillPayments from 'api/controllers/Purchases/BillsPayments';
|
import BillPayments from 'api/controllers/Purchases/BillsPayments';
|
||||||
|
import BillAllocateLandedCost from './LandedCost';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class PurchasesController {
|
export default class PurchasesController {
|
||||||
@@ -11,6 +12,7 @@ export default class PurchasesController {
|
|||||||
|
|
||||||
router.use('/bills', Container.get(Bills).router());
|
router.use('/bills', Container.get(Bills).router());
|
||||||
router.use('/bill_payments', Container.get(BillPayments).router());
|
router.use('/bill_payments', Container.get(BillPayments).router());
|
||||||
|
router.use('/landed-cost', Container.get(BillAllocateLandedCost).router());
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -347,7 +347,7 @@ export default class SaleInvoicesController extends BaseController {
|
|||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
* @param {NextFunction} next
|
* @param {NextFunction} next
|
||||||
*/
|
*/
|
||||||
handleServiceErrors(
|
private handleServiceErrors(
|
||||||
error: Error,
|
error: Error,
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response,
|
res: Response,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ exports.up = function (knex) {
|
|||||||
table.text('sell_description').nullable();
|
table.text('sell_description').nullable();
|
||||||
table.text('purchase_description').nullable();
|
table.text('purchase_description').nullable();
|
||||||
table.integer('quantity_on_hand');
|
table.integer('quantity_on_hand');
|
||||||
|
table.boolean('landed_cost').nullable();
|
||||||
|
|
||||||
table.text('note').nullable();
|
table.text('note').nullable();
|
||||||
table.boolean('active');
|
table.boolean('active');
|
||||||
|
|||||||
@@ -1,20 +1,29 @@
|
|||||||
|
exports.up = function (knex) {
|
||||||
|
return knex.schema
|
||||||
|
.createTable('expenses_transactions', (table) => {
|
||||||
|
table.increments();
|
||||||
|
table.string('currency_code', 3);
|
||||||
|
table.text('description');
|
||||||
|
table
|
||||||
|
.integer('payment_account_id')
|
||||||
|
.unsigned()
|
||||||
|
.references('id')
|
||||||
|
.inTable('accounts');
|
||||||
|
table.integer('payee_id').unsigned().references('id').inTable('contacts');
|
||||||
|
table.string('reference_no');
|
||||||
|
|
||||||
exports.up = function(knex) {
|
table.decimal('total_amount', 13, 3);
|
||||||
return knex.schema.createTable('expenses_transactions', (table) => {
|
table.decimal('landed_cost_amount', 13, 3).defaultTo(0);
|
||||||
table.increments();
|
table.decimal('allocated_cost_amount', 13, 3).defaultTo(0);
|
||||||
table.decimal('total_amount', 13, 3);
|
|
||||||
table.string('currency_code', 3);
|
table.date('published_at').index();
|
||||||
table.text('description');
|
table.integer('user_id').unsigned().index();
|
||||||
table.integer('payment_account_id').unsigned().references('id').inTable('accounts');
|
table.date('payment_date').index();
|
||||||
table.integer('payee_id').unsigned().references('id').inTable('contacts');;
|
table.timestamps();
|
||||||
table.string('reference_no');
|
})
|
||||||
table.date('published_at').index();
|
.raw('ALTER TABLE `EXPENSES_TRANSACTIONS` AUTO_INCREMENT = 1000');
|
||||||
table.integer('user_id').unsigned().index();
|
|
||||||
table.date('payment_date').index();
|
|
||||||
table.timestamps();
|
|
||||||
}).raw('ALTER TABLE `EXPENSES_TRANSACTIONS` AUTO_INCREMENT = 1000');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.down = function(knex) {
|
exports.down = function (knex) {
|
||||||
return knex.schema.dropTableIfExists('expenses');
|
return knex.schema.dropTableIfExists('expenses');
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,16 +1,29 @@
|
|||||||
|
exports.up = function (knex) {
|
||||||
exports.up = function(knex) {
|
return knex.schema
|
||||||
return knex.schema.createTable('expense_transaction_categories', table => {
|
.createTable('expense_transaction_categories', (table) => {
|
||||||
table.increments();
|
table.increments();
|
||||||
table.integer('expense_account_id').unsigned().index().references('id').inTable('accounts');
|
table
|
||||||
table.integer('index').unsigned();
|
.integer('expense_account_id')
|
||||||
table.text('description');
|
.unsigned()
|
||||||
table.decimal('amount', 13, 3);
|
.index()
|
||||||
table.integer('expense_id').unsigned().index().references('id').inTable('expenses_transactions');
|
.references('id')
|
||||||
table.timestamps();
|
.inTable('accounts');
|
||||||
}).raw('ALTER TABLE `EXPENSE_TRANSACTION_CATEGORIES` AUTO_INCREMENT = 1000');;
|
table.integer('index').unsigned();
|
||||||
|
table.text('description');
|
||||||
|
table.decimal('amount', 13, 3);
|
||||||
|
table.decimal('allocated_cost_amount', 13, 3).defaultTo(0);
|
||||||
|
table.boolean('landed_cost').defaultTo(false);
|
||||||
|
table
|
||||||
|
.integer('expense_id')
|
||||||
|
.unsigned()
|
||||||
|
.index()
|
||||||
|
.references('id')
|
||||||
|
.inTable('expenses_transactions');
|
||||||
|
table.timestamps();
|
||||||
|
})
|
||||||
|
.raw('ALTER TABLE `EXPENSE_TRANSACTION_CATEGORIES` AUTO_INCREMENT = 1000');
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.down = function(knex) {
|
exports.down = function (knex) {
|
||||||
return knex.schema.dropTableIfExists('expense_transaction_categories');
|
return knex.schema.dropTableIfExists('expense_transaction_categories');
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
|
exports.up = function (knex) {
|
||||||
exports.up = function(knex) {
|
|
||||||
return knex.schema.createTable('bills', (table) => {
|
return knex.schema.createTable('bills', (table) => {
|
||||||
table.increments();
|
table.increments();
|
||||||
table.integer('vendor_id').unsigned().index().references('id').inTable('contacts');
|
table
|
||||||
|
.integer('vendor_id')
|
||||||
|
.unsigned()
|
||||||
|
.index()
|
||||||
|
.references('id')
|
||||||
|
.inTable('contacts');
|
||||||
table.string('bill_number');
|
table.string('bill_number');
|
||||||
table.date('bill_date').index();
|
table.date('bill_date').index();
|
||||||
table.date('due_date').index();
|
table.date('due_date').index();
|
||||||
@@ -12,6 +16,8 @@ exports.up = function(knex) {
|
|||||||
table.decimal('amount', 13, 3).defaultTo(0);
|
table.decimal('amount', 13, 3).defaultTo(0);
|
||||||
table.string('currency_code');
|
table.string('currency_code');
|
||||||
table.decimal('payment_amount', 13, 3).defaultTo(0);
|
table.decimal('payment_amount', 13, 3).defaultTo(0);
|
||||||
|
table.decimal('landed_cost_amount', 13, 3).defaultTo(0);
|
||||||
|
table.decimal('allocated_cost_amount', 13, 3).defaultTo(0);
|
||||||
table.string('inv_lot_number').index();
|
table.string('inv_lot_number').index();
|
||||||
table.date('opened_at').index();
|
table.date('opened_at').index();
|
||||||
table.integer('user_id').unsigned();
|
table.integer('user_id').unsigned();
|
||||||
@@ -19,6 +25,6 @@ exports.up = function(knex) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.down = function(knex) {
|
exports.down = function (knex) {
|
||||||
return knex.schema.dropTableIfExists('bills');
|
return knex.schema.dropTableIfExists('bills');
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
exports.up = function (knex) {
|
||||||
|
return knex.schema.createTable('bill_located_costs', (table) => {
|
||||||
|
table.increments();
|
||||||
|
|
||||||
|
table.decimal('amount', 13, 3).unsigned();
|
||||||
|
|
||||||
|
table.integer('fromTransactionId').unsigned();
|
||||||
|
table.string('fromTransactionType');
|
||||||
|
table.integer('fromTransactionEntryId').unsigned();
|
||||||
|
|
||||||
|
table.string('allocationMethod');
|
||||||
|
table.integer('costAccountId').unsigned();
|
||||||
|
table.text('description');
|
||||||
|
|
||||||
|
table.integer('billId').unsigned();
|
||||||
|
|
||||||
|
table.timestamps();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function (knex) {};
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
exports.up = function (knex) {
|
||||||
|
return knex.schema.createTable('bill_located_cost_entries', (table) => {
|
||||||
|
table.increments();
|
||||||
|
|
||||||
|
table.decimal('cost', 13, 3).unsigned();
|
||||||
|
table.integer('entry_id').unsigned();
|
||||||
|
table.integer('bill_located_cost_id').unsigned();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function (knex) {};
|
||||||
@@ -15,6 +15,8 @@ exports.up = function(knex) {
|
|||||||
table.integer('sell_account_id').unsigned().references('id').inTable('accounts');
|
table.integer('sell_account_id').unsigned().references('id').inTable('accounts');
|
||||||
table.integer('cost_account_id').unsigned().references('id').inTable('accounts');
|
table.integer('cost_account_id').unsigned().references('id').inTable('accounts');
|
||||||
|
|
||||||
|
table.boolean('landed_cost').defaultTo(false);
|
||||||
|
table.decimal('allocated_cost_amount', 13, 3);
|
||||||
table.timestamps();
|
table.timestamps();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,64 +1,69 @@
|
|||||||
import { IDynamicListFilterDTO } from "./DynamicFilter";
|
import { IDynamicListFilterDTO } from './DynamicFilter';
|
||||||
import { IItemEntry, IItemEntryDTO } from "./ItemEntry";
|
import { IItemEntry, IItemEntryDTO } from './ItemEntry';
|
||||||
|
|
||||||
export interface IBillDTO {
|
export interface IBillDTO {
|
||||||
vendorId: number,
|
vendorId: number;
|
||||||
billNumber: string,
|
billNumber: string;
|
||||||
billDate: Date,
|
billDate: Date;
|
||||||
dueDate: Date,
|
dueDate: Date;
|
||||||
referenceNo: string,
|
referenceNo: string;
|
||||||
status: string,
|
status: string;
|
||||||
note: string,
|
note: string;
|
||||||
amount: number,
|
amount: number;
|
||||||
paymentAmount: number,
|
paymentAmount: number;
|
||||||
open: boolean,
|
open: boolean;
|
||||||
entries: IItemEntryDTO[],
|
entries: IItemEntryDTO[];
|
||||||
};
|
}
|
||||||
|
|
||||||
export interface IBillEditDTO {
|
export interface IBillEditDTO {
|
||||||
vendorId: number,
|
vendorId: number;
|
||||||
billNumber: string,
|
billNumber: string;
|
||||||
billDate: Date,
|
billDate: Date;
|
||||||
dueDate: Date,
|
dueDate: Date;
|
||||||
referenceNo: string,
|
referenceNo: string;
|
||||||
status: string,
|
status: string;
|
||||||
note: string,
|
note: string;
|
||||||
amount: number,
|
amount: number;
|
||||||
paymentAmount: number,
|
paymentAmount: number;
|
||||||
open: boolean,
|
open: boolean;
|
||||||
entries: IItemEntryDTO[],
|
entries: IItemEntryDTO[];
|
||||||
};
|
}
|
||||||
|
|
||||||
export interface IBill {
|
export interface IBill {
|
||||||
id?: number,
|
id?: number;
|
||||||
|
|
||||||
vendorId: number,
|
vendorId: number;
|
||||||
billNumber: string,
|
billNumber: string;
|
||||||
billDate: Date,
|
billDate: Date;
|
||||||
dueDate: Date,
|
dueDate: Date;
|
||||||
referenceNo: string,
|
referenceNo: string;
|
||||||
status: string,
|
status: string;
|
||||||
note: string,
|
note: string;
|
||||||
amount: number,
|
|
||||||
paymentAmount: number,
|
|
||||||
currencyCode: string,
|
|
||||||
|
|
||||||
dueAmount: number,
|
amount: number;
|
||||||
overdueDays: number,
|
allocatedCostAmount: number;
|
||||||
|
landedCostAmount: number;
|
||||||
|
unallocatedCostAmount: number;
|
||||||
|
|
||||||
openedAt: Date | string,
|
paymentAmount: number;
|
||||||
|
currencyCode: string;
|
||||||
|
|
||||||
entries: IItemEntry[],
|
dueAmount: number;
|
||||||
userId: number,
|
overdueDays: number;
|
||||||
|
|
||||||
createdAt: Date,
|
openedAt: Date | string;
|
||||||
updateAt: Date,
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface IBillsFilter extends IDynamicListFilterDTO {
|
entries: IItemEntry[];
|
||||||
stringifiedFilterRoles?: string,
|
userId: number;
|
||||||
|
|
||||||
|
createdAt: Date;
|
||||||
|
updateAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IBillsFilter extends IDynamicListFilterDTO {
|
||||||
|
stringifiedFilterRoles?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IBillsService {
|
export interface IBillsService {
|
||||||
validateVendorHasNoBills(tenantId: number, vendorId: number): Promise<void>;
|
validateVendorHasNoBills(tenantId: number, vendorId: number): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,15 +27,20 @@ export interface IExpense {
|
|||||||
userId: number;
|
userId: number;
|
||||||
paymentDate: Date;
|
paymentDate: Date;
|
||||||
payeeId: number;
|
payeeId: number;
|
||||||
|
landedCostAmount: number;
|
||||||
|
allocatedCostAmount: number;
|
||||||
|
unallocatedCostAmount: number;
|
||||||
categories: IExpenseCategory[];
|
categories: IExpenseCategory[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IExpenseCategory {
|
export interface IExpenseCategory {
|
||||||
|
id?: number;
|
||||||
expenseAccountId: number;
|
expenseAccountId: number;
|
||||||
index: number;
|
index: number;
|
||||||
description: string;
|
description: string;
|
||||||
expenseId: number;
|
expenseId: number;
|
||||||
amount: number;
|
amount: number;
|
||||||
|
landedCost: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IExpenseDTO {
|
export interface IExpenseDTO {
|
||||||
@@ -56,6 +61,7 @@ export interface IExpenseCategoryDTO {
|
|||||||
index: number;
|
index: number;
|
||||||
description?: string;
|
description?: string;
|
||||||
expenseId: number;
|
expenseId: number;
|
||||||
|
landedCost?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IExpensesService {
|
export interface IExpensesService {
|
||||||
|
|||||||
@@ -17,8 +17,10 @@ export interface IItemEntry {
|
|||||||
|
|
||||||
sellAccountId: number,
|
sellAccountId: number,
|
||||||
costAccountId: number,
|
costAccountId: number,
|
||||||
|
|
||||||
|
landedCost?: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IItemEntryDTO {
|
export interface IItemEntryDTO {
|
||||||
|
landedCost?: boolean
|
||||||
}
|
}
|
||||||
85
server/src/interfaces/LandedCost.ts
Normal file
85
server/src/interfaces/LandedCost.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
export interface IBillLandedCost {
|
||||||
|
fromTransactionId: number;
|
||||||
|
fromTransactionType: string;
|
||||||
|
amount: number;
|
||||||
|
BillId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IBillLandedCostEntry {
|
||||||
|
id?: number,
|
||||||
|
cost: number,
|
||||||
|
entryId: number,
|
||||||
|
billLocatedCostId: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ILandedCostItemDTO {
|
||||||
|
entryId: number,
|
||||||
|
cost: number;
|
||||||
|
}
|
||||||
|
export type ILandedCostType = 'Expense' | 'Bill';
|
||||||
|
|
||||||
|
export interface ILandedCostDTO {
|
||||||
|
transactionType: ILandedCostType;
|
||||||
|
transactionId: number;
|
||||||
|
transactionEntryId: number,
|
||||||
|
allocationMethod: string;
|
||||||
|
description: string;
|
||||||
|
items: ILandedCostItemDTO[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ILandedCostQueryDTO {
|
||||||
|
vendorId: number;
|
||||||
|
fromDate: Date;
|
||||||
|
toDate: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IUnallocatedListCost {
|
||||||
|
costNumber: string;
|
||||||
|
costAmount: number;
|
||||||
|
unallocatedAmount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ILandedCostTransactionsQueryDTO {
|
||||||
|
transactionType: string,
|
||||||
|
date: Date,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ILandedCostEntriesQueryDTO {
|
||||||
|
transactionType: string,
|
||||||
|
transactionId: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ILandedCostTransaction {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
amount: number;
|
||||||
|
allocatedCostAmount: number;
|
||||||
|
unallocatedCostAmount: number;
|
||||||
|
transactionType: string;
|
||||||
|
entries?: ILandedCostTransactionEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ILandedCostTransactionEntry {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
code: string;
|
||||||
|
amount: number;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ILandedCostEntry {
|
||||||
|
id: number;
|
||||||
|
landedCost?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IBillLandedCostTransaction {
|
||||||
|
id: number,
|
||||||
|
fromTranscationId: number,
|
||||||
|
fromTransactionType: string;
|
||||||
|
fromTransactionEntryId: number;
|
||||||
|
|
||||||
|
billId: number,
|
||||||
|
allocationMethod: string;
|
||||||
|
costAccountId: number,
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
@@ -53,6 +53,7 @@ export * from './Table';
|
|||||||
export * from './Ledger';
|
export * from './Ledger';
|
||||||
export * from './CashFlow';
|
export * from './CashFlow';
|
||||||
export * from './InventoryDetails';
|
export * from './InventoryDetails';
|
||||||
|
export * from './LandedCost';
|
||||||
|
|
||||||
export interface I18nService {
|
export interface I18nService {
|
||||||
__: (input: string) => string;
|
__: (input: string) => string;
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ import Media from 'models/Media';
|
|||||||
import MediaLink from 'models/MediaLink';
|
import MediaLink from 'models/MediaLink';
|
||||||
import InventoryAdjustment from 'models/InventoryAdjustment';
|
import InventoryAdjustment from 'models/InventoryAdjustment';
|
||||||
import InventoryAdjustmentEntry from 'models/InventoryAdjustmentEntry';
|
import InventoryAdjustmentEntry from 'models/InventoryAdjustmentEntry';
|
||||||
|
import BillLandedCost from 'models/BillLandedCost';
|
||||||
|
import BillLandedCostEntry from 'models/BillLandedCostEntry';
|
||||||
|
|
||||||
export default (knex) => {
|
export default (knex) => {
|
||||||
const models = {
|
const models = {
|
||||||
@@ -75,6 +77,8 @@ export default (knex) => {
|
|||||||
Contact,
|
Contact,
|
||||||
InventoryAdjustment,
|
InventoryAdjustment,
|
||||||
InventoryAdjustmentEntry,
|
InventoryAdjustmentEntry,
|
||||||
|
BillLandedCost,
|
||||||
|
BillLandedCostEntry
|
||||||
};
|
};
|
||||||
return mapValues(models, (model) => model.bindKnex(knex));
|
return mapValues(models, (model) => model.bindKnex(knex));
|
||||||
}
|
}
|
||||||
@@ -103,6 +103,7 @@ export default class Bill extends TenantModel {
|
|||||||
'remainingDays',
|
'remainingDays',
|
||||||
'overdueDays',
|
'overdueDays',
|
||||||
'isOverdue',
|
'isOverdue',
|
||||||
|
'unallocatedCostAmount'
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,6 +179,14 @@ export default class Bill extends TenantModel {
|
|||||||
return this.overdueDays > 0;
|
return this.overdueDays > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the unallocated cost amount.
|
||||||
|
* @return {number}
|
||||||
|
*/
|
||||||
|
get unallocatedCostAmount() {
|
||||||
|
return Math.max(this.landedCostAmount - this.allocatedCostAmount, 0);
|
||||||
|
}
|
||||||
|
|
||||||
getOverdueDays(asDate = moment().format('YYYY-MM-DD')) {
|
getOverdueDays(asDate = moment().format('YYYY-MM-DD')) {
|
||||||
// Can't continue in case due date not defined.
|
// Can't continue in case due date not defined.
|
||||||
if (!this.dueDate) {
|
if (!this.dueDate) {
|
||||||
@@ -195,6 +204,7 @@ export default class Bill extends TenantModel {
|
|||||||
static get relationMappings() {
|
static get relationMappings() {
|
||||||
const Contact = require('models/Contact');
|
const Contact = require('models/Contact');
|
||||||
const ItemEntry = require('models/ItemEntry');
|
const ItemEntry = require('models/ItemEntry');
|
||||||
|
const BillLandedCost = require('models/BillLandedCost');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
vendor: {
|
vendor: {
|
||||||
@@ -220,6 +230,15 @@ export default class Bill extends TenantModel {
|
|||||||
builder.where('reference_type', 'Bill');
|
builder.where('reference_type', 'Bill');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
locatedLandedCosts: {
|
||||||
|
relation: Model.HasManyRelation,
|
||||||
|
modelClass: BillLandedCost.default,
|
||||||
|
join: {
|
||||||
|
from: 'bills.id',
|
||||||
|
to: 'bill_located_costs.billId',
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
36
server/src/models/BillLandedCost.js
Normal file
36
server/src/models/BillLandedCost.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { Model } from 'objection';
|
||||||
|
import TenantModel from 'models/TenantModel';
|
||||||
|
|
||||||
|
export default class BillLandedCost extends TenantModel {
|
||||||
|
/**
|
||||||
|
* Table name
|
||||||
|
*/
|
||||||
|
static get tableName() {
|
||||||
|
return 'bill_located_costs';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model timestamps.
|
||||||
|
*/
|
||||||
|
get timestamps() {
|
||||||
|
return ['createdAt', 'updatedAt'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relationship mapping.
|
||||||
|
*/
|
||||||
|
static get relationMappings() {
|
||||||
|
const BillLandedCostEntry = require('models/BillLandedCostEntry');
|
||||||
|
|
||||||
|
return {
|
||||||
|
allocateEntries: {
|
||||||
|
relation: Model.HasManyRelation,
|
||||||
|
modelClass: BillLandedCostEntry.default,
|
||||||
|
join: {
|
||||||
|
from: 'bill_located_costs.id',
|
||||||
|
to: 'bill_located_cost_entries.billLocatedCostId',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
10
server/src/models/BillLandedCostEntry.js
Normal file
10
server/src/models/BillLandedCostEntry.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import TenantModel from 'models/TenantModel';
|
||||||
|
|
||||||
|
export default class BillLandedCostEntry extends TenantModel {
|
||||||
|
/**
|
||||||
|
* Table name
|
||||||
|
*/
|
||||||
|
static get tableName() {
|
||||||
|
return 'bill_located_cost_entries';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,27 +1,27 @@
|
|||||||
import { Model } from "objection";
|
import { Model } from 'objection';
|
||||||
import TenantModel from "models/TenantModel";
|
import TenantModel from 'models/TenantModel';
|
||||||
import { viewRolesBuilder } from "lib/ViewRolesBuilder";
|
import { viewRolesBuilder } from 'lib/ViewRolesBuilder';
|
||||||
|
|
||||||
export default class Expense extends TenantModel {
|
export default class Expense extends TenantModel {
|
||||||
/**
|
/**
|
||||||
* Table name
|
* Table name
|
||||||
*/
|
*/
|
||||||
static get tableName() {
|
static get tableName() {
|
||||||
return "expenses_transactions";
|
return 'expenses_transactions';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Account transaction reference type.
|
* Account transaction reference type.
|
||||||
*/
|
*/
|
||||||
static get referenceType() {
|
static get referenceType() {
|
||||||
return "Expense";
|
return 'Expense';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Model timestamps.
|
* Model timestamps.
|
||||||
*/
|
*/
|
||||||
get timestamps() {
|
get timestamps() {
|
||||||
return ["createdAt", "updatedAt"];
|
return ['createdAt', 'updatedAt'];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -37,14 +37,19 @@ export default class Expense extends TenantModel {
|
|||||||
static get media() {
|
static get media() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static get virtualAttributes() {
|
static get virtualAttributes() {
|
||||||
return ["isPublished"];
|
return ['isPublished', 'unallocatedLandedCost'];
|
||||||
}
|
}
|
||||||
|
|
||||||
isPublished() {
|
isPublished() {
|
||||||
return Boolean(this.publishedAt);
|
return Boolean(this.publishedAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unallocatedLandedCost() {
|
||||||
|
return Math.max(this.amount - this.allocatedCostAmount, 0);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Model modifiers.
|
* Model modifiers.
|
||||||
*/
|
*/
|
||||||
@@ -52,28 +57,28 @@ export default class Expense extends TenantModel {
|
|||||||
return {
|
return {
|
||||||
filterByDateRange(query, startDate, endDate) {
|
filterByDateRange(query, startDate, endDate) {
|
||||||
if (startDate) {
|
if (startDate) {
|
||||||
query.where("date", ">=", startDate);
|
query.where('date', '>=', startDate);
|
||||||
}
|
}
|
||||||
if (endDate) {
|
if (endDate) {
|
||||||
query.where("date", "<=", endDate);
|
query.where('date', '<=', endDate);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
filterByAmountRange(query, from, to) {
|
filterByAmountRange(query, from, to) {
|
||||||
if (from) {
|
if (from) {
|
||||||
query.where("amount", ">=", from);
|
query.where('amount', '>=', from);
|
||||||
}
|
}
|
||||||
if (to) {
|
if (to) {
|
||||||
query.where("amount", "<=", to);
|
query.where('amount', '<=', to);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
filterByExpenseAccount(query, accountId) {
|
filterByExpenseAccount(query, accountId) {
|
||||||
if (accountId) {
|
if (accountId) {
|
||||||
query.where("expense_account_id", accountId);
|
query.where('expense_account_id', accountId);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
filterByPaymentAccount(query, accountId) {
|
filterByPaymentAccount(query, accountId) {
|
||||||
if (accountId) {
|
if (accountId) {
|
||||||
query.where("payment_account_id", accountId);
|
query.where('payment_account_id', accountId);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
viewRolesBuilder(query, conditionals, expression) {
|
viewRolesBuilder(query, conditionals, expression) {
|
||||||
@@ -94,40 +99,40 @@ export default class Expense extends TenantModel {
|
|||||||
* Relationship mapping.
|
* Relationship mapping.
|
||||||
*/
|
*/
|
||||||
static get relationMappings() {
|
static get relationMappings() {
|
||||||
const Account = require("models/Account");
|
const Account = require('models/Account');
|
||||||
const ExpenseCategory = require("models/ExpenseCategory");
|
const ExpenseCategory = require('models/ExpenseCategory');
|
||||||
const Media = require("models/Media");
|
const Media = require('models/Media');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
paymentAccount: {
|
paymentAccount: {
|
||||||
relation: Model.BelongsToOneRelation,
|
relation: Model.BelongsToOneRelation,
|
||||||
modelClass: Account.default,
|
modelClass: Account.default,
|
||||||
join: {
|
join: {
|
||||||
from: "expenses_transactions.paymentAccountId",
|
from: 'expenses_transactions.paymentAccountId',
|
||||||
to: "accounts.id",
|
to: 'accounts.id',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
categories: {
|
categories: {
|
||||||
relation: Model.HasManyRelation,
|
relation: Model.HasManyRelation,
|
||||||
modelClass: ExpenseCategory.default,
|
modelClass: ExpenseCategory.default,
|
||||||
join: {
|
join: {
|
||||||
from: "expenses_transactions.id",
|
from: 'expenses_transactions.id',
|
||||||
to: "expense_transaction_categories.expenseId",
|
to: 'expense_transaction_categories.expenseId',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
media: {
|
media: {
|
||||||
relation: Model.ManyToManyRelation,
|
relation: Model.ManyToManyRelation,
|
||||||
modelClass: Media.default,
|
modelClass: Media.default,
|
||||||
join: {
|
join: {
|
||||||
from: "expenses_transactions.id",
|
from: 'expenses_transactions.id',
|
||||||
through: {
|
through: {
|
||||||
from: "media_links.model_id",
|
from: 'media_links.model_id',
|
||||||
to: "media_links.media_id",
|
to: 'media_links.media_id',
|
||||||
},
|
},
|
||||||
to: "media.id",
|
to: 'media.id',
|
||||||
},
|
},
|
||||||
filter(query) {
|
filter(query) {
|
||||||
query.where("model_name", "Expense");
|
query.where('model_name', 'Expense');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -139,39 +144,39 @@ export default class Expense extends TenantModel {
|
|||||||
static get fields() {
|
static get fields() {
|
||||||
return {
|
return {
|
||||||
payment_date: {
|
payment_date: {
|
||||||
label: "Payment date",
|
label: 'Payment date',
|
||||||
column: "payment_date",
|
column: 'payment_date',
|
||||||
columnType: "date",
|
columnType: 'date',
|
||||||
},
|
},
|
||||||
payment_account: {
|
payment_account: {
|
||||||
label: "Payment account",
|
label: 'Payment account',
|
||||||
column: "payment_account_id",
|
column: 'payment_account_id',
|
||||||
relation: "accounts.id",
|
relation: 'accounts.id',
|
||||||
optionsResource: "account",
|
optionsResource: 'account',
|
||||||
},
|
},
|
||||||
amount: {
|
amount: {
|
||||||
label: "Amount",
|
label: 'Amount',
|
||||||
column: "total_amount",
|
column: 'total_amount',
|
||||||
columnType: "number",
|
columnType: 'number',
|
||||||
},
|
},
|
||||||
currency_code: {
|
currency_code: {
|
||||||
label: "Currency",
|
label: 'Currency',
|
||||||
column: "currency_code",
|
column: 'currency_code',
|
||||||
optionsResource: "currency",
|
optionsResource: 'currency',
|
||||||
},
|
},
|
||||||
reference_no: {
|
reference_no: {
|
||||||
label: "Reference No.",
|
label: 'Reference No.',
|
||||||
column: "reference_no",
|
column: 'reference_no',
|
||||||
columnType: "string",
|
columnType: 'string',
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
label: "Description",
|
label: 'Description',
|
||||||
column: "description",
|
column: 'description',
|
||||||
columnType: "string",
|
columnType: 'string',
|
||||||
},
|
},
|
||||||
published: {
|
published: {
|
||||||
label: "Published",
|
label: 'Published',
|
||||||
column: "published_at",
|
column: 'published_at',
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
label: 'Status',
|
label: 'Status',
|
||||||
@@ -194,9 +199,9 @@ export default class Expense extends TenantModel {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
created_at: {
|
created_at: {
|
||||||
label: "Created at",
|
label: 'Created at',
|
||||||
column: "created_at",
|
column: 'created_at',
|
||||||
columnType: "date",
|
columnType: 'date',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,21 @@ export default class ExpenseCategory extends TenantModel {
|
|||||||
return 'expense_transaction_categories';
|
return 'expense_transaction_categories';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Virtual attributes.
|
||||||
|
*/
|
||||||
|
static get virtualAttributes() {
|
||||||
|
return ['unallocatedLandedCost'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remain unallocated landed cost.
|
||||||
|
* @return {number}
|
||||||
|
*/
|
||||||
|
get unallocatedLandedCost() {
|
||||||
|
return Math.max(this.amount - this.allocatedCostAmount, 0);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Relationship mapping.
|
* Relationship mapping.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -17,11 +17,12 @@ import {
|
|||||||
IExpensesService,
|
IExpensesService,
|
||||||
ISystemUser,
|
ISystemUser,
|
||||||
IPaginationMeta,
|
IPaginationMeta,
|
||||||
|
IExpenseCategory,
|
||||||
} from 'interfaces';
|
} from 'interfaces';
|
||||||
import DynamicListingService from 'services/DynamicListing/DynamicListService';
|
import DynamicListingService from 'services/DynamicListing/DynamicListService';
|
||||||
import events from 'subscribers/events';
|
import events from 'subscribers/events';
|
||||||
import ContactsService from 'services/Contacts/ContactsService';
|
import ContactsService from 'services/Contacts/ContactsService';
|
||||||
import { ACCOUNT_PARENT_TYPE, ACCOUNT_ROOT_TYPE } from 'data/AccountTypes'
|
import { ACCOUNT_PARENT_TYPE, ACCOUNT_ROOT_TYPE } from 'data/AccountTypes';
|
||||||
|
|
||||||
const ERRORS = {
|
const ERRORS = {
|
||||||
EXPENSE_NOT_FOUND: 'expense_not_found',
|
EXPENSE_NOT_FOUND: 'expense_not_found',
|
||||||
@@ -32,6 +33,7 @@ const ERRORS = {
|
|||||||
PAYMENT_ACCOUNT_HAS_INVALID_TYPE: 'payment_account_has_invalid_type',
|
PAYMENT_ACCOUNT_HAS_INVALID_TYPE: 'payment_account_has_invalid_type',
|
||||||
EXPENSES_ACCOUNT_HAS_INVALID_TYPE: 'expenses_account_has_invalid_type',
|
EXPENSES_ACCOUNT_HAS_INVALID_TYPE: 'expenses_account_has_invalid_type',
|
||||||
EXPENSE_ALREADY_PUBLISHED: 'expense_already_published',
|
EXPENSE_ALREADY_PUBLISHED: 'expense_already_published',
|
||||||
|
EXPENSE_HAS_ASSOCIATED_LANDED_COST: 'EXPENSE_HAS_ASSOCIATED_LANDED_COST',
|
||||||
};
|
};
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
@@ -308,6 +310,27 @@ export default class ExpensesService implements IExpensesService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the expense landed cost amount.
|
||||||
|
* @param {IExpenseDTO} expenseDTO
|
||||||
|
* @return {number}
|
||||||
|
*/
|
||||||
|
private getExpenseLandedCostAmount(expenseDTO: IExpenseDTO): number {
|
||||||
|
const landedCostEntries = expenseDTO.categories.filter((entry) => {
|
||||||
|
return entry.landedCost === true;
|
||||||
|
});
|
||||||
|
return this.getExpenseCategoriesTotal(landedCostEntries);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the given expense categories total.
|
||||||
|
* @param {IExpenseCategory} categories
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
private getExpenseCategoriesTotal(categories): number {
|
||||||
|
return sumBy(categories, 'amount');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mapping expense DTO to model.
|
* Mapping expense DTO to model.
|
||||||
* @param {IExpenseDTO} expenseDTO
|
* @param {IExpenseDTO} expenseDTO
|
||||||
@@ -315,12 +338,14 @@ export default class ExpensesService implements IExpensesService {
|
|||||||
* @return {IExpense}
|
* @return {IExpense}
|
||||||
*/
|
*/
|
||||||
private expenseDTOToModel(expenseDTO: IExpenseDTO, user?: ISystemUser) {
|
private expenseDTOToModel(expenseDTO: IExpenseDTO, user?: ISystemUser) {
|
||||||
const totalAmount = sumBy(expenseDTO.categories, 'amount');
|
const landedCostAmount = this.getExpenseLandedCostAmount(expenseDTO);
|
||||||
|
const totalAmount = this.getExpenseCategoriesTotal(expenseDTO.categories);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
categories: [],
|
categories: [],
|
||||||
...omit(expenseDTO, ['publish']),
|
...omit(expenseDTO, ['publish']),
|
||||||
totalAmount,
|
totalAmount,
|
||||||
|
landedCostAmount,
|
||||||
paymentDate: moment(expenseDTO.paymentDate).toMySqlDateTime(),
|
paymentDate: moment(expenseDTO.paymentDate).toMySqlDateTime(),
|
||||||
...(user
|
...(user
|
||||||
? {
|
? {
|
||||||
@@ -340,7 +365,7 @@ export default class ExpensesService implements IExpensesService {
|
|||||||
* @param {IExpenseDTO} expenseDTO
|
* @param {IExpenseDTO} expenseDTO
|
||||||
* @return {number[]}
|
* @return {number[]}
|
||||||
*/
|
*/
|
||||||
mapExpensesAccountsIdsFromDTO(expenseDTO: IExpenseDTO) {
|
private mapExpensesAccountsIdsFromDTO(expenseDTO: IExpenseDTO) {
|
||||||
return expenseDTO.categories.map((category) => category.expenseAccountId);
|
return expenseDTO.categories.map((category) => category.expenseAccountId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -544,15 +569,16 @@ export default class ExpensesService implements IExpensesService {
|
|||||||
authorizedUser: ISystemUser
|
authorizedUser: ISystemUser
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const oldExpense = await this.getExpenseOrThrowError(tenantId, expenseId);
|
const oldExpense = await this.getExpenseOrThrowError(tenantId, expenseId);
|
||||||
const {
|
const { expenseRepository, expenseEntryRepository } =
|
||||||
expenseRepository,
|
this.tenancy.repositories(tenantId);
|
||||||
expenseEntryRepository,
|
|
||||||
} = this.tenancy.repositories(tenantId);
|
|
||||||
|
|
||||||
this.logger.info('[expense] trying to delete the expense.', {
|
this.logger.info('[expense] trying to delete the expense.', {
|
||||||
tenantId,
|
tenantId,
|
||||||
expenseId,
|
expenseId,
|
||||||
});
|
});
|
||||||
|
// Validates the expense has no associated landed cost.
|
||||||
|
await this.validateNoAssociatedLandedCost(tenantId, expenseId);
|
||||||
|
|
||||||
await expenseEntryRepository.deleteBy({ expenseId });
|
await expenseEntryRepository.deleteBy({ expenseId });
|
||||||
await expenseRepository.deleteById(expenseId);
|
await expenseRepository.deleteById(expenseId);
|
||||||
|
|
||||||
@@ -572,7 +598,7 @@ export default class ExpensesService implements IExpensesService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Filters the not published expenses.
|
* Filters the not published expenses.
|
||||||
* @param {IExpense[]} expenses -
|
* @param {IExpense[]} expenses -
|
||||||
*/
|
*/
|
||||||
public getNonePublishedExpenses(expenses: IExpense[]): IExpense[] {
|
public getNonePublishedExpenses(expenses: IExpense[]): IExpense[] {
|
||||||
return expenses.filter((expense) => !expense.publishedAt);
|
return expenses.filter((expense) => !expense.publishedAt);
|
||||||
@@ -648,4 +674,25 @@ export default class ExpensesService implements IExpensesService {
|
|||||||
}
|
}
|
||||||
return expense;
|
return expense;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the expense has not associated landed cost
|
||||||
|
* references to the given expense.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {number} expenseId
|
||||||
|
*/
|
||||||
|
public async validateNoAssociatedLandedCost(
|
||||||
|
tenantId: number,
|
||||||
|
expenseId: number
|
||||||
|
) {
|
||||||
|
const { BillLandedCost } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
const associatedLandedCosts = await BillLandedCost.query()
|
||||||
|
.where('fromTransactionType', 'Expense')
|
||||||
|
.where('fromTransactionId', expenseId);
|
||||||
|
|
||||||
|
if (associatedLandedCosts.length > 0) {
|
||||||
|
throw new ServiceError(ERRORS.EXPENSE_HAS_ASSOCIATED_LANDED_COST);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ import { ERRORS } from './constants';
|
|||||||
@Service('Bills')
|
@Service('Bills')
|
||||||
export default class BillsService
|
export default class BillsService
|
||||||
extends SalesInvoicesCost
|
extends SalesInvoicesCost
|
||||||
implements IBillsService {
|
implements IBillsService
|
||||||
|
{
|
||||||
@Inject()
|
@Inject()
|
||||||
inventoryService: InventoryService;
|
inventoryService: InventoryService;
|
||||||
|
|
||||||
@@ -100,7 +101,7 @@ export default class BillsService
|
|||||||
* @param {number} tenantId -
|
* @param {number} tenantId -
|
||||||
* @param {number} billId -
|
* @param {number} billId -
|
||||||
*/
|
*/
|
||||||
private async getBillOrThrowError(tenantId: number, billId: number) {
|
public async getBillOrThrowError(tenantId: number, billId: number) {
|
||||||
const { Bill } = this.tenancy.models(tenantId);
|
const { Bill } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
this.logger.info('[bill] trying to get bill.', { tenantId, billId });
|
this.logger.info('[bill] trying to get bill.', { tenantId, billId });
|
||||||
@@ -194,6 +195,28 @@ export default class BillsService
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the bill entries total.
|
||||||
|
* @param {IItemEntry[]} entries
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
private getBillEntriesTotal(tenantId: number, entries: IItemEntry[]): number {
|
||||||
|
const { ItemEntry } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
return sumBy(entries, (e) => ItemEntry.calcAmount(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the bill landed cost amount.
|
||||||
|
* @param {IBillDTO} billDTO
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
private getBillLandedCostAmount(tenantId: number, billDTO: IBillDTO): number {
|
||||||
|
const costEntries = billDTO.entries.filter((entry) => entry.landedCost);
|
||||||
|
|
||||||
|
return this.getBillEntriesTotal(tenantId, costEntries);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts create bill DTO to model.
|
* Converts create bill DTO to model.
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
@@ -211,6 +234,9 @@ export default class BillsService
|
|||||||
|
|
||||||
const amount = sumBy(billDTO.entries, (e) => ItemEntry.calcAmount(e));
|
const amount = sumBy(billDTO.entries, (e) => ItemEntry.calcAmount(e));
|
||||||
|
|
||||||
|
// Retrieve the landed cost amount from landed cost entries.
|
||||||
|
const landedCostAmount = this.getBillLandedCostAmount(tenantId, billDTO);
|
||||||
|
|
||||||
// Bill number from DTO or from auto-increment.
|
// Bill number from DTO or from auto-increment.
|
||||||
const billNumber = billDTO.billNumber || oldBill?.billNumber;
|
const billNumber = billDTO.billNumber || oldBill?.billNumber;
|
||||||
|
|
||||||
@@ -234,6 +260,7 @@ export default class BillsService
|
|||||||
'dueDate',
|
'dueDate',
|
||||||
]),
|
]),
|
||||||
amount,
|
amount,
|
||||||
|
landedCostAmount,
|
||||||
currencyCode: vendor.currencyCode,
|
currencyCode: vendor.currencyCode,
|
||||||
billNumber,
|
billNumber,
|
||||||
entries,
|
entries,
|
||||||
@@ -498,7 +525,7 @@ export default class BillsService
|
|||||||
const bill = await Bill.query()
|
const bill = await Bill.query()
|
||||||
.findById(billId)
|
.findById(billId)
|
||||||
.withGraphFetched('vendor')
|
.withGraphFetched('vendor')
|
||||||
.withGraphFetched('entries');
|
.withGraphFetched('entries.item');
|
||||||
|
|
||||||
if (!bill) {
|
if (!bill) {
|
||||||
throw new ServiceError(ERRORS.BILL_NOT_FOUND);
|
throw new ServiceError(ERRORS.BILL_NOT_FOUND);
|
||||||
@@ -538,10 +565,11 @@ export default class BillsService
|
|||||||
override?: boolean
|
override?: boolean
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// Loads the inventory items entries of the given sale invoice.
|
// Loads the inventory items entries of the given sale invoice.
|
||||||
const inventoryEntries = await this.itemsEntriesService.filterInventoryEntries(
|
const inventoryEntries =
|
||||||
tenantId,
|
await this.itemsEntriesService.filterInventoryEntries(
|
||||||
bill.entries
|
tenantId,
|
||||||
);
|
bill.entries
|
||||||
|
);
|
||||||
const transaction = {
|
const transaction = {
|
||||||
transactionId: bill.id,
|
transactionId: bill.id,
|
||||||
transactionType: 'Bill',
|
transactionType: 'Bill',
|
||||||
|
|||||||
55
server/src/services/Purchases/LandedCost/BillLandedCost.ts
Normal file
55
server/src/services/Purchases/LandedCost/BillLandedCost.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { Service } from 'typedi';
|
||||||
|
import { isEmpty } from 'lodash';
|
||||||
|
import {
|
||||||
|
IBill,
|
||||||
|
IItem,
|
||||||
|
ILandedCostTransactionEntry,
|
||||||
|
ILandedCostTransaction,
|
||||||
|
IItemEntry,
|
||||||
|
} from 'interfaces';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export default class BillLandedCost {
|
||||||
|
/**
|
||||||
|
* Retrieve the landed cost transaction from the given bill transaction.
|
||||||
|
* @param {IBill} bill
|
||||||
|
* @returns {ILandedCostTransaction}
|
||||||
|
*/
|
||||||
|
public transformToLandedCost = (bill: IBill): ILandedCostTransaction => {
|
||||||
|
const number = bill.billNumber || bill.referenceNo;
|
||||||
|
const name = [
|
||||||
|
number,
|
||||||
|
bill.currencyCode + ' ' + bill.unallocatedCostAmount,
|
||||||
|
].join(' - ');
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: bill.id,
|
||||||
|
name,
|
||||||
|
allocatedCostAmount: bill.allocatedCostAmount,
|
||||||
|
amount: bill.landedCostAmount,
|
||||||
|
unallocatedCostAmount: bill.unallocatedCostAmount,
|
||||||
|
transactionType: 'Bill',
|
||||||
|
|
||||||
|
...(!isEmpty(bill.entries)) && {
|
||||||
|
entries: bill.entries.map(this.transformToLandedCostEntry),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transformes bill entry to landed cost entry.
|
||||||
|
* @param {IItemEntry} billEntry - Bill entry.
|
||||||
|
* @return {ILandedCostTransactionEntry}
|
||||||
|
*/
|
||||||
|
public transformToLandedCostEntry(
|
||||||
|
billEntry: IItemEntry & { item: IItem }
|
||||||
|
): ILandedCostTransactionEntry {
|
||||||
|
return {
|
||||||
|
id: billEntry.id,
|
||||||
|
name: billEntry.item.name,
|
||||||
|
code: billEntry.item.code,
|
||||||
|
amount: billEntry.amount,
|
||||||
|
description: billEntry.description,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { Service } from 'typedi';
|
||||||
|
import { isEmpty } from 'lodash';
|
||||||
|
import {
|
||||||
|
IExpense,
|
||||||
|
ILandedCostTransactionEntry,
|
||||||
|
IExpenseCategory,
|
||||||
|
IAccount,
|
||||||
|
ILandedCostTransaction,
|
||||||
|
} from 'interfaces';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export default class ExpenseLandedCost {
|
||||||
|
/**
|
||||||
|
* Retrieve the landed cost transaction from the given expense transaction.
|
||||||
|
* @param {IExpense} expense
|
||||||
|
* @returns {ILandedCostTransaction}
|
||||||
|
*/
|
||||||
|
public transformToLandedCost = (
|
||||||
|
expense: IExpense
|
||||||
|
): ILandedCostTransaction => {
|
||||||
|
const name = [expense.currencyCode + ' ' + expense.totalAmount].join(' - ');
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: expense.id,
|
||||||
|
name,
|
||||||
|
allocatedCostAmount: expense.allocatedCostAmount,
|
||||||
|
amount: expense.landedCostAmount,
|
||||||
|
unallocatedCostAmount: expense.unallocatedCostAmount,
|
||||||
|
transactionType: 'Expense',
|
||||||
|
|
||||||
|
...(!isEmpty(expense.categories) && {
|
||||||
|
entries: expense.categories.map(this.transformToLandedCostEntry),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transformes expense entry to landed cost entry.
|
||||||
|
* @param {IExpenseCategory & { expenseAccount: IAccount }} expenseEntry -
|
||||||
|
* @return {ILandedCostTransactionEntry}
|
||||||
|
*/
|
||||||
|
public transformToLandedCostEntry = (
|
||||||
|
expenseEntry: IExpenseCategory & { expenseAccount: IAccount }
|
||||||
|
): ILandedCostTransactionEntry => {
|
||||||
|
return {
|
||||||
|
id: expenseEntry.id,
|
||||||
|
name: expenseEntry.expenseAccount.name,
|
||||||
|
code: expenseEntry.expenseAccount.code,
|
||||||
|
amount: expenseEntry.amount,
|
||||||
|
description: expenseEntry.description,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { ref } from 'objection';
|
||||||
|
import {
|
||||||
|
ILandedCostTransactionsQueryDTO,
|
||||||
|
ILandedCostTransaction,
|
||||||
|
IBillLandedCostTransaction,
|
||||||
|
} from 'interfaces';
|
||||||
|
import TransactionLandedCost from './TransctionLandedCost';
|
||||||
|
import BillsService from '../Bills';
|
||||||
|
import HasTenancyService from 'services/Tenancy/TenancyService';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export default class LandedCostListing {
|
||||||
|
@Inject()
|
||||||
|
transactionLandedCost: TransactionLandedCost;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
billsService: BillsService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
tenancy: HasTenancyService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the landed costs based on the given query.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {ILandedCostTransactionsQueryDTO} query
|
||||||
|
* @returns {Promise<ILandedCostTransaction[]>}
|
||||||
|
*/
|
||||||
|
public getLandedCostTransactions = async (
|
||||||
|
tenantId: number,
|
||||||
|
query: ILandedCostTransactionsQueryDTO
|
||||||
|
): Promise<ILandedCostTransaction[]> => {
|
||||||
|
const { transactionType } = query;
|
||||||
|
const Model = this.transactionLandedCost.getModel(
|
||||||
|
tenantId,
|
||||||
|
query.transactionType
|
||||||
|
);
|
||||||
|
|
||||||
|
// Retrieve the model entities.
|
||||||
|
const transactions = await Model.query().onBuild((q) => {
|
||||||
|
q.where('allocated_cost_amount', '<', ref('landed_cost_amount'));
|
||||||
|
|
||||||
|
if (query.transactionType === 'Bill') {
|
||||||
|
q.withGraphFetched('entries.item');
|
||||||
|
} else if (query.transactionType === 'Expense') {
|
||||||
|
q.withGraphFetched('categories.expenseAccount');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return transactions.map((transaction) => ({
|
||||||
|
...this.transactionLandedCost.transformToLandedCost(
|
||||||
|
transactionType,
|
||||||
|
transaction
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the bill associated landed cost transactions.
|
||||||
|
* @param {number} tenantId - Tenant id.
|
||||||
|
* @param {number} billId - Bill id.
|
||||||
|
* @return {Promise<IBillLandedCostTransaction>}
|
||||||
|
*/
|
||||||
|
public getBillLandedCostTransactions = async (
|
||||||
|
tenantId: number,
|
||||||
|
billId: number
|
||||||
|
): Promise<IBillLandedCostTransaction> => {
|
||||||
|
const { BillLandedCost } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
// Retrieve the given bill id or throw not found service error.
|
||||||
|
const bill = await this.billsService.getBillOrThrowError(tenantId, billId);
|
||||||
|
|
||||||
|
const landedCostTransactions = await BillLandedCost.query()
|
||||||
|
.where('bill_id', billId)
|
||||||
|
.withGraphFetched('allocateEntries');
|
||||||
|
|
||||||
|
return landedCostTransactions;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import * as R from 'ramda';
|
||||||
|
import { IBill, IExpense, ILandedCostTransaction } from 'interfaces';
|
||||||
|
import { ServiceError } from 'exceptions';
|
||||||
|
import BillLandedCost from './BillLandedCost';
|
||||||
|
import ExpenseLandedCost from './ExpenseLandedCost';
|
||||||
|
import HasTenancyService from 'services/Tenancy/TenancyService';
|
||||||
|
import { ERRORS } from './constants';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export default class TransactionLandedCost {
|
||||||
|
@Inject()
|
||||||
|
billLandedCost: BillLandedCost;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
expenseLandedCost: ExpenseLandedCost;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
tenancy: HasTenancyService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the cost transaction code model.
|
||||||
|
* @param {number} tenantId - Tenant id.
|
||||||
|
* @param {string} transactionType - Transaction type.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
public getModel = (
|
||||||
|
tenantId: number,
|
||||||
|
transactionType: string
|
||||||
|
): IBill | IExpense => {
|
||||||
|
const Models = this.tenancy.models(tenantId);
|
||||||
|
const Model = Models[transactionType];
|
||||||
|
|
||||||
|
if (!Model) {
|
||||||
|
throw new ServiceError(ERRORS.COST_TYPE_UNDEFINED);
|
||||||
|
}
|
||||||
|
return Model;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mappes the given expense or bill transaction to landed cost transaction.
|
||||||
|
* @param {string} transactionType - Transaction type.
|
||||||
|
* @param {IBill|IExpense} transaction - Expense or bill transaction.
|
||||||
|
* @returns {ILandedCostTransaction}
|
||||||
|
*/
|
||||||
|
public transformToLandedCost = (
|
||||||
|
transactionType: string,
|
||||||
|
transaction: IBill | IExpense
|
||||||
|
): ILandedCostTransaction => {
|
||||||
|
return R.compose(
|
||||||
|
R.when(
|
||||||
|
R.always(transactionType === 'Bill'),
|
||||||
|
this.billLandedCost.transformToLandedCost,
|
||||||
|
),
|
||||||
|
R.when(
|
||||||
|
R.always(transactionType === 'Expense'),
|
||||||
|
this.expenseLandedCost.transformToLandedCost,
|
||||||
|
),
|
||||||
|
)(transaction);
|
||||||
|
}
|
||||||
|
}
|
||||||
15
server/src/services/Purchases/LandedCost/constants.ts
Normal file
15
server/src/services/Purchases/LandedCost/constants.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export const ERRORS = {
|
||||||
|
COST_TYPE_UNDEFINED: 'COST_TYPE_UNDEFINED',
|
||||||
|
LANDED_COST_ITEMS_IDS_NOT_FOUND: 'LANDED_COST_ITEMS_IDS_NOT_FOUND',
|
||||||
|
COST_TRANSACTION_HAS_NO_ENOUGH_TO_LOCATE:
|
||||||
|
'COST_TRANSACTION_HAS_NO_ENOUGH_TO_LOCATE',
|
||||||
|
BILL_LANDED_COST_NOT_FOUND: 'BILL_LANDED_COST_NOT_FOUND',
|
||||||
|
COST_ENTRY_ID_NOT_FOUND: 'COST_ENTRY_ID_NOT_FOUND',
|
||||||
|
LANDED_COST_TRANSACTION_NOT_FOUND: 'LANDED_COST_TRANSACTION_NOT_FOUND',
|
||||||
|
LANDED_COST_ENTRY_NOT_FOUND: 'LANDED_COST_ENTRY_NOT_FOUND',
|
||||||
|
COST_AMOUNT_BIGGER_THAN_UNALLOCATED_AMOUNT: 'COST_AMOUNT_BIGGER_THAN_UNALLOCATED_AMOUNT',
|
||||||
|
ALLOCATE_COST_SHOULD_NOT_BE_BILL: 'ALLOCATE_COST_SHOULD_NOT_BE_BILL'
|
||||||
|
};
|
||||||
504
server/src/services/Purchases/LandedCost/index.ts
Normal file
504
server/src/services/Purchases/LandedCost/index.ts
Normal file
@@ -0,0 +1,504 @@
|
|||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { difference, sumBy } from 'lodash';
|
||||||
|
import BillsService from '../Bills';
|
||||||
|
import { ServiceError } from 'exceptions';
|
||||||
|
import {
|
||||||
|
IItemEntry,
|
||||||
|
IBill,
|
||||||
|
IBillLandedCost,
|
||||||
|
ILandedCostItemDTO,
|
||||||
|
ILandedCostDTO,
|
||||||
|
} from 'interfaces';
|
||||||
|
import InventoryService from 'services/Inventory/Inventory';
|
||||||
|
import HasTenancyService from 'services/Tenancy/TenancyService';
|
||||||
|
import { ERRORS } from './constants';
|
||||||
|
import { mergeObjectsBykey } from 'utils';
|
||||||
|
import JournalPoster from 'services/Accounting/JournalPoster';
|
||||||
|
import JournalEntry from 'services/Accounting/JournalEntry';
|
||||||
|
import TransactionLandedCost from './TransctionLandedCost';
|
||||||
|
|
||||||
|
const CONFIG = {
|
||||||
|
COST_TYPES: {
|
||||||
|
Expense: {
|
||||||
|
entries: 'categories',
|
||||||
|
},
|
||||||
|
Bill: {
|
||||||
|
entries: 'entries',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export default class AllocateLandedCostService {
|
||||||
|
@Inject()
|
||||||
|
public billsService: BillsService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
public inventoryService: InventoryService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
public tenancy: HasTenancyService;
|
||||||
|
|
||||||
|
@Inject('logger')
|
||||||
|
public logger: any;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
public transactionLandedCost: TransactionLandedCost;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates allocate cost items association with the purchase invoice entries.
|
||||||
|
* @param {IItemEntry[]} purchaseInvoiceEntries
|
||||||
|
* @param {ILandedCostItemDTO[]} landedCostItems
|
||||||
|
*/
|
||||||
|
private validateAllocateCostItems = (
|
||||||
|
purchaseInvoiceEntries: IItemEntry[],
|
||||||
|
landedCostItems: ILandedCostItemDTO[]
|
||||||
|
): void => {
|
||||||
|
// Purchase invoice entries items ids.
|
||||||
|
const purchaseInvoiceItems = purchaseInvoiceEntries.map((e) => e.id);
|
||||||
|
const landedCostItemsIds = landedCostItems.map((item) => item.entryId);
|
||||||
|
|
||||||
|
// Not found items ids.
|
||||||
|
const notFoundItemsIds = difference(
|
||||||
|
purchaseInvoiceItems,
|
||||||
|
landedCostItemsIds
|
||||||
|
);
|
||||||
|
// Throw items ids not found service error.
|
||||||
|
if (notFoundItemsIds.length > 0) {
|
||||||
|
throw new ServiceError(ERRORS.LANDED_COST_ITEMS_IDS_NOT_FOUND);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the bill landed cost model.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {ILandedCostDTO} landedCostDTO
|
||||||
|
* @param {number} purchaseInvoiceId
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
private saveBillLandedCostModel = (
|
||||||
|
tenantId: number,
|
||||||
|
landedCostDTO: ILandedCostDTO,
|
||||||
|
purchaseInvoiceId: number
|
||||||
|
): Promise<IBillLandedCost> => {
|
||||||
|
const { BillLandedCost } = this.tenancy.models(tenantId);
|
||||||
|
const amount = sumBy(landedCostDTO.items, 'cost');
|
||||||
|
|
||||||
|
// Inserts the bill landed cost to the storage.
|
||||||
|
return BillLandedCost.query().insertGraph({
|
||||||
|
billId: purchaseInvoiceId,
|
||||||
|
fromTransactionType: landedCostDTO.transactionType,
|
||||||
|
fromTransactionId: landedCostDTO.transactionId,
|
||||||
|
fromTransactionEntryId: landedCostDTO.transactionEntryId,
|
||||||
|
amount,
|
||||||
|
allocationMethod: landedCostDTO.allocationMethod,
|
||||||
|
description: landedCostDTO.description,
|
||||||
|
allocateEntries: landedCostDTO.items,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allocate the landed cost amount to cost transactions.
|
||||||
|
* @param {number} tenantId -
|
||||||
|
* @param {string} transactionType
|
||||||
|
* @param {number} transactionId
|
||||||
|
*/
|
||||||
|
private incrementLandedCostAmount = async (
|
||||||
|
tenantId: number,
|
||||||
|
transactionType: string,
|
||||||
|
transactionId: number,
|
||||||
|
transactionEntryId: number,
|
||||||
|
amount: number
|
||||||
|
): Promise<void> => {
|
||||||
|
const Model = this.transactionLandedCost.getModel(
|
||||||
|
tenantId,
|
||||||
|
transactionType
|
||||||
|
);
|
||||||
|
const relation = CONFIG.COST_TYPES[transactionType].entries;
|
||||||
|
|
||||||
|
// Increment the landed cost transaction amount.
|
||||||
|
await Model.query()
|
||||||
|
.where('id', transactionId)
|
||||||
|
.increment('allocatedCostAmount', amount);
|
||||||
|
|
||||||
|
// Increment the landed cost entry.
|
||||||
|
await Model.relatedQuery(relation)
|
||||||
|
.for(transactionId)
|
||||||
|
.where('id', transactionEntryId)
|
||||||
|
.increment('allocatedCostAmount', amount);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverts the landed cost amount to cost transaction.
|
||||||
|
* @param {number} tenantId - Tenant id.
|
||||||
|
* @param {string} transactionType - Transaction type.
|
||||||
|
* @param {number} transactionId - Transaction id.
|
||||||
|
* @param {number} amount - Amount
|
||||||
|
*/
|
||||||
|
private revertLandedCostAmount = (
|
||||||
|
tenantId: number,
|
||||||
|
transactionType: string,
|
||||||
|
transactionId: number,
|
||||||
|
amount: number
|
||||||
|
) => {
|
||||||
|
const Model = this.transactionLandedCost.getModel(tenantId, transactionType);
|
||||||
|
|
||||||
|
// Decrement the allocate cost amount of cost transaction.
|
||||||
|
return Model.query()
|
||||||
|
.where('id', transactionId)
|
||||||
|
.decrement('allocatedCostAmount', amount);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the cost transaction or throw not found error.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {transactionType} transactionType -
|
||||||
|
* @param {transactionId} transactionId -
|
||||||
|
*/
|
||||||
|
public getLandedCostOrThrowError = async (
|
||||||
|
tenantId: number,
|
||||||
|
transactionType: string,
|
||||||
|
transactionId: number
|
||||||
|
) => {
|
||||||
|
const Model = this.transactionLandedCost.getModel(
|
||||||
|
tenantId,
|
||||||
|
transactionType
|
||||||
|
);
|
||||||
|
const model = await Model.query().findById(transactionId);
|
||||||
|
|
||||||
|
if (!model) {
|
||||||
|
throw new ServiceError(ERRORS.LANDED_COST_TRANSACTION_NOT_FOUND);
|
||||||
|
}
|
||||||
|
return this.transactionLandedCost.transformToLandedCost(
|
||||||
|
transactionType,
|
||||||
|
model
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the landed cost entries.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {string} transactionType
|
||||||
|
* @param {number} transactionId
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
public getLandedCostEntry = async (
|
||||||
|
tenantId: number,
|
||||||
|
transactionType: string,
|
||||||
|
transactionId: number,
|
||||||
|
transactionEntryId: number
|
||||||
|
): Promise<any> => {
|
||||||
|
const Model = this.transactionLandedCost.getModel(
|
||||||
|
tenantId,
|
||||||
|
transactionType
|
||||||
|
);
|
||||||
|
const relation = CONFIG.COST_TYPES[transactionType].entries;
|
||||||
|
|
||||||
|
const entry = await Model.relatedQuery(relation)
|
||||||
|
.for(transactionId)
|
||||||
|
.findOne('id', transactionEntryId)
|
||||||
|
.where('landedCost', true);
|
||||||
|
|
||||||
|
if (!entry) {
|
||||||
|
throw new ServiceError(ERRORS.LANDED_COST_ENTRY_NOT_FOUND);
|
||||||
|
}
|
||||||
|
return entry;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve allocate items cost total.
|
||||||
|
* @param {ILandedCostDTO} landedCostDTO
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
private getAllocateItemsCostTotal = (
|
||||||
|
landedCostDTO: ILandedCostDTO
|
||||||
|
): number => {
|
||||||
|
return sumBy(landedCostDTO.items, 'cost');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate allocate cost transaction should not be bill transaction.
|
||||||
|
* @param {number} purchaseInvoiceId
|
||||||
|
* @param {string} transactionType
|
||||||
|
* @param {number} transactionId
|
||||||
|
*/
|
||||||
|
private validateAllocateCostNotSameBill = (
|
||||||
|
purchaseInvoiceId: number,
|
||||||
|
transactionType: string,
|
||||||
|
transactionId: number
|
||||||
|
): void => {
|
||||||
|
if (transactionType === 'Bill' && transactionId === purchaseInvoiceId) {
|
||||||
|
throw new ServiceError(ERRORS.ALLOCATE_COST_SHOULD_NOT_BE_BILL);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the landed cost entry amount.
|
||||||
|
* @param {number} unallocatedCost -
|
||||||
|
* @param {number} amount -
|
||||||
|
*/
|
||||||
|
private validateLandedCostEntryAmount = (
|
||||||
|
unallocatedCost: number,
|
||||||
|
amount: number
|
||||||
|
): void => {
|
||||||
|
console.log(unallocatedCost, amount, '123');
|
||||||
|
|
||||||
|
if (unallocatedCost < amount) {
|
||||||
|
throw new ServiceError(ERRORS.COST_AMOUNT_BIGGER_THAN_UNALLOCATED_AMOUNT);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records inventory transactions.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {} allocateEntries
|
||||||
|
*/
|
||||||
|
private recordInventoryTransactions = async (
|
||||||
|
tenantId: number,
|
||||||
|
allocateEntries,
|
||||||
|
purchaseInvoice: IBill,
|
||||||
|
landedCostId: number
|
||||||
|
) => {
|
||||||
|
const costEntries = mergeObjectsBykey(
|
||||||
|
purchaseInvoice.entries,
|
||||||
|
allocateEntries.map((e) => ({ ...e, id: e.itemId })),
|
||||||
|
'id'
|
||||||
|
);
|
||||||
|
// Inventory transaction.
|
||||||
|
const inventoryTransactions = costEntries.map((entry) => ({
|
||||||
|
date: purchaseInvoice.billDate,
|
||||||
|
itemId: entry.itemId,
|
||||||
|
direction: 'IN',
|
||||||
|
quantity: 0,
|
||||||
|
rate: entry.cost,
|
||||||
|
transactionType: 'LandedCost',
|
||||||
|
transactionId: landedCostId,
|
||||||
|
entryId: entry.id,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return this.inventoryService.recordInventoryTransactions(
|
||||||
|
tenantId,
|
||||||
|
inventoryTransactions
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* =================================
|
||||||
|
* Allocate landed cost.
|
||||||
|
* =================================
|
||||||
|
* - Validates the allocate cost not the same purchase invoice id.
|
||||||
|
* - Get the given bill (purchase invoice) or throw not found error.
|
||||||
|
* - Get the given landed cost transaction or throw not found error.
|
||||||
|
* - Validate landed cost transaction has enough unallocated cost amount.
|
||||||
|
* - Validate landed cost transaction entry has enough unallocated cost amount.
|
||||||
|
* - Validate allocate entries existance and associated with cost bill transaction.
|
||||||
|
* - Writes inventory landed cost transaction.
|
||||||
|
* - Increment the allocated landed cost transaction.
|
||||||
|
* - Increment the allocated landed cost transaction entry.
|
||||||
|
*
|
||||||
|
* @param {ILandedCostDTO} landedCostDTO - Landed cost DTO.
|
||||||
|
* @param {number} tenantId - Tenant id.
|
||||||
|
* @param {number} purchaseInvoiceId - Purchase invoice id.
|
||||||
|
*/
|
||||||
|
public allocateLandedCost = async (
|
||||||
|
tenantId: number,
|
||||||
|
allocateCostDTO: ILandedCostDTO,
|
||||||
|
purchaseInvoiceId: number
|
||||||
|
): Promise<{
|
||||||
|
billLandedCost: IBillLandedCost;
|
||||||
|
}> => {
|
||||||
|
// Retrieve total cost of allocated items.
|
||||||
|
const amount = this.getAllocateItemsCostTotal(allocateCostDTO);
|
||||||
|
|
||||||
|
// Retrieve the purchase invoice or throw not found error.
|
||||||
|
const purchaseInvoice = await this.billsService.getBillOrThrowError(
|
||||||
|
tenantId,
|
||||||
|
purchaseInvoiceId
|
||||||
|
);
|
||||||
|
// Retrieve landed cost transaction or throw not found service error.
|
||||||
|
const landedCostTransaction = await this.getLandedCostOrThrowError(
|
||||||
|
tenantId,
|
||||||
|
allocateCostDTO.transactionType,
|
||||||
|
allocateCostDTO.transactionId
|
||||||
|
);
|
||||||
|
// Retrieve landed cost transaction entries.
|
||||||
|
const landedCostEntry = await this.getLandedCostEntry(
|
||||||
|
tenantId,
|
||||||
|
allocateCostDTO.transactionType,
|
||||||
|
allocateCostDTO.transactionId,
|
||||||
|
allocateCostDTO.transactionEntryId
|
||||||
|
);
|
||||||
|
// Validates allocate cost items association with the purchase invoice entries.
|
||||||
|
this.validateAllocateCostItems(
|
||||||
|
purchaseInvoice.entries,
|
||||||
|
allocateCostDTO.items
|
||||||
|
);
|
||||||
|
// Validate the amount of cost with unallocated landed cost.
|
||||||
|
this.validateLandedCostEntryAmount(
|
||||||
|
landedCostEntry.unallocatedLandedCost,
|
||||||
|
amount
|
||||||
|
);
|
||||||
|
// Save the bill landed cost model.
|
||||||
|
const billLandedCost = await this.saveBillLandedCostModel(
|
||||||
|
tenantId,
|
||||||
|
allocateCostDTO,
|
||||||
|
purchaseInvoiceId
|
||||||
|
);
|
||||||
|
// Records the inventory transactions.
|
||||||
|
// await this.recordInventoryTransactions(
|
||||||
|
// tenantId,
|
||||||
|
// allocateCostDTO.items,
|
||||||
|
// purchaseInvoice,
|
||||||
|
// landedCostTransaction.id
|
||||||
|
// );
|
||||||
|
// Increment landed cost amount on transaction and entry.
|
||||||
|
await this.incrementLandedCostAmount(
|
||||||
|
tenantId,
|
||||||
|
allocateCostDTO.transactionType,
|
||||||
|
allocateCostDTO.transactionId,
|
||||||
|
allocateCostDTO.transactionEntryId,
|
||||||
|
amount
|
||||||
|
);
|
||||||
|
// Write the landed cost journal entries.
|
||||||
|
// await this.writeJournalEntry(tenantId, purchaseInvoice, billLandedCost);
|
||||||
|
|
||||||
|
return { billLandedCost };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write journal entries of the given purchase invoice landed cost.
|
||||||
|
* @param tenantId
|
||||||
|
* @param purchaseInvoice
|
||||||
|
* @param landedCost
|
||||||
|
*/
|
||||||
|
private writeJournalEntry = async (
|
||||||
|
tenantId: number,
|
||||||
|
purchaseInvoice: IBill,
|
||||||
|
landedCost: IBillLandedCost
|
||||||
|
) => {
|
||||||
|
const journal = new JournalPoster(tenantId);
|
||||||
|
const billEntriesById = purchaseInvoice.entries;
|
||||||
|
|
||||||
|
const commonEntry = {
|
||||||
|
referenceType: 'Bill',
|
||||||
|
referenceId: purchaseInvoice.id,
|
||||||
|
date: purchaseInvoice.billDate,
|
||||||
|
indexGroup: 300,
|
||||||
|
};
|
||||||
|
const costEntry = new JournalEntry({
|
||||||
|
...commonEntry,
|
||||||
|
credit: landedCost.amount,
|
||||||
|
account: landedCost.costAccountId,
|
||||||
|
index: 1,
|
||||||
|
});
|
||||||
|
journal.credit(costEntry);
|
||||||
|
|
||||||
|
landedCost.allocateEntries.forEach((entry, index) => {
|
||||||
|
const billEntry = billEntriesById[entry.entryId];
|
||||||
|
|
||||||
|
const inventoryEntry = new JournalEntry({
|
||||||
|
...commonEntry,
|
||||||
|
debit: entry.cost,
|
||||||
|
account: billEntry.item.inventoryAccountId,
|
||||||
|
index: 1 + index,
|
||||||
|
});
|
||||||
|
journal.debit(inventoryEntry);
|
||||||
|
});
|
||||||
|
return journal;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the give bill landed cost or throw not found service error.
|
||||||
|
* @param {number} tenantId - Tenant id.
|
||||||
|
* @param {number} landedCostId - Landed cost id.
|
||||||
|
* @returns {Promise<IBillLandedCost>}
|
||||||
|
*/
|
||||||
|
public getBillLandedCostOrThrowError = async (
|
||||||
|
tenantId: number,
|
||||||
|
landedCostId: number
|
||||||
|
): Promise<IBillLandedCost> => {
|
||||||
|
const { BillLandedCost } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
// Retrieve the bill landed cost model.
|
||||||
|
const billLandedCost = await BillLandedCost.query().findById(landedCostId);
|
||||||
|
|
||||||
|
if (!billLandedCost) {
|
||||||
|
throw new ServiceError(ERRORS.BILL_LANDED_COST_NOT_FOUND);
|
||||||
|
}
|
||||||
|
return billLandedCost;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the landed cost transaction with assocaited allocate entries.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {number} landedCostId
|
||||||
|
*/
|
||||||
|
public deleteLandedCost = async (
|
||||||
|
tenantId: number,
|
||||||
|
landedCostId: number
|
||||||
|
): Promise<void> => {
|
||||||
|
const { BillLandedCost, BillLandedCostEntry } =
|
||||||
|
this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
// Deletes the bill landed cost allocated entries associated to landed cost.
|
||||||
|
await BillLandedCostEntry.query()
|
||||||
|
.where('bill_located_cost_id', landedCostId)
|
||||||
|
.delete();
|
||||||
|
|
||||||
|
// Delete the bill landed cost from the storage.
|
||||||
|
await BillLandedCost.query().where('id', landedCostId).delete();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the allocated landed cost.
|
||||||
|
* ==================================
|
||||||
|
* - Delete bill landed cost transaction with associated allocate entries.
|
||||||
|
* - Delete the associated inventory transactions.
|
||||||
|
* - Decrement allocated amount of landed cost transaction and entry.
|
||||||
|
* - Revert journal entries.
|
||||||
|
*
|
||||||
|
* @param {number} tenantId - Tenant id.
|
||||||
|
* @param {number} landedCostId - Landed cost id.
|
||||||
|
* @return {Promise<void>}
|
||||||
|
*/
|
||||||
|
public deleteAllocatedLandedCost = async (
|
||||||
|
tenantId: number,
|
||||||
|
landedCostId: number
|
||||||
|
): Promise<{
|
||||||
|
landedCostId: number;
|
||||||
|
}> => {
|
||||||
|
// Retrieves the bill landed cost.
|
||||||
|
const oldBillLandedCost = await this.getBillLandedCostOrThrowError(
|
||||||
|
tenantId,
|
||||||
|
landedCostId
|
||||||
|
);
|
||||||
|
// Delete landed cost transaction with assocaited locate entries.
|
||||||
|
await this.deleteLandedCost(tenantId, landedCostId);
|
||||||
|
|
||||||
|
// Removes the inventory transactions.
|
||||||
|
await this.removeInventoryTransactions(tenantId, landedCostId);
|
||||||
|
|
||||||
|
// Reverts the landed cost amount to the cost transaction.
|
||||||
|
await this.revertLandedCostAmount(
|
||||||
|
tenantId,
|
||||||
|
oldBillLandedCost.fromTransactionType,
|
||||||
|
oldBillLandedCost.fromTransactionId,
|
||||||
|
oldBillLandedCost.amount
|
||||||
|
);
|
||||||
|
return { landedCostId };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the inventory transaction.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {number} landedCostId
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
private removeInventoryTransactions = (tenantId, landedCostId: number) => {
|
||||||
|
return this.inventoryService.deleteInventoryTransactions(
|
||||||
|
tenantId,
|
||||||
|
landedCostId,
|
||||||
|
'LandedCost'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -203,5 +203,15 @@ export default {
|
|||||||
onQuickCreated: 'onInventoryAdjustmentQuickCreated',
|
onQuickCreated: 'onInventoryAdjustmentQuickCreated',
|
||||||
onDeleted: 'onInventoryAdjustmentDeleted',
|
onDeleted: 'onInventoryAdjustmentDeleted',
|
||||||
onPublished: 'onInventoryAdjustmentPublished',
|
onPublished: 'onInventoryAdjustmentPublished',
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bill landed cost.
|
||||||
|
*/
|
||||||
|
billLandedCost: {
|
||||||
|
onCreate: 'onBillLandedCostCreate',
|
||||||
|
onCreated: 'onBillLandedCostCreated',
|
||||||
|
onDelete: 'onBillLandedCostDelete',
|
||||||
|
onDeleted: 'onBillLandedCostDeleted'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -373,6 +373,11 @@ const accumSum = (data, callback) => {
|
|||||||
}, 0)
|
}, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const mergeObjectsBykey = (object1, object2, key) => {
|
||||||
|
var merged = _.merge(_.keyBy(object1, key), _.keyBy(object2, key));
|
||||||
|
return _.values(merged);
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
accumSum,
|
accumSum,
|
||||||
increment,
|
increment,
|
||||||
@@ -400,5 +405,6 @@ export {
|
|||||||
transactionIncrement,
|
transactionIncrement,
|
||||||
transformToMapBy,
|
transformToMapBy,
|
||||||
dateRangeFromToCollection,
|
dateRangeFromToCollection,
|
||||||
transformToMapKeyValue
|
transformToMapKeyValue,
|
||||||
|
mergeObjectsBykey
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user