Added Section 179 Limits

This commit is contained in:
2026-06-05 05:55:43 -05:00
parent 4072695432
commit dfd999b6a9
11 changed files with 454 additions and 16 deletions

View File

@@ -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() {

View File

@@ -4,8 +4,9 @@
<div>
<h2 class="section-title">Depreciation zones</h2>
<p class="quiet text-body-2">
Special-allowance zones (e.g. New York Liberty Zone). Assign one to an asset and the engine applies the zones §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 zones §168 allowance and/or increased §179 limit to the federal book for assets placed in service within the
date window.
</p>
</div>
<v-spacer />
@@ -27,6 +28,7 @@
<v-chip v-if="!row.enabled" size="x-small" variant="tonal" class="ml-2">Disabled</v-chip>
</template>
<template #cell-allowance_percent="{ row }">{{ row.allowance_percent }}%</template>
<template #cell-section_179_increase="{ row }">{{ row.section_179_increase ? `+$${Number(row.section_179_increase).toLocaleString()}` : '—' }}</template>
<template #cell-window="{ row }">{{ row.pis_start || '—' }} {{ row.pis_end || '—' }}</template>
<template #actions="{ row }">
<v-btn icon="mdi-pencil" size="small" variant="text" @click="openEdit(row)" />
@@ -49,6 +51,8 @@
<v-text-field v-model="form.real_property_end" type="date" label="Real-property PIS end (optional)" />
<v-text-field v-model.number="form.max_recovery_years" type="number" label="Max recovery years (optional)" />
<v-text-field v-model.number="form.leasehold_life_years" type="number" label="Leasehold improvement life (yrs, optional)" />
<v-text-field v-model.number="form.section_179_increase" type="number" label="§179 limit increase ($)" prefix="$" hint="Enterprise/empowerment zones, e.g. 35000" persistent-hint />
<v-text-field v-model.number="form.section_179_cost_factor" type="number" step="0.01" label="§179 cost factor for phaseout" hint="Share of cost counted toward the threshold (e.g. 0.5)" persistent-hint />
</div>
<v-textarea v-model="form.notes" class="mt-2" label="Notes" rows="3" />
<v-alert v-if="dialogError" type="error" density="compact" class="mt-2">{{ dialogError }}</v-alert>
@@ -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;

View File

@@ -0,0 +1,162 @@
<template>
<v-card class="span-12 panel-pad">
<div class="toolbar-row mb-3">
<div>
<h2 class="section-title">Section 179 limits</h2>
<p class="quiet text-body-2">
Annual §179 deduction limits and investment-phaseout thresholds. The engine caps each assets §179 deduction at the
applicable years limit (less any dollar-for-dollar phaseout). Enterprise/empowerment-zone assets get the increased limit
configured on their depreciation zone.
</p>
</div>
<v-spacer />
<v-btn color="primary" prepend-icon="mdi-plus" @click="openNew">New year</v-btn>
</div>
<DataTable
:columns="columns"
:rows="limits"
row-key="tax_year"
:page-size="10"
has-actions
empty-text="No §179 limits configured deductions are uncapped."
>
<template #cell-base_limit="{ row }">${{ Number(row.base_limit).toLocaleString() }}</template>
<template #cell-phaseout_threshold="{ row }">${{ Number(row.phaseout_threshold).toLocaleString() }}</template>
<template #actions="{ row }">
<v-btn icon="mdi-pencil" size="small" variant="text" @click="openEdit(row)" />
<v-btn icon="mdi-delete-outline" size="small" variant="text" color="error" @click="confirmDelete(row)" />
</template>
</DataTable>
<v-alert v-if="error" type="error" density="compact" class="mt-3">{{ error }}</v-alert>
<v-dialog v-model="dialog" max-width="480">
<v-card>
<v-card-title>{{ form.exists ? `Edit ${form.tax_year}` : 'New §179 limit year' }}</v-card-title>
<v-card-text>
<div class="form-grid">
<v-text-field v-model.number="form.tax_year" type="number" label="Tax year *" :disabled="form.exists" />
<v-text-field v-model.number="form.base_limit" type="number" label="Base deduction limit ($)" prefix="$" />
<v-text-field v-model.number="form.phaseout_threshold" type="number" label="Investment phaseout threshold ($)" prefix="$" />
</div>
<v-alert v-if="dialogError" type="error" density="compact" class="mt-2">{{ dialogError }}</v-alert>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn variant="text" @click="dialog = false">Cancel</v-btn>
<v-btn color="primary" prepend-icon="mdi-content-save" :loading="saving" :disabled="!form.tax_year" @click="save">Save</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog v-model="deleteDialog" max-width="440">
<v-card>
<v-card-title class="text-error">Delete limit</v-card-title>
<v-card-text>
<p>Remove the §179 limit for <strong>{{ deleteTarget?.tax_year }}</strong>? Assets placed in service that year fall back to
the nearest earlier configured year (or become uncapped).</p>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn variant="text" @click="deleteDialog = false">Cancel</v-btn>
<v-btn color="error" prepend-icon="mdi-delete-outline" @click="performDelete">Delete</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-card>
</template>
<script>
import DataTable from './DataTable.vue';
import { apiRequest } from '../services/api';
function blankForm() {
return { exists: false, tax_year: new Date().getFullYear(), base_limit: 0, phaseout_threshold: 0 };
}
export default {
components: { DataTable },
props: {
token: { type: String, required: true }
},
emits: ['updated'],
data() {
return {
limits: [],
dialog: false,
deleteDialog: false,
deleteTarget: null,
form: blankForm(),
saving: false,
error: '',
dialogError: '',
columns: [
{ key: 'tax_year', label: 'Tax year' },
{ key: 'base_limit', label: 'Base deduction limit' },
{ key: 'phaseout_threshold', label: 'Phaseout threshold' }
]
};
},
mounted() {
this.load();
},
methods: {
async api(path, options = {}) {
return apiRequest(path, this.token, options);
},
async load() {
this.error = '';
try {
const data = await (await this.api('/api/section-179-limits')).json();
this.limits = data.limits;
} catch (error) {
this.error = error.message;
}
},
openNew() {
this.form = blankForm();
this.dialogError = '';
this.dialog = true;
},
openEdit(row) {
this.form = { exists: true, tax_year: row.tax_year, base_limit: row.base_limit, phaseout_threshold: row.phaseout_threshold };
this.dialogError = '';
this.dialog = true;
},
async save() {
if (!this.form.tax_year) return;
this.saving = true;
this.dialogError = '';
try {
const method = this.form.exists ? 'PUT' : 'POST';
const url = this.form.exists ? `/api/section-179-limits/${this.form.tax_year}` : '/api/section-179-limits';
await this.api(url, { method, body: JSON.stringify(this.form) });
this.dialog = false;
await this.load();
this.$emit('updated');
} catch (error) {
this.dialogError = error.message;
} finally {
this.saving = false;
}
},
confirmDelete(row) {
this.deleteTarget = row;
this.deleteDialog = true;
},
async performDelete() {
const target = this.deleteTarget;
this.deleteDialog = false;
this.deleteTarget = null;
if (!target) return;
try {
await this.api(`/api/section-179-limits/${target.tax_year}`, { method: 'DELETE' });
await this.load();
this.$emit('updated');
} catch (error) {
this.error = error.message;
}
}
}
};
</script>

View File

@@ -291,6 +291,17 @@
and any leasehold-improvement life. Zones are managed in <strong>Admin Application Configuration Depreciation zones</strong>
(create your own, set the allowance %, date window, and notes).
</p>
<p>
A zone can instead (or also) carry an <strong>increased §179 limit</strong> this is how
<strong>Enterprise / Empowerment Zone</strong> property is handled. Set the zones <em>§179 limit increase</em>
(e.g. +$35,000 for 20022020) and its <em>§179 cost factor</em> (e.g. 0.5, meaning only 50% of the propertys cost counts
toward the investment-phaseout threshold). When such a zone is assigned to an asset placed in service within the window,
the federal books §179 deduction may exceed the standard annual limit by that increase. The base annual §179 deduction
limits and phaseout thresholds themselves are maintained in
<strong>Admin Application Configuration Section 179 limits</strong>; the engine caps each assets §179 deduction at the
applicable years limit (less any dollar-for-dollar phaseout). If no limit is configured for an assets placed-in-service
year, the entered §179 amount is left uncapped.
</p>
<h3>The PM (maintenance) book</h3>
<p>
@@ -620,7 +631,7 @@
<h2>Administration</h2>
<p>
<strong>Admin</strong> (admin role) is where the application is configured. It is split into three sub-screens in the
navigation rail: <strong>Users &amp; Teams</strong>, <strong>Application Configuration</strong> (application settings, asset classes, depreciation zones, asset ID templates, asset categories,
navigation rail: <strong>Users &amp; Teams</strong>, <strong>Application Configuration</strong> (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 <strong>Integrations</strong> (email, webhook, ServiceNow, Workday). Each is described below.
</p>

View File

@@ -172,6 +172,8 @@
<DepreciationZoneSettings :token="token" />
<Section179LimitSettings :token="token" />
<AssetIdTemplateSettings :token="token" />
<CategorySettings :token="token" @updated="$emit('categories-updated')" />
@@ -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' },