mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-15 20:30:33 +00:00
423 lines
12 KiB
TypeScript
423 lines
12 KiB
TypeScript
import * as R from 'ramda';
|
|
import { defaultTo, sumBy, get } from 'lodash';
|
|
import moment from 'moment';
|
|
import {
|
|
IInventoryDetailsQuery,
|
|
IItem,
|
|
IInventoryTransaction,
|
|
TInventoryTransactionDirection,
|
|
IInventoryDetailsNumber,
|
|
IInventoryDetailsDate,
|
|
IInventoryDetailsData,
|
|
IInventoryDetailsItem,
|
|
IInventoryDetailsClosing,
|
|
INumberFormatQuery,
|
|
IInventoryDetailsOpening,
|
|
IInventoryDetailsItemTransaction,
|
|
IFormatNumberSettings,
|
|
} from 'interfaces';
|
|
import FinancialSheet from '../FinancialSheet';
|
|
import { transformToMapBy, transformToMapKeyValue } from 'utils';
|
|
import { filterDeep } from 'utils/deepdash';
|
|
|
|
const MAP_CONFIG = { childrenPath: 'children', pathFormat: 'array' };
|
|
|
|
enum INodeTypes {
|
|
ITEM = 'item',
|
|
TRANSACTION = 'transaction',
|
|
OPENING_ENTRY = 'OPENING_ENTRY',
|
|
CLOSING_ENTRY = 'CLOSING_ENTRY',
|
|
}
|
|
|
|
export default class InventoryDetails extends FinancialSheet {
|
|
readonly inventoryTransactionsByItemId: Map<number, IInventoryTransaction[]>;
|
|
readonly openingBalanceTransactions: Map<number, IInventoryTransaction>;
|
|
readonly query: IInventoryDetailsQuery;
|
|
readonly numberFormat: INumberFormatQuery;
|
|
readonly baseCurrency: string;
|
|
readonly items: IItem[];
|
|
|
|
/**
|
|
* Constructor method.
|
|
* @param {IItem[]} items - Items.
|
|
* @param {IInventoryTransaction[]} inventoryTransactions - Inventory transactions.
|
|
* @param {IInventoryDetailsQuery} query - Report query.
|
|
* @param {string} baseCurrency - The base currency.
|
|
*/
|
|
constructor(
|
|
items: IItem[],
|
|
openingBalanceTransactions: IInventoryTransaction[],
|
|
inventoryTransactions: IInventoryTransaction[],
|
|
query: IInventoryDetailsQuery,
|
|
baseCurrency: string
|
|
) {
|
|
super();
|
|
|
|
this.inventoryTransactionsByItemId = transformToMapBy(
|
|
inventoryTransactions,
|
|
'itemId'
|
|
);
|
|
this.openingBalanceTransactions = transformToMapKeyValue(
|
|
openingBalanceTransactions,
|
|
'itemId'
|
|
);
|
|
this.query = query;
|
|
this.numberFormat = this.query.numberFormat;
|
|
this.items = items;
|
|
this.baseCurrency = baseCurrency;
|
|
}
|
|
|
|
/**
|
|
* Retrieve the number meta.
|
|
* @param {number} number
|
|
* @returns
|
|
*/
|
|
private getNumberMeta(
|
|
number: number,
|
|
settings?: IFormatNumberSettings
|
|
): IInventoryDetailsNumber {
|
|
return {
|
|
formattedNumber: this.formatNumber(number, {
|
|
excerptZero: true,
|
|
money: false,
|
|
...settings,
|
|
}),
|
|
number: number,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Retrieve the total number meta.
|
|
* @param {number} number -
|
|
* @param {IFormatNumberSettings} settings -
|
|
* @retrun {IInventoryDetailsNumber}
|
|
*/
|
|
private getTotalNumberMeta(
|
|
number: number,
|
|
settings?: IFormatNumberSettings
|
|
): IInventoryDetailsNumber {
|
|
return this.getNumberMeta(number, { excerptZero: false, ...settings });
|
|
}
|
|
|
|
/**
|
|
* Retrieve the date meta.
|
|
* @param {Date|string} date
|
|
* @returns {IInventoryDetailsDate}
|
|
*/
|
|
private getDateMeta(date: Date | string): IInventoryDetailsDate {
|
|
return {
|
|
formattedDate: moment(date).format('YYYY-MM-DD'),
|
|
date: moment(date).toDate(),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Adjusts the movement amount.
|
|
* @param {number} amount
|
|
* @param {TInventoryTransactionDirection} direction
|
|
* @returns {number}
|
|
*/
|
|
private adjustAmountMovement(
|
|
direction: TInventoryTransactionDirection,
|
|
amount: number
|
|
): number {
|
|
return direction === 'OUT' ? amount * -1 : amount;
|
|
}
|
|
|
|
/**
|
|
* Accumlate and mapping running quantity on transactions.
|
|
* @param {IInventoryDetailsItemTransaction[]} transactions
|
|
* @returns {IInventoryDetailsItemTransaction[]}
|
|
*/
|
|
private mapAccumTransactionsRunningQuantity(
|
|
transactions: IInventoryDetailsItemTransaction[]
|
|
): IInventoryDetailsItemTransaction[] {
|
|
const initial = this.getNumberMeta(0);
|
|
|
|
const mapAccumAppender = (a, b) => {
|
|
const total = a.runningQuantity.number + b.quantityMovement.number;
|
|
const totalMeta = this.getNumberMeta(total, { excerptZero: false });
|
|
const accum = { ...b, runningQuantity: totalMeta };
|
|
|
|
return [accum, accum];
|
|
};
|
|
return R.mapAccum(
|
|
mapAccumAppender,
|
|
{ runningQuantity: initial },
|
|
transactions
|
|
)[1];
|
|
}
|
|
|
|
/**
|
|
* Accumlate and mapping running valuation on transactions.
|
|
* @param {IInventoryDetailsItemTransaction[]} transactions
|
|
* @returns {IInventoryDetailsItemTransaction}
|
|
*/
|
|
private mapAccumTransactionsRunningValuation(
|
|
transactions: IInventoryDetailsItemTransaction[]
|
|
): IInventoryDetailsItemTransaction[] {
|
|
const initial = this.getNumberMeta(0);
|
|
|
|
const mapAccumAppender = (a, b) => {
|
|
const adjusmtent = b.direction === 'OUT' ? -1 : 1;
|
|
const total = a.runningValuation.number + b.cost.number * adjusmtent;
|
|
const totalMeta = this.getNumberMeta(total, { excerptZero: false });
|
|
const accum = { ...b, runningValuation: totalMeta };
|
|
|
|
return [accum, accum];
|
|
};
|
|
return R.mapAccum(
|
|
mapAccumAppender,
|
|
{ runningValuation: initial },
|
|
transactions
|
|
)[1];
|
|
}
|
|
|
|
/**
|
|
* Mappes the item transaction to inventory item transaction node.
|
|
* @param {IItem} item
|
|
* @param {IInvetoryTransaction} transaction
|
|
* @returns {IInventoryDetailsItemTransaction}
|
|
*/
|
|
private itemTransactionMapper(
|
|
item: IItem,
|
|
transaction: IInventoryTransaction
|
|
): IInventoryDetailsItemTransaction {
|
|
const total = transaction.quantity * transaction.rate;
|
|
const amountMovement = R.curry(this.adjustAmountMovement)(
|
|
transaction.direction
|
|
);
|
|
// Quantity movement.
|
|
const quantityMovement = amountMovement(transaction.quantity);
|
|
const cost = defaultTo(transaction?.costLotAggregated.cost, 0);
|
|
|
|
// Profit margin.
|
|
const profitMargin = total - cost;
|
|
|
|
// Value from computed cost in `OUT` or from total sell price in `IN` transaction.
|
|
const value = transaction.direction === 'OUT' ? cost : total;
|
|
|
|
// Value movement depends on transaction direction.
|
|
const valueMovement = amountMovement(value);
|
|
|
|
return {
|
|
nodeType: INodeTypes.TRANSACTION,
|
|
date: this.getDateMeta(transaction.date),
|
|
transactionType: transaction.transcationTypeFormatted,
|
|
transactionNumber: transaction.meta.transactionNumber,
|
|
direction: transaction.direction,
|
|
|
|
quantityMovement: this.getNumberMeta(quantityMovement),
|
|
valueMovement: this.getNumberMeta(valueMovement),
|
|
|
|
quantity: this.getNumberMeta(transaction.quantity),
|
|
total: this.getNumberMeta(total),
|
|
|
|
rate: this.getNumberMeta(transaction.rate),
|
|
cost: this.getNumberMeta(transaction.costLotAggregated.cost),
|
|
value: this.getNumberMeta(value),
|
|
|
|
profitMargin: this.getNumberMeta(profitMargin),
|
|
|
|
runningQuantity: this.getNumberMeta(0),
|
|
runningValuation: this.getNumberMeta(0),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Retrieve the inventory transcations by item id.
|
|
* @param {number} itemId
|
|
* @returns {IInventoryTransaction[]}
|
|
*/
|
|
private getInventoryTransactionsByItemId(
|
|
itemId: number
|
|
): IInventoryTransaction[] {
|
|
return defaultTo(this.inventoryTransactionsByItemId.get(itemId + ''), []);
|
|
}
|
|
|
|
/**
|
|
* Retrieve the item transaction node by the given item.
|
|
* @param {IItem} item
|
|
* @returns {IInventoryDetailsItemTransaction[]}
|
|
*/
|
|
private getItemTransactions(item: IItem): IInventoryDetailsItemTransaction[] {
|
|
const transactions = this.getInventoryTransactionsByItemId(item.id);
|
|
|
|
return R.compose(
|
|
this.mapAccumTransactionsRunningQuantity.bind(this),
|
|
this.mapAccumTransactionsRunningValuation.bind(this),
|
|
R.map(R.curry(this.itemTransactionMapper.bind(this))(item))
|
|
)(transactions);
|
|
}
|
|
|
|
/**
|
|
* Mappes the given item transactions.
|
|
* @param {IItem} item -
|
|
* @returns {(
|
|
* IInventoryDetailsItemTransaction
|
|
* | IInventoryDetailsOpening
|
|
* | IInventoryDetailsClosing
|
|
* )[]}
|
|
*/
|
|
private itemTransactionsMapper(
|
|
item: IItem
|
|
): (
|
|
| IInventoryDetailsItemTransaction
|
|
| IInventoryDetailsOpening
|
|
| IInventoryDetailsClosing
|
|
)[] {
|
|
const transactions = this.getItemTransactions(item);
|
|
const openingValuation = this.getItemOpeingValuation(item);
|
|
const closingValuation = this.getItemClosingValuation(
|
|
item,
|
|
transactions,
|
|
openingValuation
|
|
);
|
|
const hasTransactions = transactions.length > 0;
|
|
const isItemHasOpeningBalance = this.isItemHasOpeningBalance(item.id);
|
|
|
|
return R.pipe(
|
|
R.concat(transactions),
|
|
R.when(R.always(isItemHasOpeningBalance), R.prepend(openingValuation)),
|
|
R.when(R.always(hasTransactions), R.append(closingValuation))
|
|
)([]);
|
|
}
|
|
|
|
/**
|
|
* Detarmines the given item has opening balance transaction.
|
|
* @param {number} itemId - Item id.
|
|
* @return {boolean}
|
|
*/
|
|
private isItemHasOpeningBalance(itemId: number): boolean {
|
|
return !!this.openingBalanceTransactions.get(itemId);
|
|
}
|
|
|
|
/**
|
|
* Retrieve the given item opening valuation.
|
|
* @param {IItem} item -
|
|
* @returns {IInventoryDetailsOpening}
|
|
*/
|
|
private getItemOpeingValuation(item: IItem): IInventoryDetailsOpening {
|
|
const openingBalance = this.openingBalanceTransactions.get(item.id);
|
|
const quantity = defaultTo(get(openingBalance, 'quantity'), 0);
|
|
const value = defaultTo(get(openingBalance, 'value'), 0);
|
|
|
|
return {
|
|
nodeType: INodeTypes.OPENING_ENTRY,
|
|
date: this.getDateMeta(this.query.fromDate),
|
|
quantity: this.getTotalNumberMeta(quantity),
|
|
value: this.getTotalNumberMeta(value),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Retrieve the given item closing valuation.
|
|
* @param {IItem} item -
|
|
* @returns {IInventoryDetailsOpening}
|
|
*/
|
|
private getItemClosingValuation(
|
|
item: IItem,
|
|
transactions: IInventoryDetailsItemTransaction[],
|
|
openingValuation: IInventoryDetailsOpening
|
|
): IInventoryDetailsOpening {
|
|
const value = sumBy(transactions, 'valueMovement.number');
|
|
const quantity = sumBy(transactions, 'quantityMovement.number');
|
|
const profitMargin = sumBy(transactions, 'profitMargin.number');
|
|
|
|
const closingQuantity = quantity + openingValuation.quantity.number;
|
|
const closingValue = value + openingValuation.value.number;
|
|
|
|
return {
|
|
nodeType: INodeTypes.CLOSING_ENTRY,
|
|
date: this.getDateMeta(this.query.toDate),
|
|
quantity: this.getTotalNumberMeta(closingQuantity),
|
|
value: this.getTotalNumberMeta(closingValue),
|
|
profitMargin: this.getTotalNumberMeta(profitMargin),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Retrieve the item node of the report.
|
|
* @param {IItem} item
|
|
* @returns {IInventoryDetailsItem}
|
|
*/
|
|
private itemsNodeMapper(item: IItem): IInventoryDetailsItem {
|
|
return {
|
|
id: item.id,
|
|
name: item.name,
|
|
code: item.code,
|
|
nodeType: INodeTypes.ITEM,
|
|
children: this.itemTransactionsMapper(item),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Detarmines the given node equals the given type.
|
|
* @param {string} nodeType
|
|
* @param {IItem} node
|
|
* @returns {boolean}
|
|
*/
|
|
private isNodeTypeEquals(
|
|
nodeType: string,
|
|
node: IInventoryDetailsItem
|
|
): boolean {
|
|
return nodeType === node.nodeType;
|
|
}
|
|
|
|
/**
|
|
* Detarmines whether the given item node has transactions.
|
|
* @param {IInventoryDetailsItem} item
|
|
* @returns {boolean}
|
|
*/
|
|
private isItemNodeHasTransactions(item: IInventoryDetailsItem) {
|
|
return !!this.inventoryTransactionsByItemId.get(item.id);
|
|
}
|
|
|
|
/**
|
|
* Detarmines the filter
|
|
* @param {IInventoryDetailsItem} item
|
|
* @return {boolean}
|
|
*/
|
|
private isFilterNode(item: IInventoryDetailsItem): boolean {
|
|
return R.ifElse(
|
|
R.curry(this.isNodeTypeEquals)(INodeTypes.ITEM),
|
|
this.isItemNodeHasTransactions.bind(this),
|
|
R.always(true)
|
|
)(item);
|
|
}
|
|
|
|
/**
|
|
* Filters items nodes.
|
|
* @param {IInventoryDetailsItem[]} items -
|
|
* @returns {IInventoryDetailsItem[]}
|
|
*/
|
|
private filterItemsNodes(items: IInventoryDetailsItem[]) {
|
|
const filtered = filterDeep(
|
|
items,
|
|
this.isFilterNode.bind(this),
|
|
MAP_CONFIG
|
|
);
|
|
return defaultTo(filtered, []);
|
|
}
|
|
|
|
/**
|
|
* Retrieve the items nodes of the report.
|
|
* @param {IItem} items
|
|
* @returns {IInventoryDetailsItem[]}
|
|
*/
|
|
private itemsNodes(items: IItem[]): IInventoryDetailsItem[] {
|
|
return R.compose(
|
|
this.filterItemsNodes.bind(this),
|
|
R.map(this.itemsNodeMapper.bind(this))
|
|
)(items);
|
|
}
|
|
|
|
/**
|
|
* Retrieve the inventory item details report data.
|
|
* @returns {IInventoryDetailsData}
|
|
*/
|
|
public reportData(): IInventoryDetailsData {
|
|
return this.itemsNodes(this.items);
|
|
}
|
|
}
|