Pub 946 Compliance

This commit is contained in:
2026-06-10 02:09:25 -05:00
parent 7a54d1a386
commit 221ccb7826
18 changed files with 863 additions and 19 deletions

View File

@@ -172,6 +172,10 @@
v-if="activeView === 'close'"
:token="token"
/>
<Section179View
v-if="activeView === 'section-179'"
:token="token"
/>
<TemplatesView
v-if="activeView === 'templates'"
:category-items="categoryItems"
@@ -315,6 +319,7 @@ import AssignmentsView from './views/AssignmentsView.vue';
import BooksView from './views/BooksView.vue';
import GlJournalsView from './views/GlJournalsView.vue';
import CloseView from './views/CloseView.vue';
import Section179View from './views/Section179View.vue';
import ContactsView from './views/ContactsView.vue';
import AssetsView from './views/AssetsView.vue';
import DashboardView from './views/DashboardView.vue';
@@ -347,6 +352,7 @@ export default {
BooksView,
GlJournalsView,
CloseView,
Section179View,
ChangePasswordCard,
ContactsView,
DashboardView,
@@ -518,6 +524,7 @@ export default {
books: 'Configure books, assign tax rule sets, and set entry defaults',
'gl-journals': 'General ledger rollup and editable journal entries per book',
close: 'Guided month-end / year-end close: post, reconcile, and lock the period',
'section-179': 'Section 179 deduction limitation, taxable-income cap, and carryover',
templates: 'JSON-driven entry defaults and custom fields',
'tax-rules': 'Upload, edit, export, and activate depreciation rule sets',
admin: 'Users, configuration, and integrations',
@@ -543,6 +550,7 @@ export default {
books: 'Books',
'gl-journals': 'GL and Journals',
close: 'Period Close',
'section-179': 'Section 179',
templates: 'Templates',
'tax-rules': 'Tax rules',
admin: 'Configuration',
@@ -611,6 +619,7 @@ export default {
books: ['finance.manage'],
'gl-journals': ['finance.view', 'finance.manage'],
close: ['finance.close', 'finance.manage'],
'section-179': ['finance.view', 'finance.manage'],
'tax-rules': ['finance.manage'],
'admin-users': ['admin.users'],
'admin-security': ['admin.security'],

View File

@@ -53,7 +53,8 @@
<v-select v-model="localAsset.new_or_used" :items="newUsedItems" item-title="title" item-value="value" label="New / used" />
<v-text-field v-model.number="localAsset.business_use_percent" type="number" label="Business use %" />
<v-text-field v-model.number="localAsset.investment_use_percent" type="number" label="Investment use %" />
<v-switch v-model="localAsset.listed_property" label="Listed property (luxury auto limits)" color="primary" density="compact" hide-details class="full" />
<v-switch v-model="localAsset.listed_property" label="Listed property (≤50% business use → ADS straight-line)" color="primary" density="compact" hide-details class="full" />
<v-switch v-model="localAsset.passenger_auto" label="Passenger automobile (§280F annual depreciation caps)" color="primary" density="compact" hide-details class="full" />
<v-select
v-model="localAsset.special_zone"
@@ -895,6 +896,8 @@ export default {
useful_life_months: Number(asset.useful_life_months || 0),
salvage_value: Number(asset.salvage_value || 0),
business_use_percent: Number(asset.business_use_percent || 0),
listed_property: Boolean(asset.listed_property),
passenger_auto: Boolean(asset.passenger_auto),
depreciation_method: asset.depreciation_method ?? null,
special_zone: asset.special_zone ?? null,
books: (asset.books || []).map((b) => ({

View File

@@ -0,0 +1,124 @@
<template>
<v-card class="span-12 panel-pad">
<div class="toolbar-row mb-3">
<div>
<h2 class="section-title">§280F passenger-auto limits</h2>
<p class="quiet text-body-2">
Annual luxury-automobile depreciation caps (IRS Rev. Proc., indexed yearly). When an asset is flagged as a
<strong>passenger automobile</strong>, the engine limits each year's depreciation to the applicable cap (reduced by
business-use %), using the higher first-year figure when bonus depreciation is claimed.
</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="pis_year" :page-size="10" has-actions empty-text="No §280F caps configured — passenger autos are uncapped.">
<template #cell-year1_no_bonus="{ row }">${{ Number(row.year1_no_bonus).toLocaleString() }}</template>
<template #cell-year1_bonus="{ row }">${{ Number(row.year1_bonus).toLocaleString() }}</template>
<template #cell-year2="{ row }">${{ Number(row.year2).toLocaleString() }}</template>
<template #cell-year3="{ row }">${{ Number(row.year3).toLocaleString() }}</template>
<template #cell-year4plus="{ row }">${{ Number(row.year4plus).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="520">
<v-card>
<v-card-title>{{ form.exists ? `Edit ${form.pis_year}` : 'New §280F year' }}</v-card-title>
<v-card-text>
<div class="form-grid">
<v-text-field v-model.number="form.pis_year" type="number" label="Placed-in-service year *" :disabled="form.exists" />
<v-text-field v-model.number="form.year1_no_bonus" type="number" prefix="$" label="Year 1 (no bonus)" />
<v-text-field v-model.number="form.year1_bonus" type="number" prefix="$" label="Year 1 (with bonus)" />
<v-text-field v-model.number="form.year2" type="number" prefix="$" label="Year 2" />
<v-text-field v-model.number="form.year3" type="number" prefix="$" label="Year 3" />
<v-text-field v-model.number="form.year4plus" type="number" prefix="$" label="Year 4 and later" />
</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.pis_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 caps</v-card-title>
<v-card-text><p>Remove the §280F caps for <strong>{{ deleteTarget?.pis_year }}</strong>? Autos placed in service that year fall back to the nearest earlier 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';
// §280F passenger-automobile annual depreciation caps editor (Admin → Application Configuration). Mirrors
// the §179 limits editor; CRUD against /api/section-280f-limits.
function blankForm() {
return { exists: false, pis_year: new Date().getFullYear(), year1_no_bonus: 0, year1_bonus: 0, year2: 0, year3: 0, year4plus: 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: 'pis_year', label: 'Year' },
{ key: 'year1_no_bonus', label: 'Y1 (no bonus)' },
{ key: 'year1_bonus', label: 'Y1 (bonus)' },
{ key: 'year2', label: 'Y2' },
{ key: 'year3', label: 'Y3' },
{ key: 'year4plus', label: 'Y4+' }
]
};
},
mounted() { this.load(); },
methods: {
async api(path, options = {}) { return apiRequest(path, this.token, options); },
async load() {
this.error = '';
try { this.limits = (await (await this.api('/api/section-280f-limits')).json()).limits; }
catch (error) { this.error = error.message; }
},
openNew() { this.form = blankForm(); this.dialogError = ''; this.dialog = true; },
openEdit(row) { this.form = { exists: true, ...row }; this.dialogError = ''; this.dialog = true; },
async save() {
if (!this.form.pis_year) return;
this.saving = true; this.dialogError = '';
try {
const method = this.form.exists ? 'PUT' : 'POST';
const url = this.form.exists ? `/api/section-280f-limits/${this.form.pis_year}` : '/api/section-280f-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-280f-limits/${target.pis_year}`, { method: 'DELETE' }); await this.load(); this.$emit('updated'); }
catch (error) { this.error = error.message; }
}
}
};
</script>

View File

@@ -339,6 +339,36 @@
+ $100,000 §179 increase, placed in service 20072008).
</p>
<h3>Listed property &amp; passenger automobiles (§280F)</h3>
<p>
Two switches on an assets Details tab drive the §280F rules (IRS Pub 946):
</p>
<ul>
<li><strong>Listed property</strong> when such an asset is used <strong>50% or less</strong> for qualified
business (its <em>Business use %</em>), the engine automatically switches it to <strong>ADS straight-line</strong>
and disallows §179 and bonus depreciation, exactly as §280F requires. Above 50%, it depreciates normally.</li>
<li><strong>Passenger automobile</strong> flags the asset for the annual <strong>luxury-auto depreciation caps</strong>.
Each years depreciation is limited to the §280F cap for the placed-in-service year (the higher first-year figure
applies when bonus is claimed), reduced by the business-use %. Any basis not recovered within the recovery period
continues at the final-year cap until the asset is fully depreciated.</li>
</ul>
<v-alert type="info" variant="tonal" density="comfortable" class="manual-callout">
The yearly cap amounts are maintained in <strong>Admin Application Configuration §280F passenger-auto limits</strong>
(indexed annually by the IRS); like the §179 limits, they are editable data no software update needed when the IRS
publishes new figures.
</v-alert>
<h3>Section 179 limitation &amp; carryover</h3>
<p>
While the engine caps <em>each assets</em> §179 at the annual dollar limit and investment phase-out, the
<strong>Financial Section 179</strong> screen provides the <strong>return-level</strong> view: it totals the §179
elected across the companys assets for a tax year and applies the <strong>annual dollar limit</strong> (reduced by the
phase-out once §179 property cost exceeds the threshold) <em>and</em> your <strong>business taxable-income limit</strong>
(§179 cant create a loss). Enter your taxable income, and the worksheet shows the allowed deduction and the
<strong>carryover</strong> of any disallowed amount. Saving stores the carryover so the <strong>next year automatically
picks it up</strong> as additional available §179.
</p>
<h3>The PM (maintenance) book</h3>
<p>
A dedicated <strong>PM book</strong> tracks actual maintenance <em>spend</em> per asset rather than depreciation. Its ledger
@@ -864,7 +894,7 @@
<strong>Admin</strong> (admin role) is where the application is configured. It is split into sub-screens in the
navigation rail: <strong>Users &amp; Teams</strong>, <strong>Password &amp; Security</strong>,
<strong>Activity &amp; Audit Trail</strong>, <strong>Application Configuration</strong> (companies, application
settings, asset classes, depreciation zones, Section 179 limits, asset ID templates, asset categories,
settings, asset classes, depreciation zones, Section 179 limits, §280F passenger-auto limits, asset ID templates, asset categories,
asset departments, PM categories, parts categories, maintenance defaults, tax rule sets), and
<strong>Integrations</strong> (email, webhook, Microsoft Teams, ServiceNow, Workday). Each is described below.
</p>
@@ -1186,7 +1216,7 @@ export default {
{ id: 'assets', title: 'Assets — creating & managing', icon: 'mdi-archive-search-outline', keywords: 'asset register create edit import export mass edit workbench files notes tasks warranty' },
{ id: 'lifecycle', title: 'Asset lifecycle', icon: 'mdi-cash-remove', keywords: 'dispose disposal sale retire gain loss lease amortization impairment adjustment write down' },
{ id: 'barcodes', title: 'Barcodes & scanning', icon: 'mdi-barcode-scan', keywords: 'barcode label print scan camera tag' },
{ id: 'books', title: 'Books, depreciation & tax', icon: 'mdi-book-open-variant', keywords: 'book gaap federal depreciation general ledger gl method convention 179 bonus reports journal entry entries import export csv excel conflict merge history zone section 179' },
{ id: 'books', title: 'Books, depreciation & tax', icon: 'mdi-book-open-variant', keywords: 'book gaap federal depreciation general ledger gl method convention 179 bonus reports journal entry entries import export csv excel conflict merge history zone section 179 280f listed property passenger automobile luxury auto caps ads straight line mid-quarter limitation carryover taxable income macrs' },
{ id: 'close', title: 'Period close (month & year-end)', icon: 'mdi-calendar-lock', keywords: 'close month end year end period lock reconcile subledger gl roll forward retained earnings depreciation post reopen finalize wip capitalize disposal impairment audit' },
{ id: 'taxrules', title: 'Tax rule sets', icon: 'mdi-scale-balance', keywords: 'tax rules jurisdiction method limits activate version' },
{ id: 'templates', title: 'Templates', icon: 'mdi-form-select', keywords: 'template defaults custom fields data entry' },

View File

@@ -162,6 +162,7 @@ export const navItems = [
{ key: 'books', label: 'Books', icon: 'mdi-book-open-variant' },
{ key: 'gl-journals', label: 'GL and Journals', icon: 'mdi-book-account-outline' },
{ key: 'close', label: 'Period Close', icon: 'mdi-calendar-lock' },
{ key: 'section-179', label: 'Section 179', icon: 'mdi-cash-multiple' },
{ key: 'tax-rules', label: 'Tax rules', icon: 'mdi-scale-balance' }
]
},

View File

@@ -194,6 +194,8 @@
<Section179LimitSettings :token="token" />
<Section280fLimitSettings :token="token" />
<AssetIdTemplateSettings :token="token" />
<CategorySettings :token="token" @updated="$emit('categories-updated')" />
@@ -323,6 +325,7 @@ 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 Section280fLimitSettings from '../components/Section280fLimitSettings.vue';
import NotificationSettings from '../components/NotificationSettings.vue';
import PartCategorySettings from '../components/PartCategorySettings.vue';
import PmCategorySettings from '../components/PmCategorySettings.vue';
@@ -332,7 +335,7 @@ import TeamsSettings from '../components/TeamsSettings.vue';
import WebhookSettings from '../components/WebhookSettings.vue';
export default {
components: { AppLogsSettings, AssetClassSettings, AssetIdTemplateSettings, AuditTrailSettings, CategorySettings, CompanySettings, DataTable, DepartmentSettings, DepreciationZoneSettings, NotificationSettings, PartCategorySettings, PasswordPolicySettings, PmCategorySettings, PmSettings, Section179LimitSettings, ServiceNowSettings, TeamsSettings, WebhookSettings },
components: { AppLogsSettings, AssetClassSettings, AssetIdTemplateSettings, AuditTrailSettings, CategorySettings, CompanySettings, DataTable, DepartmentSettings, DepreciationZoneSettings, NotificationSettings, PartCategorySettings, PasswordPolicySettings, PmCategorySettings, PmSettings, Section179LimitSettings, Section280fLimitSettings, ServiceNowSettings, TeamsSettings, WebhookSettings },
props: {
token: { type: String, default: '' },
section: { type: String, default: 'admin-users' },

View File

@@ -0,0 +1,109 @@
<template>
<section class="content-grid">
<v-card class="span-12 panel-pad">
<h2 class="section-title mb-1">Section 179 deduction limitation &amp; carryover</h2>
<p class="quiet text-body-2 mb-3">
Return-level §179: the total §179 elected across this company's assets for a tax year, capped by the annual dollar
limit (reduced by the investment phase-out) and by your business taxable income. Any disallowed amount carries
forward to next year. The per-asset engine still caps each asset individually; this is the aggregate return view.
</p>
<div class="toolbar-row" style="flex-wrap:wrap;gap:12px">
<v-select v-model="book" :items="bookItems" label="Book" density="compact" hide-details style="max-width:200px" @update:model-value="load" />
<v-text-field v-model.number="year" type="number" label="Tax year" density="compact" hide-details style="max-width:130px" @update:model-value="load" />
<v-text-field v-model.number="taxableIncome" type="number" prefix="$" label="Business taxable income" density="compact" hide-details style="max-width:240px" hint="Leave blank to skip the income limit" persistent-hint />
<v-btn color="primary" prepend-icon="mdi-content-save" :loading="saving" @click="save">Save &amp; carry forward</v-btn>
</div>
<v-alert v-if="message" type="success" density="compact" class="mt-3">{{ message }}</v-alert>
<v-alert v-if="error" type="error" density="compact" class="mt-3">{{ error }}</v-alert>
</v-card>
<v-card v-if="s" class="span-7 panel-pad">
<h3 class="section-title mb-3">Limitation worksheet — {{ s.book_code }} {{ s.tax_year }}</h3>
<table class="worksheet">
<tbody>
<tr><td>§179 elected ({{ s.asset_count }} asset{{ s.asset_count === 1 ? '' : 's' }})</td><td class="r">{{ currency(s.elected) }}</td></tr>
<tr><td>Carryover from {{ s.tax_year - 1 }}</td><td class="r">{{ currency(s.prior_carryover) }}</td></tr>
<tr class="sub"><td>Available for deduction</td><td class="r">{{ currency(s.available) }}</td></tr>
<tr><td>Annual dollar limit{{ s.base_limit ? ` (base ${currency(s.base_limit)})` : '' }}</td><td class="r">{{ s.dollar_limit === null ? 'uncapped' : currency(s.dollar_limit) }}</td></tr>
<tr v-if="s.phaseout_reduction"><td class="quiet">— investment phase-out reduction (cost {{ currency(s.property_cost) }} over {{ currency(s.phaseout_threshold) }})</td><td class="r quiet">{{ currency(s.phaseout_reduction) }}</td></tr>
<tr class="sub"><td>Allowed after dollar limit</td><td class="r">{{ currency(s.dollar_allowed) }}</td></tr>
<tr v-if="s.taxable_income !== null"><td>Business taxable income limit</td><td class="r">{{ currency(s.taxable_income) }}</td></tr>
<tr class="total"><td>Allowed §179 deduction</td><td class="r">{{ currency(s.allowed_deduction) }}</td></tr>
<tr class="carry"><td>Carryover to {{ s.tax_year + 1 }}</td><td class="r">{{ currency(s.carryover_to_next) }}</td></tr>
</tbody>
</table>
<v-alert v-if="s.income_limited" type="info" density="compact" variant="tonal" class="mt-3">
The deduction is limited by taxable income; {{ currency(s.carryover_to_next) }} carries forward.
</v-alert>
</v-card>
<v-card v-if="s" class="span-5 panel-pad">
<h3 class="section-title mb-2">How it works</h3>
<ul class="quiet text-body-2">
<li>§179 elected = sum of each asset's §179 amount on the selected book, placed in service in the year.</li>
<li>The <strong>annual dollar limit</strong> ({{ year }}) is reduced dollar-for-dollar once §179 property cost exceeds the phase-out threshold.</li>
<li>The <strong>taxable-income limit</strong> caps the deduction at your business income; it can't create a loss.</li>
<li><strong>Carryover</strong> = available allowed, and is added to next year's available amount when you save.</li>
</ul>
<p class="quiet text-caption mt-2">Limit figures are maintained in Admin Application Configuration Section 179 limits.</p>
</v-card>
</section>
</template>
<script>
import { apiRequest } from '../services/api';
import { currency } from '../utils/format';
// Section 179 return-level limitation & carryover. Aggregates §179 elected for a (book, year), applies the
// annual dollar limit + phase-out and the business taxable-income limit, and persists the carryover so the
// next year picks it up. Backed by /api/section-179/limitation (GET summary, PUT save).
export default {
props: { token: { type: String, required: true } },
data() {
return { books: [], book: 'FEDERAL', year: new Date().getFullYear(), taxableIncome: null, s: null, saving: false, message: '', error: '' };
},
computed: {
bookItems() { return this.books.map((b) => b.code); }
},
mounted() { this.loadBooks(); },
methods: {
currency,
async api(path, options = {}) { return apiRequest(path, this.token, options); },
async loadBooks() {
try {
const data = await (await this.api('/api/books')).json();
this.books = data.books;
const primary = this.books.find((b) => b.is_primary && b.code !== 'GAAP') || this.books.find((b) => b.code === 'FEDERAL') || this.books[0];
if (primary) this.book = primary.code;
} catch (error) { this.error = error.message; }
this.load();
},
async load() {
this.error = '';
try {
const q = new URLSearchParams({ book: this.book, year: String(this.year) });
const data = await (await this.api(`/api/section-179/limitation?${q.toString()}`)).json();
this.s = data.summary;
if (this.taxableIncome === null && this.s.taxable_income !== null) this.taxableIncome = this.s.taxable_income;
} catch (error) { this.error = error.message; }
},
async save() {
this.saving = true; this.message = ''; this.error = '';
try {
const data = await (await this.api('/api/section-179/limitation', { method: 'PUT', body: JSON.stringify({ book: this.book, year: this.year, taxable_income: this.taxableIncome }) })).json();
this.s = data.summary;
this.message = `Saved. Carryover to ${this.s.tax_year + 1}: ${currency(this.s.carryover_to_next)}.`;
} catch (error) { this.error = error.message; } finally { this.saving = false; }
}
}
};
</script>
<style scoped>
.worksheet { width: 100%; border-collapse: collapse; }
.worksheet td { padding: 6px 8px; border-bottom: 1px solid rgba(128, 128, 128, 0.14); }
.worksheet td.r { text-align: right; font-variant-numeric: tabular-nums; }
.worksheet tr.sub td { font-weight: 600; border-top: 1px solid rgba(128, 128, 128, 0.3); }
.worksheet tr.total td { font-weight: 700; font-size: 1.05rem; border-top: 2px solid rgba(var(--v-theme-primary), 0.5); }
.worksheet tr.carry td { color: rgb(var(--v-theme-primary)); font-weight: 600; }
</style>