WIP: Allocate landed cost.

This commit is contained in:
a.bouhuolia
2021-07-24 03:10:32 +02:00
parent 70aea9bf2d
commit cf2ebe9597
30 changed files with 602 additions and 218 deletions

View File

@@ -12,8 +12,8 @@ import {
export default class BillLandedCost {
/**
* Retrieve the landed cost transaction from the given bill transaction.
* @param {IBill} bill
* @returns {ILandedCostTransaction}
* @param {IBill} bill - Bill transaction.
* @returns {ILandedCostTransaction} - Landed cost transaction.
*/
public transformToLandedCost = (bill: IBill): ILandedCostTransaction => {
const number = bill.billNumber || bill.referenceNo;
@@ -49,7 +49,10 @@ export default class BillLandedCost {
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,
};
}
}

View File

@@ -48,6 +48,9 @@ export default class ExpenseLandedCost {
code: expenseEntry.expenseAccount.code,
amount: expenseEntry.amount,
description: expenseEntry.description,
allocatedCostAmount: expenseEntry.allocatedCostAmount,
unallocatedCostAmount: expenseEntry.unallocatedCostAmount,
costAccountId: expenseEntry.expenseAccount.id,
};
};
}

View File

@@ -1,11 +1,12 @@
import { Inject, Service } from 'typedi';
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 BillLandedCost from './BillLandedCost';
import ExpenseLandedCost from './ExpenseLandedCost';
import HasTenancyService from 'services/Tenancy/TenancyService';
import { ERRORS } from './constants';
import { ERRORS } from './utils';
@Service()
export default class TransactionLandedCost {
@@ -27,7 +28,7 @@ export default class TransactionLandedCost {
public getModel = (
tenantId: number,
transactionType: string
): IBill | IExpense => {
): Model => {
const Models = this.tenancy.models(tenantId);
const Model = Models[transactionType];
@@ -58,4 +59,26 @@ export default class TransactionLandedCost {
),
)(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);
}
}

View File

@@ -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'
};

View File

