erp_be/src/modules/assets/assets.depreciation.js

204 lines
7.1 KiB
JavaScript

const round4 = (value) => Math.round(Number(value || 0) * 10000) / 10000;
const DEPRECIATION_METHOD_OPTIONS = [
{
value: 'SLM',
label: 'Straight Line Method (SLM)',
description:
'Equal depreciation each year on original cost. Rate auto-calculated from useful life when not provided.',
},
{
value: 'WDV',
label: 'Written Down Value (WDV)',
description:
'Depreciation on reducing book value. Rate auto-calculated from cost, salvage and useful life when not provided.',
},
{
value: 'CUSTOM',
label: 'Custom',
description: 'Custom method. Depreciation rate must be entered manually.',
},
];
/** Map legacy OTHER → CUSTOM for reads/writes. */
const normalizeDepreciationMethod = (method) => {
if (method === 'OTHER') return 'CUSTOM';
return method || null;
};
/**
* Resolve salvage amount and % from either input.
* - If only salvage_percentage → derive salvage_value from purchase_cost
* - If only salvage_value → derive salvage_percentage
* - If both → prefer salvage_percentage and recompute salvage_value
* - If neither → both 0
*/
const resolveSalvageFields = ({
purchase_cost: cost,
salvage_value: salvageAmount,
salvage_percentage: salvagePct,
} = {}) => {
const purchaseCost = Number(cost || 0);
const hasPct = salvagePct !== undefined && salvagePct !== null && salvagePct !== '';
const hasAmount =
salvageAmount !== undefined && salvageAmount !== null && salvageAmount !== '';
let salvagePercentage = 0;
let salvageValue = 0;
if (hasPct && !hasAmount) {
salvagePercentage = round4(Math.min(Math.max(Number(salvagePct), 0), 100));
salvageValue = purchaseCost > 0 ? round4((purchaseCost * salvagePercentage) / 100) : 0;
} else if (hasAmount && !hasPct) {
salvageValue = round4(Math.max(Number(salvageAmount), 0));
salvagePercentage =
purchaseCost > 0 ? round4(Math.min((salvageValue / purchaseCost) * 100, 100)) : 0;
} else if (hasPct && hasAmount) {
// Both sent: percentage is source of truth (FE amount/% toggle)
salvagePercentage = round4(Math.min(Math.max(Number(salvagePct), 0), 100));
salvageValue = purchaseCost > 0 ? round4((purchaseCost * salvagePercentage) / 100) : 0;
}
return {
salvage_value: salvageValue,
salvage_percentage: salvagePercentage,
};
};
const yearsElapsed = (purchaseDate, asOfDate = new Date()) => {
if (!purchaseDate) return 0;
const start = new Date(purchaseDate);
const end = new Date(asOfDate);
if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime()) || end < start) return 0;
const ms = end.getTime() - start.getTime();
return round4(ms / (365.25 * 24 * 60 * 60 * 1000));
};
const resolveDepreciationRate = ({
method,
depreciation_rate: rate,
purchase_cost: cost,
salvage_value: salvage,
useful_life_years: lifeYears,
}) => {
if (rate !== undefined && rate !== null && rate !== '') {
return round4(rate);
}
const purchaseCost = Number(cost || 0);
const salvageValue = Number(salvage || 0);
const years = Number(lifeYears || 0);
const normalizedMethod = normalizeDepreciationMethod(method);
if (!normalizedMethod || normalizedMethod === 'CUSTOM' || years <= 0) return null;
if (normalizedMethod === 'SLM') {
if (purchaseCost <= 0) return round4(100 / years);
const depreciable = Math.max(purchaseCost - salvageValue, 0);
return round4((depreciable / purchaseCost / years) * 100);
}
if (normalizedMethod === 'WDV') {
if (purchaseCost <= 0 || salvageValue <= 0 || salvageValue >= purchaseCost) return null;
return round4((1 - (salvageValue / purchaseCost) ** (1 / years)) * 100);
}
return null;
};
const calculateDepreciation = ({
depreciation_method: method,
depreciation_rate: rateInput,
purchase_cost: cost,
salvage_value: salvage,
salvage_percentage: salvagePct,
useful_life_years: lifeYears,
commencement_date: commencementDate,
purchase_date: purchaseDate,
as_of_date: asOfDate,
} = {}) => {
const purchaseCost = Number(cost || 0);
const { salvage_value: salvageValue, salvage_percentage: salvagePercentage } =
resolveSalvageFields({
purchase_cost: purchaseCost,
salvage_value: salvage,
salvage_percentage: salvagePct,
});
const normalizedMethod = normalizeDepreciationMethod(method);
const rate = resolveDepreciationRate({
method: normalizedMethod,
depreciation_rate: rateInput,
purchase_cost: purchaseCost,
salvage_value: salvageValue,
useful_life_years: lifeYears,
});
// Depreciation starts from the asset's usage start date (commencement_date).
// Fall back to purchase_date for assets created before this field existed.
const depreciationStartDate = commencementDate || purchaseDate;
const elapsed = yearsElapsed(depreciationStartDate, asOfDate || new Date());
const cappedYears =
lifeYears && Number(lifeYears) > 0 ? Math.min(elapsed, Number(lifeYears)) : elapsed;
let annualDepreciation = 0;
let accumulatedDepreciation = 0;
let bookValue = purchaseCost;
if (normalizedMethod === 'SLM' && rate !== null) {
annualDepreciation = round4((purchaseCost * rate) / 100);
const maxDepreciable = Math.max(purchaseCost - salvageValue, 0);
accumulatedDepreciation = round4(Math.min(annualDepreciation * cappedYears, maxDepreciable));
bookValue = round4(Math.max(purchaseCost - accumulatedDepreciation, salvageValue));
} else if (normalizedMethod === 'WDV' && rate !== null) {
annualDepreciation = round4((purchaseCost * rate) / 100);
bookValue = purchaseCost;
const fullYears = Math.floor(cappedYears);
const fraction = cappedYears - fullYears;
for (let i = 0; i < fullYears; i += 1) {
const yearDep = round4((bookValue * rate) / 100);
bookValue = round4(Math.max(bookValue - yearDep, salvageValue));
accumulatedDepreciation = round4(purchaseCost - bookValue);
if (bookValue <= salvageValue) break;
}
if (fraction > 0 && bookValue > salvageValue) {
const yearDep = round4(((bookValue * rate) / 100) * fraction);
bookValue = round4(Math.max(bookValue - yearDep, salvageValue));
accumulatedDepreciation = round4(purchaseCost - bookValue);
}
annualDepreciation = round4((bookValue * rate) / 100);
} else if (normalizedMethod === 'CUSTOM' && rate !== null) {
annualDepreciation = round4((purchaseCost * rate) / 100);
const maxDepreciable = Math.max(purchaseCost - salvageValue, 0);
accumulatedDepreciation = round4(Math.min(annualDepreciation * cappedYears, maxDepreciable));
bookValue = round4(Math.max(purchaseCost - accumulatedDepreciation, salvageValue));
}
return {
depreciation_method: normalizedMethod,
depreciation_rate: rate,
annual_depreciation: annualDepreciation,
accumulated_depreciation: accumulatedDepreciation,
book_value: bookValue,
years_elapsed: elapsed,
purchase_cost: purchaseCost,
salvage_value: salvageValue,
salvage_percentage: salvagePercentage,
useful_life_years: lifeYears ?? null,
depreciation_start_date: depreciationStartDate || null,
};
};
module.exports = {
DEPRECIATION_METHOD_OPTIONS,
resolveDepreciationRate,
resolveSalvageFields,
normalizeDepreciationMethod,
calculateDepreciation,
yearsElapsed,
round4,
};