mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-18 05:40:31 +00:00
feat: Inventory item details report.
feat: Cash flow statement report.
This commit is contained in:
@@ -0,0 +1,407 @@
|
||||
import * as R from 'ramda';
|
||||
import { defaultTo, sumBy, get } from 'lodash';
|
||||
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';
|
||||
import moment from 'moment';
|
||||
|
||||
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 total = a.runningValuation.number + b.valueMovement.number;
|
||||
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 value = transaction.quantity * transaction.rate;
|
||||
const amountMovement = R.curry(this.adjustAmountMovement)(
|
||||
transaction.direction
|
||||
);
|
||||
// Quantity movement.
|
||||
const quantityMovement = amountMovement(transaction.quantity);
|
||||
const valueMovement = amountMovement(value);
|
||||
|
||||
// Profit margin.
|
||||
const profitMargin = Math.max(
|
||||
value - transaction.costLotAggregated.cost,
|
||||
0
|
||||
);
|
||||
|
||||
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),
|
||||
value: this.getNumberMeta(value),
|
||||
|
||||
rate: this.getNumberMeta(transaction.rate),
|
||||
cost: this.getNumberMeta(transaction.costLotAggregated.cost),
|
||||
|
||||
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);
|
||||
|
||||
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[]
|
||||
): IInventoryDetailsOpening {
|
||||
const value = sumBy(transactions, 'valueMovement.number');
|
||||
const quantity = sumBy(transactions, 'quantityMovement.number');
|
||||
const profitMargin = sumBy(transactions, 'profitMargin.number');
|
||||
const cost = sumBy(transactions, 'cost.number');
|
||||
|
||||
return {
|
||||
nodeType: INodeTypes.CLOSING_ENTRY,
|
||||
date: this.getDateMeta(this.query.toDate),
|
||||
quantity: this.getTotalNumberMeta(quantity),
|
||||
value: this.getTotalNumberMeta(value),
|
||||
cost: this.getTotalNumberMeta(cost),
|
||||
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[]) {
|
||||
return filterDeep(items, this.isFilterNode.bind(this), MAP_CONFIG);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user