Updated A LOT
This commit is contained in:
187
server/services/disposals.js
Normal file
187
server/services/disposals.js
Normal file
@@ -0,0 +1,187 @@
|
||||
const { all, audit, now, one, run, tx } = require('../db');
|
||||
const { annualSchedule, money } = require('../depreciation');
|
||||
const { ruleSetForBook } = require('./ruleSets');
|
||||
const { hydrateAsset } = require('./assets');
|
||||
|
||||
const DISPOSAL_METHODS = ['sale', 'exchange', 'involuntary_conversion', 'like_kind', 'abandonment'];
|
||||
|
||||
function yearOf(value) {
|
||||
if (!value) return new Date().getFullYear();
|
||||
return new Date(`${value}T00:00:00`).getFullYear();
|
||||
}
|
||||
|
||||
function netAdjustments(assetId, bookType) {
|
||||
// Impairments and write-downs reduce carrying value; write-ups/revaluations restore it.
|
||||
const rows = all('SELECT type, amount FROM asset_adjustments WHERE asset_id = ? AND book_type = ?', [assetId, bookType]);
|
||||
return rows.reduce((sum, row) => {
|
||||
const sign = row.type === 'write_up' || row.type === 'revaluation' ? -1 : 1;
|
||||
return sum + sign * Number(row.amount || 0);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function bookFor(asset, bookType) {
|
||||
return asset.books.find((book) => book.book_type === bookType) || asset.books[0] || { book_type: bookType };
|
||||
}
|
||||
|
||||
function computeDisposal(asset, input = {}) {
|
||||
const bookType = input.book_type || 'GAAP';
|
||||
const book = bookFor(asset, bookType);
|
||||
const rules = ruleSetForBook(bookType);
|
||||
const disposalDate = input.disposal_date || new Date().toISOString().slice(0, 10);
|
||||
const year = yearOf(disposalDate);
|
||||
|
||||
const totalCost = Number(book.cost ?? asset.acquisition_cost ?? 0);
|
||||
const partial = Boolean(input.partial);
|
||||
const disposedCost = partial ? Math.min(totalCost, Number(input.disposed_cost || 0)) : totalCost;
|
||||
const proportion = totalCost > 0 ? disposedCost / totalCost : 1;
|
||||
|
||||
const row = annualSchedule(asset, book, rules, { startYear: year, endYear: year })[0] || {};
|
||||
const accumulatedFull = Number(row.accumulated || 0);
|
||||
const nbvFull = row.netBookValue != null ? Number(row.netBookValue) : Math.max(0, totalCost - accumulatedFull);
|
||||
|
||||
const accumulated = money(accumulatedFull * proportion);
|
||||
const impairment = money(netAdjustments(asset.id, bookType) * proportion);
|
||||
const netBookValue = money(Math.max(0, nbvFull * proportion - impairment));
|
||||
|
||||
const price = Number(input.disposal_price || 0);
|
||||
const expense = Number(input.disposal_expense || 0);
|
||||
const realized = money(price - expense);
|
||||
const gainLoss = money(realized - netBookValue);
|
||||
|
||||
return {
|
||||
book_type: bookType,
|
||||
disposal_date: disposalDate,
|
||||
method: DISPOSAL_METHODS.includes(input.method) ? input.method : 'sale',
|
||||
property_type: input.property_type || asset.property_type || null,
|
||||
partial: partial ? 1 : 0,
|
||||
disposed_cost: money(disposedCost),
|
||||
total_cost: money(totalCost),
|
||||
disposal_price: money(price),
|
||||
disposal_expense: money(expense),
|
||||
accumulated_depreciation: accumulated,
|
||||
impairment,
|
||||
net_book_value: netBookValue,
|
||||
realized,
|
||||
gain_loss: gainLoss
|
||||
};
|
||||
}
|
||||
|
||||
function previewDisposal(assetId, input) {
|
||||
const asset = hydrateAsset(assetId);
|
||||
if (!asset) return null;
|
||||
return computeDisposal(asset, input);
|
||||
}
|
||||
|
||||
function recordDisposal(assetId, input, userId) {
|
||||
const asset = hydrateAsset(assetId);
|
||||
if (!asset) return null;
|
||||
const result = computeDisposal(asset, input);
|
||||
const timestamp = now();
|
||||
|
||||
return tx(() => {
|
||||
const inserted = run(
|
||||
`INSERT INTO disposals (
|
||||
asset_id, disposal_date, method, property_type, partial, disposed_cost,
|
||||
disposal_price, disposal_expense, accumulated_depreciation, net_book_value,
|
||||
gain_loss, book_type, notes, created_by, created_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
assetId, result.disposal_date, result.method, result.property_type, result.partial,
|
||||
result.disposed_cost, result.disposal_price, result.disposal_expense,
|
||||
result.accumulated_depreciation, result.net_book_value, result.gain_loss,
|
||||
result.book_type, input.notes || null, userId, timestamp
|
||||
]
|
||||
);
|
||||
|
||||
if (result.partial) {
|
||||
const totalCost = result.total_cost;
|
||||
const remainingProportion = totalCost > 0 ? Math.max(0, (totalCost - result.disposed_cost) / totalCost) : 1;
|
||||
run('UPDATE assets SET acquisition_cost = acquisition_cost - ?, updated_at = ? WHERE id = ?', [
|
||||
result.disposed_cost,
|
||||
timestamp,
|
||||
assetId
|
||||
]);
|
||||
run('UPDATE asset_books SET cost = cost * ? WHERE asset_id = ? AND cost IS NOT NULL', [remainingProportion, assetId]);
|
||||
} else {
|
||||
run(
|
||||
`UPDATE assets SET
|
||||
status = 'disposed', disposal_date = ?, disposal_price = ?, disposal_expense = ?,
|
||||
disposal_type = ?, property_type = ?, updated_at = ?
|
||||
WHERE id = ?`,
|
||||
[result.disposal_date, result.disposal_price, result.disposal_expense, result.method, result.property_type, timestamp, assetId]
|
||||
);
|
||||
}
|
||||
|
||||
const disposal = one('SELECT * FROM disposals WHERE id = ?', [inserted.lastInsertRowid]);
|
||||
audit(userId, 'dispose', 'asset', assetId, null, disposal);
|
||||
return { disposal, asset: hydrateAsset(assetId) };
|
||||
});
|
||||
}
|
||||
|
||||
function reverseDisposal(assetId, disposalId, userId) {
|
||||
const disposal = one('SELECT * FROM disposals WHERE id = ? AND asset_id = ?', [disposalId, assetId]);
|
||||
if (!disposal) return null;
|
||||
const timestamp = now();
|
||||
|
||||
return tx(() => {
|
||||
run('DELETE FROM disposals WHERE id = ?', [disposalId]);
|
||||
if (disposal.partial) {
|
||||
run('UPDATE assets SET acquisition_cost = acquisition_cost + ?, updated_at = ? WHERE id = ?', [
|
||||
disposal.disposed_cost,
|
||||
timestamp,
|
||||
assetId
|
||||
]);
|
||||
} else if (!one('SELECT id FROM disposals WHERE asset_id = ? AND partial = 0', [assetId])) {
|
||||
run(
|
||||
`UPDATE assets SET
|
||||
status = 'in_service', disposal_date = NULL, disposal_price = NULL,
|
||||
disposal_expense = NULL, disposal_type = NULL, updated_at = ?
|
||||
WHERE id = ?`,
|
||||
[timestamp, assetId]
|
||||
);
|
||||
}
|
||||
audit(userId, 'reverse_disposal', 'asset', assetId, disposal, null);
|
||||
return hydrateAsset(assetId);
|
||||
});
|
||||
}
|
||||
|
||||
function recordAdjustment(assetId, input, userId) {
|
||||
const asset = one('SELECT id FROM assets WHERE id = ?', [assetId]);
|
||||
if (!asset) return null;
|
||||
const timestamp = now();
|
||||
const result = run(
|
||||
`INSERT INTO asset_adjustments (asset_id, adjustment_date, type, amount, book_type, reason, created_by, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
assetId,
|
||||
input.adjustment_date || timestamp.slice(0, 10),
|
||||
input.type || 'impairment',
|
||||
Number(input.amount || 0),
|
||||
input.book_type || 'GAAP',
|
||||
input.reason || null,
|
||||
userId,
|
||||
timestamp
|
||||
]
|
||||
);
|
||||
const adjustment = one('SELECT * FROM asset_adjustments WHERE id = ?', [result.lastInsertRowid]);
|
||||
audit(userId, 'adjust', 'asset', assetId, null, adjustment);
|
||||
return adjustment;
|
||||
}
|
||||
|
||||
function deleteAdjustment(assetId, adjustmentId, userId) {
|
||||
const result = run('DELETE FROM asset_adjustments WHERE id = ? AND asset_id = ?', [adjustmentId, assetId]);
|
||||
if (!result.changes) return false;
|
||||
audit(userId, 'delete', 'asset_adjustment', adjustmentId, null, { asset_id: assetId });
|
||||
return true;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
DISPOSAL_METHODS,
|
||||
computeDisposal,
|
||||
deleteAdjustment,
|
||||
netAdjustments,
|
||||
previewDisposal,
|
||||
recordAdjustment,
|
||||
recordDisposal,
|
||||
reverseDisposal
|
||||
};
|
||||
Reference in New Issue
Block a user