mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 12:50:38 +00:00
WIP: Allocate landed cost.
This commit is contained in:
@@ -17,11 +17,12 @@ 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';
|
||||
|
||||
const ERRORS = {
|
||||
EXPENSE_NOT_FOUND: 'expense_not_found',
|
||||
@@ -32,6 +33,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()
|
||||
@@ -308,6 +310,27 @@ export default class ExpensesService implements IExpensesService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the expense landed cost amount.
|
||||
* @param {IExpenseDTO} expenseDTO
|
||||
* @return {number}
|
||||
*/
|
||||
private getExpenseLandedCostAmount(expenseDTO: IExpenseDTO): number {
|
||||
const landedCostEntries = expenseDTO.categories.filter((entry) => {
|
||||
return entry.landedCost === true;
|
||||
});
|
||||
return this.getExpenseCategoriesTotal(landedCostEntries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the given expense categories total.
|
||||
* @param {IExpenseCategory} categories
|
||||
* @returns {number}
|
||||
*/
|
||||
private getExpenseCategoriesTotal(categories): number {
|
||||
return sumBy(categories, 'amount');
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapping expense DTO to model.
|
||||
* @param {IExpenseDTO} expenseDTO
|
||||
@@ -315,12 +338,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 +365,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);
|
||||
}
|
||||
|
||||
@@ -544,15 +569,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 +598,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 +674,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,8 @@ import { ERRORS } from './constants';
|
||||
@Service('Bills')
|
||||
export default class BillsService
|
||||
extends SalesInvoicesCost
|
||||
implements IBillsService {
|
||||
implements IBillsService
|
||||
{
|
||||
@Inject()
|
||||
inventoryService: InventoryService;
|
||||
|
||||
@@ -100,7 +101,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 });
|
||||
@@ -194,6 +195,28 @@ export default class BillsService
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the bill entries total.
|
||||
* @param {IItemEntry[]} entries
|
||||
* @returns {number}
|
||||
*/
|
||||
private getBillEntriesTotal(tenantId: number, entries: IItemEntry[]): number {
|
||||
const { ItemEntry } = this.tenancy.models(tenantId);
|
||||
|
||||
return sumBy(entries, (e) => ItemEntry.calcAmount(e));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the bill landed cost amount.
|
||||
* @param {IBillDTO} billDTO
|
||||
* @returns {number}
|
||||
*/
|
||||
private getBillLandedCostAmount(tenantId: number, billDTO: IBillDTO): number {
|
||||
const costEntries = billDTO.entries.filter((entry) => entry.landedCost);
|
||||
|
||||
return this.getBillEntriesTotal(tenantId, costEntries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts create bill DTO to model.
|
||||
* @param {number} tenantId
|
||||
@@ -211,6 +234,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;
|
||||
|
||||
@@ -234,6 +260,7 @@ export default class BillsService
|
||||
'dueDate',
|
||||
]),
|
||||
amount,
|
||||
landedCostAmount,
|
||||
currencyCode: vendor.currencyCode,
|
||||
billNumber,
|
||||
entries,
|
||||
@@ -498,7 +525,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);
|
||||
@@ -538,10 +565,11 @@ export default class BillsService
|
||||
override?: boolean
|
||||
): Promise<void> {
|
||||
// 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',
|
||||
|
||||
55
server/src/services/Purchases/LandedCost/BillLandedCost.ts
Normal file
55
server/src/services/Purchases/LandedCost/BillLandedCost.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { Service } from 'typedi';
|
||||
import { isEmpty } from 'lodash';
|
||||
import {
|
||||
IBill,
|
||||
IItem,
|
||||
ILandedCostTransactionEntry,
|
||||
ILandedCostTransaction,
|
||||
IItemEntry,
|
||||
} from 'interfaces';
|
||||
|
||||
@Service()
|
||||
export default class BillLandedCost {
|
||||
/**
|
||||
* Retrieve the landed cost transaction from the given bill transaction.
|
||||
* @param {IBill} bill
|
||||
* @returns {ILandedCostTransaction}
|
||||
*/
|
||||
public transformToLandedCost = (bill: IBill): ILandedCostTransaction => {
|
||||
const number = bill.billNumber || bill.referenceNo;
|
||||
const name = [
|
||||
number,
|
||||
bill.currencyCode + ' ' + bill.unallocatedCostAmount,
|
||||
].join(' - ');
|
||||
|
||||
return {
|
||||
id: bill.id,
|
||||
name,
|
||||
allocatedCostAmount: bill.allocatedCostAmount,
|
||||
amount: bill.landedCostAmount,
|
||||
unallocatedCostAmount: bill.unallocatedCostAmount,
|
||||
transactionType: 'Bill',
|
||||
|
||||
...(!isEmpty(bill.entries)) && {
|
||||
entries: bill.entries.map(this.transformToLandedCostEntry),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Transformes bill entry to landed cost entry.
|
||||
* @param {IItemEntry} billEntry - Bill entry.
|
||||
* @return {ILandedCostTransactionEntry}
|
||||
*/
|
||||
public transformToLandedCostEntry(
|
||||
billEntry: IItemEntry & { item: IItem }
|
||||
): ILandedCostTransactionEntry {
|
||||
return {
|
||||
id: billEntry.id,
|
||||
name: billEntry.item.name,
|
||||
code: billEntry.item.code,
|
||||
amount: billEntry.amount,
|
||||
description: billEntry.description,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Service } from 'typedi';
|
||||
import { isEmpty } from 'lodash';
|
||||
import {
|
||||
IExpense,
|
||||
ILandedCostTransactionEntry,
|
||||
IExpenseCategory,
|
||||
IAccount,
|
||||
ILandedCostTransaction,
|
||||
} from 'interfaces';
|
||||
|
||||
@Service()
|
||||
export default class ExpenseLandedCost {
|
||||
/**
|
||||
* Retrieve the landed cost transaction from the given expense transaction.
|
||||
* @param {IExpense} expense
|
||||
* @returns {ILandedCostTransaction}
|
||||
*/
|
||||
public transformToLandedCost = (
|
||||
expense: IExpense
|
||||
): ILandedCostTransaction => {
|
||||
const name = [expense.currencyCode + ' ' + expense.totalAmount].join(' - ');
|
||||
|
||||
return {
|
||||
id: expense.id,
|
||||
name,
|
||||
allocatedCostAmount: expense.allocatedCostAmount,
|
||||
amount: expense.landedCostAmount,
|
||||
unallocatedCostAmount: expense.unallocatedCostAmount,
|
||||
transactionType: 'Expense',
|
||||
|
||||
...(!isEmpty(expense.categories) && {
|
||||
entries: expense.categories.map(this.transformToLandedCostEntry),
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Transformes expense entry to landed cost entry.
|
||||
* @param {IExpenseCategory & { expenseAccount: IAccount }} expenseEntry -
|
||||
* @return {ILandedCostTransactionEntry}
|
||||
*/
|
||||
public transformToLandedCostEntry = (
|
||||
expenseEntry: IExpenseCategory & { expenseAccount: IAccount }
|
||||
): ILandedCostTransactionEntry => {
|
||||
return {
|
||||
id: expenseEntry.id,
|
||||
name: expenseEntry.expenseAccount.name,
|
||||
code: expenseEntry.expenseAccount.code,
|
||||
amount: expenseEntry.amount,
|
||||
description: expenseEntry.description,
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { ref } from 'objection';
|
||||
import {
|
||||
ILandedCostTransactionsQueryDTO,
|
||||
ILandedCostTransaction,
|
||||
IBillLandedCostTransaction,
|
||||
} from 'interfaces';
|
||||
import TransactionLandedCost from './TransctionLandedCost';
|
||||
import BillsService from '../Bills';
|
||||
import HasTenancyService from 'services/Tenancy/TenancyService';
|
||||
|
||||
@Service()
|
||||
export default class LandedCostListing {
|
||||
@Inject()
|
||||
transactionLandedCost: TransactionLandedCost;
|
||||
|
||||
@Inject()
|
||||
billsService: BillsService;
|
||||
|
||||
@Inject()
|
||||
tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Retrieve the landed costs based on the given query.
|
||||
* @param {number} tenantId
|
||||
* @param {ILandedCostTransactionsQueryDTO} query
|
||||
* @returns {Promise<ILandedCostTransaction[]>}
|
||||
*/
|
||||
public getLandedCostTransactions = async (
|
||||
tenantId: number,
|
||||
query: ILandedCostTransactionsQueryDTO
|
||||
): Promise<ILandedCostTransaction[]> => {
|
||||
const { transactionType } = query;
|
||||
const Model = this.transactionLandedCost.getModel(
|
||||
tenantId,
|
||||
query.transactionType
|
||||
);
|
||||
|
||||
// Retrieve the model entities.
|
||||
const transactions = await Model.query().onBuild((q) => {
|
||||
q.where('allocated_cost_amount', '<', ref('landed_cost_amount'));
|
||||
|
||||
if (query.transactionType === 'Bill') {
|
||||
q.withGraphFetched('entries.item');
|
||||
} else if (query.transactionType === 'Expense') {
|
||||
q.withGraphFetched('categories.expenseAccount');
|
||||
}
|
||||
});
|
||||
return transactions.map((transaction) => ({
|
||||
...this.transactionLandedCost.transformToLandedCost(
|
||||
transactionType,
|
||||
transaction
|
||||
),
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the bill associated landed cost transactions.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {number} billId - Bill id.
|
||||
* @return {Promise<IBillLandedCostTransaction>}
|
||||
*/
|
||||
public getBillLandedCostTransactions = async (
|
||||
tenantId: number,
|
||||
billId: number
|
||||
): Promise<IBillLandedCostTransaction> => {
|
||||
const { BillLandedCost } = this.tenancy.models(tenantId);
|
||||
|
||||
// Retrieve the given bill id or throw not found service error.
|
||||
const bill = await this.billsService.getBillOrThrowError(tenantId, billId);
|
||||
|
||||
const landedCostTransactions = await BillLandedCost.query()
|
||||
.where('bill_id', billId)
|
||||
.withGraphFetched('allocateEntries');
|
||||
|
||||
return landedCostTransactions;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import * as R from 'ramda';
|
||||
import { IBill, IExpense, ILandedCostTransaction } from 'interfaces';
|
||||
import { ServiceError } from 'exceptions';
|
||||
import BillLandedCost from './BillLandedCost';
|
||||
import ExpenseLandedCost from './ExpenseLandedCost';
|
||||
import HasTenancyService from 'services/Tenancy/TenancyService';
|
||||
import { ERRORS } from './constants';
|
||||
|
||||
@Service()
|
||||
export default class TransactionLandedCost {
|
||||
@Inject()
|
||||
billLandedCost: BillLandedCost;
|
||||
|
||||
@Inject()
|
||||
expenseLandedCost: ExpenseLandedCost;
|
||||
|
||||
@Inject()
|
||||
tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Retrieve the cost transaction code model.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {string} transactionType - Transaction type.
|
||||
* @returns
|
||||
*/
|
||||
public getModel = (
|
||||
tenantId: number,
|
||||
transactionType: string
|
||||
): IBill | IExpense => {
|
||||
const Models = this.tenancy.models(tenantId);
|
||||
const Model = Models[transactionType];
|
||||
|
||||
if (!Model) {
|
||||
throw new ServiceError(ERRORS.COST_TYPE_UNDEFINED);
|
||||
}
|
||||
return Model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mappes the given expense or bill transaction to landed cost transaction.
|
||||
* @param {string} transactionType - Transaction type.
|
||||
* @param {IBill|IExpense} transaction - Expense or bill transaction.
|
||||
* @returns {ILandedCostTransaction}
|
||||
*/
|
||||
public transformToLandedCost = (
|
||||
transactionType: string,
|
||||
transaction: IBill | IExpense
|
||||
): ILandedCostTransaction => {
|
||||
return R.compose(
|
||||
R.when(
|
||||
R.always(transactionType === 'Bill'),
|
||||
this.billLandedCost.transformToLandedCost,
|
||||
),
|
||||
R.when(
|
||||
R.always(transactionType === 'Expense'),
|
||||
this.expenseLandedCost.transformToLandedCost,
|
||||
),
|
||||
)(transaction);
|
||||
}
|
||||
}
|
||||
15
server/src/services/Purchases/LandedCost/constants.ts
Normal file
15
server/src/services/Purchases/LandedCost/constants.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
|
||||
|
||||
|
||||
export const ERRORS = {
|
||||
COST_TYPE_UNDEFINED: 'COST_TYPE_UNDEFINED',
|
||||
LANDED_COST_ITEMS_IDS_NOT_FOUND: 'LANDED_COST_ITEMS_IDS_NOT_FOUND',
|
||||
COST_TRANSACTION_HAS_NO_ENOUGH_TO_LOCATE:
|
||||
'COST_TRANSACTION_HAS_NO_ENOUGH_TO_LOCATE',
|
||||
BILL_LANDED_COST_NOT_FOUND: 'BILL_LANDED_COST_NOT_FOUND',
|
||||
COST_ENTRY_ID_NOT_FOUND: 'COST_ENTRY_ID_NOT_FOUND',
|
||||
LANDED_COST_TRANSACTION_NOT_FOUND: 'LANDED_COST_TRANSACTION_NOT_FOUND',
|
||||
LANDED_COST_ENTRY_NOT_FOUND: 'LANDED_COST_ENTRY_NOT_FOUND',
|
||||
COST_AMOUNT_BIGGER_THAN_UNALLOCATED_AMOUNT: 'COST_AMOUNT_BIGGER_THAN_UNALLOCATED_AMOUNT',
|
||||
ALLOCATE_COST_SHOULD_NOT_BE_BILL: 'ALLOCATE_COST_SHOULD_NOT_BE_BILL'
|
||||
};
|
||||
504
server/src/services/Purchases/LandedCost/index.ts
Normal file
504
server/src/services/Purchases/LandedCost/index.ts
Normal file
@@ -0,0 +1,504 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { difference, sumBy } from 'lodash';
|
||||
import BillsService from '../Bills';
|
||||
import { ServiceError } from 'exceptions';
|
||||
import {
|
||||
IItemEntry,
|
||||
IBill,
|
||||
IBillLandedCost,
|
||||
ILandedCostItemDTO,
|
||||
ILandedCostDTO,
|
||||
} from 'interfaces';
|
||||
import InventoryService from 'services/Inventory/Inventory';
|
||||
import HasTenancyService from 'services/Tenancy/TenancyService';
|
||||
import { ERRORS } from './constants';
|
||||
import { mergeObjectsBykey } from 'utils';
|
||||
import JournalPoster from 'services/Accounting/JournalPoster';
|
||||
import JournalEntry from 'services/Accounting/JournalEntry';
|
||||
import TransactionLandedCost from './TransctionLandedCost';
|
||||
|
||||
const CONFIG = {
|
||||
COST_TYPES: {
|
||||
Expense: {
|
||||
entries: 'categories',
|
||||
},
|
||||
Bill: {
|
||||
entries: 'entries',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@Service()
|
||||
export default class AllocateLandedCostService {
|
||||
@Inject()
|
||||
public billsService: BillsService;
|
||||
|
||||
@Inject()
|
||||
public inventoryService: InventoryService;
|
||||
|
||||
@Inject()
|
||||
public tenancy: HasTenancyService;
|
||||
|
||||
@Inject('logger')
|
||||
public logger: any;
|
||||
|
||||
@Inject()
|
||||
public transactionLandedCost: TransactionLandedCost;
|
||||
|
||||
/**
|
||||
* Validates allocate cost items association with the purchase invoice entries.
|
||||
* @param {IItemEntry[]} purchaseInvoiceEntries
|
||||
* @param {ILandedCostItemDTO[]} landedCostItems
|
||||
*/
|
||||
private validateAllocateCostItems = (
|
||||
purchaseInvoiceEntries: IItemEntry[],
|
||||
landedCostItems: ILandedCostItemDTO[]
|
||||
): void => {
|
||||
// Purchase invoice entries items ids.
|
||||
const purchaseInvoiceItems = purchaseInvoiceEntries.map((e) => e.id);
|
||||
const landedCostItemsIds = landedCostItems.map((item) => item.entryId);
|
||||
|
||||
// Not found items ids.
|
||||
const notFoundItemsIds = difference(
|
||||
purchaseInvoiceItems,
|
||||
landedCostItemsIds
|
||||
);
|
||||
// Throw items ids not found service error.
|
||||
if (notFoundItemsIds.length > 0) {
|
||||
throw new ServiceError(ERRORS.LANDED_COST_ITEMS_IDS_NOT_FOUND);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Saves the bill landed cost model.
|
||||
* @param {number} tenantId
|
||||
* @param {ILandedCostDTO} landedCostDTO
|
||||
* @param {number} purchaseInvoiceId
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
private saveBillLandedCostModel = (
|
||||
tenantId: number,
|
||||
landedCostDTO: ILandedCostDTO,
|
||||
purchaseInvoiceId: number
|
||||
): Promise<IBillLandedCost> => {
|
||||
const { BillLandedCost } = this.tenancy.models(tenantId);
|
||||
const amount = sumBy(landedCostDTO.items, 'cost');
|
||||
|
||||
// Inserts the bill landed cost to the storage.
|
||||
return BillLandedCost.query().insertGraph({
|
||||
billId: purchaseInvoiceId,
|
||||
fromTransactionType: landedCostDTO.transactionType,
|
||||
fromTransactionId: landedCostDTO.transactionId,
|
||||
fromTransactionEntryId: landedCostDTO.transactionEntryId,
|
||||
amount,
|
||||
allocationMethod: landedCostDTO.allocationMethod,
|
||||
description: landedCostDTO.description,
|
||||
allocateEntries: landedCostDTO.items,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Allocate the landed cost amount to cost transactions.
|
||||
* @param {number} tenantId -
|
||||
* @param {string} transactionType
|
||||
* @param {number} transactionId
|
||||
*/
|
||||
private incrementLandedCostAmount = async (
|
||||
tenantId: number,
|
||||
transactionType: string,
|
||||
transactionId: number,
|
||||
transactionEntryId: number,
|
||||
amount: number
|
||||
): Promise<void> => {
|
||||
const Model = this.transactionLandedCost.getModel(
|
||||
tenantId,
|
||||
transactionType
|
||||
);
|
||||
const relation = CONFIG.COST_TYPES[transactionType].entries;
|
||||
|
||||
// Increment the landed cost transaction amount.
|
||||
await Model.query()
|
||||
.where('id', transactionId)
|
||||
.increment('allocatedCostAmount', amount);
|
||||
|
||||
// Increment the landed cost entry.
|
||||
await Model.relatedQuery(relation)
|
||||
.for(transactionId)
|
||||
.where('id', transactionEntryId)
|
||||
.increment('allocatedCostAmount', amount);
|
||||
};
|
||||
|
||||
/**
|
||||
* Reverts the landed cost amount to cost transaction.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {string} transactionType - Transaction type.
|
||||
* @param {number} transactionId - Transaction id.
|
||||
* @param {number} amount - Amount
|
||||
*/
|
||||
private revertLandedCostAmount = (
|
||||
tenantId: number,
|
||||
transactionType: string,
|
||||
transactionId: number,
|
||||
amount: number
|
||||
) => {
|
||||
const Model = this.transactionLandedCost.getModel(tenantId, transactionType);
|
||||
|
||||
// Decrement the allocate cost amount of cost transaction.
|
||||
return Model.query()
|
||||
.where('id', transactionId)
|
||||
.decrement('allocatedCostAmount', amount);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the cost transaction or throw not found error.
|
||||
* @param {number} tenantId
|
||||
* @param {transactionType} transactionType -
|
||||
* @param {transactionId} transactionId -
|
||||
*/
|
||||
public getLandedCostOrThrowError = async (
|
||||
tenantId: number,
|
||||
transactionType: string,
|
||||
transactionId: number
|
||||
) => {
|
||||
const Model = this.transactionLandedCost.getModel(
|
||||
tenantId,
|
||||
transactionType
|
||||
);
|
||||
const model = await Model.query().findById(transactionId);
|
||||
|
||||
if (!model) {
|
||||
throw new ServiceError(ERRORS.LANDED_COST_TRANSACTION_NOT_FOUND);
|
||||
}
|
||||
return this.transactionLandedCost.transformToLandedCost(
|
||||
transactionType,
|
||||
model
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the landed cost entries.
|
||||
* @param {number} tenantId
|
||||
* @param {string} transactionType
|
||||
* @param {number} transactionId
|
||||
* @returns
|
||||
*/
|
||||
public getLandedCostEntry = async (
|
||||
tenantId: number,
|
||||
transactionType: string,
|
||||
transactionId: number,
|
||||
transactionEntryId: number
|
||||
): Promise<any> => {
|
||||
const Model = this.transactionLandedCost.getModel(
|
||||
tenantId,
|
||||
transactionType
|
||||
);
|
||||
const relation = CONFIG.COST_TYPES[transactionType].entries;
|
||||
|
||||
const entry = await Model.relatedQuery(relation)
|
||||
.for(transactionId)
|
||||
.findOne('id', transactionEntryId)
|
||||
.where('landedCost', true);
|
||||
|
||||
if (!entry) {
|
||||
throw new ServiceError(ERRORS.LANDED_COST_ENTRY_NOT_FOUND);
|
||||
}
|
||||
return entry;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve allocate items cost total.
|
||||
* @param {ILandedCostDTO} landedCostDTO
|
||||
* @returns {number}
|
||||
*/
|
||||
private getAllocateItemsCostTotal = (
|
||||
landedCostDTO: ILandedCostDTO
|
||||
): number => {
|
||||
return sumBy(landedCostDTO.items, 'cost');
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate allocate cost transaction should not be bill transaction.
|
||||
* @param {number} purchaseInvoiceId
|
||||
* @param {string} transactionType
|
||||
* @param {number} transactionId
|
||||
*/
|
||||
private validateAllocateCostNotSameBill = (
|
||||
purchaseInvoiceId: number,
|
||||
transactionType: string,
|
||||
transactionId: number
|
||||
): void => {
|
||||
if (transactionType === 'Bill' && transactionId === purchaseInvoiceId) {
|
||||
throw new ServiceError(ERRORS.ALLOCATE_COST_SHOULD_NOT_BE_BILL);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates the landed cost entry amount.
|
||||
* @param {number} unallocatedCost -
|
||||
* @param {number} amount -
|
||||
*/
|
||||
private validateLandedCostEntryAmount = (
|
||||
unallocatedCost: number,
|
||||
amount: number
|
||||
): void => {
|
||||
console.log(unallocatedCost, amount, '123');
|
||||
|
||||
if (unallocatedCost < amount) {
|
||||
throw new ServiceError(ERRORS.COST_AMOUNT_BIGGER_THAN_UNALLOCATED_AMOUNT);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Records inventory transactions.
|
||||
* @param {number} tenantId
|
||||
* @param {} allocateEntries
|
||||
*/
|
||||
private recordInventoryTransactions = async (
|
||||
tenantId: number,
|
||||
allocateEntries,
|
||||
purchaseInvoice: IBill,
|
||||
landedCostId: number
|
||||
) => {
|
||||
const costEntries = mergeObjectsBykey(
|
||||
purchaseInvoice.entries,
|
||||
allocateEntries.map((e) => ({ ...e, id: e.itemId })),
|
||||
'id'
|
||||
);
|
||||
// Inventory transaction.
|
||||
const inventoryTransactions = costEntries.map((entry) => ({
|
||||
date: purchaseInvoice.billDate,
|
||||
itemId: entry.itemId,
|
||||
direction: 'IN',
|
||||
quantity: 0,
|
||||
rate: entry.cost,
|
||||
transactionType: 'LandedCost',
|
||||
transactionId: landedCostId,
|
||||
entryId: entry.id,
|
||||
}));
|
||||
|
||||
return this.inventoryService.recordInventoryTransactions(
|
||||
tenantId,
|
||||
inventoryTransactions
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* =================================
|
||||
* Allocate landed cost.
|
||||
* =================================
|
||||
* - Validates the allocate cost not the same purchase invoice id.
|
||||
* - Get the given bill (purchase invoice) or throw not found error.
|
||||
* - Get the given landed cost transaction or throw not found error.
|
||||
* - Validate landed cost transaction has enough unallocated cost amount.
|
||||
* - Validate landed cost transaction entry has enough unallocated cost amount.
|
||||
* - Validate allocate entries existance and associated with cost bill transaction.
|
||||
* - Writes inventory landed cost transaction.
|
||||
* - Increment the allocated landed cost transaction.
|
||||
* - Increment the allocated landed cost transaction entry.
|
||||
*
|
||||
* @param {ILandedCostDTO} landedCostDTO - Landed cost DTO.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {number} purchaseInvoiceId - Purchase invoice id.
|
||||
*/
|
||||
public allocateLandedCost = async (
|
||||
tenantId: number,
|
||||
allocateCostDTO: ILandedCostDTO,
|
||||
purchaseInvoiceId: number
|
||||
): Promise<{
|
||||
billLandedCost: IBillLandedCost;
|
||||
}> => {
|
||||
// Retrieve total cost of allocated items.
|
||||
const amount = this.getAllocateItemsCostTotal(allocateCostDTO);
|
||||
|
||||
// Retrieve the purchase invoice or throw not found error.
|
||||
const purchaseInvoice = await this.billsService.getBillOrThrowError(
|
||||
tenantId,
|
||||
purchaseInvoiceId
|
||||
);
|
||||
// Retrieve landed cost transaction or throw not found service error.
|
||||
const landedCostTransaction = await this.getLandedCostOrThrowError(
|
||||
tenantId,
|
||||
allocateCostDTO.transactionType,
|
||||
allocateCostDTO.transactionId
|
||||
);
|
||||
// Retrieve landed cost transaction entries.
|
||||
const landedCostEntry = await this.getLandedCostEntry(
|
||||
tenantId,
|
||||
allocateCostDTO.transactionType,
|
||||
allocateCostDTO.transactionId,
|
||||
allocateCostDTO.transactionEntryId
|
||||
);
|
||||
// Validates allocate cost items association with the purchase invoice entries.
|
||||
this.validateAllocateCostItems(
|
||||
purchaseInvoice.entries,
|
||||
allocateCostDTO.items
|
||||
);
|
||||
// Validate the amount of cost with unallocated landed cost.
|
||||
this.validateLandedCostEntryAmount(
|
||||
landedCostEntry.unallocatedLandedCost,
|
||||
amount
|
||||
);
|
||||
// Save the bill landed cost model.
|
||||
const billLandedCost = await this.saveBillLandedCostModel(
|
||||
tenantId,
|
||||
allocateCostDTO,
|
||||
purchaseInvoiceId
|
||||
);
|
||||
// Records the inventory transactions.
|
||||
// await this.recordInventoryTransactions(
|
||||
// tenantId,
|
||||
// allocateCostDTO.items,
|
||||
// purchaseInvoice,
|
||||
// landedCostTransaction.id
|
||||
// );
|
||||
// Increment landed cost amount on transaction and entry.
|
||||
await this.incrementLandedCostAmount(
|
||||
tenantId,
|
||||
allocateCostDTO.transactionType,
|
||||
allocateCostDTO.transactionId,
|
||||
allocateCostDTO.transactionEntryId,
|
||||
amount
|
||||
);
|
||||
// Write the landed cost journal entries.
|
||||
// await this.writeJournalEntry(tenantId, purchaseInvoice, billLandedCost);
|
||||
|
||||
return { billLandedCost };
|
||||
};
|
||||
|
||||
/**
|
||||
* Write journal entries of the given purchase invoice landed cost.
|
||||
* @param tenantId
|
||||
* @param purchaseInvoice
|
||||
* @param landedCost
|
||||
*/
|
||||
private writeJournalEntry = async (
|
||||
tenantId: number,
|
||||
purchaseInvoice: IBill,
|
||||
landedCost: IBillLandedCost
|
||||
) => {
|
||||
const journal = new JournalPoster(tenantId);
|
||||
const billEntriesById = purchaseInvoice.entries;
|
||||
|
||||
const commonEntry = {
|
||||
referenceType: 'Bill',
|
||||
referenceId: purchaseInvoice.id,
|
||||
date: purchaseInvoice.billDate,
|
||||
indexGroup: 300,
|
||||
};
|
||||
const costEntry = new JournalEntry({
|
||||
...commonEntry,
|
||||
credit: landedCost.amount,
|
||||
account: landedCost.costAccountId,
|
||||
index: 1,
|
||||
});
|
||||
journal.credit(costEntry);
|
||||
|
||||
landedCost.allocateEntries.forEach((entry, index) => {
|
||||
const billEntry = billEntriesById[entry.entryId];
|
||||
|
||||
const inventoryEntry = new JournalEntry({
|
||||
...commonEntry,
|
||||
debit: entry.cost,
|
||||
account: billEntry.item.inventoryAccountId,
|
||||
index: 1 + index,
|
||||
});
|
||||
journal.debit(inventoryEntry);
|
||||
});
|
||||
return journal;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the give bill landed cost or throw not found service error.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {number} landedCostId - Landed cost id.
|
||||
* @returns {Promise<IBillLandedCost>}
|
||||
*/
|
||||
public getBillLandedCostOrThrowError = async (
|
||||
tenantId: number,
|
||||
landedCostId: number
|
||||
): Promise<IBillLandedCost> => {
|
||||
const { BillLandedCost } = this.tenancy.models(tenantId);
|
||||
|
||||
// Retrieve the bill landed cost model.
|
||||
const billLandedCost = await BillLandedCost.query().findById(landedCostId);
|
||||
|
||||
if (!billLandedCost) {
|
||||
throw new ServiceError(ERRORS.BILL_LANDED_COST_NOT_FOUND);
|
||||
}
|
||||
return billLandedCost;
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes the landed cost transaction with assocaited allocate entries.
|
||||
* @param {number} tenantId
|
||||
* @param {number} landedCostId
|
||||
*/
|
||||
public deleteLandedCost = async (
|
||||
tenantId: number,
|
||||
landedCostId: number
|
||||
): Promise<void> => {
|
||||
const { BillLandedCost, BillLandedCostEntry } =
|
||||
this.tenancy.models(tenantId);
|
||||
|
||||
// Deletes the bill landed cost allocated entries associated to landed cost.
|
||||
await BillLandedCostEntry.query()
|
||||
.where('bill_located_cost_id', landedCostId)
|
||||
.delete();
|
||||
|
||||
// Delete the bill landed cost from the storage.
|
||||
await BillLandedCost.query().where('id', landedCostId).delete();
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes the allocated landed cost.
|
||||
* ==================================
|
||||
* - Delete bill landed cost transaction with associated allocate entries.
|
||||
* - Delete the associated inventory transactions.
|
||||
* - Decrement allocated amount of landed cost transaction and entry.
|
||||
* - Revert journal entries.
|
||||
*
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {number} landedCostId - Landed cost id.
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public deleteAllocatedLandedCost = async (
|
||||
tenantId: number,
|
||||
landedCostId: number
|
||||
): Promise<{
|
||||
landedCostId: number;
|
||||
}> => {
|
||||
// Retrieves the bill landed cost.
|
||||
const oldBillLandedCost = await this.getBillLandedCostOrThrowError(
|
||||
tenantId,
|
||||
landedCostId
|
||||
);
|
||||
// Delete landed cost transaction with assocaited locate entries.
|
||||
await this.deleteLandedCost(tenantId, landedCostId);
|
||||
|
||||
// Removes the inventory transactions.
|
||||
await this.removeInventoryTransactions(tenantId, landedCostId);
|
||||
|
||||
// Reverts the landed cost amount to the cost transaction.
|
||||
await this.revertLandedCostAmount(
|
||||
tenantId,
|
||||
oldBillLandedCost.fromTransactionType,
|
||||
oldBillLandedCost.fromTransactionId,
|
||||
oldBillLandedCost.amount
|
||||
);
|
||||
return { landedCostId };
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes the inventory transaction.
|
||||
* @param {number} tenantId
|
||||
* @param {number} landedCostId
|
||||
* @returns
|
||||
*/
|
||||
private removeInventoryTransactions = (tenantId, landedCostId: number) => {
|
||||
return this.inventoryService.deleteInventoryTransactions(
|
||||
tenantId,
|
||||
landedCostId,
|
||||
'LandedCost'
|
||||
);
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user