From dfd999b6a938a0348b7c490990d009bf64bfb422 Mon Sep 17 00:00:00 2001
From: mpuckett
Date: Fri, 5 Jun 2026 05:55:43 -0500
Subject: [PATCH] Added Section 179 Limits
---
server/db.js | 39 +++++
server/depreciation.js | 41 ++++-
server/routes/reference.js | 30 ++++
server/services/section179Limits.js | 95 ++++++++++++
server/services/zones.js | 14 +-
server/smoke-test.js | 38 +++++
src/components/AssetDrawer.vue | 14 +-
src/components/DepreciationZoneSettings.vue | 19 ++-
src/components/Section179LimitSettings.vue | 162 ++++++++++++++++++++
src/components/UserManual.vue | 13 +-
src/views/AdminView.vue | 5 +-
11 files changed, 454 insertions(+), 16 deletions(-)
create mode 100644 server/services/section179Limits.js
create mode 100644 src/components/Section179LimitSettings.vue
diff --git a/server/db.js b/server/db.js
index 357448d..e632f08 100644
--- a/server/db.js
+++ b/server/db.js
@@ -652,6 +652,15 @@ function initialize() {
updated_at TEXT NOT NULL
);
+ CREATE TABLE IF NOT EXISTS section_179_limits (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ tax_year INTEGER NOT NULL UNIQUE,
+ base_limit REAL NOT NULL DEFAULT 0,
+ phaseout_threshold REAL NOT NULL DEFAULT 0,
+ created_at TEXT NOT NULL,
+ updated_at TEXT NOT NULL
+ );
+
CREATE TABLE IF NOT EXISTS audit_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
actor_id INTEGER REFERENCES users(id),
@@ -694,6 +703,8 @@ function migrate() {
ensureColumn('assets', 'servicenow_sys_id', 'TEXT');
ensureColumn('assets', 'asset_class_number', 'TEXT');
ensureColumn('assets', 'special_zone', 'TEXT');
+ ensureColumn('depreciation_zones', 'section_179_increase', 'REAL NOT NULL DEFAULT 0');
+ ensureColumn('depreciation_zones', 'section_179_cost_factor', 'REAL NOT NULL DEFAULT 1');
dropUsersRoleCheck();
}
@@ -758,6 +769,34 @@ function seed() {
'30% special depreciation allowance for qualified NY Liberty Zone property; qualified leasehold improvements use a 5-year life; increased Section 179. Verify current tax law before filing.', 1, ?, ?)`,
[createdAt, createdAt]
);
+
+ // Enterprise / Empowerment Zone: raises the Section 179 limit by $35,000 (2002-2020) and counts
+ // only 50% of the property's cost toward the §179 investment-phaseout threshold.
+ run(
+ `INSERT OR IGNORE INTO depreciation_zones
+ (code, name, allowance_percent, pis_start, pis_end, real_property_end, max_recovery_years, leasehold_life_years,
+ section_179_increase, section_179_cost_factor, notes, enabled, created_at, updated_at)
+ VALUES ('enterprise_zone', 'Enterprise Zone (Empowerment Zone)', 0, '2002-01-01', '2020-12-31', NULL, NULL, NULL,
+ 35000, 0.5,
+ 'Section 179 limit increase for qualified empowerment/enterprise zone property: +$35,000 (2002-2020; +$20,000 for 1993-2001) and only 50% of cost counts toward the investment-phaseout threshold. Not available after 2020. Verify current tax law before filing.', 1, ?, ?)`,
+ [createdAt, createdAt]
+ );
+
+ // Annual Section 179 deduction limits & investment-phaseout thresholds (IRS historical values).
+ // Seeded once, then user-managed in Admin → Application Configuration → Section 179 limits.
+ const section179Limits = [
+ [2014, 500000, 2000000], [2015, 500000, 2000000], [2016, 500000, 2010000], [2017, 510000, 2030000],
+ [2018, 1000000, 2500000], [2019, 1020000, 2550000], [2020, 1040000, 2590000], [2021, 1050000, 2620000],
+ [2022, 1080000, 2700000], [2023, 1160000, 2890000], [2024, 1220000, 3050000], [2025, 1250000, 3130000],
+ [2026, 1250000, 3130000]
+ ];
+ for (const [year, limit, threshold] of section179Limits) {
+ run(
+ `INSERT OR IGNORE INTO section_179_limits (tax_year, base_limit, phaseout_threshold, created_at, updated_at)
+ VALUES (?, ?, ?, ?, ?)`,
+ [year, limit, threshold, createdAt, createdAt]
+ );
+ }
if (!one('SELECT id FROM teams LIMIT 1')) {
run('INSERT INTO teams (name, description, created_at) VALUES (?, ?, ?)', [
'Finance Operations',
diff --git a/server/depreciation.js b/server/depreciation.js
index 061511c..f694e47 100644
--- a/server/depreciation.js
+++ b/server/depreciation.js
@@ -1,6 +1,7 @@
const { parseJson } = require('./db');
const { buildDepreciationMethods } = require('./rule-catalog');
const { getZone } = require('./services/zones');
+const { getLimit: getSection179Limit } = require('./services/section179Limits');
// Built-in method catalog, used as a fallback so codes like MF200/DB200 always resolve
// even if a particular rule set hasn't been re-expanded to include them.
@@ -47,7 +48,45 @@ function salvageValue(asset, book, methodRule) {
}
function section179Amount(asset, book) {
- return Math.min(grossBasis(asset, book), Number(book.section_179_amount || 0));
+ return Math.min(grossBasis(asset, book), Number(book.section_179_amount || 0), section179Limit(asset, book));
+}
+
+// Enterprise/empowerment-zone Section 179 treatment for the federal book: an increased dollar
+// limit and a reduced (e.g. 50%) share of cost counting toward the phaseout threshold. Applies
+// only when the asset is flagged for such a zone and placed in service within the date window.
+function zoneSection179(asset, book) {
+ if (!asset.special_zone || String(book.book_type) !== 'FEDERAL') return null;
+ const zone = getZone(asset.special_zone);
+ if (!zone) return null;
+ const increase = Number(zone.section_179_increase || 0);
+ const costFactor = Number(zone.section_179_cost_factor || 0) || 1;
+ if (!increase && costFactor === 1) return null;
+ const pis = asset.in_service_date || asset.acquired_date || '';
+ if (pis && zone.pis_start && pis < zone.pis_start) return null;
+ if (pis && zone.pis_end && pis > zone.pis_end) return null;
+ return { increase, costFactor };
+}
+
+// Applicable §179 dollar cap for an asset's placed-in-service year: the configured base limit
+// (plus any enterprise-zone increase) less the dollar-for-dollar investment phaseout. Returns
+// Infinity when no limit is configured for the year, leaving the entered §179 amount uncapped.
+function section179Limit(asset, book) {
+ const pisYear = yearFromDate(asset.in_service_date || asset.acquired_date);
+ const limitRow = getSection179Limit(pisYear);
+ if (!limitRow) return Infinity;
+ let limit = Number(limitRow.base_limit) || 0;
+ const threshold = Number(limitRow.phaseout_threshold) || 0;
+ let costFactor = 1;
+ const zone = zoneSection179(asset, book);
+ if (zone) {
+ limit += zone.increase;
+ costFactor = zone.costFactor;
+ }
+ if (threshold > 0) {
+ const investment = grossBasis(asset, book) * costFactor;
+ limit -= Math.max(0, investment - threshold);
+ }
+ return Math.max(0, limit);
}
// Special-zone §168 allowance (e.g. New York Liberty Zone): applies to the federal tax book
diff --git a/server/routes/reference.js b/server/routes/reference.js
index 580cb83..acdf758 100644
--- a/server/routes/reference.js
+++ b/server/routes/reference.js
@@ -4,6 +4,7 @@ const { requireCapability } = require('../middleware/auth');
const { formatAssetId } = require('../services/assets');
const assetClasses = require('../services/assetClasses');
const zones = require('../services/zones');
+const section179Limits = require('../services/section179Limits');
const router = express.Router();
@@ -36,6 +37,35 @@ router.delete('/depreciation-zones/:code', requireCapability('config.manage'), (
return res.status(204).end();
});
+// Annual Section 179 deduction limits & phaseout thresholds. GET is reference data; mutations
+// require config.manage. The engine caps each asset's §179 deduction at the applicable year's limit.
+router.get('/section-179-limits', (req, res) => {
+ res.json({ limits: section179Limits.list() });
+});
+
+router.post('/section-179-limits', requireCapability('config.manage'), (req, res, next) => {
+ try {
+ res.status(201).json({ limit: section179Limits.createLimit(req.body, req.user.id) });
+ } catch (error) {
+ next(error);
+ }
+});
+
+router.put('/section-179-limits/:year', requireCapability('config.manage'), (req, res, next) => {
+ try {
+ const limit = section179Limits.updateLimit(req.params.year, req.body, req.user.id);
+ if (!limit) return res.status(404).json({ error: 'Limit not found' });
+ return res.json({ limit });
+ } catch (error) {
+ return next(error);
+ }
+});
+
+router.delete('/section-179-limits/:year', requireCapability('config.manage'), (req, res) => {
+ if (!section179Limits.deleteLimit(req.params.year, req.user.id)) return res.status(404).json({ error: 'Limit not found' });
+ return res.status(204).end();
+});
+
// Asset class catalog (seeded from the IRS tables, then user-managed). GET is reference
// data used by the category picker; mutations require config.manage.
router.get('/asset-classes', (req, res) => {
diff --git a/server/services/section179Limits.js b/server/services/section179Limits.js
new file mode 100644
index 0000000..f7530d7
--- /dev/null
+++ b/server/services/section179Limits.js
@@ -0,0 +1,95 @@
+const { all, audit, now, one, run } = require('../db');
+
+let cache = null;
+
+function clearCache() {
+ cache = null;
+}
+
+// Sorted ascending by tax_year, cached for the engine.
+function rows() {
+ if (!cache) cache = all('SELECT * FROM section_179_limits ORDER BY tax_year');
+ return cache;
+}
+
+// Applicable limit for a placed-in-service year: the most recent year at or before it.
+// Returns null when nothing is configured for/under the year, so the engine leaves §179 uncapped.
+function getLimit(year) {
+ const y = Number(year);
+ if (!y) return null;
+ let match = null;
+ for (const row of rows()) {
+ if (row.tax_year <= y) match = row;
+ else break;
+ }
+ return match;
+}
+
+function fromRow(row) {
+ return {
+ id: row.id,
+ tax_year: row.tax_year,
+ base_limit: row.base_limit,
+ phaseout_threshold: row.phaseout_threshold
+ };
+}
+
+function list() {
+ return all('SELECT * FROM section_179_limits ORDER BY tax_year DESC').map(fromRow);
+}
+
+function normalize(body) {
+ return {
+ tax_year: Math.trunc(Number(body.tax_year)) || 0,
+ base_limit: Number(body.base_limit) || 0,
+ phaseout_threshold: Number(body.phaseout_threshold) || 0
+ };
+}
+
+function createLimit(body, userId) {
+ const input = normalize(body);
+ if (!input.tax_year) throw Object.assign(new Error('A valid tax year is required'), { status: 400 });
+ if (one('SELECT id FROM section_179_limits WHERE tax_year = ?', [input.tax_year])) {
+ throw Object.assign(new Error('A limit for that tax year already exists'), { status: 400 });
+ }
+ const ts = now();
+ run(
+ `INSERT INTO section_179_limits (tax_year, base_limit, phaseout_threshold, created_at, updated_at)
+ VALUES (?, ?, ?, ?, ?)`,
+ [input.tax_year, input.base_limit, input.phaseout_threshold, ts, ts]
+ );
+ clearCache();
+ audit(userId, 'create', 'section_179_limit', String(input.tax_year), null, input);
+ return fromRow(one('SELECT * FROM section_179_limits WHERE tax_year = ?', [input.tax_year]));
+}
+
+function updateLimit(taxYear, body, userId) {
+ const before = one('SELECT * FROM section_179_limits WHERE tax_year = ?', [Number(taxYear)]);
+ if (!before) return null;
+ const input = normalize({ ...before, ...body, tax_year: before.tax_year });
+ run(
+ 'UPDATE section_179_limits SET base_limit = ?, phaseout_threshold = ?, updated_at = ? WHERE tax_year = ?',
+ [input.base_limit, input.phaseout_threshold, now(), before.tax_year]
+ );
+ clearCache();
+ audit(userId, 'update', 'section_179_limit', String(before.tax_year), before, input);
+ return fromRow(one('SELECT * FROM section_179_limits WHERE tax_year = ?', [before.tax_year]));
+}
+
+function deleteLimit(taxYear, userId) {
+ const before = one('SELECT * FROM section_179_limits WHERE tax_year = ?', [Number(taxYear)]);
+ if (!before) return false;
+ run('DELETE FROM section_179_limits WHERE tax_year = ?', [before.tax_year]);
+ clearCache();
+ audit(userId, 'delete', 'section_179_limit', String(before.tax_year), before, null);
+ return true;
+}
+
+module.exports = {
+ clearCache,
+ createLimit,
+ deleteLimit,
+ getLimit,
+ list,
+ updateLimit
+};
diff --git a/server/services/zones.js b/server/services/zones.js
index ada1d9a..1da626c 100644
--- a/server/services/zones.js
+++ b/server/services/zones.js
@@ -37,6 +37,8 @@ function fromRow(row) {
real_property_end: row.real_property_end,
max_recovery_years: row.max_recovery_years,
leasehold_life_years: row.leasehold_life_years,
+ section_179_increase: row.section_179_increase,
+ section_179_cost_factor: row.section_179_cost_factor,
notes: row.notes,
enabled: Boolean(row.enabled),
asset_count: one('SELECT COUNT(*) AS c FROM assets WHERE special_zone = ?', [row.code]).c
@@ -57,6 +59,8 @@ function normalize(body) {
real_property_end: body.real_property_end || null,
max_recovery_years: num(body.max_recovery_years),
leasehold_life_years: num(body.leasehold_life_years),
+ section_179_increase: Number(body.section_179_increase) || 0,
+ section_179_cost_factor: body.section_179_cost_factor === '' || body.section_179_cost_factor === null || body.section_179_cost_factor === undefined ? 1 : Number(body.section_179_cost_factor),
notes: body.notes || null,
enabled: body.enabled === false ? 0 : 1
};
@@ -72,9 +76,9 @@ function createZone(body, userId) {
}
const ts = now();
run(
- `INSERT INTO depreciation_zones (code, name, allowance_percent, pis_start, pis_end, real_property_end, max_recovery_years, leasehold_life_years, notes, enabled, created_at, updated_at)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
- [code, input.name, input.allowance_percent, input.pis_start, input.pis_end, input.real_property_end, input.max_recovery_years, input.leasehold_life_years, input.notes, input.enabled, ts, ts]
+ `INSERT INTO depreciation_zones (code, name, allowance_percent, pis_start, pis_end, real_property_end, max_recovery_years, leasehold_life_years, section_179_increase, section_179_cost_factor, notes, enabled, created_at, updated_at)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
+ [code, input.name, input.allowance_percent, input.pis_start, input.pis_end, input.real_property_end, input.max_recovery_years, input.leasehold_life_years, input.section_179_increase, input.section_179_cost_factor, input.notes, input.enabled, ts, ts]
);
clearCache();
audit(userId, 'create', 'depreciation_zone', code, null, input);
@@ -87,8 +91,8 @@ function updateZone(code, body, userId) {
const input = normalize({ ...before, ...body, enabled: body.enabled === undefined ? Boolean(before.enabled) : body.enabled });
run(
`UPDATE depreciation_zones SET name = ?, allowance_percent = ?, pis_start = ?, pis_end = ?, real_property_end = ?,
- max_recovery_years = ?, leasehold_life_years = ?, notes = ?, enabled = ?, updated_at = ? WHERE code = ?`,
- [input.name, input.allowance_percent, input.pis_start, input.pis_end, input.real_property_end, input.max_recovery_years, input.leasehold_life_years, input.notes, input.enabled, now(), code]
+ max_recovery_years = ?, leasehold_life_years = ?, section_179_increase = ?, section_179_cost_factor = ?, notes = ?, enabled = ?, updated_at = ? WHERE code = ?`,
+ [input.name, input.allowance_percent, input.pis_start, input.pis_end, input.real_property_end, input.max_recovery_years, input.leasehold_life_years, input.section_179_increase, input.section_179_cost_factor, input.notes, input.enabled, now(), code]
);
clearCache();
audit(userId, 'update', 'depreciation_zone', code, before, input);
diff --git a/server/smoke-test.js b/server/smoke-test.js
index 9210e7c..0ca23b5 100644
--- a/server/smoke-test.js
+++ b/server/smoke-test.js
@@ -911,6 +911,44 @@ async function main() {
.schedules.filter((r) => r.book === 'FEDERAL')[0].depreciation);
if (outYear1 !== 2000) throw new Error(`Out-of-window asset should get plain MACRS year-1 (2000), got ${outYear1}`);
+ // Section 179 limits + Enterprise Zone: the seeded 2015 limit ($500,000) caps a plain asset's
+ // §179, but an Enterprise Zone asset gets the +$35,000 increase, letting a larger §179 through.
+ const limitList = await request('/api/section-179-limits', { headers: auth });
+ if (!limitList.limits.some((l) => l.tax_year === 2015 && l.base_limit === 500000)) {
+ throw new Error('2015 §179 limit was not seeded');
+ }
+ const ezList = await request('/api/depreciation-zones', { headers: auth });
+ const ez = ezList.zones.find((z) => z.code === 'enterprise_zone');
+ if (!ez || ez.section_179_increase !== 35000 || ez.section_179_cost_factor !== 0.5) {
+ throw new Error('Enterprise Zone was not seeded with the §179 increase/cost factor');
+ }
+ // Plain asset (no zone): §179 entered 520k is capped at the 500k base limit. Straight-line, 5yr,
+ // half-year → basis 100k, year-1 SL 10k → year-1 = 500000 + 10000 = 510000.
+ const s179Plain = await request('/api/assets', {
+ method: 'POST', headers: auth,
+ body: JSON.stringify({
+ asset_id: `S179-${suffix}`, description: '§179 capped', category: 'Computer Equipment',
+ acquisition_cost: 600000, in_service_date: '2015-01-01', useful_life_months: 60,
+ books: [{ book_type: 'FEDERAL', active: true, depreciation_method: 'straight_line', convention: 'half_year', useful_life_months: 60, section_179_amount: 520000, cost: 600000 }]
+ })
+ });
+ const plainY1 = round((await request(`/api/assets/${s179Plain.asset.id}/depreciation?startYear=2015&endYear=2015`, { headers: auth }))
+ .schedules.filter((r) => r.book === 'FEDERAL')[0].depreciation);
+ if (plainY1 !== 510000) throw new Error(`§179 should cap at the 500k base limit (year-1 510000), got ${plainY1}`);
+ // Enterprise Zone asset: limit 500k + 35k = 535k, so the full 520k §179 applies. basis 80k,
+ // year-1 SL 8k → year-1 = 520000 + 8000 = 528000.
+ const s179Ez = await request('/api/assets', {
+ method: 'POST', headers: auth,
+ body: JSON.stringify({
+ asset_id: `S179EZ-${suffix}`, description: '§179 enterprise zone', category: 'Computer Equipment',
+ acquisition_cost: 600000, in_service_date: '2015-01-01', useful_life_months: 60, special_zone: 'enterprise_zone',
+ books: [{ book_type: 'FEDERAL', active: true, depreciation_method: 'straight_line', convention: 'half_year', useful_life_months: 60, section_179_amount: 520000, cost: 600000 }]
+ })
+ });
+ const ezY1 = round((await request(`/api/assets/${s179Ez.asset.id}/depreciation?startYear=2015&endYear=2015`, { headers: auth }))
+ .schedules.filter((r) => r.book === 'FEDERAL')[0].depreciation);
+ if (ezY1 !== 528000) throw new Error(`Enterprise Zone §179 increase should allow 520k (year-1 528000), got ${ezY1}`);
+
// Disposal: preview gain/loss, then record a full disposal.
const disposalAsset = await request('/api/assets', {
method: 'POST',
diff --git a/src/components/AssetDrawer.vue b/src/components/AssetDrawer.vue
index 18511b6..22234bf 100644
--- a/src/components/AssetDrawer.vue
+++ b/src/components/AssetDrawer.vue
@@ -748,10 +748,18 @@ export default {
if (!zone) return '';
const pis = this.localAsset.in_service_date || this.localAsset.acquired_date || '';
const inWindow = (!zone.pis_start || !pis || pis >= zone.pis_start) && (!zone.pis_end || !pis || pis <= zone.pis_end);
- const base = `${zone.name}: ${zone.allowance_percent}% §168 allowance applied to the FEDERAL book`
- + `${zone.pis_start || zone.pis_end ? ` for assets placed in service ${zone.pis_start || '…'} to ${zone.pis_end || '…'}` : ''}.`;
+ const benefits = [];
+ if (Number(zone.allowance_percent) > 0) benefits.push(`a ${zone.allowance_percent}% §168 special allowance`);
+ if (Number(zone.section_179_increase) > 0) {
+ const factor = Number(zone.section_179_cost_factor);
+ const phaseout = factor && factor !== 1 ? ` (only ${Math.round(factor * 100)}% of cost counts toward the §179 phaseout)` : '';
+ benefits.push(`an increased §179 limit of +$${Number(zone.section_179_increase).toLocaleString()}${phaseout}`);
+ }
+ if (!benefits.length) benefits.push('special depreciation treatment');
+ const window = zone.pis_start || zone.pis_end ? ` for assets placed in service ${zone.pis_start || '…'} to ${zone.pis_end || '…'}` : '';
+ const base = `${zone.name}: ${benefits.join(' and ')} on the FEDERAL book${window}.`;
const lease = zone.leasehold_life_years ? ` Qualified leasehold improvements use a ${zone.leasehold_life_years}-year life.` : '';
- if (pis && !inWindow) return `${base} This asset's placed-in-service date is OUTSIDE the window, so the allowance will not apply.${lease}`;
+ if (pis && !inWindow) return `${base} This asset's placed-in-service date is OUTSIDE the window, so it will not apply.${lease}`;
return `${base}${lease}`;
},
changedCriticalLabels() {
diff --git a/src/components/DepreciationZoneSettings.vue b/src/components/DepreciationZoneSettings.vue
index 779a132..e658bbe 100644
--- a/src/components/DepreciationZoneSettings.vue
+++ b/src/components/DepreciationZoneSettings.vue
@@ -4,8 +4,9 @@
Depreciation zones
- Special-allowance zones (e.g. New York Liberty Zone). Assign one to an asset and the engine applies the zone’s §168
- allowance to the federal book for assets placed in service within the date window.
+ Special-allowance zones (e.g. New York Liberty Zone, Enterprise/Empowerment Zone). Assign one to an asset and the engine
+ applies the zone’s §168 allowance and/or increased §179 limit to the federal book for assets placed in service within the
+ date window.
@@ -27,6 +28,7 @@
Disabled
{{ row.allowance_percent }}%
+ {{ row.section_179_increase ? `+$${Number(row.section_179_increase).toLocaleString()}` : '—' }}
{{ row.pis_start || '—' }} → {{ row.pis_end || '—' }}
@@ -49,6 +51,8 @@
+
+
{{ dialogError }}
@@ -87,7 +91,8 @@ import { apiRequest } from '../services/api';
function blankForm() {
return {
exists: false, code: '', name: '', allowance_percent: 0, enabled: true,
- pis_start: '', pis_end: '', real_property_end: '', max_recovery_years: null, leasehold_life_years: null, notes: ''
+ pis_start: '', pis_end: '', real_property_end: '', max_recovery_years: null, leasehold_life_years: null,
+ section_179_increase: 0, section_179_cost_factor: 1, notes: ''
};
}
@@ -109,7 +114,8 @@ export default {
dialogError: '',
columns: [
{ key: 'name', label: 'Zone' },
- { key: 'allowance_percent', label: 'Allowance' },
+ { key: 'allowance_percent', label: '§168 allowance' },
+ { key: 'section_179_increase', label: '§179 increase', sortable: false },
{ key: 'window', label: 'Placed-in-service window', sortable: false },
{ key: 'asset_count', label: 'Assets', format: 'number' }
]
@@ -140,7 +146,10 @@ export default {
this.form = {
exists: true, code: row.code, name: row.name, allowance_percent: row.allowance_percent, enabled: row.enabled,
pis_start: row.pis_start || '', pis_end: row.pis_end || '', real_property_end: row.real_property_end || '',
- max_recovery_years: row.max_recovery_years, leasehold_life_years: row.leasehold_life_years, notes: row.notes || ''
+ max_recovery_years: row.max_recovery_years, leasehold_life_years: row.leasehold_life_years,
+ section_179_increase: row.section_179_increase || 0,
+ section_179_cost_factor: row.section_179_cost_factor === null || row.section_179_cost_factor === undefined ? 1 : row.section_179_cost_factor,
+ notes: row.notes || ''
};
this.dialogError = '';
this.dialog = true;
diff --git a/src/components/Section179LimitSettings.vue b/src/components/Section179LimitSettings.vue
new file mode 100644
index 0000000..f94ed3d
--- /dev/null
+++ b/src/components/Section179LimitSettings.vue
@@ -0,0 +1,162 @@
+
+
+
+
+
+ ${{ Number(row.base_limit).toLocaleString() }}
+ ${{ Number(row.phaseout_threshold).toLocaleString() }}
+
+
+
+
+
+ {{ error }}
+
+
+
+ {{ form.exists ? `Edit ${form.tax_year}` : 'New §179 limit year' }}
+
+
+
+
+
+
+ {{ dialogError }}
+
+
+
+ Cancel
+ Save
+
+
+
+
+
+
+ Delete limit
+
+ Remove the §179 limit for {{ deleteTarget?.tax_year }}? Assets placed in service that year fall back to
+ the nearest earlier configured year (or become uncapped).
+
+
+
+ Cancel
+ Delete
+
+
+
+
+
+
+
diff --git a/src/components/UserManual.vue b/src/components/UserManual.vue
index 5e36794..a6e9597 100644
--- a/src/components/UserManual.vue
+++ b/src/components/UserManual.vue
@@ -291,6 +291,17 @@
and any leasehold-improvement life. Zones are managed in Admin → Application Configuration → Depreciation zones
(create your own, set the allowance %, date window, and notes).
+
+ A zone can instead (or also) carry an increased §179 limit — this is how
+ Enterprise / Empowerment Zone property is handled. Set the zone’s §179 limit increase
+ (e.g. +$35,000 for 2002–2020) and its §179 cost factor (e.g. 0.5, meaning only 50% of the property’s cost counts
+ toward the investment-phaseout threshold). When such a zone is assigned to an asset placed in service within the window,
+ the federal book’s §179 deduction may exceed the standard annual limit by that increase. The base annual §179 deduction
+ limits and phaseout thresholds themselves are maintained in
+ Admin → Application Configuration → Section 179 limits; the engine caps each asset’s §179 deduction at the
+ applicable year’s limit (less any dollar-for-dollar phaseout). If no limit is configured for an asset’s placed-in-service
+ year, the entered §179 amount is left uncapped.
+
The PM (maintenance) book
@@ -620,7 +631,7 @@
Administration
Admin (admin role) is where the application is configured. It is split into three sub-screens in the
- navigation rail: Users & Teams, Application Configuration (application settings, asset classes, depreciation zones, asset ID templates, asset categories,
+ navigation rail: Users & Teams, Application Configuration (application settings, asset classes, depreciation zones, Section 179 limits, asset ID templates, asset categories,
asset departments, PM categories, parts categories, maintenance defaults, tax rule sets), and Integrations (email, webhook, ServiceNow, Workday). Each is described below.
diff --git a/src/views/AdminView.vue b/src/views/AdminView.vue
index df3c974..efde433 100644
--- a/src/views/AdminView.vue
+++ b/src/views/AdminView.vue
@@ -172,6 +172,8 @@
+
+
@@ -280,6 +282,7 @@ import CategorySettings from '../components/CategorySettings.vue';
import DataTable from '../components/DataTable.vue';
import DepartmentSettings from '../components/DepartmentSettings.vue';
import DepreciationZoneSettings from '../components/DepreciationZoneSettings.vue';
+import Section179LimitSettings from '../components/Section179LimitSettings.vue';
import NotificationSettings from '../components/NotificationSettings.vue';
import PartCategorySettings from '../components/PartCategorySettings.vue';
import PmCategorySettings from '../components/PmCategorySettings.vue';
@@ -288,7 +291,7 @@ import ServiceNowSettings from '../components/ServiceNowSettings.vue';
import WebhookSettings from '../components/WebhookSettings.vue';
export default {
- components: { AssetClassSettings, AssetIdTemplateSettings, CategorySettings, DataTable, DepartmentSettings, DepreciationZoneSettings, NotificationSettings, PartCategorySettings, PmCategorySettings, PmSettings, ServiceNowSettings, WebhookSettings },
+ components: { AssetClassSettings, AssetIdTemplateSettings, CategorySettings, DataTable, DepartmentSettings, DepreciationZoneSettings, NotificationSettings, PartCategorySettings, PmCategorySettings, PmSettings, Section179LimitSettings, ServiceNowSettings, WebhookSettings },
props: {
token: { type: String, default: '' },
section: { type: String, default: 'admin-users' },