@@ -1,5 +1,9 @@
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 {
@@ -9,15 +13,14 @@ import {
ILandedCostItemDTO,
ILandedCostDTO,
IBillLandedCostTransaction,
IBillLandedCostTransactionEntry,
ILandedCostTransaction,
ILandedCostTransactionEntry,
} from 'interfaces';
import events from 'subscribers/events';
import InventoryService from 'services/Inventory/Inventory';
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 { ERRORS, mergeLocatedWithBillEntries } from './utils';
const CONFIG = {
COST_TYPES: {
@@ -47,6 +50,9 @@ export default class AllocateLandedCostService {
@Inject()
public transactionLandedCost: TransactionLandedCost;
@EventDispatcher()
eventDispatcher: EventDispatcherInterface;
/**
* Validates allocate cost items association with the purchase invoice entries.
* @param {IItemEntry[]} purchaseInvoiceEntries
@@ -72,23 +78,23 @@ export default class AllocateLandedCostService {
};
/**
* Saves the bill landed cost model.
* @param {number} tenantId
* @param {ILandedCostDTO} landedCostDTO
* @param {number} purchaseInvoiceId
* @returns {Promise<void>}
* Transformes DTO to bill landed cost model object.
* @param landedCostDTO
* @param bill
* @param costTransaction
* @param costTransactionEntry
* @returns
*/
private saveBillLandedCostModel = (
tenantId: number,
private transformToBillLandedCost(
landedCostDTO: ILandedCostDTO,
purchaseInvoiceId: number
): Promise<IBillLandedCost> => {
const { BillLandedCost } = this.tenancy.models(tenantId);
bill: IBill,
costTransaction: ILandedCostTransaction,
costTransactionEntry: ILandedCostTransactionEntry
) {
const amount = sumBy(landedCostDTO.items, 'cost');
// Inserts the bill landed cost to the storage.
return BillLandedCost.query().insertGraph({
billId: purchaseInvoiceId,
return {
billId: bill.id,
fromTransactionType: landedCostDTO.transactionType,
fromTransactionId: landedCostDTO.transactionId,
fromTransactionEntryId: landedCostDTO.transactionEntryId,
@@ -96,8 +102,9 @@ export default class AllocateLandedCostService {
allocationMethod: landedCostDTO.allocationMethod,
description: landedCostDTO.description,
allocateEntries: landedCostDTO.items,
});
};
costAccountId: costTransactionEntry.costAccountId,
};
}
/**
* Allocate the landed cost amount to cost transactions.
@@ -147,7 +154,6 @@ export default class AllocateLandedCostService {
tenantId,
transactionType
);
// Decrement the allocate cost amount of cost transaction.
return Model.query()
.where('id', transactionId)
@@ -202,12 +208,22 @@ export default class AllocateLandedCostService {
const entry = await Model.relatedQuery(relation)
.for(transactionId)
.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) {
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,
amount: number
): void => {
console.log(unallocatedCost, amount, '123');
if (unallocatedCost < 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.
* @param {number} tenantId
@@ -266,7 +262,7 @@ export default class AllocateLandedCostService {
bill: IBill
) => {
// Retrieve the merged allocated entries with bill entries.
const allocateEntries = this.mergeLocatedWithBillEntries(
const allocateEntries = mergeLocatedWithBillEntries(
billLandedCost.allocateEntries,
bill.entries
);
@@ -304,22 +300,24 @@ export default class AllocateLandedCostService {
*
* @param {ILandedCostDTO} landedCostDTO - Landed cost DTO.
* @param {number} tenantId - Tenant id.
* @param {number} purchaseInvoiceId - Purchase invoice id.
* @param {number} billId - Purchase invoice id.
*/
public allocateLandedCost = async (
tenantId: number,
allocateCostDTO: ILandedCostDTO,
purchaseInvoiceId: number
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 purchaseInvoice = await this.billsService.getBillOrThrowError(
const bill = await this.billsService.getBillOrThrowError(
tenantId,
purchaseInvoiceId
billId
);
// Retrieve landed cost transaction or throw not found service error.
const landedCostTransaction = await this.getLandedCostOrThrowError(
@@ -336,25 +334,36 @@ export default class AllocateLandedCostService {
);
// Validates allocate cost items association with the purchase invoice entries.
this.validateAllocateCostItems(
purchaseInvoice.entries,
bill.entries,
allocateCostDTO.items
);
// Validate the amount of cost with unallocated landed cost.
this.validateLandedCostEntryAmount(
landedCostEntry.unallocatedLandedCost,
landedCostEntry.unallocatedCostAmount,
amount
);
// Save the bill landed cost model.
const billLandedCost = await this.saveBillLandedCostModel(
tenantId,
// Transformes DTO to bill landed cost model object.
const billLandedCostObj = this.transformToBillLandedCost(
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.
await this.recordInventoryTransactions(
tenantId,
billLandedCost,
purchaseInvoice
bill
);
// Increment landed cost amount on transaction and entry.
await this.incrementLandedCostAmount(
@@ -364,55 +373,9 @@ export default class AllocateLandedCostService {
allocateCostDTO.transactionEntryId,
amount
);
// Write the landed cost journal entries.
// await this.writeJournalEntry(tenantId, billLandedCost, purchaseInvoice);
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.
* @param {number} tenantId - Tenant id.
@@ -422,7 +385,7 @@ export default class AllocateLandedCostService {
public getBillLandedCostOrThrowError = async (
tenantId: number,
landedCostId: number
): Promise<IBillLandedCost> => {
): Promise<IBillLandedCostTransaction> => {
const { BillLandedCost } = this.tenancy.models(tenantId);
// Retrieve the bill landed cost model.
@@ -462,7 +425,7 @@ export default class AllocateLandedCostService {
* - 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>}
@@ -481,6 +444,12 @@ export default class AllocateLandedCostService {
// 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);

View 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),
}));
};