mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 13:20:31 +00:00
Merge branch 'feature/landed-cost'
This commit is contained in:
@@ -39,7 +39,7 @@ export default class ExpensesController extends BaseController {
|
||||
);
|
||||
router.post(
|
||||
'/:id',
|
||||
[...this.expenseDTOSchema, ...this.expenseParamSchema],
|
||||
[...this.editExpenseDTOSchema, ...this.expenseParamSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.editExpense.bind(this)),
|
||||
this.catchServiceErrors
|
||||
@@ -111,16 +111,67 @@ export default class ExpensesController extends BaseController {
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('categories.*.landed_cost').optional().isBoolean().toBoolean(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Expense param schema.
|
||||
* Edit expense validation schema.
|
||||
*/
|
||||
get editExpenseDTOSchema() {
|
||||
return [
|
||||
check('reference_no')
|
||||
.optional({ nullable: true })
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('payment_date').exists().isISO8601(),
|
||||
check('payment_account_id')
|
||||
.exists()
|
||||
.isInt({ max: DATATYPES_LENGTH.INT_10 })
|
||||
.toInt(),
|
||||
check('description')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.isLength({ max: DATATYPES_LENGTH.TEXT }),
|
||||
check('currency_code').optional().isString().isLength({ max: 3 }),
|
||||
check('exchange_rate').optional({ nullable: true }).isNumeric().toFloat(),
|
||||
check('publish').optional().isBoolean().toBoolean(),
|
||||
check('payee_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
|
||||
check('categories').exists().isArray({ min: 1 }),
|
||||
check('categories.*.id').optional().isNumeric().toInt(),
|
||||
check('categories.*.index')
|
||||
.exists()
|
||||
.isInt({ max: DATATYPES_LENGTH.INT_10 })
|
||||
.toInt(),
|
||||
check('categories.*.expense_account_id')
|
||||
.exists()
|
||||
.isInt({ max: DATATYPES_LENGTH.INT_10 })
|
||||
.toInt(),
|
||||
check('categories.*.amount')
|
||||
.optional({ nullable: true })
|
||||
.isFloat({ max: DATATYPES_LENGTH.DECIMAL_13_3 }) // 13, 3
|
||||
.toFloat(),
|
||||
check('categories.*.description')
|
||||
.optional()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('categories.*.landed_cost').optional().isBoolean().toBoolean(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Expense param validation schema.
|
||||
*/
|
||||
get expenseParamSchema() {
|
||||
return [param('id').exists().isNumeric().toInt()];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Expenses list validation schema.
|
||||
*/
|
||||
get expensesListSchema() {
|
||||
return [
|
||||
query('custom_view_id').optional().isNumeric().toInt(),
|
||||
@@ -251,11 +302,8 @@ export default class ExpensesController extends BaseController {
|
||||
}
|
||||
|
||||
try {
|
||||
const {
|
||||
expenses,
|
||||
pagination,
|
||||
filterMeta,
|
||||
} = await this.expensesService.getExpensesList(tenantId, filter);
|
||||
const { expenses, pagination, filterMeta } =
|
||||
await this.expensesService.getExpensesList(tenantId, filter);
|
||||
|
||||
return res.status(200).send({
|
||||
expenses,
|
||||
@@ -293,7 +341,7 @@ export default class ExpensesController extends BaseController {
|
||||
* @param {Response} res
|
||||
* @param {ServiceError} error
|
||||
*/
|
||||
catchServiceErrors(
|
||||
private catchServiceErrors(
|
||||
error: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
@@ -345,6 +393,30 @@ export default class ExpensesController extends BaseController {
|
||||
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 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'ENTRIES_ALLOCATED_COST_COULD_NOT_DELETED') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{ type: 'ENTRIES_ALLOCATED_COST_COULD_NOT_DELETED', code: 1000 },
|
||||
],
|
||||
});
|
||||
}
|
||||
if (
|
||||
error.errorType === 'LOCATED_COST_ENTRIES_SHOULD_BIGGE_THAN_NEW_ENTRIES'
|
||||
) {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'LOCATED_COST_ENTRIES_SHOULD_BIGGE_THAN_NEW_ENTRIES',
|
||||
code: 1100,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
|
||||
@@ -71,7 +71,6 @@ export default class CashFlowController extends BaseFinancialReportController {
|
||||
/**
|
||||
* Transformes the report statement to table rows.
|
||||
* @param {ITransactionsByVendorsStatement} statement -
|
||||
*
|
||||
*/
|
||||
private transformToTableRows(cashFlowDOO: ICashFlowStatementDOO, tenantId: number) {
|
||||
const i18n = this.tenancy.i18n(tenantId);
|
||||
|
||||
@@ -23,6 +23,7 @@ export default class CustomerBalanceSummaryReportController extends BaseFinancia
|
||||
router.get(
|
||||
'/',
|
||||
this.validationSchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.customerBalanceSummary.bind(this))
|
||||
);
|
||||
return router;
|
||||
@@ -34,7 +35,13 @@ export default class CustomerBalanceSummaryReportController extends BaseFinancia
|
||||
get validationSchema() {
|
||||
return [
|
||||
...this.sheetNumberFormatValidationSchema,
|
||||
|
||||
// As date.
|
||||
query('as_date').optional().isISO8601(),
|
||||
|
||||
// Customers ids.
|
||||
query('customers_ids').optional().isArray({ min: 1 }),
|
||||
query('customers_ids.*').exists().isInt().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -53,8 +53,12 @@ export default class InventoryDetailsController extends BaseController {
|
||||
.escape(),
|
||||
query('from_date').optional(),
|
||||
query('to_date').optional(),
|
||||
|
||||
query('none_zero').optional().isBoolean().toBoolean(),
|
||||
query('none_transactions').optional().isBoolean().toBoolean(),
|
||||
|
||||
query('items_ids').optional().isArray(),
|
||||
query('items_ids.*').optional().isInt().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,10 @@ export default class InventoryValuationReportController extends BaseFinancialRep
|
||||
return [
|
||||
query('from_date').optional().isISO8601(),
|
||||
query('to_date').optional().isISO8601(),
|
||||
|
||||
query('items_ids').optional().isArray(),
|
||||
query('items_ids.*').optional().isInt().toInt(),
|
||||
|
||||
query('number_format.no_cents').optional().isBoolean().toBoolean(),
|
||||
query('number_format.divide_1000').optional().isBoolean().toBoolean(),
|
||||
query('none_transactions').default(true).isBoolean().toBoolean(),
|
||||
|
||||
@@ -28,14 +28,20 @@ export default class PurchasesByItemReportController extends BaseFinancialReport
|
||||
|
||||
/**
|
||||
* Validation schema.
|
||||
* @return {ValidationChain[]}
|
||||
*/
|
||||
get validationSchema(): ValidationChain[] {
|
||||
return [
|
||||
query('from_date').optional().isISO8601(),
|
||||
query('to_date').optional().isISO8601(),
|
||||
|
||||
query('number_format.no_cents').optional().isBoolean().toBoolean(),
|
||||
query('number_format.divide_1000').optional().isBoolean().toBoolean(),
|
||||
query('none_transactions').default(true).isBoolean().toBoolean(),
|
||||
|
||||
query('items_ids').optional().isArray(),
|
||||
query('items_ids.*').optional().isInt().toInt(),
|
||||
|
||||
query('orderBy').optional().isIn(['created_at', 'name', 'code']),
|
||||
query('order').optional().isIn(['desc', 'asc']),
|
||||
];
|
||||
|
||||
@@ -33,6 +33,10 @@ export default class SalesByItemsReportController extends BaseFinancialReportCon
|
||||
return [
|
||||
query('from_date').optional().isISO8601(),
|
||||
query('to_date').optional().isISO8601(),
|
||||
|
||||
query('items_ids').optional().isArray(),
|
||||
query('items_ids.*').optional().isInt().toInt(),
|
||||
|
||||
query('number_format.no_cents').optional().isBoolean().toBoolean(),
|
||||
query('number_format.divide_1000').optional().isBoolean().toBoolean(),
|
||||
query('none_transactions').default(true).isBoolean().toBoolean(),
|
||||
|
||||
@@ -23,6 +23,7 @@ export default class TransactionsByCustomersReportController extends BaseFinanci
|
||||
router.get(
|
||||
'/',
|
||||
this.validationSchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.transactionsByCustomers.bind(this))
|
||||
);
|
||||
return router;
|
||||
@@ -31,13 +32,18 @@ export default class TransactionsByCustomersReportController extends BaseFinanci
|
||||
/**
|
||||
* Validation schema.
|
||||
*/
|
||||
get validationSchema() {
|
||||
private get validationSchema() {
|
||||
return [
|
||||
...this.sheetNumberFormatValidationSchema,
|
||||
query('from_date').optional().isISO8601(),
|
||||
query('to_date').optional().isISO8601(),
|
||||
|
||||
query('none_zero').optional().isBoolean().toBoolean(),
|
||||
query('none_transactions').optional().isBoolean().toBoolean(),
|
||||
|
||||
// Customers ids.
|
||||
query('customers_ids').optional().isArray({ min: 1 }),
|
||||
query('customers_ids.*').exists().isInt().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -45,7 +51,9 @@ export default class TransactionsByCustomersReportController extends BaseFinanci
|
||||
* Transformes the statement to table rows response.
|
||||
* @param {ITransactionsByCustomersStatement} statement -
|
||||
*/
|
||||
transformToTableResponse({ data }: ITransactionsByCustomersStatement) {
|
||||
private transformToTableResponse({
|
||||
data,
|
||||
}: ITransactionsByCustomersStatement) {
|
||||
return {
|
||||
table: {
|
||||
rows: this.transactionsByCustomersTableRows.tableRows(data),
|
||||
@@ -57,7 +65,7 @@ export default class TransactionsByCustomersReportController extends BaseFinanci
|
||||
* Transformes the statement to json response.
|
||||
* @param {ITransactionsByCustomersStatement} statement -
|
||||
*/
|
||||
transfromToJsonResponse({
|
||||
private transfromToJsonResponse({
|
||||
data,
|
||||
columns,
|
||||
}: ITransactionsByCustomersStatement) {
|
||||
@@ -83,10 +91,11 @@ export default class TransactionsByCustomersReportController extends BaseFinanci
|
||||
const filter = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const transactionsByCustomers = await this.transactionsByCustomersService.transactionsByCustomers(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
const transactionsByCustomers =
|
||||
await this.transactionsByCustomersService.transactionsByCustomers(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
const accept = this.accepts(req);
|
||||
const acceptType = accept.types(['json', 'application/json+table']);
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ export default class TransactionsByVendorsReportController extends BaseFinancial
|
||||
router.get(
|
||||
'/',
|
||||
this.validationSchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.transactionsByVendors.bind(this))
|
||||
);
|
||||
return router;
|
||||
@@ -34,10 +35,16 @@ export default class TransactionsByVendorsReportController extends BaseFinancial
|
||||
get validationSchema(): ValidationChain[] {
|
||||
return [
|
||||
...this.sheetNumberFormatValidationSchema,
|
||||
|
||||
query('from_date').optional().isISO8601(),
|
||||
query('to_date').optional().isISO8601(),
|
||||
|
||||
query('none_zero').optional().isBoolean().toBoolean(),
|
||||
query('none_transactions').optional().isBoolean().toBoolean(),
|
||||
|
||||
// Vendors ids.
|
||||
query('vendors_ids').optional().isArray({ min: 1 }),
|
||||
query('vendors_ids.*').exists().isInt().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -80,10 +87,11 @@ export default class TransactionsByVendorsReportController extends BaseFinancial
|
||||
const filter = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const transactionsByVendors = await this.transactionsByVendorsService.transactionsByVendors(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
const transactionsByVendors =
|
||||
await this.transactionsByVendorsService.transactionsByVendors(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
const accept = this.accepts(req);
|
||||
const acceptType = accept.types(['json', 'application/json+table']);
|
||||
|
||||
|
||||
@@ -34,6 +34,10 @@ export default class VendorBalanceSummaryReportController extends BaseFinancialR
|
||||
return [
|
||||
...this.sheetNumberFormatValidationSchema,
|
||||
query('as_date').optional().isISO8601(),
|
||||
|
||||
// Vendors ids.
|
||||
query('vendors_ids').optional().isArray({ min: 1 }),
|
||||
query('vendors_ids.*').exists().isInt().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -41,7 +45,7 @@ export default class VendorBalanceSummaryReportController extends BaseFinancialR
|
||||
* Transformes the report statement to table rows.
|
||||
* @param {IVendorBalanceSummaryStatement} statement -
|
||||
*/
|
||||
transformToTableRows({ data }: IVendorBalanceSummaryStatement) {
|
||||
private transformToTableRows({ data }: IVendorBalanceSummaryStatement) {
|
||||
return {
|
||||
table: {
|
||||
data: this.vendorBalanceSummaryTableRows.tableRowsTransformer(data),
|
||||
@@ -53,7 +57,10 @@ export default class VendorBalanceSummaryReportController extends BaseFinancialR
|
||||
* Transformes the report statement to raw json.
|
||||
* @param {IVendorBalanceSummaryStatement} statement -
|
||||
*/
|
||||
transformToJsonResponse({ data, columns }: IVendorBalanceSummaryStatement) {
|
||||
private transformToJsonResponse({
|
||||
data,
|
||||
columns,
|
||||
}: IVendorBalanceSummaryStatement) {
|
||||
return {
|
||||
data: this.transfromToResponse(data),
|
||||
columns: this.transfromToResponse(columns),
|
||||
@@ -72,10 +79,11 @@ export default class VendorBalanceSummaryReportController extends BaseFinancialR
|
||||
const filter = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const vendorBalanceSummary = await this.vendorBalanceSummaryService.vendorBalanceSummary(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
const vendorBalanceSummary =
|
||||
await this.vendorBalanceSummaryService.vendorBalanceSummary(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
const accept = this.accepts(req);
|
||||
const acceptType = accept.types(['json', 'application/json+table']);
|
||||
|
||||
|
||||
@@ -406,7 +406,7 @@ export default class ItemsController extends BaseController {
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
handlerServiceErrors(
|
||||
private handlerServiceErrors(
|
||||
error: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
|
||||
@@ -110,6 +110,10 @@ export default class BillsController extends BaseController {
|
||||
.optional({ nullable: true })
|
||||
.trim()
|
||||
.escape(),
|
||||
check('entries.*.landed_cost')
|
||||
.optional({ nullable: true })
|
||||
.isBoolean()
|
||||
.toBoolean(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -141,6 +145,10 @@ export default class BillsController extends BaseController {
|
||||
.optional({ nullable: true })
|
||||
.trim()
|
||||
.escape(),
|
||||
check('entries.*.landed_cost')
|
||||
.optional({ nullable: true })
|
||||
.isBoolean()
|
||||
.toBoolean(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -301,11 +309,8 @@ export default class BillsController extends BaseController {
|
||||
filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles);
|
||||
}
|
||||
try {
|
||||
const {
|
||||
bills,
|
||||
pagination,
|
||||
filterMeta,
|
||||
} = await this.billsService.getBills(tenantId, filter);
|
||||
const { bills, pagination, filterMeta } =
|
||||
await this.billsService.getBills(tenantId, filter);
|
||||
|
||||
return res.status(200).send({
|
||||
bills,
|
||||
@@ -342,7 +347,7 @@ export default class BillsController extends BaseController {
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
handleServiceError(
|
||||
private handleServiceError(
|
||||
error: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
@@ -397,17 +402,72 @@ export default class BillsController extends BaseController {
|
||||
if (error.errorType === 'contact_not_found') {
|
||||
return res.boom.badRequest(null, {
|
||||
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') {
|
||||
return res.status(400).send({
|
||||
errors: [{
|
||||
type: 'BILL_HAS_ASSOCIATED_PAYMENT_ENTRIES',
|
||||
message: 'Cannot delete bill that has associated payment transactions.',
|
||||
code: 1200
|
||||
}],
|
||||
errors: [
|
||||
{
|
||||
type: 'BILL_HAS_ASSOCIATED_PAYMENT_ENTRIES',
|
||||
message:
|
||||
'Cannot delete bill that has associated payment transactions.',
|
||||
code: 1200,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'BILL_HAS_ASSOCIATED_LANDED_COSTS') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'BILL_HAS_ASSOCIATED_LANDED_COSTS',
|
||||
message:
|
||||
'Cannot delete bill that has associated landed cost transactions.',
|
||||
code: 1300,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'ENTRIES_ALLOCATED_COST_COULD_NOT_DELETED') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'ENTRIES_ALLOCATED_COST_COULD_NOT_DELETED',
|
||||
code: 1400,
|
||||
message:
|
||||
'Bill entries that have landed cost type can not be deleted.',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (
|
||||
error.errorType === 'LOCATED_COST_ENTRIES_SHOULD_BIGGE_THAN_NEW_ENTRIES'
|
||||
) {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'LOCATED_COST_ENTRIES_SHOULD_BIGGE_THAN_NEW_ENTRIES',
|
||||
code: 1500,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'LANDED_COST_ENTRIES_SHOULD_BE_INVENTORY_ITEMS') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'LANDED_COST_ENTRIES_SHOULD_BE_INVENTORY_ITEMS',
|
||||
message:
|
||||
'Landed cost entries should be only with inventory items.',
|
||||
code: 1600,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
291
server/src/api/controllers/Purchases/LandedCost.ts
Normal file
291
server/src/api/controllers/Purchases/LandedCost.ts
Normal file
@@ -0,0 +1,291 @@
|
||||
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';
|
||||
|
||||
@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 - Request
|
||||
* @param {Response} res - Response.
|
||||
* @param {NextFunction} next - Next function.
|
||||
*/
|
||||
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: this.transfromToResponse(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',
|
||||
message: 'The give bill id not found.',
|
||||
code: 100,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'LANDED_COST_TRANSACTION_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'LANDED_COST_TRANSACTION_NOT_FOUND',
|
||||
message: 'The given landed cost transaction id not found.',
|
||||
code: 200,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'LANDED_COST_ENTRY_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'LANDED_COST_ENTRY_NOT_FOUND',
|
||||
message: 'The given landed cost tranasction entry id not found.',
|
||||
code: 300,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'COST_AMOUNT_BIGGER_THAN_UNALLOCATED_AMOUNT') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'COST_AMOUNT_BIGGER_THAN_UNALLOCATED_AMOUNT',
|
||||
code: 400,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'LANDED_COST_ITEMS_IDS_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'LANDED_COST_ITEMS_IDS_NOT_FOUND',
|
||||
message: 'The given entries ids of purchase invoice not found.',
|
||||
code: 500,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'BILL_LANDED_COST_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'BILL_LANDED_COST_NOT_FOUND',
|
||||
message: 'The given bill located landed cost not found.',
|
||||
code: 600,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
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 Bills from 'api/controllers/Purchases/Bills'
|
||||
import BillPayments from 'api/controllers/Purchases/BillsPayments';
|
||||
import BillAllocateLandedCost from './LandedCost';
|
||||
|
||||
@Service()
|
||||
export default class PurchasesController {
|
||||
@@ -11,6 +12,7 @@ export default class PurchasesController {
|
||||
|
||||
router.use('/bills', Container.get(Bills).router());
|
||||
router.use('/bill_payments', Container.get(BillPayments).router());
|
||||
router.use('/landed-cost', Container.get(BillAllocateLandedCost).router());
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -347,7 +347,7 @@ export default class SaleInvoicesController extends BaseController {
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
handleServiceErrors(
|
||||
private handleServiceErrors(
|
||||
error: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
|
||||
@@ -40,7 +40,6 @@ import Ping from 'api/controllers/Ping';
|
||||
import Subscription from 'api/controllers/Subscription';
|
||||
import Licenses from 'api/controllers/Subscription/Licenses';
|
||||
import InventoryAdjustments from 'api/controllers/Inventory/InventoryAdjustments';
|
||||
|
||||
import Setup from 'api/controllers/Setup';
|
||||
|
||||
export default () => {
|
||||
|
||||
@@ -17,6 +17,7 @@ exports.up = function (knex) {
|
||||
table.text('sell_description').nullable();
|
||||
table.text('purchase_description').nullable();
|
||||
table.integer('quantity_on_hand');
|
||||
table.boolean('landed_cost').nullable();
|
||||
|
||||
table.text('note').nullable();
|
||||
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) {
|
||||
return knex.schema.createTable('expenses_transactions', (table) => {
|
||||
table.increments();
|
||||
table.decimal('total_amount', 13, 3);
|
||||
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');
|
||||
table.date('published_at').index();
|
||||
table.integer('user_id').unsigned().index();
|
||||
table.date('payment_date').index();
|
||||
table.timestamps();
|
||||
}).raw('ALTER TABLE `EXPENSES_TRANSACTIONS` AUTO_INCREMENT = 1000');
|
||||
table.decimal('total_amount', 13, 3);
|
||||
table.decimal('landed_cost_amount', 13, 3).defaultTo(0);
|
||||
table.decimal('allocated_cost_amount', 13, 3).defaultTo(0);
|
||||
|
||||
table.date('published_at').index();
|
||||
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');
|
||||
};
|
||||
|
||||
@@ -1,16 +1,29 @@
|
||||
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.createTable('expense_transaction_categories', table => {
|
||||
table.increments();
|
||||
table.integer('expense_account_id').unsigned().index().references('id').inTable('accounts');
|
||||
table.integer('index').unsigned();
|
||||
table.text('description');
|
||||
table.decimal('amount', 13, 3);
|
||||
table.integer('expense_id').unsigned().index().references('id').inTable('expenses_transactions');
|
||||
table.timestamps();
|
||||
}).raw('ALTER TABLE `EXPENSE_TRANSACTION_CATEGORIES` AUTO_INCREMENT = 1000');;
|
||||
exports.up = function (knex) {
|
||||
return knex.schema
|
||||
.createTable('expense_transaction_categories', (table) => {
|
||||
table.increments();
|
||||
table
|
||||
.integer('expense_account_id')
|
||||
.unsigned()
|
||||
.index()
|
||||
.references('id')
|
||||
.inTable('accounts');
|
||||
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');
|
||||
};
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
|
||||
exports.up = function(knex) {
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.createTable('bills', (table) => {
|
||||
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.date('bill_date').index();
|
||||
table.date('due_date').index();
|
||||
@@ -12,6 +16,8 @@ exports.up = function(knex) {
|
||||
table.decimal('amount', 13, 3).defaultTo(0);
|
||||
table.string('currency_code');
|
||||
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.date('opened_at').index();
|
||||
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');
|
||||
};
|
||||
|
||||
@@ -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) {};
|
||||
@@ -1,24 +1,39 @@
|
||||
|
||||
exports.up = function(knex) {
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.createTable('items_entries', (table) => {
|
||||
table.increments();
|
||||
table.string('reference_type').index();
|
||||
table.string('reference_id').index();
|
||||
|
||||
table.integer('index').unsigned();
|
||||
table.integer('item_id').unsigned().index().references('id').inTable('items');
|
||||
table
|
||||
.integer('item_id')
|
||||
.unsigned()
|
||||
.index()
|
||||
.references('id')
|
||||
.inTable('items');
|
||||
table.text('description');
|
||||
table.integer('discount').unsigned();
|
||||
table.integer('quantity').unsigned();
|
||||
table.integer('rate').unsigned();
|
||||
|
||||
table.integer('sell_account_id').unsigned().references('id').inTable('accounts');
|
||||
table.integer('cost_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.boolean('landed_cost').defaultTo(false);
|
||||
table.decimal('allocated_cost_amount', 13, 3).defaultTo(0);
|
||||
|
||||
table.timestamps();
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(knex) {
|
||||
exports.down = function (knex) {
|
||||
return knex.schema.dropTableIfExists('items_entries');
|
||||
};
|
||||
|
||||
@@ -1,64 +1,69 @@
|
||||
import { IDynamicListFilterDTO } from "./DynamicFilter";
|
||||
import { IItemEntry, IItemEntryDTO } from "./ItemEntry";
|
||||
import { IDynamicListFilterDTO } from './DynamicFilter';
|
||||
import { IItemEntry, IItemEntryDTO } from './ItemEntry';
|
||||
|
||||
export interface IBillDTO {
|
||||
vendorId: number,
|
||||
billNumber: string,
|
||||
billDate: Date,
|
||||
dueDate: Date,
|
||||
referenceNo: string,
|
||||
status: string,
|
||||
note: string,
|
||||
amount: number,
|
||||
paymentAmount: number,
|
||||
open: boolean,
|
||||
entries: IItemEntryDTO[],
|
||||
};
|
||||
vendorId: number;
|
||||
billNumber: string;
|
||||
billDate: Date;
|
||||
dueDate: Date;
|
||||
referenceNo: string;
|
||||
status: string;
|
||||
note: string;
|
||||
amount: number;
|
||||
paymentAmount: number;
|
||||
open: boolean;
|
||||
entries: IItemEntryDTO[];
|
||||
}
|
||||
|
||||
export interface IBillEditDTO {
|
||||
vendorId: number,
|
||||
billNumber: string,
|
||||
billDate: Date,
|
||||
dueDate: Date,
|
||||
referenceNo: string,
|
||||
status: string,
|
||||
note: string,
|
||||
amount: number,
|
||||
paymentAmount: number,
|
||||
open: boolean,
|
||||
entries: IItemEntryDTO[],
|
||||
};
|
||||
vendorId: number;
|
||||
billNumber: string;
|
||||
billDate: Date;
|
||||
dueDate: Date;
|
||||
referenceNo: string;
|
||||
status: string;
|
||||
note: string;
|
||||
amount: number;
|
||||
paymentAmount: number;
|
||||
open: boolean;
|
||||
entries: IItemEntryDTO[];
|
||||
}
|
||||
|
||||
export interface IBill {
|
||||
id?: number,
|
||||
id?: number;
|
||||
|
||||
vendorId: number,
|
||||
billNumber: string,
|
||||
billDate: Date,
|
||||
dueDate: Date,
|
||||
referenceNo: string,
|
||||
status: string,
|
||||
note: string,
|
||||
amount: number,
|
||||
paymentAmount: number,
|
||||
currencyCode: string,
|
||||
vendorId: number;
|
||||
billNumber: string;
|
||||
billDate: Date;
|
||||
dueDate: Date;
|
||||
referenceNo: string;
|
||||
status: string;
|
||||
note: string;
|
||||
|
||||
dueAmount: number,
|
||||
overdueDays: number,
|
||||
amount: number;
|
||||
allocatedCostAmount: number;
|
||||
landedCostAmount: number;
|
||||
unallocatedCostAmount: number;
|
||||
|
||||
openedAt: Date | string,
|
||||
paymentAmount: number;
|
||||
currencyCode: string;
|
||||
|
||||
entries: IItemEntry[],
|
||||
userId: number,
|
||||
dueAmount: number;
|
||||
overdueDays: number;
|
||||
|
||||
createdAt: Date,
|
||||
updateAt: Date,
|
||||
};
|
||||
openedAt: Date | string;
|
||||
|
||||
export interface IBillsFilter extends IDynamicListFilterDTO {
|
||||
stringifiedFilterRoles?: string,
|
||||
entries: IItemEntry[];
|
||||
userId: number;
|
||||
|
||||
createdAt: Date;
|
||||
updateAt: Date;
|
||||
}
|
||||
|
||||
export interface IBillsFilter extends IDynamicListFilterDTO {
|
||||
stringifiedFilterRoles?: string;
|
||||
}
|
||||
|
||||
export interface IBillsService {
|
||||
validateVendorHasNoBills(tenantId: number, vendorId: number): Promise<void>;
|
||||
}
|
||||
}
|
||||
|
||||
18
server/src/interfaces/Entry.ts
Normal file
18
server/src/interfaces/Entry.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export interface ICommonEntry {
|
||||
id: number;
|
||||
amount: number;
|
||||
}
|
||||
|
||||
export interface ICommonLandedCostEntry extends ICommonEntry {
|
||||
landedCost: boolean;
|
||||
allocatedCostAmount: number;
|
||||
}
|
||||
|
||||
export interface ICommonEntryDTO {
|
||||
id?: number;
|
||||
amount: number;
|
||||
}
|
||||
|
||||
export interface ICommonLandedCostEntryDTO extends ICommonEntryDTO {
|
||||
landedCost?: boolean;
|
||||
}
|
||||
@@ -27,15 +27,23 @@ export interface IExpense {
|
||||
userId: number;
|
||||
paymentDate: Date;
|
||||
payeeId: number;
|
||||
landedCostAmount: number;
|
||||
allocatedCostAmount: number;
|
||||
unallocatedCostAmount: number;
|
||||
categories: IExpenseCategory[];
|
||||
}
|
||||
|
||||
export interface IExpenseCategory {
|
||||
id?: number;
|
||||
expenseAccountId: number;
|
||||
index: number;
|
||||
description: string;
|
||||
expenseId: number;
|
||||
amount: number;
|
||||
|
||||
allocatedCostAmount: number;
|
||||
unallocatedCostAmount: number;
|
||||
landedCost: boolean;
|
||||
}
|
||||
|
||||
export interface IExpenseDTO {
|
||||
@@ -52,10 +60,13 @@ export interface IExpenseDTO {
|
||||
}
|
||||
|
||||
export interface IExpenseCategoryDTO {
|
||||
id?: number;
|
||||
expenseAccountId: number;
|
||||
index: number;
|
||||
amount: number;
|
||||
description?: string;
|
||||
expenseId: number;
|
||||
landedCost?: boolean;
|
||||
}
|
||||
|
||||
export interface IExpensesService {
|
||||
|
||||
@@ -7,6 +7,7 @@ export interface IInventoryValuationReportQuery {
|
||||
asDate: Date | string;
|
||||
numberFormat: INumberFormatQuery;
|
||||
noneTransactions: boolean;
|
||||
itemsIds: number[],
|
||||
};
|
||||
|
||||
export interface IInventoryValuationSheetMeta {
|
||||
|
||||
@@ -7,6 +7,7 @@ export interface IInventoryDetailsQuery {
|
||||
toDate: Date | string;
|
||||
numberFormat: INumberFormatQuery;
|
||||
noneTransactions: boolean;
|
||||
itemsIds: number[]
|
||||
}
|
||||
|
||||
export interface IInventoryDetailsNumber {
|
||||
|
||||
@@ -1,24 +1,30 @@
|
||||
|
||||
export type IItemEntryTransactionType = 'SaleInvoice' | 'Bill' | 'SaleReceipt';
|
||||
|
||||
export interface IItemEntry {
|
||||
id?: number,
|
||||
id?: number;
|
||||
|
||||
referenceType: string,
|
||||
referenceId: number,
|
||||
referenceType: string;
|
||||
referenceId: number;
|
||||
|
||||
index: number,
|
||||
index: number;
|
||||
|
||||
itemId: number,
|
||||
description: string,
|
||||
discount: number,
|
||||
quantity: number,
|
||||
rate: number,
|
||||
itemId: number;
|
||||
description: string;
|
||||
discount: number;
|
||||
quantity: number;
|
||||
rate: number;
|
||||
amount: number;
|
||||
|
||||
sellAccountId: number,
|
||||
costAccountId: number,
|
||||
landedCost: number;
|
||||
allocatedCostAmount: number;
|
||||
unallocatedCostAmount: number;
|
||||
|
||||
sellAccountId: number;
|
||||
costAccountId: number;
|
||||
}
|
||||
|
||||
export interface IItemEntryDTO {
|
||||
|
||||
}
|
||||
id?: number,
|
||||
itemId: number;
|
||||
landedCost?: boolean;
|
||||
}
|
||||
|
||||
96
server/src/interfaces/LandedCost.ts
Normal file
96
server/src/interfaces/LandedCost.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
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;
|
||||
unallocatedCostAmount: number;
|
||||
allocatedCostAmount: number;
|
||||
description: string;
|
||||
costAccountId: number;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
allocateEntries?: IBillLandedCostTransactionEntry[],
|
||||
};
|
||||
|
||||
export interface IBillLandedCostTransactionEntry {
|
||||
cost: number;
|
||||
entryId: number;
|
||||
billLocatedCostId: number,
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
export interface ISalesByItemsReportQuery {
|
||||
fromDate: Date | string;
|
||||
toDate: Date | string;
|
||||
itemsIds: number[],
|
||||
numberFormat: INumberFormatQuery;
|
||||
noneTransactions: boolean;
|
||||
};
|
||||
|
||||
@@ -18,7 +18,9 @@ export interface ITransactionsByCustomersCustomer {
|
||||
}
|
||||
|
||||
export interface ITransactionsByCustomersFilter
|
||||
extends ITransactionsByContactsFilter {}
|
||||
extends ITransactionsByContactsFilter {
|
||||
customersIds: number[];
|
||||
}
|
||||
|
||||
export type ITransactionsByCustomersData = ITransactionsByCustomersCustomer[];
|
||||
|
||||
|
||||
@@ -18,7 +18,9 @@ export interface ITransactionsByVendorsVendor {
|
||||
}
|
||||
|
||||
export interface ITransactionsByVendorsFilter
|
||||
extends ITransactionsByContactsFilter {}
|
||||
extends ITransactionsByContactsFilter {
|
||||
vendorsIds: number[];
|
||||
}
|
||||
|
||||
export type ITransactionsByVendorsData = ITransactionsByVendorsVendor[];
|
||||
|
||||
|
||||
@@ -53,6 +53,8 @@ export * from './Table';
|
||||
export * from './Ledger';
|
||||
export * from './CashFlow';
|
||||
export * from './InventoryDetails';
|
||||
export * from './LandedCost';
|
||||
export * from './Entry';
|
||||
|
||||
export interface I18nService {
|
||||
__: (input: string) => string;
|
||||
|
||||
@@ -26,4 +26,6 @@ import 'subscribers/vendors';
|
||||
import 'subscribers/paymentMades';
|
||||
import 'subscribers/paymentReceives';
|
||||
import 'subscribers/saleEstimates';
|
||||
import 'subscribers/items';
|
||||
import 'subscribers/items';
|
||||
|
||||
import 'subscribers/LandedCost';
|
||||
@@ -36,6 +36,8 @@ import Media from 'models/Media';
|
||||
import MediaLink from 'models/MediaLink';
|
||||
import InventoryAdjustment from 'models/InventoryAdjustment';
|
||||
import InventoryAdjustmentEntry from 'models/InventoryAdjustmentEntry';
|
||||
import BillLandedCost from 'models/BillLandedCost';
|
||||
import BillLandedCostEntry from 'models/BillLandedCostEntry';
|
||||
|
||||
export default (knex) => {
|
||||
const models = {
|
||||
@@ -75,6 +77,8 @@ export default (knex) => {
|
||||
Contact,
|
||||
InventoryAdjustment,
|
||||
InventoryAdjustmentEntry,
|
||||
BillLandedCost,
|
||||
BillLandedCostEntry
|
||||
};
|
||||
return mapValues(models, (model) => model.bindKnex(knex));
|
||||
}
|
||||
@@ -103,6 +103,7 @@ export default class Bill extends TenantModel {
|
||||
'remainingDays',
|
||||
'overdueDays',
|
||||
'isOverdue',
|
||||
'unallocatedCostAmount'
|
||||
];
|
||||
}
|
||||
|
||||
@@ -178,6 +179,14 @@ export default class Bill extends TenantModel {
|
||||
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')) {
|
||||
// Can't continue in case due date not defined.
|
||||
if (!this.dueDate) {
|
||||
@@ -195,6 +204,7 @@ export default class Bill extends TenantModel {
|
||||
static get relationMappings() {
|
||||
const Contact = require('models/Contact');
|
||||
const ItemEntry = require('models/ItemEntry');
|
||||
const BillLandedCost = require('models/BillLandedCost');
|
||||
|
||||
return {
|
||||
vendor: {
|
||||
@@ -220,6 +230,15 @@ export default class Bill extends TenantModel {
|
||||
builder.where('reference_type', 'Bill');
|
||||
},
|
||||
},
|
||||
|
||||
locatedLandedCosts: {
|
||||
relation: Model.HasManyRelation,
|
||||
modelClass: BillLandedCost.default,
|
||||
join: {
|
||||
from: 'bills.id',
|
||||
to: 'bill_located_costs.billId',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
65
server/src/models/BillLandedCost.js
Normal file
65
server/src/models/BillLandedCost.js
Normal file
@@ -0,0 +1,65 @@
|
||||
import { Model } from 'objection';
|
||||
import { lowerCase } from 'lodash';
|
||||
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'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Virtual attributes.
|
||||
*/
|
||||
static get virtualAttributes() {
|
||||
return ['allocationMethodFormatted'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Allocation method formatted.
|
||||
*/
|
||||
get allocationMethodFormatted() {
|
||||
const allocationMethod = lowerCase(this.allocationMethod);
|
||||
const keyLabelsPairs = {
|
||||
value: 'Value',
|
||||
quantity: 'Quantity',
|
||||
};
|
||||
return keyLabelsPairs[allocationMethod] || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship mapping.
|
||||
*/
|
||||
static get relationMappings() {
|
||||
const BillLandedCostEntry = require('models/BillLandedCostEntry');
|
||||
const Bill = require('models/Bill');
|
||||
|
||||
return {
|
||||
bill: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: Bill.default,
|
||||
join: {
|
||||
from: 'bill_located_costs.billId',
|
||||
to: 'bills.id',
|
||||
},
|
||||
},
|
||||
allocateEntries: {
|
||||
relation: Model.HasManyRelation,
|
||||
modelClass: BillLandedCostEntry.default,
|
||||
join: {
|
||||
from: 'bill_located_costs.id',
|
||||
to: 'bill_located_cost_entries.billLocatedCostId',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
32
server/src/models/BillLandedCostEntry.js
Normal file
32
server/src/models/BillLandedCostEntry.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Model } from 'objection';
|
||||
import TenantModel from 'models/TenantModel';
|
||||
|
||||
export default class BillLandedCostEntry extends TenantModel {
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
static get tableName() {
|
||||
return 'bill_located_cost_entries';
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship mapping.
|
||||
*/
|
||||
static get relationMappings() {
|
||||
const ItemEntry = require('models/ItemEntry');
|
||||
|
||||
return {
|
||||
itemEntry: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: ItemEntry.default,
|
||||
join: {
|
||||
from: 'bill_located_cost_entries.entryId',
|
||||
to: 'items_entries.referenceId',
|
||||
},
|
||||
filter(builder) {
|
||||
builder.where('reference_type', 'Bill');
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,27 @@
|
||||
import { Model } from "objection";
|
||||
import TenantModel from "models/TenantModel";
|
||||
import { viewRolesBuilder } from "lib/ViewRolesBuilder";
|
||||
import { Model } from 'objection';
|
||||
import TenantModel from 'models/TenantModel';
|
||||
import { viewRolesBuilder } from 'lib/ViewRolesBuilder';
|
||||
|
||||
export default class Expense extends TenantModel {
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
static get tableName() {
|
||||
return "expenses_transactions";
|
||||
return 'expenses_transactions';
|
||||
}
|
||||
|
||||
/**
|
||||
* Account transaction reference type.
|
||||
*/
|
||||
static get referenceType() {
|
||||
return "Expense";
|
||||
return 'Expense';
|
||||
}
|
||||
|
||||
/**
|
||||
* Model timestamps.
|
||||
*/
|
||||
get timestamps() {
|
||||
return ["createdAt", "updatedAt"];
|
||||
return ['createdAt', 'updatedAt'];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -37,14 +37,23 @@ export default class Expense extends TenantModel {
|
||||
static get media() {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
static get virtualAttributes() {
|
||||
return ["isPublished"];
|
||||
return ['isPublished', 'unallocatedCostAmount'];
|
||||
}
|
||||
|
||||
isPublished() {
|
||||
return Boolean(this.publishedAt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the unallocated cost amount.
|
||||
* @return {number}
|
||||
*/
|
||||
get unallocatedCostAmount() {
|
||||
return Math.max(this.amount - this.allocatedCostAmount, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Model modifiers.
|
||||
*/
|
||||
@@ -52,28 +61,28 @@ export default class Expense extends TenantModel {
|
||||
return {
|
||||
filterByDateRange(query, startDate, endDate) {
|
||||
if (startDate) {
|
||||
query.where("date", ">=", startDate);
|
||||
query.where('date', '>=', startDate);
|
||||
}
|
||||
if (endDate) {
|
||||
query.where("date", "<=", endDate);
|
||||
query.where('date', '<=', endDate);
|
||||
}
|
||||
},
|
||||
filterByAmountRange(query, from, to) {
|
||||
if (from) {
|
||||
query.where("amount", ">=", from);
|
||||
query.where('amount', '>=', from);
|
||||
}
|
||||
if (to) {
|
||||
query.where("amount", "<=", to);
|
||||
query.where('amount', '<=', to);
|
||||
}
|
||||
},
|
||||
filterByExpenseAccount(query, accountId) {
|
||||
if (accountId) {
|
||||
query.where("expense_account_id", accountId);
|
||||
query.where('expense_account_id', accountId);
|
||||
}
|
||||
},
|
||||
filterByPaymentAccount(query, accountId) {
|
||||
if (accountId) {
|
||||
query.where("payment_account_id", accountId);
|
||||
query.where('payment_account_id', accountId);
|
||||
}
|
||||
},
|
||||
viewRolesBuilder(query, conditionals, expression) {
|
||||
@@ -94,40 +103,40 @@ export default class Expense extends TenantModel {
|
||||
* Relationship mapping.
|
||||
*/
|
||||
static get relationMappings() {
|
||||
const Account = require("models/Account");
|
||||
const ExpenseCategory = require("models/ExpenseCategory");
|
||||
const Media = require("models/Media");
|
||||
const Account = require('models/Account');
|
||||
const ExpenseCategory = require('models/ExpenseCategory');
|
||||
const Media = require('models/Media');
|
||||
|
||||
return {
|
||||
paymentAccount: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: Account.default,
|
||||
join: {
|
||||
from: "expenses_transactions.paymentAccountId",
|
||||
to: "accounts.id",
|
||||
from: 'expenses_transactions.paymentAccountId',
|
||||
to: 'accounts.id',
|
||||
},
|
||||
},
|
||||
categories: {
|
||||
relation: Model.HasManyRelation,
|
||||
modelClass: ExpenseCategory.default,
|
||||
join: {
|
||||
from: "expenses_transactions.id",
|
||||
to: "expense_transaction_categories.expenseId",
|
||||
from: 'expenses_transactions.id',
|
||||
to: 'expense_transaction_categories.expenseId',
|
||||
},
|
||||
},
|
||||
media: {
|
||||
relation: Model.ManyToManyRelation,
|
||||
modelClass: Media.default,
|
||||
join: {
|
||||
from: "expenses_transactions.id",
|
||||
from: 'expenses_transactions.id',
|
||||
through: {
|
||||
from: "media_links.model_id",
|
||||
to: "media_links.media_id",
|
||||
from: 'media_links.model_id',
|
||||
to: 'media_links.media_id',
|
||||
},
|
||||
to: "media.id",
|
||||
to: 'media.id',
|
||||
},
|
||||
filter(query) {
|
||||
query.where("model_name", "Expense");
|
||||
query.where('model_name', 'Expense');
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -139,39 +148,39 @@ export default class Expense extends TenantModel {
|
||||
static get fields() {
|
||||
return {
|
||||
payment_date: {
|
||||
label: "Payment date",
|
||||
column: "payment_date",
|
||||
columnType: "date",
|
||||
label: 'Payment date',
|
||||
column: 'payment_date',
|
||||
columnType: 'date',
|
||||
},
|
||||
payment_account: {
|
||||
label: "Payment account",
|
||||
column: "payment_account_id",
|
||||
relation: "accounts.id",
|
||||
optionsResource: "account",
|
||||
label: 'Payment account',
|
||||
column: 'payment_account_id',
|
||||
relation: 'accounts.id',
|
||||
optionsResource: 'account',
|
||||
},
|
||||
amount: {
|
||||
label: "Amount",
|
||||
column: "total_amount",
|
||||
columnType: "number",
|
||||
label: 'Amount',
|
||||
column: 'total_amount',
|
||||
columnType: 'number',
|
||||
},
|
||||
currency_code: {
|
||||
label: "Currency",
|
||||
column: "currency_code",
|
||||
optionsResource: "currency",
|
||||
label: 'Currency',
|
||||
column: 'currency_code',
|
||||
optionsResource: 'currency',
|
||||
},
|
||||
reference_no: {
|
||||
label: "Reference No.",
|
||||
column: "reference_no",
|
||||
columnType: "string",
|
||||
label: 'Reference No.',
|
||||
column: 'reference_no',
|
||||
columnType: 'string',
|
||||
},
|
||||
description: {
|
||||
label: "Description",
|
||||
column: "description",
|
||||
columnType: "string",
|
||||
label: 'Description',
|
||||
column: 'description',
|
||||
columnType: 'string',
|
||||
},
|
||||
published: {
|
||||
label: "Published",
|
||||
column: "published_at",
|
||||
label: 'Published',
|
||||
column: 'published_at',
|
||||
},
|
||||
status: {
|
||||
label: 'Status',
|
||||
@@ -194,9 +203,9 @@ export default class Expense extends TenantModel {
|
||||
},
|
||||
},
|
||||
created_at: {
|
||||
label: "Created at",
|
||||
column: "created_at",
|
||||
columnType: "date",
|
||||
label: 'Created at',
|
||||
column: 'created_at',
|
||||
columnType: 'date',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,6 +9,21 @@ export default class ExpenseCategory extends TenantModel {
|
||||
return 'expense_transaction_categories';
|
||||
}
|
||||
|
||||
/**
|
||||
* Virtual attributes.
|
||||
*/
|
||||
static get virtualAttributes() {
|
||||
return ['unallocatedCostAmount'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Remain unallocated landed cost.
|
||||
* @return {number}
|
||||
*/
|
||||
get unallocatedCostAmount() {
|
||||
return Math.max(this.amount - this.allocatedCostAmount, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship mapping.
|
||||
*/
|
||||
|
||||
@@ -21,8 +21,8 @@ export default class ItemEntry extends TenantModel {
|
||||
return ['amount'];
|
||||
}
|
||||
|
||||
static amount() {
|
||||
return this.calcAmount(this);
|
||||
get amount() {
|
||||
return ItemEntry.calcAmount(this);
|
||||
}
|
||||
|
||||
static calcAmount(itemEntry) {
|
||||
@@ -34,6 +34,7 @@ export default class ItemEntry extends TenantModel {
|
||||
|
||||
static get relationMappings() {
|
||||
const Item = require('models/Item');
|
||||
const BillLandedCostEntry = require('models/BillLandedCostEntry');
|
||||
|
||||
return {
|
||||
item: {
|
||||
@@ -44,6 +45,14 @@ export default class ItemEntry extends TenantModel {
|
||||
to: 'items.id',
|
||||
},
|
||||
},
|
||||
allocatedCostEntries: {
|
||||
relation: Model.HasManyRelation,
|
||||
modelClass: BillLandedCostEntry.default,
|
||||
join: {
|
||||
from: 'items_entries.referenceId',
|
||||
to: 'bill_located_cost_entries.entryId',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import moment from 'moment';
|
||||
import { sumBy } from 'lodash';
|
||||
import {
|
||||
IBill,
|
||||
IManualJournalEntry,
|
||||
ISaleReceipt,
|
||||
ISystemUser,
|
||||
IAccount,
|
||||
} from 'interfaces';
|
||||
import JournalPoster from './JournalPoster';
|
||||
import JournalEntry from './JournalEntry';
|
||||
@@ -17,7 +19,6 @@ import {
|
||||
IItemEntry,
|
||||
} from 'interfaces';
|
||||
import { increment } from 'utils';
|
||||
|
||||
export default class JournalCommands {
|
||||
journal: JournalPoster;
|
||||
models: any;
|
||||
@@ -37,45 +38,20 @@ export default class JournalCommands {
|
||||
/**
|
||||
* Records the bill journal entries.
|
||||
* @param {IBill} bill
|
||||
* @param {boolean} override - Override the old bill entries.
|
||||
* @param {IAccount} payableAccount -
|
||||
*/
|
||||
async bill(bill: IBill, override: boolean = false): Promise<void> {
|
||||
const { transactionsRepository, accountRepository } = this.repositories;
|
||||
const { Item, ItemEntry } = this.models;
|
||||
|
||||
const entriesItemsIds = bill.entries.map((entry) => entry.itemId);
|
||||
|
||||
// Retrieve the bill transaction items.
|
||||
const storedItems = await Item.query().whereIn('id', entriesItemsIds);
|
||||
|
||||
const storedItemsMap = new Map(storedItems.map((item) => [item.id, item]));
|
||||
const payableAccount = await accountRepository.findOne({
|
||||
slug: 'accounts-payable',
|
||||
});
|
||||
const formattedDate = moment(bill.billDate).format('YYYY-MM-DD');
|
||||
|
||||
bill(bill: IBill, payableAccount: IAccount): void {
|
||||
const commonJournalMeta = {
|
||||
debit: 0,
|
||||
credit: 0,
|
||||
referenceId: bill.id,
|
||||
referenceType: 'Bill',
|
||||
date: formattedDate,
|
||||
date: moment(bill.billDate).format('YYYY-MM-DD'),
|
||||
userId: bill.userId,
|
||||
|
||||
referenceNumber: bill.referenceNo,
|
||||
transactionNumber: bill.billNumber,
|
||||
|
||||
createdAt: bill.createdAt,
|
||||
};
|
||||
// Overrides the old bill entries.
|
||||
if (override) {
|
||||
const entries = await transactionsRepository.journal({
|
||||
referenceType: ['Bill'],
|
||||
referenceId: [bill.id],
|
||||
});
|
||||
this.journal.fromTransactions(entries);
|
||||
this.journal.removeEntries();
|
||||
}
|
||||
const payableEntry = new JournalEntry({
|
||||
...commonJournalMeta,
|
||||
credit: bill.amount,
|
||||
@@ -86,15 +62,15 @@ export default class JournalCommands {
|
||||
this.journal.credit(payableEntry);
|
||||
|
||||
bill.entries.forEach((entry, index) => {
|
||||
const item: IItem = storedItemsMap.get(entry.itemId);
|
||||
const amount = ItemEntry.calcAmount(entry);
|
||||
const landedCostAmount = sumBy(entry.allocatedCostEntries, 'cost');
|
||||
|
||||
// Inventory or cost entry.
|
||||
const debitEntry = new JournalEntry({
|
||||
...commonJournalMeta,
|
||||
debit: amount,
|
||||
debit: entry.amount + landedCostAmount,
|
||||
account:
|
||||
['inventory'].indexOf(item.type) !== -1
|
||||
? item.inventoryAccountId
|
||||
['inventory'].indexOf(entry.item.type) !== -1
|
||||
? entry.item.inventoryAccountId
|
||||
: entry.costAccountId,
|
||||
index: index + 2,
|
||||
itemId: entry.itemId,
|
||||
@@ -102,6 +78,16 @@ export default class JournalCommands {
|
||||
});
|
||||
this.journal.debit(debitEntry);
|
||||
});
|
||||
|
||||
// Allocate cost entries journal entries.
|
||||
bill.locatedLandedCosts.forEach((landedCost) => {
|
||||
const creditEntry = new JournalEntry({
|
||||
...commonJournalMeta,
|
||||
credit: landedCost.amount,
|
||||
account: landedCost.costAccountId,
|
||||
});
|
||||
this.journal.credit(creditEntry);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
78
server/src/services/Entries/index.ts
Normal file
78
server/src/services/Entries/index.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { Service } from 'typedi';
|
||||
import { ServiceError } from 'exceptions';
|
||||
import { transformToMap } from 'utils';
|
||||
import {
|
||||
ICommonLandedCostEntry,
|
||||
ICommonLandedCostEntryDTO
|
||||
} from 'interfaces';
|
||||
|
||||
const ERRORS = {
|
||||
ENTRIES_ALLOCATED_COST_COULD_NOT_DELETED:
|
||||
'ENTRIES_ALLOCATED_COST_COULD_NOT_DELETED',
|
||||
LOCATED_COST_ENTRIES_SHOULD_BIGGE_THAN_NEW_ENTRIES:
|
||||
'LOCATED_COST_ENTRIES_SHOULD_BIGGE_THAN_NEW_ENTRIES',
|
||||
};
|
||||
|
||||
@Service()
|
||||
export default class EntriesService {
|
||||
/**
|
||||
* Validates bill entries that has allocated landed cost amount not deleted.
|
||||
* @param {IItemEntry[]} oldCommonEntries -
|
||||
* @param {IItemEntry[]} newBillEntries -
|
||||
*/
|
||||
public getLandedCostEntriesDeleted(
|
||||
oldCommonEntries: ICommonLandedCostEntry[],
|
||||
newCommonEntriesDTO: ICommonLandedCostEntryDTO[]
|
||||
): ICommonLandedCostEntry[] {
|
||||
const newBillEntriesById = transformToMap(newCommonEntriesDTO, 'id');
|
||||
|
||||
return oldCommonEntries.filter((entry) => {
|
||||
const newEntry = newBillEntriesById.get(entry.id);
|
||||
|
||||
if (entry.allocatedCostAmount > 0 && typeof newEntry === 'undefined') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the bill entries that have located cost amount should not be deleted.
|
||||
* @param {IItemEntry[]} oldCommonEntries - Old bill entries.
|
||||
* @param {IItemEntryDTO[]} newBillEntries - New DTO bill entries.
|
||||
*/
|
||||
public validateLandedCostEntriesNotDeleted(
|
||||
oldCommonEntries: ICommonLandedCostEntry[],
|
||||
newCommonEntriesDTO: ICommonLandedCostEntryDTO[]
|
||||
): void {
|
||||
const entriesDeleted = this.getLandedCostEntriesDeleted(
|
||||
oldCommonEntries,
|
||||
newCommonEntriesDTO
|
||||
);
|
||||
if (entriesDeleted.length > 0) {
|
||||
throw new ServiceError(ERRORS.ENTRIES_ALLOCATED_COST_COULD_NOT_DELETED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate allocated cost amount entries should be smaller than new entries amount.
|
||||
* @param {IItemEntry[]} oldCommonEntries - Old bill entries.
|
||||
* @param {IItemEntryDTO[]} newBillEntries - New DTO bill entries.
|
||||
*/
|
||||
public validateLocatedCostEntriesSmallerThanNewEntries(
|
||||
oldCommonEntries: ICommonLandedCostEntry[],
|
||||
newCommonEntriesDTO: ICommonLandedCostEntryDTO[]
|
||||
): void {
|
||||
const oldBillEntriesById = transformToMap(oldCommonEntries, 'id');
|
||||
|
||||
newCommonEntriesDTO.forEach((entry) => {
|
||||
const oldEntry = oldBillEntriesById.get(entry.id);
|
||||
|
||||
if (oldEntry && oldEntry.allocatedCostAmount > entry.amount) {
|
||||
throw new ServiceError(
|
||||
ERRORS.LOCATED_COST_ENTRIES_SHOULD_BIGGE_THAN_NEW_ENTRIES
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -17,11 +17,13 @@ import {
|
||||
IExpensesService,
|
||||
ISystemUser,
|
||||
IPaginationMeta,
|
||||
IExpenseCategory,
|
||||
} from 'interfaces';
|
||||
import DynamicListingService from 'services/DynamicListing/DynamicListService';
|
||||
import events from 'subscribers/events';
|
||||
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';
|
||||
import EntriesService from 'services/Entries';
|
||||
|
||||
const ERRORS = {
|
||||
EXPENSE_NOT_FOUND: 'expense_not_found',
|
||||
@@ -32,6 +34,7 @@ const ERRORS = {
|
||||
PAYMENT_ACCOUNT_HAS_INVALID_TYPE: 'payment_account_has_invalid_type',
|
||||
EXPENSES_ACCOUNT_HAS_INVALID_TYPE: 'expenses_account_has_invalid_type',
|
||||
EXPENSE_ALREADY_PUBLISHED: 'expense_already_published',
|
||||
EXPENSE_HAS_ASSOCIATED_LANDED_COST: 'EXPENSE_HAS_ASSOCIATED_LANDED_COST',
|
||||
};
|
||||
|
||||
@Service()
|
||||
@@ -51,6 +54,9 @@ export default class ExpensesService implements IExpensesService {
|
||||
@Inject()
|
||||
contactsService: ContactsService;
|
||||
|
||||
@Inject()
|
||||
entriesService: EntriesService;
|
||||
|
||||
/**
|
||||
* Retrieve the payment account details or returns not found server error in case the
|
||||
* given account not found on the storage.
|
||||
@@ -249,14 +255,16 @@ export default class ExpensesService implements IExpensesService {
|
||||
* @returns {IExpense|ServiceError}
|
||||
*/
|
||||
private async getExpenseOrThrowError(tenantId: number, expenseId: number) {
|
||||
const { expenseRepository } = this.tenancy.repositories(tenantId);
|
||||
const { Expense } = this.tenancy.models(tenantId);
|
||||
|
||||
this.logger.info('[expense] trying to get the given expense.', {
|
||||
tenantId,
|
||||
expenseId,
|
||||
});
|
||||
// Retrieve the given expense by id.
|
||||
const expense = await expenseRepository.findOneById(expenseId);
|
||||
const expense = await Expense.query()
|
||||
.findById(expenseId)
|
||||
.withGraphFetched('categories');
|
||||
|
||||
if (!expense) {
|
||||
this.logger.info('[expense] the given expense not found.', {
|
||||
@@ -308,6 +316,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.
|
||||
* @param {IExpenseDTO} expenseDTO
|
||||
@@ -315,12 +344,14 @@ export default class ExpensesService implements IExpensesService {
|
||||
* @return {IExpense}
|
||||
*/
|
||||
private expenseDTOToModel(expenseDTO: IExpenseDTO, user?: ISystemUser) {
|
||||
const totalAmount = sumBy(expenseDTO.categories, 'amount');
|
||||
const landedCostAmount = this.getExpenseLandedCostAmount(expenseDTO);
|
||||
const totalAmount = this.getExpenseCategoriesTotal(expenseDTO.categories);
|
||||
|
||||
return {
|
||||
categories: [],
|
||||
...omit(expenseDTO, ['publish']),
|
||||
totalAmount,
|
||||
landedCostAmount,
|
||||
paymentDate: moment(expenseDTO.paymentDate).toMySqlDateTime(),
|
||||
...(user
|
||||
? {
|
||||
@@ -340,7 +371,7 @@ export default class ExpensesService implements IExpensesService {
|
||||
* @param {IExpenseDTO} expenseDTO
|
||||
* @return {number[]}
|
||||
*/
|
||||
mapExpensesAccountsIdsFromDTO(expenseDTO: IExpenseDTO) {
|
||||
private mapExpensesAccountsIdsFromDTO(expenseDTO: IExpenseDTO) {
|
||||
return expenseDTO.categories.map((category) => category.expenseAccountId);
|
||||
}
|
||||
|
||||
@@ -434,36 +465,47 @@ export default class ExpensesService implements IExpensesService {
|
||||
const { expenseRepository } = this.tenancy.repositories(tenantId);
|
||||
const oldExpense = await this.getExpenseOrThrowError(tenantId, expenseId);
|
||||
|
||||
// - Validate payment account existance on the storage.
|
||||
// Validate payment account existance on the storage.
|
||||
const paymentAccount = await this.getPaymentAccountOrThrowError(
|
||||
tenantId,
|
||||
expenseDTO.paymentAccountId
|
||||
);
|
||||
// - Validate expense accounts exist on the storage.
|
||||
// Validate expense accounts exist on the storage.
|
||||
const expensesAccounts = await this.getExpensesAccountsOrThrowError(
|
||||
tenantId,
|
||||
this.mapExpensesAccountsIdsFromDTO(expenseDTO)
|
||||
);
|
||||
// - Validate payment account type.
|
||||
// Validate payment account type.
|
||||
await this.validatePaymentAccountType(tenantId, paymentAccount);
|
||||
|
||||
// - Validate expenses accounts type.
|
||||
// Validate expenses accounts type.
|
||||
await this.validateExpensesAccountsType(tenantId, expensesAccounts);
|
||||
|
||||
// - Validate the expense payee contact id existance on storage.
|
||||
// Validate the expense payee contact id existance on storage.
|
||||
if (expenseDTO.payeeId) {
|
||||
await this.contactsService.getContactByIdOrThrowError(
|
||||
tenantId,
|
||||
expenseDTO.payeeId
|
||||
);
|
||||
}
|
||||
// - Validate the given expense categories not equal zero.
|
||||
// Validate the given expense categories not equal zero.
|
||||
this.validateCategoriesNotEqualZero(expenseDTO);
|
||||
|
||||
// - Update the expense on the storage.
|
||||
// Update the expense on the storage.
|
||||
const expenseObj = this.expenseDTOToModel(expenseDTO);
|
||||
|
||||
// - Upsert the expense object with expense entries.
|
||||
// Validate expense entries that have allocated landed cost cannot be deleted.
|
||||
this.entriesService.validateLandedCostEntriesNotDeleted(
|
||||
oldExpense.categories,
|
||||
expenseDTO.categories,
|
||||
);
|
||||
// Validate expense entries that have allocated cost amount should be bigger than amount.
|
||||
this.entriesService.validateLocatedCostEntriesSmallerThanNewEntries(
|
||||
oldExpense.categories,
|
||||
expenseDTO.categories,
|
||||
);
|
||||
|
||||
// Upsert the expense object with expense entries.
|
||||
const expense = await expenseRepository.upsertGraph({
|
||||
id: expenseId,
|
||||
...expenseObj,
|
||||
@@ -544,15 +586,16 @@ export default class ExpensesService implements IExpensesService {
|
||||
authorizedUser: ISystemUser
|
||||
): Promise<void> {
|
||||
const oldExpense = await this.getExpenseOrThrowError(tenantId, expenseId);
|
||||
const {
|
||||
expenseRepository,
|
||||
expenseEntryRepository,
|
||||
} = this.tenancy.repositories(tenantId);
|
||||
const { expenseRepository, expenseEntryRepository } =
|
||||
this.tenancy.repositories(tenantId);
|
||||
|
||||
this.logger.info('[expense] trying to delete the expense.', {
|
||||
tenantId,
|
||||
expenseId,
|
||||
});
|
||||
// Validates the expense has no associated landed cost.
|
||||
await this.validateNoAssociatedLandedCost(tenantId, expenseId);
|
||||
|
||||
await expenseEntryRepository.deleteBy({ expenseId });
|
||||
await expenseRepository.deleteById(expenseId);
|
||||
|
||||
@@ -572,7 +615,7 @@ export default class ExpensesService implements IExpensesService {
|
||||
|
||||
/**
|
||||
* Filters the not published expenses.
|
||||
* @param {IExpense[]} expenses -
|
||||
* @param {IExpense[]} expenses -
|
||||
*/
|
||||
public getNonePublishedExpenses(expenses: IExpense[]): IExpense[] {
|
||||
return expenses.filter((expense) => !expense.publishedAt);
|
||||
@@ -648,4 +691,25 @@ export default class ExpensesService implements IExpensesService {
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ export default class ARAgingSummaryService {
|
||||
});
|
||||
// Retrieve all customers from the storage.
|
||||
const customers =
|
||||
filter.customersIds.length > 0
|
||||
(filter.customersIds.length > 0)
|
||||
? await customerRepository.findWhereIn('id', filter.customersIds)
|
||||
: await customerRepository.all();
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Inject } from 'typedi';
|
||||
import { raw } from 'objection';
|
||||
import { isEmpty } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import {
|
||||
IItem,
|
||||
@@ -17,10 +18,16 @@ export default class InventoryDetailsRepository {
|
||||
* @param {number} tenantId -
|
||||
* @returns {Promise<IItem>}
|
||||
*/
|
||||
public getInventoryItems(tenantId: number): Promise<IItem[]> {
|
||||
public getInventoryItems(tenantId: number, itemsIds?: number[]): Promise<IItem[]> {
|
||||
const { Item } = this.tenancy.models(tenantId);
|
||||
|
||||
return Item.query().where('type', 'inventory');
|
||||
return Item.query().onBuild((q) => {
|
||||
q.where('type', 'inventory');
|
||||
|
||||
if (!isEmpty(itemsIds)) {
|
||||
q.whereIn('id', itemsIds);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -31,6 +31,7 @@ export default class InventoryDetailsService extends FinancialSheet {
|
||||
return {
|
||||
fromDate: moment().startOf('year').format('YYYY-MM-DD'),
|
||||
toDate: moment().endOf('year').format('YYYY-MM-DD'),
|
||||
itemsIds: [],
|
||||
numberFormat: {
|
||||
precision: 2,
|
||||
divideOn1000: false,
|
||||
@@ -91,8 +92,10 @@ export default class InventoryDetailsService extends FinancialSheet {
|
||||
...query,
|
||||
};
|
||||
// Retrieves the items.
|
||||
const items = await this.reportRepo.getInventoryItems(tenantId);
|
||||
|
||||
const items = await this.reportRepo.getInventoryItems(
|
||||
tenantId,
|
||||
filter.itemsIds
|
||||
);
|
||||
// Opening balance transactions.
|
||||
const openingBalanceTransactions =
|
||||
await this.reportRepo.openingBalanceTransactions(tenantId, filter);
|
||||
|
||||
@@ -26,6 +26,7 @@ export default class InventoryValuationSheetService {
|
||||
get defaultQuery(): IInventoryValuationReportQuery {
|
||||
return {
|
||||
asDate: moment().endOf('year').format('YYYY-MM-DD'),
|
||||
itemsIds: [],
|
||||
numberFormat: {
|
||||
precision: 2,
|
||||
divideOn1000: false,
|
||||
@@ -75,9 +76,6 @@ export default class InventoryValuationSheetService {
|
||||
) {
|
||||
const { Item, InventoryCostLotTracker } = this.tenancy.models(tenantId);
|
||||
|
||||
const inventoryItems = await Item.query().where('type', 'inventory');
|
||||
const inventoryItemsIds = inventoryItems.map((item) => item.id);
|
||||
|
||||
// Settings tenant service.
|
||||
const settings = this.tenancy.settings(tenantId);
|
||||
const baseCurrency = settings.get({
|
||||
@@ -89,6 +87,15 @@ export default class InventoryValuationSheetService {
|
||||
...this.defaultQuery,
|
||||
...query,
|
||||
};
|
||||
const inventoryItems = await Item.query().onBuild(q => {
|
||||
q.where('type', 'inventory');
|
||||
|
||||
if (filter.itemsIds.length > 0) {
|
||||
q.whereIn('id', filter.itemsIds);
|
||||
}
|
||||
});
|
||||
const inventoryItemsIds = inventoryItems.map((item) => item.id);
|
||||
|
||||
const commonQuery = (builder) => {
|
||||
builder.whereIn('item_id', inventoryItemsIds);
|
||||
builder.sum('rate as rate');
|
||||
|
||||
@@ -24,6 +24,7 @@ export default class InventoryValuationReportService {
|
||||
return {
|
||||
fromDate: moment().startOf('year').format('YYYY-MM-DD'),
|
||||
toDate: moment().endOf('year').format('YYYY-MM-DD'),
|
||||
itemsIds: [],
|
||||
numberFormat: {
|
||||
precision: 2,
|
||||
divideOn1000: false,
|
||||
@@ -91,7 +92,13 @@ export default class InventoryValuationReportService {
|
||||
filter,
|
||||
tenantId,
|
||||
});
|
||||
const inventoryItems = await Item.query().where('type', 'inventory');
|
||||
const inventoryItems = await Item.query().onBuild(q => {
|
||||
q.where('type', 'inventory');
|
||||
|
||||
if (filter.itemsIds.length > 0) {
|
||||
q.whereIn('id', filter.itemsIds);
|
||||
}
|
||||
});
|
||||
const inventoryItemsIds = inventoryItems.map((item) => item.id);
|
||||
|
||||
// Calculates the total inventory total quantity and rate `IN` transactions.
|
||||
|
||||
@@ -24,6 +24,7 @@ export default class SalesByItemsReportService {
|
||||
return {
|
||||
fromDate: moment().startOf('year').format('YYYY-MM-DD'),
|
||||
toDate: moment().endOf('year').format('YYYY-MM-DD'),
|
||||
itemsIds: [],
|
||||
numberFormat: {
|
||||
precision: 2,
|
||||
divideOn1000: false,
|
||||
@@ -91,7 +92,14 @@ export default class SalesByItemsReportService {
|
||||
filter,
|
||||
tenantId,
|
||||
});
|
||||
const inventoryItems = await Item.query().where('type', 'inventory');
|
||||
// Inventory items for sales report.
|
||||
const inventoryItems = await Item.query().onBuild((q) => {
|
||||
q.where('type', 'inventory');
|
||||
|
||||
if (filter.itemsIds.length > 0) {
|
||||
q.whereIn('id', filter.itemsIds);
|
||||
}
|
||||
});
|
||||
const inventoryItemsIds = inventoryItems.map((item) => item.id);
|
||||
|
||||
// Calculates the total inventory total quantity and rate `IN` transactions.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { map } from 'lodash';
|
||||
import { isEmpty, map } from 'lodash';
|
||||
import { IAccount, IAccountTransaction } from 'interfaces';
|
||||
import { ACCOUNT_TYPE } from 'data/AccountTypes';
|
||||
import HasTenancyService from 'services/Tenancy/TenancyService';
|
||||
@@ -13,10 +13,16 @@ export default class TransactionsByCustomersRepository {
|
||||
* @param {number} tenantId
|
||||
* @returns {Promise<ICustomer[]>}
|
||||
*/
|
||||
public async getCustomers(tenantId: number) {
|
||||
public async getCustomers(tenantId: number, customersIds?: number[]) {
|
||||
const { Customer } = this.tenancy.models(tenantId);
|
||||
|
||||
return Customer.query().orderBy('displayName');
|
||||
return Customer.query().onBuild((q) => {
|
||||
q.orderBy('displayName');
|
||||
|
||||
if (!isEmpty(customersIds)) {
|
||||
q.whereIn('id', customersIds);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -44,6 +44,8 @@ export default class TransactionsByCustomersService
|
||||
},
|
||||
noneZero: false,
|
||||
noneTransactions: false,
|
||||
|
||||
customersIds: [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -125,7 +127,7 @@ export default class TransactionsByCustomersService
|
||||
const accountsGraph = await accountRepository.getDependencyGraph();
|
||||
|
||||
// Retrieve the report customers.
|
||||
const customers = await this.reportRepository.getCustomers(tenantId);
|
||||
const customers = await this.reportRepository.getCustomers(tenantId, filter.customersIds);
|
||||
|
||||
const openingBalanceDate = moment(filter.fromDate)
|
||||
.subtract(1, 'days')
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { map } from 'lodash';
|
||||
import { isEmpty, map } from 'lodash';
|
||||
import { IVendor, IAccount, IAccountTransaction } from 'interfaces';
|
||||
import HasTenancyService from 'services/Tenancy/TenancyService';
|
||||
import { ACCOUNT_TYPE } from 'data/AccountTypes';
|
||||
@@ -14,10 +14,19 @@ export default class TransactionsByVendorRepository {
|
||||
* @param {number} tenantId
|
||||
* @returns {Promise<IVendor[]>}
|
||||
*/
|
||||
public getVendors(tenantId: number): Promise<IVendor[]> {
|
||||
public getVendors(
|
||||
tenantId: number,
|
||||
vendorsIds?: number[]
|
||||
): Promise<IVendor[]> {
|
||||
const { Vendor } = this.tenancy.models(tenantId);
|
||||
|
||||
return Vendor.query().orderBy('displayName');
|
||||
return Vendor.query().onBuild((q) => {
|
||||
q.orderBy('displayName');
|
||||
|
||||
if (!isEmpty(vendorsIds)) {
|
||||
q.whereIn('id', vendorsIds);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -67,7 +76,7 @@ export default class TransactionsByVendorRepository {
|
||||
* @param {Date|string} openingDate
|
||||
* @param {number[]} customersIds
|
||||
*/
|
||||
public async getVendorsPeriodTransactions(
|
||||
public async getVendorsPeriodTransactions(
|
||||
tenantId: number,
|
||||
fromDate: Date,
|
||||
toDate: Date
|
||||
|
||||
@@ -45,6 +45,8 @@ export default class TransactionsByVendorsService
|
||||
},
|
||||
noneZero: false,
|
||||
noneTransactions: false,
|
||||
|
||||
vendorsIds: [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -139,12 +141,13 @@ export default class TransactionsByVendorsService
|
||||
group: 'organization',
|
||||
key: 'base_currency',
|
||||
});
|
||||
|
||||
const filter = { ...this.defaultQuery, ...query };
|
||||
|
||||
// Retrieve the report vendors.
|
||||
const vendors = await this.reportRepository.getVendors(tenantId);
|
||||
|
||||
const vendors = await this.reportRepository.getVendors(
|
||||
tenantId,
|
||||
filter.vendorsIds
|
||||
);
|
||||
// Retrieve the accounts graph.
|
||||
const accountsGraph = await accountRepository.getDependencyGraph();
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { omit, sumBy } from 'lodash';
|
||||
import { omit, runInContext, sumBy } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import composeAsync from 'async/compose';
|
||||
@@ -13,7 +13,7 @@ import InventoryService from 'services/Inventory/Inventory';
|
||||
import SalesInvoicesCost from 'services/Sales/SalesInvoicesCost';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import DynamicListingService from 'services/DynamicListing/DynamicListService';
|
||||
import { formatDateFields } from 'utils';
|
||||
import { formatDateFields, transformToMap } from 'utils';
|
||||
import {
|
||||
IBillDTO,
|
||||
IBill,
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
IBillsFilter,
|
||||
IBillsService,
|
||||
IItemEntry,
|
||||
IItemEntryDTO,
|
||||
} from 'interfaces';
|
||||
import { ServiceError } from 'exceptions';
|
||||
import ItemsService from 'services/Items/ItemsService';
|
||||
@@ -32,6 +33,7 @@ import JournalCommands from 'services/Accounting/JournalCommands';
|
||||
import JournalPosterService from 'services/Sales/JournalPosterService';
|
||||
import VendorsService from 'services/Contacts/VendorsService';
|
||||
import { ERRORS } from './constants';
|
||||
import EntriesService from 'services/Entries';
|
||||
|
||||
/**
|
||||
* Vendor bills services.
|
||||
@@ -40,7 +42,8 @@ import { ERRORS } from './constants';
|
||||
@Service('Bills')
|
||||
export default class BillsService
|
||||
extends SalesInvoicesCost
|
||||
implements IBillsService {
|
||||
implements IBillsService
|
||||
{
|
||||
@Inject()
|
||||
inventoryService: InventoryService;
|
||||
|
||||
@@ -71,6 +74,9 @@ export default class BillsService
|
||||
@Inject()
|
||||
vendorsService: VendorsService;
|
||||
|
||||
@Inject()
|
||||
entriesService: EntriesService;
|
||||
|
||||
/**
|
||||
* Validates whether the vendor is exist.
|
||||
* @async
|
||||
@@ -100,7 +106,7 @@ export default class BillsService
|
||||
* @param {number} tenantId -
|
||||
* @param {number} billId -
|
||||
*/
|
||||
private async getBillOrThrowError(tenantId: number, billId: number) {
|
||||
public async getBillOrThrowError(tenantId: number, billId: number) {
|
||||
const { Bill } = this.tenancy.models(tenantId);
|
||||
|
||||
this.logger.info('[bill] trying to get bill.', { tenantId, billId });
|
||||
@@ -165,16 +171,63 @@ export default class BillsService
|
||||
* Validate the bill number require.
|
||||
* @param {string} billNo -
|
||||
*/
|
||||
validateBillNoRequire(billNo: string) {
|
||||
private validateBillNoRequire(billNo: string) {
|
||||
if (!billNo) {
|
||||
throw new ServiceError(ERRORS.BILL_NO_IS_REQUIRED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate bill transaction has no associated allocated landed cost transactions.
|
||||
* @param {number} tenantId
|
||||
* @param {number} billId
|
||||
*/
|
||||
private async validateBillHasNoLandedCost(tenantId: number, billId: number) {
|
||||
const { BillLandedCost } = this.tenancy.models(tenantId);
|
||||
|
||||
const billLandedCosts = await BillLandedCost.query().where(
|
||||
'billId',
|
||||
billId
|
||||
);
|
||||
if (billLandedCosts.length > 0) {
|
||||
throw new ServiceError(ERRORS.BILL_HAS_ASSOCIATED_LANDED_COSTS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate transaction entries that have landed cost type should not be
|
||||
* inventory items.
|
||||
* @param {number} tenantId -
|
||||
* @param {IItemEntryDTO[]} newEntriesDTO -
|
||||
*/
|
||||
public async validateCostEntriesShouldBeInventoryItems(
|
||||
tenantId: number,
|
||||
newEntriesDTO: IItemEntryDTO[]
|
||||
) {
|
||||
const { Item } = this.tenancy.models(tenantId);
|
||||
|
||||
const entriesItemsIds = newEntriesDTO.map((e) => e.itemId);
|
||||
const entriesItems = await Item.query().whereIn('id', entriesItemsIds);
|
||||
|
||||
const entriesItemsById = transformToMap(entriesItems, 'id');
|
||||
|
||||
// Filter the landed cost entries that not associated with inventory item.
|
||||
const nonInventoryHasCost = newEntriesDTO.filter((entry) => {
|
||||
const item = entriesItemsById.get(entry.itemId);
|
||||
|
||||
return entry.landedCost && item.type !== 'inventory';
|
||||
});
|
||||
if (nonInventoryHasCost.length > 0) {
|
||||
throw new ServiceError(
|
||||
ERRORS.LANDED_COST_ENTRIES_SHOULD_BE_INVENTORY_ITEMS
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the default cost account to the bill entries.
|
||||
*/
|
||||
setBillEntriesDefaultAccounts(tenantId: number) {
|
||||
private setBillEntriesDefaultAccounts(tenantId: number) {
|
||||
return async (entries: IItemEntry[]) => {
|
||||
const { Item } = this.tenancy.models(tenantId);
|
||||
|
||||
@@ -194,6 +247,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.
|
||||
* @param {number} tenantId
|
||||
@@ -211,6 +286,9 @@ export default class BillsService
|
||||
|
||||
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.
|
||||
const billNumber = billDTO.billNumber || oldBill?.billNumber;
|
||||
|
||||
@@ -220,6 +298,7 @@ export default class BillsService
|
||||
billDTO.vendorId
|
||||
);
|
||||
const initialEntries = billDTO.entries.map((entry) => ({
|
||||
amount: ItemEntry.calcAmount(entry),
|
||||
reference_type: 'Bill',
|
||||
...omit(entry, ['amount']),
|
||||
}));
|
||||
@@ -234,6 +313,7 @@ export default class BillsService
|
||||
'dueDate',
|
||||
]),
|
||||
amount,
|
||||
landedCostAmount,
|
||||
currencyCode: vendor.currencyCode,
|
||||
billNumber,
|
||||
entries,
|
||||
@@ -284,6 +364,10 @@ export default class BillsService
|
||||
tenantId,
|
||||
billDTO.entries
|
||||
);
|
||||
await this.validateCostEntriesShouldBeInventoryItems(
|
||||
tenantId,
|
||||
billDTO.entries,
|
||||
);
|
||||
this.logger.info('[bill] trying to create a new bill', {
|
||||
tenantId,
|
||||
billDTO,
|
||||
@@ -370,6 +454,16 @@ export default class BillsService
|
||||
authorizedUser,
|
||||
oldBill
|
||||
);
|
||||
// Validate landed cost entries that have allocated cost could not be deleted.
|
||||
await this.entriesService.validateLandedCostEntriesNotDeleted(
|
||||
oldBill.entries,
|
||||
billObj.entries
|
||||
);
|
||||
// Validate new landed cost entries should be bigger than new entries.
|
||||
await this.entriesService.validateLocatedCostEntriesSmallerThanNewEntries(
|
||||
oldBill.entries,
|
||||
billObj.entries
|
||||
);
|
||||
// Update the bill transaction.
|
||||
const bill = await billRepository.upsertGraph({
|
||||
id: billId,
|
||||
@@ -402,6 +496,9 @@ export default class BillsService
|
||||
// Retrieve the given bill or throw not found error.
|
||||
const oldBill = await this.getBillOrThrowError(tenantId, billId);
|
||||
|
||||
// Validate the givne bill has no associated landed cost transactions.
|
||||
await this.validateBillHasNoLandedCost(tenantId, billId);
|
||||
|
||||
// Validate the purchase bill has no assocaited payments transactions.
|
||||
await this.validateBillHasNoEntries(tenantId, billId);
|
||||
|
||||
@@ -498,7 +595,7 @@ export default class BillsService
|
||||
const bill = await Bill.query()
|
||||
.findById(billId)
|
||||
.withGraphFetched('vendor')
|
||||
.withGraphFetched('entries');
|
||||
.withGraphFetched('entries.item');
|
||||
|
||||
if (!bill) {
|
||||
throw new ServiceError(ERRORS.BILL_NOT_FOUND);
|
||||
@@ -534,18 +631,25 @@ export default class BillsService
|
||||
*/
|
||||
public async recordInventoryTransactions(
|
||||
tenantId: number,
|
||||
bill: IBill,
|
||||
billId: number,
|
||||
override?: boolean
|
||||
): Promise<void> {
|
||||
const { Bill } = this.tenancy.models(tenantId);
|
||||
|
||||
// Retireve bill with assocaited entries and allocated cost entries.
|
||||
const bill = await Bill.query()
|
||||
.findById(billId)
|
||||
.withGraphFetched('entries.allocatedCostEntries');
|
||||
|
||||
// Loads the inventory items entries of the given sale invoice.
|
||||
const inventoryEntries = await this.itemsEntriesService.filterInventoryEntries(
|
||||
tenantId,
|
||||
bill.entries
|
||||
);
|
||||
const inventoryEntries =
|
||||
await this.itemsEntriesService.filterInventoryEntries(
|
||||
tenantId,
|
||||
bill.entries
|
||||
);
|
||||
const transaction = {
|
||||
transactionId: bill.id,
|
||||
transactionType: 'Bill',
|
||||
|
||||
date: bill.billDate,
|
||||
direction: 'IN',
|
||||
entries: inventoryEntries,
|
||||
@@ -581,13 +685,30 @@ export default class BillsService
|
||||
*/
|
||||
public async recordJournalTransactions(
|
||||
tenantId: number,
|
||||
bill: IBill,
|
||||
billId: number,
|
||||
override: boolean = false
|
||||
) {
|
||||
const { Bill, Account } = this.tenancy.models(tenantId);
|
||||
|
||||
const journal = new JournalPoster(tenantId);
|
||||
const journalCommands = new JournalCommands(journal);
|
||||
|
||||
await journalCommands.bill(bill, override);
|
||||
const bill = await Bill.query()
|
||||
.findById(billId)
|
||||
.withGraphFetched('entries.item')
|
||||
.withGraphFetched('entries.allocatedCostEntries')
|
||||
.withGraphFetched('locatedLandedCosts.allocateEntries');
|
||||
|
||||
const payableAccount = await Account.query().findOne({
|
||||
slug: 'accounts-payable',
|
||||
});
|
||||
|
||||
// Overrides the bill journal entries.
|
||||
if (override) {
|
||||
await journalCommands.revertJournalEntries(billId, 'Bill');
|
||||
}
|
||||
// Writes the bill journal entries.
|
||||
journalCommands.bill(bill, payableAccount);
|
||||
|
||||
return Promise.all([
|
||||
journal.deleteEntries(),
|
||||
|
||||
58
server/src/services/Purchases/LandedCost/BillLandedCost.ts
Normal file
58
server/src/services/Purchases/LandedCost/BillLandedCost.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
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 - Bill transaction.
|
||||
* @returns {ILandedCostTransaction} - Landed cost transaction.
|
||||
*/
|
||||
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,
|
||||
unallocatedCostAmount: billEntry.unallocatedCostAmount,
|
||||
allocatedCostAmount: billEntry.allocatedCostAmount,
|
||||
description: billEntry.description,
|
||||
costAccountId: billEntry.costAccountId || billEntry.item.costAccountId,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
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,
|
||||
allocatedCostAmount: expenseEntry.allocatedCostAmount,
|
||||
unallocatedCostAmount: expenseEntry.unallocatedCostAmount,
|
||||
costAccountId: expenseEntry.expenseAccount.id,
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { ref, transaction } from 'objection';
|
||||
import {
|
||||
ILandedCostTransactionsQueryDTO,
|
||||
ILandedCostTransaction,
|
||||
IBillLandedCostTransaction,
|
||||
} from 'interfaces';
|
||||
import TransactionLandedCost from './TransctionLandedCost';
|
||||
import BillsService from '../Bills';
|
||||
import HasTenancyService from 'services/Tenancy/TenancyService';
|
||||
import { formatNumber } from 'utils';
|
||||
|
||||
@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')
|
||||
.withGraphFetched('bill');
|
||||
|
||||
return landedCostTransactions.map((transaction) => ({
|
||||
...transaction.toJSON(),
|
||||
formattedAmount: formatNumber(
|
||||
transaction.amount,
|
||||
transaction.bill.currencyCode
|
||||
),
|
||||
}));
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import * as R from 'ramda';
|
||||
import { Model } from 'objection';
|
||||
import { IBill, IExpense, ILandedCostTransaction, ILandedCostTransactionEntry } from 'interfaces';
|
||||
import { ServiceError } from 'exceptions';
|
||||
import BillLandedCost from './BillLandedCost';
|
||||
import ExpenseLandedCost from './ExpenseLandedCost';
|
||||
import HasTenancyService from 'services/Tenancy/TenancyService';
|
||||
import { ERRORS } from './utils';
|
||||
|
||||
@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
|
||||
): Model => {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformes the given expense or bill entry to landed cost transaction entry.
|
||||
* @param {string} transactionType
|
||||
* @param {} transactionEntry
|
||||
* @returns {ILandedCostTransactionEntry}
|
||||
*/
|
||||
public transformToLandedCostEntry = (
|
||||
transactionType: 'Bill' | 'Expense',
|
||||
transactionEntry,
|
||||
): ILandedCostTransactionEntry => {
|
||||
return R.compose(
|
||||
R.when(
|
||||
R.always(transactionType === 'Bill'),
|
||||
this.billLandedCost.transformToLandedCostEntry,
|
||||
),
|
||||
R.when(
|
||||
R.always(transactionType === 'Expense'),
|
||||
this.expenseLandedCost.transformToLandedCostEntry,
|
||||
),
|
||||
)(transactionEntry);
|
||||
}
|
||||
}
|
||||
479
server/src/services/Purchases/LandedCost/index.ts
Normal file
479
server/src/services/Purchases/LandedCost/index.ts
Normal file
@@ -0,0 +1,479 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { difference, sumBy } from 'lodash';
|
||||
import {
|
||||
EventDispatcher,
|
||||
EventDispatcherInterface,
|
||||
} from 'decorators/eventDispatcher';
|
||||
import BillsService from '../Bills';
|
||||
import { ServiceError } from 'exceptions';
|
||||
import {
|
||||
IItemEntry,
|
||||
IBill,
|
||||
IBillLandedCost,
|
||||
ILandedCostItemDTO,
|
||||
ILandedCostDTO,
|
||||
IBillLandedCostTransaction,
|
||||
ILandedCostTransaction,
|
||||
ILandedCostTransactionEntry,
|
||||
} from 'interfaces';
|
||||
import events from 'subscribers/events';
|
||||
import InventoryService from 'services/Inventory/Inventory';
|
||||
import HasTenancyService from 'services/Tenancy/TenancyService';
|
||||
import TransactionLandedCost from './TransctionLandedCost';
|
||||
import { ERRORS, mergeLocatedWithBillEntries } from './utils';
|
||||
|
||||
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;
|
||||
|
||||
@EventDispatcher()
|
||||
eventDispatcher: EventDispatcherInterface;
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Transformes DTO to bill landed cost model object.
|
||||
* @param landedCostDTO
|
||||
* @param bill
|
||||
* @param costTransaction
|
||||
* @param costTransactionEntry
|
||||
* @returns
|
||||
*/
|
||||
private transformToBillLandedCost(
|
||||
landedCostDTO: ILandedCostDTO,
|
||||
bill: IBill,
|
||||
costTransaction: ILandedCostTransaction,
|
||||
costTransactionEntry: ILandedCostTransactionEntry
|
||||
) {
|
||||
const amount = sumBy(landedCostDTO.items, 'cost');
|
||||
|
||||
return {
|
||||
billId: bill.id,
|
||||
fromTransactionType: landedCostDTO.transactionType,
|
||||
fromTransactionId: landedCostDTO.transactionId,
|
||||
fromTransactionEntryId: landedCostDTO.transactionEntryId,
|
||||
amount,
|
||||
allocationMethod: landedCostDTO.allocationMethod,
|
||||
description: landedCostDTO.description,
|
||||
allocateEntries: landedCostDTO.items,
|
||||
costAccountId: costTransactionEntry.costAccountId,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
.onBuild((q) => {
|
||||
if (transactionType === 'Bill') {
|
||||
q.withGraphFetched('item');
|
||||
} else if (transactionType === 'Expense') {
|
||||
q.withGraphFetched('expenseAccount');
|
||||
}
|
||||
});
|
||||
|
||||
if (!entry) {
|
||||
throw new ServiceError(ERRORS.LANDED_COST_ENTRY_NOT_FOUND);
|
||||
}
|
||||
return this.transactionLandedCost.transformToLandedCostEntry(
|
||||
transactionType,
|
||||
entry
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve allocate items cost total.
|
||||
* @param {ILandedCostDTO} landedCostDTO
|
||||
* @returns {number}
|
||||
*/
|
||||
private getAllocateItemsCostTotal = (
|
||||
landedCostDTO: ILandedCostDTO
|
||||
): number => {
|
||||
return sumBy(landedCostDTO.items, 'cost');
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates the landed cost entry amount.
|
||||
* @param {number} unallocatedCost -
|
||||
* @param {number} amount -
|
||||
*/
|
||||
private validateLandedCostEntryAmount = (
|
||||
unallocatedCost: number,
|
||||
amount: number
|
||||
): void => {
|
||||
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,
|
||||
billLandedCost: IBillLandedCostTransaction,
|
||||
bill: IBill
|
||||
) => {
|
||||
// Retrieve the merged allocated entries with bill entries.
|
||||
const allocateEntries = mergeLocatedWithBillEntries(
|
||||
billLandedCost.allocateEntries,
|
||||
bill.entries
|
||||
);
|
||||
// Mappes the allocate cost entries to inventory transactions.
|
||||
const inventoryTransactions = allocateEntries.map((allocateEntry) => ({
|
||||
date: bill.billDate,
|
||||
itemId: allocateEntry.entry.itemId,
|
||||
direction: 'IN',
|
||||
quantity: 0,
|
||||
rate: allocateEntry.cost,
|
||||
transactionType: 'LandedCost',
|
||||
transactionId: billLandedCost.id,
|
||||
entryId: allocateEntry.entryId,
|
||||
}));
|
||||
|
||||
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} billId - Purchase invoice id.
|
||||
*/
|
||||
public allocateLandedCost = async (
|
||||
tenantId: number,
|
||||
allocateCostDTO: ILandedCostDTO,
|
||||
billId: number
|
||||
): Promise<{
|
||||
billLandedCost: IBillLandedCost;
|
||||
}> => {
|
||||
const { BillLandedCost } = this.tenancy.models(tenantId);
|
||||
|
||||
// Retrieve total cost of allocated items.
|
||||
const amount = this.getAllocateItemsCostTotal(allocateCostDTO);
|
||||
|
||||
// Retrieve the purchase invoice or throw not found error.
|
||||
const bill = await this.billsService.getBillOrThrowError(
|
||||
tenantId,
|
||||
billId
|
||||
);
|
||||
// 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(
|
||||
bill.entries,
|
||||
allocateCostDTO.items
|
||||
);
|
||||
// Validate the amount of cost with unallocated landed cost.
|
||||
this.validateLandedCostEntryAmount(
|
||||
landedCostEntry.unallocatedCostAmount,
|
||||
amount
|
||||
);
|
||||
// Transformes DTO to bill landed cost model object.
|
||||
const billLandedCostObj = this.transformToBillLandedCost(
|
||||
allocateCostDTO,
|
||||
bill,
|
||||
landedCostTransaction,
|
||||
landedCostEntry
|
||||
);
|
||||
// Save the bill landed cost model.
|
||||
const billLandedCost = await BillLandedCost.query().insertGraph(
|
||||
billLandedCostObj
|
||||
);
|
||||
// Triggers the event `onBillLandedCostCreated`.
|
||||
await this.eventDispatcher.dispatch(events.billLandedCost.onCreated, {
|
||||
tenantId,
|
||||
billId,
|
||||
billLandedCostId: billLandedCost.id,
|
||||
});
|
||||
// Records the inventory transactions.
|
||||
await this.recordInventoryTransactions(
|
||||
tenantId,
|
||||
billLandedCost,
|
||||
bill
|
||||
);
|
||||
// Increment landed cost amount on transaction and entry.
|
||||
await this.incrementLandedCostAmount(
|
||||
tenantId,
|
||||
allocateCostDTO.transactionType,
|
||||
allocateCostDTO.transactionId,
|
||||
allocateCostDTO.transactionEntryId,
|
||||
amount
|
||||
);
|
||||
return { billLandedCost };
|
||||
};
|
||||
|
||||
/**
|
||||
* 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<IBillLandedCostTransaction> => {
|
||||
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);
|
||||
|
||||
// Triggers the event `onBillLandedCostCreated`.
|
||||
await this.eventDispatcher.dispatch(events.billLandedCost.onDeleted, {
|
||||
tenantId,
|
||||
billLandedCostId: oldBillLandedCost.id,
|
||||
billId: oldBillLandedCost.billId,
|
||||
});
|
||||
// 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'
|
||||
);
|
||||
};
|
||||
}
|
||||
34
server/src/services/Purchases/LandedCost/utils.ts
Normal file
34
server/src/services/Purchases/LandedCost/utils.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { IItemEntry, IBillLandedCostTransactionEntry } from 'interfaces';
|
||||
import { transformToMap } from 'utils';
|
||||
|
||||
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',
|
||||
};
|
||||
|
||||
/**
|
||||
* Merges item entry to bill located landed cost entry.
|
||||
* @param {IBillLandedCostTransactionEntry[]} locatedEntries -
|
||||
* @param {IItemEntry[]} billEntries -
|
||||
* @returns {(IBillLandedCostTransactionEntry & { entry: IItemEntry })[]}
|
||||
*/
|
||||
export const mergeLocatedWithBillEntries = (
|
||||
locatedEntries: IBillLandedCostTransactionEntry[],
|
||||
billEntries: IItemEntry[]
|
||||
): (IBillLandedCostTransactionEntry & { entry: IItemEntry })[] => {
|
||||
const billEntriesByEntryId = transformToMap(billEntries, 'id');
|
||||
|
||||
return locatedEntries.map((entry) => ({
|
||||
...entry,
|
||||
entry: billEntriesByEntryId.get(entry.entryId),
|
||||
}));
|
||||
};
|
||||
@@ -9,5 +9,9 @@ export const ERRORS = {
|
||||
BILL_ALREADY_OPEN: 'BILL_ALREADY_OPEN',
|
||||
BILL_NO_IS_REQUIRED: 'BILL_NO_IS_REQUIRED',
|
||||
BILL_HAS_ASSOCIATED_PAYMENT_ENTRIES: 'BILL_HAS_ASSOCIATED_PAYMENT_ENTRIES',
|
||||
VENDOR_HAS_BILLS: 'VENDOR_HAS_BILLS'
|
||||
VENDOR_HAS_BILLS: 'VENDOR_HAS_BILLS',
|
||||
BILL_HAS_ASSOCIATED_LANDED_COSTS: 'BILL_HAS_ASSOCIATED_LANDED_COSTS',
|
||||
BILL_ENTRIES_ALLOCATED_COST_COULD_DELETED: 'BILL_ENTRIES_ALLOCATED_COST_COULD_DELETED',
|
||||
LOCATED_COST_ENTRIES_SHOULD_BIGGE_THAN_NEW_ENTRIES: 'LOCATED_COST_ENTRIES_SHOULD_BIGGE_THAN_NEW_ENTRIES',
|
||||
LANDED_COST_ENTRIES_SHOULD_BE_INVENTORY_ITEMS: 'LANDED_COST_ENTRIES_SHOULD_BE_INVENTORY_ITEMS'
|
||||
};
|
||||
|
||||
@@ -23,20 +23,20 @@ export default class BillSubscriber {
|
||||
* Handles writing journal entries once bill created.
|
||||
*/
|
||||
@On(events.bill.onCreated)
|
||||
async handlerWriteJournalEntriesOnCreate({ tenantId, bill }) {
|
||||
async handlerWriteJournalEntriesOnCreate({ tenantId, billId }) {
|
||||
// Writes the journal entries for the given bill transaction.
|
||||
this.logger.info('[bill] writing bill journal entries.', { tenantId });
|
||||
await this.billsService.recordJournalTransactions(tenantId, bill);
|
||||
await this.billsService.recordJournalTransactions(tenantId, billId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the overwriting journal entries once bill edited.
|
||||
*/
|
||||
@On(events.bill.onEdited)
|
||||
async handleOverwriteJournalEntriesOnEdit({ tenantId, bill }) {
|
||||
async handleOverwriteJournalEntriesOnEdit({ tenantId, billId }) {
|
||||
// Overwrite the journal entries for the given bill transaction.
|
||||
this.logger.info('[bill] overwriting bill journal entries.', { tenantId });
|
||||
await this.billsService.recordJournalTransactions(tenantId, bill, true);
|
||||
await this.billsService.recordJournalTransactions(tenantId, billId, true);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
37
server/src/subscribers/LandedCost/index.ts
Normal file
37
server/src/subscribers/LandedCost/index.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Container } from 'typedi';
|
||||
import { On, EventSubscriber } from 'event-dispatch';
|
||||
import events from 'subscribers/events';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import BillsService from 'services/Purchases/Bills';
|
||||
|
||||
@EventSubscriber()
|
||||
export default class BillLandedCostSubscriber {
|
||||
logger: any;
|
||||
tenancy: TenancyService;
|
||||
billsService: BillsService;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
*/
|
||||
constructor() {
|
||||
this.logger = Container.get('logger');
|
||||
this.tenancy = Container.get(TenancyService);
|
||||
this.billsService = Container.get(BillsService);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the rewrite bill journal entries once the landed cost transaction
|
||||
* be deleted or created.
|
||||
*/
|
||||
@On(events.billLandedCost.onCreated)
|
||||
@On(events.billLandedCost.onDeleted)
|
||||
public async handleRewriteBillJournalEntries({
|
||||
tenantId,
|
||||
billId,
|
||||
bilLandedCostId,
|
||||
}) {
|
||||
// Overwrite the journal entries for the given bill transaction.
|
||||
this.logger.info('[bill] overwriting bill journal entries.', { tenantId });
|
||||
await this.billsService.recordJournalTransactions(tenantId, billId, true);
|
||||
}
|
||||
}
|
||||
@@ -203,5 +203,15 @@ export default {
|
||||
onQuickCreated: 'onInventoryAdjustmentQuickCreated',
|
||||
onDeleted: 'onInventoryAdjustmentDeleted',
|
||||
onPublished: 'onInventoryAdjustmentPublished',
|
||||
},
|
||||
|
||||
/**
|
||||
* Bill landed cost.
|
||||
*/
|
||||
billLandedCost: {
|
||||
onCreate: 'onBillLandedCostCreate',
|
||||
onCreated: 'onBillLandedCostCreated',
|
||||
onDelete: 'onBillLandedCostDelete',
|
||||
onDeleted: 'onBillLandedCostDeleted'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -373,6 +373,11 @@ const accumSum = (data, callback) => {
|
||||
}, 0)
|
||||
}
|
||||
|
||||
const mergeObjectsBykey = (object1, object2, key) => {
|
||||
var merged = _.merge(_.keyBy(object1, key), _.keyBy(object2, key));
|
||||
return _.values(merged);
|
||||
}
|
||||
|
||||
export {
|
||||
accumSum,
|
||||
increment,
|
||||
@@ -400,5 +405,6 @@ export {
|
||||
transactionIncrement,
|
||||
transformToMapBy,
|
||||
dateRangeFromToCollection,
|
||||
transformToMapKeyValue
|
||||
transformToMapKeyValue,
|
||||
mergeObjectsBykey
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user