mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-14 11:50:31 +00:00
269 lines
6.8 KiB
TypeScript
269 lines
6.8 KiB
TypeScript
// @ts-nocheck
|
|
import React, { useCallback } from 'react';
|
|
import * as R from 'ramda';
|
|
import { sumBy, isEmpty, last, keyBy } from 'lodash';
|
|
|
|
import { useItem } from '@/hooks/query';
|
|
import {
|
|
toSafeNumber,
|
|
saveInvoke,
|
|
compose,
|
|
updateTableCell,
|
|
updateAutoAddNewLine,
|
|
orderingLinesIndexes,
|
|
updateTableRow,
|
|
} from '@/utils';
|
|
import { useItemEntriesTableContext } from './ItemEntriesTableProvider';
|
|
|
|
export const ITEM_TYPE = {
|
|
SELLABLE: 'SELLABLE',
|
|
PURCHASABLE: 'PURCHASABLE',
|
|
};
|
|
|
|
/**
|
|
* Retrieve item entry total from the given rate, quantity and discount.
|
|
* @param {number} rate
|
|
* @param {number} quantity
|
|
* @param {number} discount
|
|
* @return {number}
|
|
*/
|
|
export const calcItemEntryTotal = (discount, quantity, rate) => {
|
|
const _quantity = toSafeNumber(quantity);
|
|
const _rate = toSafeNumber(rate);
|
|
const _discount = toSafeNumber(discount);
|
|
|
|
return _quantity * _rate - (_quantity * _rate * _discount) / 100;
|
|
};
|
|
|
|
/**
|
|
* Updates the items entries total.
|
|
*/
|
|
export function updateItemsEntriesTotal(rows) {
|
|
return rows.map((row) => ({
|
|
...row,
|
|
amount: calcItemEntryTotal(row.discount, row.quantity, row.rate),
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Retrieve total of the given items entries.
|
|
*/
|
|
export function getEntriesTotal(entries) {
|
|
return sumBy(entries, 'amount');
|
|
}
|
|
|
|
/**
|
|
* Ensure the given entries have enough empty line on the last.
|
|
* @param {Object} defaultEntry - Default entry.
|
|
* @param {Array} entries - Entries.
|
|
* @return {Array}
|
|
*/
|
|
export const ensureEntriesHaveEmptyLine = R.curry((defaultEntry, entries) => {
|
|
const lastEntry = last(entries);
|
|
|
|
if (isEmpty(lastEntry.account_id) || isEmpty(lastEntry.amount)) {
|
|
return [...entries, defaultEntry];
|
|
}
|
|
return entries;
|
|
});
|
|
|
|
/**
|
|
* Disable landed cost checkbox once the item type is not service or non-inventorty.
|
|
* @returns {boolean}
|
|
*/
|
|
export const isLandedCostDisabled = (item) =>
|
|
['service', 'non-inventory'].indexOf(item.type) === -1;
|
|
|
|
/**
|
|
* Handle fetch item row details and retrieves the new table row.
|
|
*/
|
|
export function useFetchItemRow({ landedCost, itemType, notifyNewRow }) {
|
|
const [itemRow, setItemRow] = React.useState(null);
|
|
const [cellsLoading, setCellsLoading] = React.useState(null);
|
|
|
|
// Fetches the item details.
|
|
const {
|
|
data: item,
|
|
isFetching: isItemFetching,
|
|
isSuccess: isItemSuccess,
|
|
} = useItem(itemRow && itemRow.itemId, {
|
|
enabled: !!(itemRow && itemRow.itemId),
|
|
});
|
|
|
|
// Once the item start loading give the table cells loading state.
|
|
React.useEffect(() => {
|
|
if (itemRow && isItemFetching) {
|
|
setCellsLoading([
|
|
[itemRow.rowIndex, 'rate'],
|
|
[itemRow.rowIndex, 'description'],
|
|
[itemRow.rowIndex, 'quantity'],
|
|
[itemRow.rowIndex, 'discount'],
|
|
]);
|
|
} else {
|
|
setCellsLoading(null);
|
|
}
|
|
}, [isItemFetching, setCellsLoading, itemRow]);
|
|
|
|
// Once the item selected and fetched set the initial details to the table.
|
|
React.useEffect(() => {
|
|
if (isItemSuccess && item && itemRow) {
|
|
const { rowIndex } = itemRow;
|
|
const price =
|
|
itemType === ITEM_TYPE.PURCHASABLE ? item.cost_price : item.sell_price;
|
|
|
|
const description =
|
|
itemType === ITEM_TYPE.PURCHASABLE
|
|
? item.purchase_description
|
|
: item.sell_description;
|
|
|
|
// Detarmines whether the landed cost checkbox should be disabled.
|
|
const landedCostDisabled = isLandedCostDisabled(item);
|
|
|
|
// The new row.
|
|
const newRow = {
|
|
rate: price,
|
|
description,
|
|
quantity: 1,
|
|
...(landedCost
|
|
? {
|
|
landed_cost: false,
|
|
landed_cost_disabled: landedCostDisabled,
|
|
}
|
|
: {}),
|
|
};
|
|
setItemRow(null);
|
|
saveInvoke(notifyNewRow, newRow, rowIndex);
|
|
}
|
|
}, [item, itemRow, itemType, isItemSuccess, landedCost, notifyNewRow]);
|
|
|
|
return {
|
|
isItemFetching,
|
|
isItemSuccess,
|
|
item,
|
|
setItemRow,
|
|
itemRow,
|
|
cellsLoading,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Compose table rows when edit specific row index of table rows.
|
|
*/
|
|
export const composeRowsOnEditCell = R.curry(
|
|
(rowIndex, columnId, value, defaultEntry, rows) => {
|
|
return compose()(rows);
|
|
},
|
|
);
|
|
|
|
/**
|
|
* Compose table rows when insert a new row to table rows.
|
|
*/
|
|
export const composeRowsOnNewRow = R.curry((rowIndex, newRow, rows) => {
|
|
return compose(
|
|
orderingLinesIndexes,
|
|
updateItemsEntriesTotal,
|
|
updateTableRow(rowIndex, newRow),
|
|
)(rows);
|
|
});
|
|
|
|
/**
|
|
* Associate tax rate to entries.
|
|
*/
|
|
export const assignEntriesTaxRate = R.curry((taxRates, entries) => {
|
|
const taxRatesById = keyBy(taxRates, 'id');
|
|
|
|
return entries.map((entry) => {
|
|
const taxRate = taxRatesById[entry.tax_rate_id];
|
|
|
|
return {
|
|
...entry,
|
|
tax_rate: taxRate?.rate || 0,
|
|
};
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Assign tax amount to entries.
|
|
* @param {boolean} isInclusiveTax
|
|
* @param entries
|
|
* @returns
|
|
*/
|
|
export const assignEntriesTaxAmount = R.curry(
|
|
(isInclusiveTax: boolean, entries) => {
|
|
return entries.map((entry) => {
|
|
const taxAmount = isInclusiveTax
|
|
? getInclusiveTaxAmount(entry.amount, entry.tax_rate)
|
|
: getExlusiveTaxAmount(entry.amount, entry.tax_rate);
|
|
|
|
return {
|
|
...entry,
|
|
tax_amount: taxAmount,
|
|
};
|
|
});
|
|
},
|
|
);
|
|
|
|
/**
|
|
* Get inclusive tax amount.
|
|
* @param {number} amount
|
|
* @param {number} taxRate
|
|
* @returns {number}
|
|
*/
|
|
export const getInclusiveTaxAmount = (amount: number, taxRate: number) => {
|
|
return (amount * taxRate) / (100 + taxRate);
|
|
};
|
|
|
|
/**
|
|
* Get exclusive tax amount.
|
|
* @param {number} amount
|
|
* @param {number} taxRate
|
|
* @returns {number}
|
|
*/
|
|
export const getExlusiveTaxAmount = (amount: number, taxRate: number) => {
|
|
return (amount * taxRate) / 100;
|
|
};
|
|
|
|
/**
|
|
* Compose rows when edit a table cell.
|
|
* @returns {Function}
|
|
*/
|
|
export const useComposeRowsOnEditTableCell = () => {
|
|
const { taxRates, isInclusiveTax, localValue, defaultEntry } =
|
|
useItemEntriesTableContext();
|
|
|
|
return useCallback(
|
|
(rowIndex, columnId, value) => {
|
|
return R.compose(
|
|
assignEntriesTaxAmount(isInclusiveTax),
|
|
assignEntriesTaxRate(taxRates),
|
|
orderingLinesIndexes,
|
|
updateAutoAddNewLine(defaultEntry, ['item_id']),
|
|
updateItemsEntriesTotal,
|
|
updateTableCell(rowIndex, columnId, value),
|
|
)(localValue);
|
|
},
|
|
[taxRates, isInclusiveTax, localValue, defaultEntry],
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Compose rows when remove a table row.
|
|
* @returns {Function}
|
|
*/
|
|
export const useComposeRowsOnRemoveTableRow = () => {
|
|
const { minLinesNumber, defaultEntry, localValue } =
|
|
useItemEntriesTableContext();
|
|
|
|
return useCallback(
|
|
(rowIndex) => {
|
|
return compose(
|
|
// Ensure minimum lines count.
|
|
updateMinEntriesLines(minLinesNumber, defaultEntry),
|
|
// Remove the line by the given index.
|
|
updateRemoveLineByIndex(rowIndex),
|
|
)(localValue);
|
|
},
|
|
[minLinesNumber, defaultEntry, localValue],
|
|
);
|
|
};
|