204 lines
7.1 KiB
JavaScript
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,
|
|
};
|