mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-19 22:30:31 +00:00
WIP: Allocate landed cost.
This commit is contained in:
@@ -39,7 +39,7 @@ export default class ExpensesController extends BaseController {
|
|||||||
);
|
);
|
||||||
router.post(
|
router.post(
|
||||||
'/:id',
|
'/:id',
|
||||||
[...this.expenseDTOSchema, ...this.expenseParamSchema],
|
[...this.editExpenseDTOSchema, ...this.expenseParamSchema],
|
||||||
this.validationResult,
|
this.validationResult,
|
||||||
asyncMiddleware(this.editExpense.bind(this)),
|
asyncMiddleware(this.editExpense.bind(this)),
|
||||||
this.catchServiceErrors
|
this.catchServiceErrors
|
||||||
@@ -116,12 +116,62 @@ export default class ExpensesController extends BaseController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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() {
|
get expenseParamSchema() {
|
||||||
return [param('id').exists().isNumeric().toInt()];
|
return [param('id').exists().isNumeric().toInt()];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expenses list validation schema.
|
||||||
|
*/
|
||||||
get expensesListSchema() {
|
get expensesListSchema() {
|
||||||
return [
|
return [
|
||||||
query('custom_view_id').optional().isNumeric().toInt(),
|
query('custom_view_id').optional().isNumeric().toInt(),
|
||||||
@@ -291,7 +341,7 @@ export default class ExpensesController extends BaseController {
|
|||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
* @param {ServiceError} error
|
* @param {ServiceError} error
|
||||||
*/
|
*/
|
||||||
catchServiceErrors(
|
private catchServiceErrors(
|
||||||
error: Error,
|
error: Error,
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response,
|
res: Response,
|
||||||
@@ -348,6 +398,25 @@ export default class ExpensesController extends BaseController {
|
|||||||
errors: [{ type: 'EXPENSE_HAS_ASSOCIATED_LANDED_COST', code: 900 }],
|
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);
|
next(error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -406,7 +406,7 @@ export default class ItemsController extends BaseController {
|
|||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
* @param {NextFunction} next
|
* @param {NextFunction} next
|
||||||
*/
|
*/
|
||||||
handlerServiceErrors(
|
private handlerServiceErrors(
|
||||||
error: Error,
|
error: Error,
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response,
|
res: Response,
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ export default class BillsController extends BaseController {
|
|||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.trim()
|
.trim()
|
||||||
.escape(),
|
.escape(),
|
||||||
check('entries.*.landedCost')
|
check('entries.*.landed_cost')
|
||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isBoolean()
|
.isBoolean()
|
||||||
.toBoolean(),
|
.toBoolean(),
|
||||||
@@ -347,7 +347,7 @@ export default class BillsController extends BaseController {
|
|||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
* @param {NextFunction} next
|
* @param {NextFunction} next
|
||||||
*/
|
*/
|
||||||
handleServiceError(
|
private handleServiceError(
|
||||||
error: Error,
|
error: Error,
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response,
|
res: Response,
|
||||||
@@ -422,6 +422,40 @@ export default class BillsController extends BaseController {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,9 +64,9 @@ export default class BillAllocateLandedCost extends BaseController {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the landed cost transactions of the given query.
|
* Retrieve the landed cost transactions of the given query.
|
||||||
* @param {Request} req
|
* @param {Request} req - Request
|
||||||
* @param {Response} res
|
* @param {Response} res - Response.
|
||||||
* @param {NextFunction} next
|
* @param {NextFunction} next - Next function.
|
||||||
*/
|
*/
|
||||||
private async getLandedCostTransactions(
|
private async getLandedCostTransactions(
|
||||||
req: Request,
|
req: Request,
|
||||||
@@ -192,10 +192,7 @@ export default class BillAllocateLandedCost extends BaseController {
|
|||||||
billId
|
billId
|
||||||
);
|
);
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({ billId, transactions });
|
||||||
billId,
|
|
||||||
transactions,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ import Ping from 'api/controllers/Ping';
|
|||||||
import Subscription from 'api/controllers/Subscription';
|
import Subscription from 'api/controllers/Subscription';
|
||||||
import Licenses from 'api/controllers/Subscription/Licenses';
|
import Licenses from 'api/controllers/Subscription/Licenses';
|
||||||
import InventoryAdjustments from 'api/controllers/Inventory/InventoryAdjustments';
|
import InventoryAdjustments from 'api/controllers/Inventory/InventoryAdjustments';
|
||||||
|
|
||||||
import Setup from 'api/controllers/Setup';
|
import Setup from 'api/controllers/Setup';
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ exports.up = function (knex) {
|
|||||||
.inTable('accounts');
|
.inTable('accounts');
|
||||||
|
|
||||||
table.boolean('landed_cost').defaultTo(false);
|
table.boolean('landed_cost').defaultTo(false);
|
||||||
table.decimal('allocated_cost_amount', 13, 3);
|
table.decimal('allocated_cost_amount', 13, 3).defaultTo(0);
|
||||||
|
|
||||||
table.timestamps();
|
table.timestamps();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
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;
|
||||||
|
}
|
||||||
@@ -40,6 +40,9 @@ export interface IExpenseCategory {
|
|||||||
description: string;
|
description: string;
|
||||||
expenseId: number;
|
expenseId: number;
|
||||||
amount: number;
|
amount: number;
|
||||||
|
|
||||||
|
allocatedCostAmount: number;
|
||||||
|
unallocatedCostAmount: number;
|
||||||
landedCost: boolean;
|
landedCost: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,8 +60,10 @@ export interface IExpenseDTO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IExpenseCategoryDTO {
|
export interface IExpenseCategoryDTO {
|
||||||
|
id?: number;
|
||||||
expenseAccountId: number;
|
expenseAccountId: number;
|
||||||
index: number;
|
index: number;
|
||||||
|
amount: number;
|
||||||
description?: string;
|
description?: string;
|
||||||
expenseId: number;
|
expenseId: number;
|
||||||
landedCost?: boolean;
|
landedCost?: boolean;
|
||||||
|
|||||||
@@ -1,26 +1,29 @@
|
|||||||
|
|
||||||
export type IItemEntryTransactionType = 'SaleInvoice' | 'Bill' | 'SaleReceipt';
|
export type IItemEntryTransactionType = 'SaleInvoice' | 'Bill' | 'SaleReceipt';
|
||||||
|
|
||||||
export interface IItemEntry {
|
export interface IItemEntry {
|
||||||
id?: number,
|
id?: number;
|
||||||
|
|
||||||
referenceType: string,
|
referenceType: string;
|
||||||
referenceId: number,
|
referenceId: number;
|
||||||
|
|
||||||
index: number,
|
index: number;
|
||||||
|
|
||||||
itemId: number,
|
itemId: number;
|
||||||
description: string,
|
description: string;
|
||||||
discount: number,
|
discount: number;
|
||||||
quantity: number,
|
quantity: number;
|
||||||
rate: number,
|
rate: number;
|
||||||
|
amount: number;
|
||||||
|
|
||||||
sellAccountId: number,
|
landedCost: number;
|
||||||
costAccountId: number,
|
allocatedCostAmount: number;
|
||||||
|
unallocatedCostAmount: number;
|
||||||
|
|
||||||
landedCost?: boolean,
|
sellAccountId: number;
|
||||||
|
costAccountId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IItemEntryDTO {
|
export interface IItemEntryDTO {
|
||||||
landedCost?: boolean
|
id?: number,
|
||||||
}
|
landedCost?: boolean;
|
||||||
|
}
|
||||||
|
|||||||
@@ -64,7 +64,10 @@ export interface ILandedCostTransactionEntry {
|
|||||||
name: string;
|
name: string;
|
||||||
code: string;
|
code: string;
|
||||||
amount: number;
|
amount: number;
|
||||||
|
unallocatedCostAmount: number;
|
||||||
|
allocatedCostAmount: number;
|
||||||
description: string;
|
description: string;
|
||||||
|
costAccountId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ILandedCostEntry {
|
interface ILandedCostEntry {
|
||||||
@@ -83,7 +86,7 @@ export interface IBillLandedCostTransaction {
|
|||||||
costAccountId: number,
|
costAccountId: number,
|
||||||
description: string;
|
description: string;
|
||||||
|
|
||||||
allocatedEntries?: IBillLandedCostTransactionEntry[],
|
allocateEntries?: IBillLandedCostTransactionEntry[],
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface IBillLandedCostTransactionEntry {
|
export interface IBillLandedCostTransactionEntry {
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ export * from './Ledger';
|
|||||||
export * from './CashFlow';
|
export * from './CashFlow';
|
||||||
export * from './InventoryDetails';
|
export * from './InventoryDetails';
|
||||||
export * from './LandedCost';
|
export * from './LandedCost';
|
||||||
|
export * from './Entry';
|
||||||
|
|
||||||
export interface I18nService {
|
export interface I18nService {
|
||||||
__: (input: string) => string;
|
__: (input: string) => string;
|
||||||
|
|||||||
@@ -26,4 +26,6 @@ import 'subscribers/vendors';
|
|||||||
import 'subscribers/paymentMades';
|
import 'subscribers/paymentMades';
|
||||||
import 'subscribers/paymentReceives';
|
import 'subscribers/paymentReceives';
|
||||||
import 'subscribers/saleEstimates';
|
import 'subscribers/saleEstimates';
|
||||||
import 'subscribers/items';
|
import 'subscribers/items';
|
||||||
|
|
||||||
|
import 'subscribers/LandedCost';
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Model } from 'objection';
|
import { Model } from 'objection';
|
||||||
|
import { lowerCase } from 'lodash';
|
||||||
import TenantModel from 'models/TenantModel';
|
import TenantModel from 'models/TenantModel';
|
||||||
|
|
||||||
export default class BillLandedCost extends TenantModel {
|
export default class BillLandedCost extends TenantModel {
|
||||||
@@ -16,6 +17,25 @@ export default class BillLandedCost extends TenantModel {
|
|||||||
return ['createdAt', 'updatedAt'];
|
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.
|
* Relationship mapping.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Model } from 'objection';
|
||||||
import TenantModel from 'models/TenantModel';
|
import TenantModel from 'models/TenantModel';
|
||||||
|
|
||||||
export default class BillLandedCostEntry extends TenantModel {
|
export default class BillLandedCostEntry extends TenantModel {
|
||||||
@@ -7,4 +8,25 @@ export default class BillLandedCostEntry extends TenantModel {
|
|||||||
static get tableName() {
|
static get tableName() {
|
||||||
return 'bill_located_cost_entries';
|
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');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,14 +39,18 @@ export default class Expense extends TenantModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static get virtualAttributes() {
|
static get virtualAttributes() {
|
||||||
return ['isPublished', 'unallocatedLandedCost'];
|
return ['isPublished', 'unallocatedCostAmount'];
|
||||||
}
|
}
|
||||||
|
|
||||||
isPublished() {
|
isPublished() {
|
||||||
return Boolean(this.publishedAt);
|
return Boolean(this.publishedAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
unallocatedLandedCost() {
|
/**
|
||||||
|
* Retrieve the unallocated cost amount.
|
||||||
|
* @return {number}
|
||||||
|
*/
|
||||||
|
get unallocatedCostAmount() {
|
||||||
return Math.max(this.amount - this.allocatedCostAmount, 0);
|
return Math.max(this.amount - this.allocatedCostAmount, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ export default class ExpenseCategory extends TenantModel {
|
|||||||
* Virtual attributes.
|
* Virtual attributes.
|
||||||
*/
|
*/
|
||||||
static get virtualAttributes() {
|
static get virtualAttributes() {
|
||||||
return ['unallocatedLandedCost'];
|
return ['unallocatedCostAmount'];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remain unallocated landed cost.
|
* Remain unallocated landed cost.
|
||||||
* @return {number}
|
* @return {number}
|
||||||
*/
|
*/
|
||||||
get unallocatedLandedCost() {
|
get unallocatedCostAmount() {
|
||||||
return Math.max(this.amount - this.allocatedCostAmount, 0);
|
return Math.max(this.amount - this.allocatedCostAmount, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ export default class ItemEntry extends TenantModel {
|
|||||||
return ['amount'];
|
return ['amount'];
|
||||||
}
|
}
|
||||||
|
|
||||||
static amount() {
|
get amount() {
|
||||||
return this.calcAmount(this);
|
return ItemEntry.calcAmount(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
static calcAmount(itemEntry) {
|
static calcAmount(itemEntry) {
|
||||||
@@ -34,6 +34,7 @@ export default class ItemEntry extends TenantModel {
|
|||||||
|
|
||||||
static get relationMappings() {
|
static get relationMappings() {
|
||||||
const Item = require('models/Item');
|
const Item = require('models/Item');
|
||||||
|
const BillLandedCostEntry = require('models/BillLandedCostEntry');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
item: {
|
item: {
|
||||||
@@ -44,6 +45,14 @@ export default class ItemEntry extends TenantModel {
|
|||||||
to: 'items.id',
|
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 moment from 'moment';
|
||||||
|
import { sumBy } from 'lodash';
|
||||||
import {
|
import {
|
||||||
IBill,
|
IBill,
|
||||||
IManualJournalEntry,
|
IManualJournalEntry,
|
||||||
ISaleReceipt,
|
ISaleReceipt,
|
||||||
ISystemUser,
|
ISystemUser,
|
||||||
|
IAccount,
|
||||||
} from 'interfaces';
|
} from 'interfaces';
|
||||||
import JournalPoster from './JournalPoster';
|
import JournalPoster from './JournalPoster';
|
||||||
import JournalEntry from './JournalEntry';
|
import JournalEntry from './JournalEntry';
|
||||||
@@ -17,7 +19,6 @@ import {
|
|||||||
IItemEntry,
|
IItemEntry,
|
||||||
} from 'interfaces';
|
} from 'interfaces';
|
||||||
import { increment } from 'utils';
|
import { increment } from 'utils';
|
||||||
|
|
||||||
export default class JournalCommands {
|
export default class JournalCommands {
|
||||||
journal: JournalPoster;
|
journal: JournalPoster;
|
||||||
models: any;
|
models: any;
|
||||||
@@ -37,45 +38,20 @@ export default class JournalCommands {
|
|||||||
/**
|
/**
|
||||||
* Records the bill journal entries.
|
* Records the bill journal entries.
|
||||||
* @param {IBill} bill
|
* @param {IBill} bill
|
||||||
* @param {boolean} override - Override the old bill entries.
|
* @param {IAccount} payableAccount -
|
||||||
*/
|
*/
|
||||||
async bill(bill: IBill, override: boolean = false): Promise<void> {
|
bill(bill: IBill, payableAccount: IAccount): 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');
|
|
||||||
|
|
||||||
const commonJournalMeta = {
|
const commonJournalMeta = {
|
||||||
debit: 0,
|
debit: 0,
|
||||||
credit: 0,
|
credit: 0,
|
||||||
referenceId: bill.id,
|
referenceId: bill.id,
|
||||||
referenceType: 'Bill',
|
referenceType: 'Bill',
|
||||||
date: formattedDate,
|
date: moment(bill.billDate).format('YYYY-MM-DD'),
|
||||||
userId: bill.userId,
|
userId: bill.userId,
|
||||||
|
|
||||||
referenceNumber: bill.referenceNo,
|
referenceNumber: bill.referenceNo,
|
||||||
transactionNumber: bill.billNumber,
|
transactionNumber: bill.billNumber,
|
||||||
|
|
||||||
createdAt: bill.createdAt,
|
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({
|
const payableEntry = new JournalEntry({
|
||||||
...commonJournalMeta,
|
...commonJournalMeta,
|
||||||
credit: bill.amount,
|
credit: bill.amount,
|
||||||
@@ -86,15 +62,15 @@ export default class JournalCommands {
|
|||||||
this.journal.credit(payableEntry);
|
this.journal.credit(payableEntry);
|
||||||
|
|
||||||
bill.entries.forEach((entry, index) => {
|
bill.entries.forEach((entry, index) => {
|
||||||
const item: IItem = storedItemsMap.get(entry.itemId);
|
const landedCostAmount = sumBy(entry.allocatedCostEntries, 'cost');
|
||||||
const amount = ItemEntry.calcAmount(entry);
|
|
||||||
|
|
||||||
|
// Inventory or cost entry.
|
||||||
const debitEntry = new JournalEntry({
|
const debitEntry = new JournalEntry({
|
||||||
...commonJournalMeta,
|
...commonJournalMeta,
|
||||||
debit: amount,
|
debit: entry.amount + landedCostAmount,
|
||||||
account:
|
account:
|
||||||
['inventory'].indexOf(item.type) !== -1
|
['inventory'].indexOf(entry.item.type) !== -1
|
||||||
? item.inventoryAccountId
|
? entry.item.inventoryAccountId
|
||||||
: entry.costAccountId,
|
: entry.costAccountId,
|
||||||
index: index + 2,
|
index: index + 2,
|
||||||
itemId: entry.itemId,
|
itemId: entry.itemId,
|
||||||
@@ -102,6 +78,16 @@ export default class JournalCommands {
|
|||||||
});
|
});
|
||||||
this.journal.debit(debitEntry);
|
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[]} oldBillEntries -
|
||||||
|
* @param {IItemEntry[]} newBillEntries -
|
||||||
|
*/
|
||||||
|
public getLandedCostEntriesDeleted(
|
||||||
|
oldBillEntries: ICommonLandedCostEntry[],
|
||||||
|
newBillEntriesDTO: ICommonLandedCostEntryDTO[]
|
||||||
|
): ICommonLandedCostEntry[] {
|
||||||
|
const newBillEntriesById = transformToMap(newBillEntriesDTO, 'id');
|
||||||
|
|
||||||
|
return oldBillEntries.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[]} oldBillEntries - Old bill entries.
|
||||||
|
* @param {IItemEntryDTO[]} newBillEntries - New DTO bill entries.
|
||||||
|
*/
|
||||||
|
public validateLandedCostEntriesNotDeleted(
|
||||||
|
oldBillEntries: ICommonLandedCostEntry[],
|
||||||
|
newBillEntriesDTO: ICommonLandedCostEntryDTO[]
|
||||||
|
): void {
|
||||||
|
const entriesDeleted = this.getLandedCostEntriesDeleted(
|
||||||
|
oldBillEntries,
|
||||||
|
newBillEntriesDTO
|
||||||
|
);
|
||||||
|
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[]} oldBillEntries - Old bill entries.
|
||||||
|
* @param {IItemEntryDTO[]} newBillEntries - New DTO bill entries.
|
||||||
|
*/
|
||||||
|
public validateLocatedCostEntriesSmallerThanNewEntries(
|
||||||
|
oldBillEntries: ICommonLandedCostEntry[],
|
||||||
|
newBillEntriesDTO: ICommonLandedCostEntryDTO[]
|
||||||
|
): void {
|
||||||
|
const oldBillEntriesById = transformToMap(oldBillEntries, 'id');
|
||||||
|
|
||||||
|
newBillEntriesDTO.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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@ import DynamicListingService from 'services/DynamicListing/DynamicListService';
|
|||||||
import events from 'subscribers/events';
|
import events from 'subscribers/events';
|
||||||
import ContactsService from 'services/Contacts/ContactsService';
|
import ContactsService from 'services/Contacts/ContactsService';
|
||||||
import { ACCOUNT_PARENT_TYPE, ACCOUNT_ROOT_TYPE } from 'data/AccountTypes';
|
import { ACCOUNT_PARENT_TYPE, ACCOUNT_ROOT_TYPE } from 'data/AccountTypes';
|
||||||
|
import EntriesService from 'services/Entries';
|
||||||
|
|
||||||
const ERRORS = {
|
const ERRORS = {
|
||||||
EXPENSE_NOT_FOUND: 'expense_not_found',
|
EXPENSE_NOT_FOUND: 'expense_not_found',
|
||||||
@@ -53,6 +54,9 @@ export default class ExpensesService implements IExpensesService {
|
|||||||
@Inject()
|
@Inject()
|
||||||
contactsService: ContactsService;
|
contactsService: ContactsService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
entriesService: EntriesService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the payment account details or returns not found server error in case the
|
* Retrieve the payment account details or returns not found server error in case the
|
||||||
* given account not found on the storage.
|
* given account not found on the storage.
|
||||||
@@ -251,14 +255,16 @@ export default class ExpensesService implements IExpensesService {
|
|||||||
* @returns {IExpense|ServiceError}
|
* @returns {IExpense|ServiceError}
|
||||||
*/
|
*/
|
||||||
private async getExpenseOrThrowError(tenantId: number, expenseId: number) {
|
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.', {
|
this.logger.info('[expense] trying to get the given expense.', {
|
||||||
tenantId,
|
tenantId,
|
||||||
expenseId,
|
expenseId,
|
||||||
});
|
});
|
||||||
// Retrieve the given expense by id.
|
// Retrieve the given expense by id.
|
||||||
const expense = await expenseRepository.findOneById(expenseId);
|
const expense = await Expense.query()
|
||||||
|
.findById(expenseId)
|
||||||
|
.withGraphFetched('categories');
|
||||||
|
|
||||||
if (!expense) {
|
if (!expense) {
|
||||||
this.logger.info('[expense] the given expense not found.', {
|
this.logger.info('[expense] the given expense not found.', {
|
||||||
@@ -459,36 +465,47 @@ export default class ExpensesService implements IExpensesService {
|
|||||||
const { expenseRepository } = this.tenancy.repositories(tenantId);
|
const { expenseRepository } = this.tenancy.repositories(tenantId);
|
||||||
const oldExpense = await this.getExpenseOrThrowError(tenantId, expenseId);
|
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(
|
const paymentAccount = await this.getPaymentAccountOrThrowError(
|
||||||
tenantId,
|
tenantId,
|
||||||
expenseDTO.paymentAccountId
|
expenseDTO.paymentAccountId
|
||||||
);
|
);
|
||||||
// - Validate expense accounts exist on the storage.
|
// Validate expense accounts exist on the storage.
|
||||||
const expensesAccounts = await this.getExpensesAccountsOrThrowError(
|
const expensesAccounts = await this.getExpensesAccountsOrThrowError(
|
||||||
tenantId,
|
tenantId,
|
||||||
this.mapExpensesAccountsIdsFromDTO(expenseDTO)
|
this.mapExpensesAccountsIdsFromDTO(expenseDTO)
|
||||||
);
|
);
|
||||||
// - Validate payment account type.
|
// Validate payment account type.
|
||||||
await this.validatePaymentAccountType(tenantId, paymentAccount);
|
await this.validatePaymentAccountType(tenantId, paymentAccount);
|
||||||
|
|
||||||
// - Validate expenses accounts type.
|
// Validate expenses accounts type.
|
||||||
await this.validateExpensesAccountsType(tenantId, expensesAccounts);
|
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) {
|
if (expenseDTO.payeeId) {
|
||||||
await this.contactsService.getContactByIdOrThrowError(
|
await this.contactsService.getContactByIdOrThrowError(
|
||||||
tenantId,
|
tenantId,
|
||||||
expenseDTO.payeeId
|
expenseDTO.payeeId
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// - Validate the given expense categories not equal zero.
|
// Validate the given expense categories not equal zero.
|
||||||
this.validateCategoriesNotEqualZero(expenseDTO);
|
this.validateCategoriesNotEqualZero(expenseDTO);
|
||||||
|
|
||||||
// - Update the expense on the storage.
|
// Update the expense on the storage.
|
||||||
const expenseObj = this.expenseDTOToModel(expenseDTO);
|
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({
|
const expense = await expenseRepository.upsertGraph({
|
||||||
id: expenseId,
|
id: expenseId,
|
||||||
...expenseObj,
|
...expenseObj,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { omit, sumBy } from 'lodash';
|
import { omit, runInContext, sumBy } from 'lodash';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import composeAsync from 'async/compose';
|
import composeAsync from 'async/compose';
|
||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
IBillsFilter,
|
IBillsFilter,
|
||||||
IBillsService,
|
IBillsService,
|
||||||
IItemEntry,
|
IItemEntry,
|
||||||
|
IItemEntryDTO,
|
||||||
} from 'interfaces';
|
} from 'interfaces';
|
||||||
import { ServiceError } from 'exceptions';
|
import { ServiceError } from 'exceptions';
|
||||||
import ItemsService from 'services/Items/ItemsService';
|
import ItemsService from 'services/Items/ItemsService';
|
||||||
@@ -32,6 +33,7 @@ import JournalCommands from 'services/Accounting/JournalCommands';
|
|||||||
import JournalPosterService from 'services/Sales/JournalPosterService';
|
import JournalPosterService from 'services/Sales/JournalPosterService';
|
||||||
import VendorsService from 'services/Contacts/VendorsService';
|
import VendorsService from 'services/Contacts/VendorsService';
|
||||||
import { ERRORS } from './constants';
|
import { ERRORS } from './constants';
|
||||||
|
import EntriesService from 'services/Entries';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vendor bills services.
|
* Vendor bills services.
|
||||||
@@ -72,6 +74,9 @@ export default class BillsService
|
|||||||
@Inject()
|
@Inject()
|
||||||
vendorsService: VendorsService;
|
vendorsService: VendorsService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
entriesService: EntriesService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates whether the vendor is exist.
|
* Validates whether the vendor is exist.
|
||||||
* @async
|
* @async
|
||||||
@@ -166,16 +171,33 @@ export default class BillsService
|
|||||||
* Validate the bill number require.
|
* Validate the bill number require.
|
||||||
* @param {string} billNo -
|
* @param {string} billNo -
|
||||||
*/
|
*/
|
||||||
validateBillNoRequire(billNo: string) {
|
private validateBillNoRequire(billNo: string) {
|
||||||
if (!billNo) {
|
if (!billNo) {
|
||||||
throw new ServiceError(ERRORS.BILL_NO_IS_REQUIRED);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the default cost account to the bill entries.
|
* Sets the default cost account to the bill entries.
|
||||||
*/
|
*/
|
||||||
setBillEntriesDefaultAccounts(tenantId: number) {
|
private setBillEntriesDefaultAccounts(tenantId: number) {
|
||||||
return async (entries: IItemEntry[]) => {
|
return async (entries: IItemEntry[]) => {
|
||||||
const { Item } = this.tenancy.models(tenantId);
|
const { Item } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
@@ -246,6 +268,7 @@ export default class BillsService
|
|||||||
billDTO.vendorId
|
billDTO.vendorId
|
||||||
);
|
);
|
||||||
const initialEntries = billDTO.entries.map((entry) => ({
|
const initialEntries = billDTO.entries.map((entry) => ({
|
||||||
|
amount: ItemEntry.calcAmount(entry),
|
||||||
reference_type: 'Bill',
|
reference_type: 'Bill',
|
||||||
...omit(entry, ['amount']),
|
...omit(entry, ['amount']),
|
||||||
}));
|
}));
|
||||||
@@ -397,6 +420,16 @@ export default class BillsService
|
|||||||
authorizedUser,
|
authorizedUser,
|
||||||
oldBill
|
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.
|
// Update the bill transaction.
|
||||||
const bill = await billRepository.upsertGraph({
|
const bill = await billRepository.upsertGraph({
|
||||||
id: billId,
|
id: billId,
|
||||||
@@ -429,6 +462,9 @@ export default class BillsService
|
|||||||
// Retrieve the given bill or throw not found error.
|
// Retrieve the given bill or throw not found error.
|
||||||
const oldBill = await this.getBillOrThrowError(tenantId, billId);
|
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.
|
// Validate the purchase bill has no assocaited payments transactions.
|
||||||
await this.validateBillHasNoEntries(tenantId, billId);
|
await this.validateBillHasNoEntries(tenantId, billId);
|
||||||
|
|
||||||
@@ -561,9 +597,16 @@ export default class BillsService
|
|||||||
*/
|
*/
|
||||||
public async recordInventoryTransactions(
|
public async recordInventoryTransactions(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
bill: IBill,
|
billId: number,
|
||||||
override?: boolean
|
override?: boolean
|
||||||
): Promise<void> {
|
): 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.
|
// Loads the inventory items entries of the given sale invoice.
|
||||||
const inventoryEntries =
|
const inventoryEntries =
|
||||||
await this.itemsEntriesService.filterInventoryEntries(
|
await this.itemsEntriesService.filterInventoryEntries(
|
||||||
@@ -573,7 +616,6 @@ export default class BillsService
|
|||||||
const transaction = {
|
const transaction = {
|
||||||
transactionId: bill.id,
|
transactionId: bill.id,
|
||||||
transactionType: 'Bill',
|
transactionType: 'Bill',
|
||||||
|
|
||||||
date: bill.billDate,
|
date: bill.billDate,
|
||||||
direction: 'IN',
|
direction: 'IN',
|
||||||
entries: inventoryEntries,
|
entries: inventoryEntries,
|
||||||
@@ -609,13 +651,30 @@ export default class BillsService
|
|||||||
*/
|
*/
|
||||||
public async recordJournalTransactions(
|
public async recordJournalTransactions(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
bill: IBill,
|
billId: number,
|
||||||
override: boolean = false
|
override: boolean = false
|
||||||
) {
|
) {
|
||||||
|
const { Bill, Account } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
const journal = new JournalPoster(tenantId);
|
const journal = new JournalPoster(tenantId);
|
||||||
const journalCommands = new JournalCommands(journal);
|
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([
|
return Promise.all([
|
||||||
journal.deleteEntries(),
|
journal.deleteEntries(),
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import {
|
|||||||
export default class BillLandedCost {
|
export default class BillLandedCost {
|
||||||
/**
|
/**
|
||||||
* Retrieve the landed cost transaction from the given bill transaction.
|
* Retrieve the landed cost transaction from the given bill transaction.
|
||||||
* @param {IBill} bill
|
* @param {IBill} bill - Bill transaction.
|
||||||
* @returns {ILandedCostTransaction}
|
* @returns {ILandedCostTransaction} - Landed cost transaction.
|
||||||
*/
|
*/
|
||||||
public transformToLandedCost = (bill: IBill): ILandedCostTransaction => {
|
public transformToLandedCost = (bill: IBill): ILandedCostTransaction => {
|
||||||
const number = bill.billNumber || bill.referenceNo;
|
const number = bill.billNumber || bill.referenceNo;
|
||||||
@@ -49,7 +49,10 @@ export default class BillLandedCost {
|
|||||||
name: billEntry.item.name,
|
name: billEntry.item.name,
|
||||||
code: billEntry.item.code,
|
code: billEntry.item.code,
|
||||||
amount: billEntry.amount,
|
amount: billEntry.amount,
|
||||||
|
unallocatedCostAmount: billEntry.unallocatedCostAmount,
|
||||||
|
allocatedCostAmount: billEntry.allocatedCostAmount,
|
||||||
description: billEntry.description,
|
description: billEntry.description,
|
||||||
|
costAccountId: billEntry.costAccountId || billEntry.item.costAccountId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ export default class ExpenseLandedCost {
|
|||||||
code: expenseEntry.expenseAccount.code,
|
code: expenseEntry.expenseAccount.code,
|
||||||
amount: expenseEntry.amount,
|
amount: expenseEntry.amount,
|
||||||
description: expenseEntry.description,
|
description: expenseEntry.description,
|
||||||
|
allocatedCostAmount: expenseEntry.allocatedCostAmount,
|
||||||
|
unallocatedCostAmount: expenseEntry.unallocatedCostAmount,
|
||||||
|
costAccountId: expenseEntry.expenseAccount.id,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import * as R from 'ramda';
|
import * as R from 'ramda';
|
||||||
import { IBill, IExpense, ILandedCostTransaction } from 'interfaces';
|
import { Model } from 'objection';
|
||||||
|
import { IBill, IExpense, ILandedCostTransaction, ILandedCostTransactionEntry } from 'interfaces';
|
||||||
import { ServiceError } from 'exceptions';
|
import { ServiceError } from 'exceptions';
|
||||||
import BillLandedCost from './BillLandedCost';
|
import BillLandedCost from './BillLandedCost';
|
||||||
import ExpenseLandedCost from './ExpenseLandedCost';
|
import ExpenseLandedCost from './ExpenseLandedCost';
|
||||||
import HasTenancyService from 'services/Tenancy/TenancyService';
|
import HasTenancyService from 'services/Tenancy/TenancyService';
|
||||||
import { ERRORS } from './constants';
|
import { ERRORS } from './utils';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class TransactionLandedCost {
|
export default class TransactionLandedCost {
|
||||||
@@ -27,7 +28,7 @@ export default class TransactionLandedCost {
|
|||||||
public getModel = (
|
public getModel = (
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
transactionType: string
|
transactionType: string
|
||||||
): IBill | IExpense => {
|
): Model => {
|
||||||
const Models = this.tenancy.models(tenantId);
|
const Models = this.tenancy.models(tenantId);
|
||||||
const Model = Models[transactionType];
|
const Model = Models[transactionType];
|
||||||
|
|
||||||
@@ -58,4 +59,26 @@ export default class TransactionLandedCost {
|
|||||||
),
|
),
|
||||||
)(transaction);
|
)(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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
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'
|
|
||||||
};
|
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import { difference, sumBy } from 'lodash';
|
import { difference, sumBy } from 'lodash';
|
||||||
|
import {
|
||||||
|
EventDispatcher,
|
||||||
|
EventDispatcherInterface,
|
||||||
|
} from 'decorators/eventDispatcher';
|
||||||
import BillsService from '../Bills';
|
import BillsService from '../Bills';
|
||||||
import { ServiceError } from 'exceptions';
|
import { ServiceError } from 'exceptions';
|
||||||
import {
|
import {
|
||||||
@@ -9,15 +13,14 @@ import {
|
|||||||
ILandedCostItemDTO,
|
ILandedCostItemDTO,
|
||||||
ILandedCostDTO,
|
ILandedCostDTO,
|
||||||
IBillLandedCostTransaction,
|
IBillLandedCostTransaction,
|
||||||
IBillLandedCostTransactionEntry,
|
ILandedCostTransaction,
|
||||||
|
ILandedCostTransactionEntry,
|
||||||
} from 'interfaces';
|
} from 'interfaces';
|
||||||
|
import events from 'subscribers/events';
|
||||||
import InventoryService from 'services/Inventory/Inventory';
|
import InventoryService from 'services/Inventory/Inventory';
|
||||||
import HasTenancyService from 'services/Tenancy/TenancyService';
|
import HasTenancyService from 'services/Tenancy/TenancyService';
|
||||||
import { ERRORS } from './constants';
|
|
||||||
import { transformToMap } from 'utils';
|
|
||||||
import JournalPoster from 'services/Accounting/JournalPoster';
|
|
||||||
import JournalEntry from 'services/Accounting/JournalEntry';
|
|
||||||
import TransactionLandedCost from './TransctionLandedCost';
|
import TransactionLandedCost from './TransctionLandedCost';
|
||||||
|
import { ERRORS, mergeLocatedWithBillEntries } from './utils';
|
||||||
|
|
||||||
const CONFIG = {
|
const CONFIG = {
|
||||||
COST_TYPES: {
|
COST_TYPES: {
|
||||||
@@ -47,6 +50,9 @@ export default class AllocateLandedCostService {
|
|||||||
@Inject()
|
@Inject()
|
||||||
public transactionLandedCost: TransactionLandedCost;
|
public transactionLandedCost: TransactionLandedCost;
|
||||||
|
|
||||||
|
@EventDispatcher()
|
||||||
|
eventDispatcher: EventDispatcherInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates allocate cost items association with the purchase invoice entries.
|
* Validates allocate cost items association with the purchase invoice entries.
|
||||||
* @param {IItemEntry[]} purchaseInvoiceEntries
|
* @param {IItemEntry[]} purchaseInvoiceEntries
|
||||||
@@ -72,23 +78,23 @@ export default class AllocateLandedCostService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saves the bill landed cost model.
|
* Transformes DTO to bill landed cost model object.
|
||||||
* @param {number} tenantId
|
* @param landedCostDTO
|
||||||
* @param {ILandedCostDTO} landedCostDTO
|
* @param bill
|
||||||
* @param {number} purchaseInvoiceId
|
* @param costTransaction
|
||||||
* @returns {Promise<void>}
|
* @param costTransactionEntry
|
||||||
|
* @returns
|
||||||
*/
|
*/
|
||||||
private saveBillLandedCostModel = (
|
private transformToBillLandedCost(
|
||||||
tenantId: number,
|
|
||||||
landedCostDTO: ILandedCostDTO,
|
landedCostDTO: ILandedCostDTO,
|
||||||
purchaseInvoiceId: number
|
bill: IBill,
|
||||||
): Promise<IBillLandedCost> => {
|
costTransaction: ILandedCostTransaction,
|
||||||
const { BillLandedCost } = this.tenancy.models(tenantId);
|
costTransactionEntry: ILandedCostTransactionEntry
|
||||||
|
) {
|
||||||
const amount = sumBy(landedCostDTO.items, 'cost');
|
const amount = sumBy(landedCostDTO.items, 'cost');
|
||||||
|
|
||||||
// Inserts the bill landed cost to the storage.
|
return {
|
||||||
return BillLandedCost.query().insertGraph({
|
billId: bill.id,
|
||||||
billId: purchaseInvoiceId,
|
|
||||||
fromTransactionType: landedCostDTO.transactionType,
|
fromTransactionType: landedCostDTO.transactionType,
|
||||||
fromTransactionId: landedCostDTO.transactionId,
|
fromTransactionId: landedCostDTO.transactionId,
|
||||||
fromTransactionEntryId: landedCostDTO.transactionEntryId,
|
fromTransactionEntryId: landedCostDTO.transactionEntryId,
|
||||||
@@ -96,8 +102,9 @@ export default class AllocateLandedCostService {
|
|||||||
allocationMethod: landedCostDTO.allocationMethod,
|
allocationMethod: landedCostDTO.allocationMethod,
|
||||||
description: landedCostDTO.description,
|
description: landedCostDTO.description,
|
||||||
allocateEntries: landedCostDTO.items,
|
allocateEntries: landedCostDTO.items,
|
||||||
});
|
costAccountId: costTransactionEntry.costAccountId,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allocate the landed cost amount to cost transactions.
|
* Allocate the landed cost amount to cost transactions.
|
||||||
@@ -147,7 +154,6 @@ export default class AllocateLandedCostService {
|
|||||||
tenantId,
|
tenantId,
|
||||||
transactionType
|
transactionType
|
||||||
);
|
);
|
||||||
|
|
||||||
// Decrement the allocate cost amount of cost transaction.
|
// Decrement the allocate cost amount of cost transaction.
|
||||||
return Model.query()
|
return Model.query()
|
||||||
.where('id', transactionId)
|
.where('id', transactionId)
|
||||||
@@ -202,12 +208,22 @@ export default class AllocateLandedCostService {
|
|||||||
const entry = await Model.relatedQuery(relation)
|
const entry = await Model.relatedQuery(relation)
|
||||||
.for(transactionId)
|
.for(transactionId)
|
||||||
.findOne('id', transactionEntryId)
|
.findOne('id', transactionEntryId)
|
||||||
.where('landedCost', true);
|
.where('landedCost', true)
|
||||||
|
.onBuild((q) => {
|
||||||
|
if (transactionType === 'Bill') {
|
||||||
|
q.withGraphFetched('item');
|
||||||
|
} else if (transactionType === 'Expense') {
|
||||||
|
q.withGraphFetched('expenseAccount');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (!entry) {
|
if (!entry) {
|
||||||
throw new ServiceError(ERRORS.LANDED_COST_ENTRY_NOT_FOUND);
|
throw new ServiceError(ERRORS.LANDED_COST_ENTRY_NOT_FOUND);
|
||||||
}
|
}
|
||||||
return entry;
|
return this.transactionLandedCost.transformToLandedCostEntry(
|
||||||
|
transactionType,
|
||||||
|
entry
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -230,31 +246,11 @@ export default class AllocateLandedCostService {
|
|||||||
unallocatedCost: number,
|
unallocatedCost: number,
|
||||||
amount: number
|
amount: number
|
||||||
): void => {
|
): void => {
|
||||||
console.log(unallocatedCost, amount, '123');
|
|
||||||
|
|
||||||
if (unallocatedCost < amount) {
|
if (unallocatedCost < amount) {
|
||||||
throw new ServiceError(ERRORS.COST_AMOUNT_BIGGER_THAN_UNALLOCATED_AMOUNT);
|
throw new ServiceError(ERRORS.COST_AMOUNT_BIGGER_THAN_UNALLOCATED_AMOUNT);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Merges item entry to bill located landed cost entry.
|
|
||||||
* @param {IBillLandedCostTransactionEntry[]} locatedEntries -
|
|
||||||
* @param {IItemEntry[]} billEntries -
|
|
||||||
* @returns {(IBillLandedCostTransactionEntry & { entry: IItemEntry })[]}
|
|
||||||
*/
|
|
||||||
private mergeLocatedWithBillEntries = (
|
|
||||||
locatedEntries: IBillLandedCostTransactionEntry[],
|
|
||||||
billEntries: IItemEntry[]
|
|
||||||
): (IBillLandedCostTransactionEntry & { entry: IItemEntry })[] => {
|
|
||||||
const billEntriesByEntryId = transformToMap(billEntries, 'id');
|
|
||||||
|
|
||||||
return locatedEntries.map((entry) => ({
|
|
||||||
...entry,
|
|
||||||
entry: billEntriesByEntryId.get(entry.entryId),
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Records inventory transactions.
|
* Records inventory transactions.
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
@@ -266,7 +262,7 @@ export default class AllocateLandedCostService {
|
|||||||
bill: IBill
|
bill: IBill
|
||||||
) => {
|
) => {
|
||||||
// Retrieve the merged allocated entries with bill entries.
|
// Retrieve the merged allocated entries with bill entries.
|
||||||
const allocateEntries = this.mergeLocatedWithBillEntries(
|
const allocateEntries = mergeLocatedWithBillEntries(
|
||||||
billLandedCost.allocateEntries,
|
billLandedCost.allocateEntries,
|
||||||
bill.entries
|
bill.entries
|
||||||
);
|
);
|
||||||
@@ -304,22 +300,24 @@ export default class AllocateLandedCostService {
|
|||||||
*
|
*
|
||||||
* @param {ILandedCostDTO} landedCostDTO - Landed cost DTO.
|
* @param {ILandedCostDTO} landedCostDTO - Landed cost DTO.
|
||||||
* @param {number} tenantId - Tenant id.
|
* @param {number} tenantId - Tenant id.
|
||||||
* @param {number} purchaseInvoiceId - Purchase invoice id.
|
* @param {number} billId - Purchase invoice id.
|
||||||
*/
|
*/
|
||||||
public allocateLandedCost = async (
|
public allocateLandedCost = async (
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
allocateCostDTO: ILandedCostDTO,
|
allocateCostDTO: ILandedCostDTO,
|
||||||
purchaseInvoiceId: number
|
billId: number
|
||||||
): Promise<{
|
): Promise<{
|
||||||
billLandedCost: IBillLandedCost;
|
billLandedCost: IBillLandedCost;
|
||||||
}> => {
|
}> => {
|
||||||
|
const { BillLandedCost } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
// Retrieve total cost of allocated items.
|
// Retrieve total cost of allocated items.
|
||||||
const amount = this.getAllocateItemsCostTotal(allocateCostDTO);
|
const amount = this.getAllocateItemsCostTotal(allocateCostDTO);
|
||||||
|
|
||||||
// Retrieve the purchase invoice or throw not found error.
|
// Retrieve the purchase invoice or throw not found error.
|
||||||
const purchaseInvoice = await this.billsService.getBillOrThrowError(
|
const bill = await this.billsService.getBillOrThrowError(
|
||||||
tenantId,
|
tenantId,
|
||||||
purchaseInvoiceId
|
billId
|
||||||
);
|
);
|
||||||
// Retrieve landed cost transaction or throw not found service error.
|
// Retrieve landed cost transaction or throw not found service error.
|
||||||
const landedCostTransaction = await this.getLandedCostOrThrowError(
|
const landedCostTransaction = await this.getLandedCostOrThrowError(
|
||||||
@@ -336,25 +334,36 @@ export default class AllocateLandedCostService {
|
|||||||
);
|
);
|
||||||
// Validates allocate cost items association with the purchase invoice entries.
|
// Validates allocate cost items association with the purchase invoice entries.
|
||||||
this.validateAllocateCostItems(
|
this.validateAllocateCostItems(
|
||||||
purchaseInvoice.entries,
|
bill.entries,
|
||||||
allocateCostDTO.items
|
allocateCostDTO.items
|
||||||
);
|
);
|
||||||
// Validate the amount of cost with unallocated landed cost.
|
// Validate the amount of cost with unallocated landed cost.
|
||||||
this.validateLandedCostEntryAmount(
|
this.validateLandedCostEntryAmount(
|
||||||
landedCostEntry.unallocatedLandedCost,
|
landedCostEntry.unallocatedCostAmount,
|
||||||
amount
|
amount
|
||||||
);
|
);
|
||||||
// Save the bill landed cost model.
|
// Transformes DTO to bill landed cost model object.
|
||||||
const billLandedCost = await this.saveBillLandedCostModel(
|
const billLandedCostObj = this.transformToBillLandedCost(
|
||||||
tenantId,
|
|
||||||
allocateCostDTO,
|
allocateCostDTO,
|
||||||
purchaseInvoiceId
|
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.
|
// Records the inventory transactions.
|
||||||
await this.recordInventoryTransactions(
|
await this.recordInventoryTransactions(
|
||||||
tenantId,
|
tenantId,
|
||||||
billLandedCost,
|
billLandedCost,
|
||||||
purchaseInvoice
|
bill
|
||||||
);
|
);
|
||||||
// Increment landed cost amount on transaction and entry.
|
// Increment landed cost amount on transaction and entry.
|
||||||
await this.incrementLandedCostAmount(
|
await this.incrementLandedCostAmount(
|
||||||
@@ -364,55 +373,9 @@ export default class AllocateLandedCostService {
|
|||||||
allocateCostDTO.transactionEntryId,
|
allocateCostDTO.transactionEntryId,
|
||||||
amount
|
amount
|
||||||
);
|
);
|
||||||
// Write the landed cost journal entries.
|
|
||||||
// await this.writeJournalEntry(tenantId, billLandedCost, purchaseInvoice);
|
|
||||||
|
|
||||||
return { billLandedCost };
|
return { billLandedCost };
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Write journal entries of the given purchase invoice landed cost.
|
|
||||||
* @param tenantId
|
|
||||||
* @param purchaseInvoice
|
|
||||||
* @param landedCost
|
|
||||||
*/
|
|
||||||
private writeJournalEntry = async (
|
|
||||||
tenantId: number,
|
|
||||||
landedCostEntry: any,
|
|
||||||
purchaseInvoice: IBill,
|
|
||||||
landedCost: IBillLandedCost
|
|
||||||
) => {
|
|
||||||
const journal = new JournalPoster(tenantId);
|
|
||||||
const billEntriesById = purchaseInvoice.entries;
|
|
||||||
|
|
||||||
const commonEntry = {
|
|
||||||
referenceType: 'Bill',
|
|
||||||
referenceId: purchaseInvoice.id,
|
|
||||||
date: purchaseInvoice.billDate,
|
|
||||||
indexGroup: 300,
|
|
||||||
};
|
|
||||||
const costEntry = new JournalEntry({
|
|
||||||
...commonEntry,
|
|
||||||
credit: landedCost.amount,
|
|
||||||
account: landedCost.costAccountId,
|
|
||||||
index: 1,
|
|
||||||
});
|
|
||||||
journal.credit(costEntry);
|
|
||||||
|
|
||||||
landedCost.allocateEntries.forEach((entry, index) => {
|
|
||||||
const billEntry = billEntriesById[entry.entryId];
|
|
||||||
|
|
||||||
const inventoryEntry = new JournalEntry({
|
|
||||||
...commonEntry,
|
|
||||||
debit: entry.cost,
|
|
||||||
account: billEntry.item.inventoryAccountId,
|
|
||||||
index: 1 + index,
|
|
||||||
});
|
|
||||||
journal.debit(inventoryEntry);
|
|
||||||
});
|
|
||||||
return journal;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the give bill landed cost or throw not found service error.
|
* Retrieve the give bill landed cost or throw not found service error.
|
||||||
* @param {number} tenantId - Tenant id.
|
* @param {number} tenantId - Tenant id.
|
||||||
@@ -422,7 +385,7 @@ export default class AllocateLandedCostService {
|
|||||||
public getBillLandedCostOrThrowError = async (
|
public getBillLandedCostOrThrowError = async (
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
landedCostId: number
|
landedCostId: number
|
||||||
): Promise<IBillLandedCost> => {
|
): Promise<IBillLandedCostTransaction> => {
|
||||||
const { BillLandedCost } = this.tenancy.models(tenantId);
|
const { BillLandedCost } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
// Retrieve the bill landed cost model.
|
// Retrieve the bill landed cost model.
|
||||||
@@ -462,7 +425,7 @@ export default class AllocateLandedCostService {
|
|||||||
* - Delete the associated inventory transactions.
|
* - Delete the associated inventory transactions.
|
||||||
* - Decrement allocated amount of landed cost transaction and entry.
|
* - Decrement allocated amount of landed cost transaction and entry.
|
||||||
* - Revert journal entries.
|
* - Revert journal entries.
|
||||||
*
|
* ----------------------------------
|
||||||
* @param {number} tenantId - Tenant id.
|
* @param {number} tenantId - Tenant id.
|
||||||
* @param {number} landedCostId - Landed cost id.
|
* @param {number} landedCostId - Landed cost id.
|
||||||
* @return {Promise<void>}
|
* @return {Promise<void>}
|
||||||
@@ -481,6 +444,12 @@ export default class AllocateLandedCostService {
|
|||||||
// Delete landed cost transaction with assocaited locate entries.
|
// Delete landed cost transaction with assocaited locate entries.
|
||||||
await this.deleteLandedCost(tenantId, landedCostId);
|
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.
|
// Removes the inventory transactions.
|
||||||
await this.removeInventoryTransactions(tenantId, landedCostId);
|
await this.removeInventoryTransactions(tenantId, landedCostId);
|
||||||
|
|
||||||
|
|||||||
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,8 @@ export const ERRORS = {
|
|||||||
BILL_ALREADY_OPEN: 'BILL_ALREADY_OPEN',
|
BILL_ALREADY_OPEN: 'BILL_ALREADY_OPEN',
|
||||||
BILL_NO_IS_REQUIRED: 'BILL_NO_IS_REQUIRED',
|
BILL_NO_IS_REQUIRED: 'BILL_NO_IS_REQUIRED',
|
||||||
BILL_HAS_ASSOCIATED_PAYMENT_ENTRIES: 'BILL_HAS_ASSOCIATED_PAYMENT_ENTRIES',
|
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'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,20 +23,20 @@ export default class BillSubscriber {
|
|||||||
* Handles writing journal entries once bill created.
|
* Handles writing journal entries once bill created.
|
||||||
*/
|
*/
|
||||||
@On(events.bill.onCreated)
|
@On(events.bill.onCreated)
|
||||||
async handlerWriteJournalEntriesOnCreate({ tenantId, bill }) {
|
async handlerWriteJournalEntriesOnCreate({ tenantId, billId }) {
|
||||||
// Writes the journal entries for the given bill transaction.
|
// Writes the journal entries for the given bill transaction.
|
||||||
this.logger.info('[bill] writing bill journal entries.', { tenantId });
|
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.
|
* Handles the overwriting journal entries once bill edited.
|
||||||
*/
|
*/
|
||||||
@On(events.bill.onEdited)
|
@On(events.bill.onEdited)
|
||||||
async handleOverwriteJournalEntriesOnEdit({ tenantId, bill }) {
|
async handleOverwriteJournalEntriesOnEdit({ tenantId, billId }) {
|
||||||
// Overwrite the journal entries for the given bill transaction.
|
// Overwrite the journal entries for the given bill transaction.
|
||||||
this.logger.info('[bill] overwriting bill journal entries.', { tenantId });
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user