Pub 225 Compliance
This commit is contained in:
@@ -32,6 +32,8 @@ const assetColumns = [
|
||||
'passenger_auto',
|
||||
'business_use_percent',
|
||||
'investment_use_percent',
|
||||
'soil_water_deductions',
|
||||
'cost_sharing_excluded',
|
||||
'disposal_date',
|
||||
'disposal_price',
|
||||
'disposal_expense',
|
||||
@@ -64,6 +66,8 @@ function assetFromRow(row) {
|
||||
...row,
|
||||
listed_property: Boolean(row.listed_property),
|
||||
passenger_auto: Boolean(row.passenger_auto),
|
||||
soil_water_deductions: Number(row.soil_water_deductions || 0),
|
||||
cost_sharing_excluded: Number(row.cost_sharing_excluded || 0),
|
||||
custom_fields: parseJson(row.custom_fields, {}),
|
||||
books: row.books ? parseJson(row.books, []) : undefined
|
||||
};
|
||||
@@ -213,6 +217,8 @@ function normalizeAssetInput(body) {
|
||||
input.prior_depreciation = Number(input.prior_depreciation || 0);
|
||||
input.business_use_percent = Number(input.business_use_percent || 100);
|
||||
input.investment_use_percent = Number(input.investment_use_percent || 0);
|
||||
input.soil_water_deductions = Number(input.soil_water_deductions || 0);
|
||||
input.cost_sharing_excluded = Number(input.cost_sharing_excluded || 0);
|
||||
input.custom_fields = json(input.custom_fields || {});
|
||||
input.listed_property = input.listed_property ? 1 : 0;
|
||||
input.passenger_auto = input.passenger_auto ? 1 : 0;
|
||||
|
||||
@@ -38,11 +38,34 @@ function isRealProperty(asset, book) {
|
||||
return months >= 330 || /real|realty|1250|building/.test(pt);
|
||||
}
|
||||
|
||||
// Whole years a property was held (for the §1252 / §1255 applicable-percentage schedules).
|
||||
function yearsHeld(asset, disposalDate) {
|
||||
const acq = asset.acquired_date || asset.in_service_date;
|
||||
if (!acq) return 0;
|
||||
return Math.max(0, Math.floor((new Date(`${disposalDate}T00:00:00`) - new Date(`${acq}T00:00:00`)) / (365.25 * 86400000)));
|
||||
}
|
||||
|
||||
// §1252 applicable percentage (IRS Pub 225): 100% if farmland is disposed of within 5 years; then reduced
|
||||
// 20 points per year for years 6–10; 0% after 10 years.
|
||||
function section1252Percent(years) {
|
||||
if (years <= 5) return 1;
|
||||
return Math.max(0, 1 - 0.2 * (years - 5));
|
||||
}
|
||||
|
||||
// §1255 applicable percentage: 100% if held under 10 years; reduced 10 points for each year (or part) the
|
||||
// property was held beyond 10 years; 0% at 20 years.
|
||||
function section1255Percent(years) {
|
||||
if (years < 10) return 1;
|
||||
return Math.max(0, 1 - 0.1 * (years - 9));
|
||||
}
|
||||
|
||||
// Characterize the gain on disposal into ordinary-income recapture vs. §1231 (capital) gain (Form 4797).
|
||||
// §1245: ordinary recapture = lesser of gain or total depreciation taken (incl. §179/bonus). §1250:
|
||||
// ordinary recapture = lesser of gain or "additional depreciation" (accelerated over straight-line); the
|
||||
// remaining gain up to straight-line taken is unrecaptured §1250 gain (a 25% capital bucket).
|
||||
function characterizeRecapture(asset, book, rules, year, proportion, gainLoss, accumulated) {
|
||||
// remaining gain up to straight-line taken is unrecaptured §1250 gain (a 25% capital bucket). Farm
|
||||
// property adds §1252 (soil & water conservation, §175) and §1255 (cost-sharing, §126) ordinary recapture,
|
||||
// each by an applicable percentage of the years held and limited to the remaining gain.
|
||||
function characterizeRecapture(asset, book, rules, year, proportion, gainLoss, accumulated, disposalDate) {
|
||||
const section = isRealProperty(asset, book) ? '1250' : '1245';
|
||||
const gain = Math.max(0, gainLoss);
|
||||
let ordinary = 0;
|
||||
@@ -51,7 +74,6 @@ function characterizeRecapture(asset, book, rules, year, proportion, gainLoss, a
|
||||
if (section === '1245') {
|
||||
ordinary = Math.min(gain, accumulated);
|
||||
} else {
|
||||
// Straight-line accumulated for the same period (additional depreciation = accelerated − SL).
|
||||
const slBook = { ...book, depreciation_method: 'straight_line', section_179_amount: 0, bonus_percent: 0 };
|
||||
const slRow = annualSchedule(asset, slBook, rules, { startYear: year, endYear: year })[0] || {};
|
||||
const slAccum = money(Number(slRow.accumulated || 0) * proportion);
|
||||
@@ -60,11 +82,21 @@ function characterizeRecapture(asset, book, rules, year, proportion, gainLoss, a
|
||||
unrecaptured1250 = Math.min(money(gain - ordinary), slAccum);
|
||||
}
|
||||
}
|
||||
// Farm-property recapture (§1252 / §1255), applied to the gain remaining after depreciation recapture.
|
||||
const years = yearsHeld(asset, disposalDate);
|
||||
let remaining = Math.max(0, gain - ordinary);
|
||||
const r1252 = Math.min(remaining, money(section1252Percent(years) * Number(asset.soil_water_deductions || 0) * proportion));
|
||||
remaining = Math.max(0, remaining - r1252);
|
||||
const r1255 = Math.min(remaining, money(section1255Percent(years) * Number(asset.cost_sharing_excluded || 0) * proportion));
|
||||
const totalOrdinary = money(ordinary + r1252 + r1255);
|
||||
|
||||
return {
|
||||
recapture_section: section,
|
||||
ordinary_recapture: money(ordinary),
|
||||
unrecaptured_1250_gain: money(unrecaptured1250),
|
||||
section_1231_gain: money(gainLoss - ordinary) // negative for a §1231 loss
|
||||
section_1252_recapture: money(r1252),
|
||||
section_1255_recapture: money(r1255),
|
||||
section_1231_gain: money(gainLoss - totalOrdinary) // negative for a §1231 loss
|
||||
};
|
||||
}
|
||||
|
||||
@@ -95,7 +127,7 @@ function computeDisposal(asset, input = {}) {
|
||||
|
||||
// Recapture characterization is computed on the disposed book's depreciation (use a tax book — FEDERAL —
|
||||
// for tax recapture). "Depreciation taken" includes the impairment write-down already reflected in NBV.
|
||||
const recapture = characterizeRecapture(asset, book, rules, year, proportion, gainLoss, money(accumulated + impairment));
|
||||
const recapture = characterizeRecapture(asset, book, rules, year, proportion, gainLoss, money(accumulated + impairment), disposalDate);
|
||||
|
||||
return {
|
||||
book_type: bookType,
|
||||
@@ -146,14 +178,15 @@ function recordDisposal(assetId, input, userId) {
|
||||
asset_id, disposal_date, method, property_type, partial, disposed_cost,
|
||||
disposal_price, disposal_expense, accumulated_depreciation, net_book_value,
|
||||
gain_loss, book_type, recapture_section, ordinary_recapture, section_1231_gain,
|
||||
unrecaptured_1250_gain, notes, created_by, created_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
unrecaptured_1250_gain, section_1252_recapture, section_1255_recapture, 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, result.recapture_section, result.ordinary_recapture, result.section_1231_gain,
|
||||
result.unrecaptured_1250_gain, input.notes || null, userId, timestamp
|
||||
result.unrecaptured_1250_gain, result.section_1252_recapture, result.section_1255_recapture,
|
||||
input.notes || null, userId, timestamp
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -296,16 +296,22 @@ function disposals(options) {
|
||||
disposal_date: row.disposal_date,
|
||||
proceeds: money(row.disposal_price - row.disposal_expense),
|
||||
net_book_value: money(row.net_book_value),
|
||||
gain_loss: money(row.gain_loss)
|
||||
gain_loss: money(row.gain_loss),
|
||||
recapture_section: row.recapture_section ? `§${row.recapture_section}` : '',
|
||||
ordinary_recapture: money(row.ordinary_recapture || 0),
|
||||
farm_recapture: money((row.section_1252_recapture || 0) + (row.section_1255_recapture || 0)),
|
||||
section_1231_gain: money(row.section_1231_gain || 0)
|
||||
}));
|
||||
return {
|
||||
columns: [
|
||||
col('asset_id', 'Asset ID'), col('description', 'Description'), col('method', 'Type'),
|
||||
col('disposal_date', 'Date', 'date'), col('proceeds', 'Net proceeds', 'currency'),
|
||||
col('net_book_value', 'Net book value', 'currency'), col('gain_loss', 'Gain / (loss)', 'currency')
|
||||
col('net_book_value', 'Net book value', 'currency'), col('gain_loss', 'Gain / (loss)', 'currency'),
|
||||
col('recapture_section', 'Recapture'), col('ordinary_recapture', 'Ordinary recapture', 'currency'),
|
||||
col('farm_recapture', '§1252/§1255 (farm)', 'currency'), col('section_1231_gain', '§1231 gain / (loss)', 'currency')
|
||||
],
|
||||
rows,
|
||||
totals: sumBy(rows, ['proceeds', 'net_book_value', 'gain_loss'])
|
||||
totals: sumBy(rows, ['proceeds', 'net_book_value', 'gain_loss', 'ordinary_recapture', 'farm_recapture', 'section_1231_gain'])
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
55
server/services/shortTaxYear.js
Normal file
55
server/services/shortTaxYear.js
Normal file
@@ -0,0 +1,55 @@
|
||||
const { all, audit, now, one, run } = require('../db');
|
||||
|
||||
// Short tax years (IRS Pub 946): when a company's tax year is shorter than 12 months, that year's MACRS
|
||||
// depreciation is prorated. This is the simplified proration — the year's deduction is multiplied by
|
||||
// (months ÷ 12); it is most accurate for the placed-in-service (first) short year. Per company + year,
|
||||
// cached for the engine and invalidated on edits.
|
||||
|
||||
const cache = new Map(); // `${entityId}|${year}` -> fraction
|
||||
|
||||
function clearCache() {
|
||||
cache.clear();
|
||||
}
|
||||
|
||||
// Proration fraction (0–1) for a company's tax year; 1 when the year is a normal 12-month year.
|
||||
function fraction(entityId, year) {
|
||||
if (!entityId || !year) return 1;
|
||||
const key = `${entityId}|${Number(year)}`;
|
||||
if (cache.has(key)) return cache.get(key);
|
||||
const row = one('SELECT months FROM short_tax_years WHERE entity_id = ? AND tax_year = ?', [entityId, Number(year)]);
|
||||
const months = row ? Math.max(0, Math.min(12, Number(row.months) || 12)) : 12;
|
||||
const f = months / 12;
|
||||
cache.set(key, f);
|
||||
return f;
|
||||
}
|
||||
|
||||
function list(companyId) {
|
||||
return all('SELECT * FROM short_tax_years WHERE entity_id = ? ORDER BY tax_year DESC', [companyId]);
|
||||
}
|
||||
|
||||
function save(companyId, body, userId) {
|
||||
const taxYear = Math.trunc(Number(body.tax_year)) || 0;
|
||||
if (!taxYear) throw Object.assign(new Error('A valid tax year is required'), { status: 400 });
|
||||
const months = Math.max(1, Math.min(12, Number(body.months) || 12));
|
||||
run(
|
||||
`INSERT INTO short_tax_years (entity_id, tax_year, months, note, updated_by, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(entity_id, tax_year) DO UPDATE SET months = excluded.months, note = excluded.note,
|
||||
updated_by = excluded.updated_by, updated_at = excluded.updated_at`,
|
||||
[companyId, taxYear, months, body.note || null, userId, now()]
|
||||
);
|
||||
clearCache();
|
||||
audit(userId, 'update', 'short_tax_year', `${companyId}:${taxYear}`, null, { tax_year: taxYear, months });
|
||||
return one('SELECT * FROM short_tax_years WHERE entity_id = ? AND tax_year = ?', [companyId, taxYear]);
|
||||
}
|
||||
|
||||
function remove(companyId, taxYear, userId) {
|
||||
const before = one('SELECT * FROM short_tax_years WHERE entity_id = ? AND tax_year = ?', [companyId, Number(taxYear)]);
|
||||
if (!before) return false;
|
||||
run('DELETE FROM short_tax_years WHERE entity_id = ? AND tax_year = ?', [companyId, Number(taxYear)]);
|
||||
clearCache();
|
||||
audit(userId, 'delete', 'short_tax_year', `${companyId}:${taxYear}`, before, null);
|
||||
return true;
|
||||
}
|
||||
|
||||
module.exports = { clearCache, fraction, list, remove, save };
|
||||
Reference in New Issue
Block a user