Added Wizard for Month/Year End Close
This commit is contained in:
@@ -171,6 +171,10 @@
|
||||
v-if="activeView === 'gl-journals'"
|
||||
:token="token"
|
||||
/>
|
||||
<CloseView
|
||||
v-if="activeView === 'close'"
|
||||
:token="token"
|
||||
/>
|
||||
<TemplatesView
|
||||
v-if="activeView === 'templates'"
|
||||
:category-items="categoryItems"
|
||||
@@ -313,6 +317,7 @@ import AlertsView from './views/AlertsView.vue';
|
||||
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 ContactsView from './views/ContactsView.vue';
|
||||
import AssetsView from './views/AssetsView.vue';
|
||||
import DashboardView from './views/DashboardView.vue';
|
||||
@@ -351,6 +356,7 @@ export default {
|
||||
AssetsView,
|
||||
BooksView,
|
||||
GlJournalsView,
|
||||
CloseView,
|
||||
ChangePasswordCard,
|
||||
ContactsView,
|
||||
DashboardView,
|
||||
@@ -534,6 +540,7 @@ export default {
|
||||
reports: 'Depreciation schedules, monthly expense, and book values',
|
||||
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',
|
||||
templates: 'JSON-driven entry defaults and custom fields',
|
||||
'tax-rules': 'Upload, edit, export, and activate depreciation rule sets',
|
||||
admin: 'Users, configuration, and integrations',
|
||||
@@ -558,6 +565,7 @@ export default {
|
||||
reports: 'Reports',
|
||||
books: 'Books',
|
||||
'gl-journals': 'GL and Journals',
|
||||
close: 'Period Close',
|
||||
templates: 'Templates',
|
||||
'tax-rules': 'Tax rules',
|
||||
admin: 'Configuration',
|
||||
@@ -625,6 +633,7 @@ export default {
|
||||
reports: ['finance.view'],
|
||||
books: ['finance.manage'],
|
||||
'gl-journals': ['finance.view', 'finance.manage'],
|
||||
close: ['finance.close', 'finance.manage'],
|
||||
'tax-rules': ['finance.manage'],
|
||||
'admin-users': ['admin.users'],
|
||||
'admin-security': ['admin.security'],
|
||||
|
||||
39
src/components/CloseTable.vue
Normal file
39
src/components/CloseTable.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div style="overflow-x:auto">
|
||||
<table class="asset-table">
|
||||
<thead>
|
||||
<tr><th v-for="col in columns" :key="col.key">{{ col.label }}</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(row, i) in rows" :key="i">
|
||||
<td v-for="col in columns" :key="col.key" :class="{ 'text-right': col.format === 'currency' }">
|
||||
<slot :name="`cell-${col.key}`" :row="row">{{ display(row[col.key], col.format) }}</slot>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!rows.length && empty">
|
||||
<td :colspan="columns.length" class="quiet">{{ empty }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { currency } from '../utils/format';
|
||||
|
||||
// Compact, read-only table for the close wizard's step panels. Columns are { key, label, format }; a
|
||||
// `cell-<key>` slot overrides rendering for a column. Lighter than DataTable (no search/pagination).
|
||||
export default {
|
||||
props: {
|
||||
rows: { type: Array, default: () => [] },
|
||||
columns: { type: Array, required: true },
|
||||
empty: { type: String, default: '' }
|
||||
},
|
||||
methods: {
|
||||
display(value, format) {
|
||||
if (value === null || value === undefined || value === '') return format === 'currency' ? currency(0) : '';
|
||||
return format === 'currency' ? currency(value) : value;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -374,6 +374,126 @@
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<!-- PERIOD CLOSE -->
|
||||
<section v-show="isShown('close')" class="manual-section">
|
||||
<h2>Period close — month-end & year-end</h2>
|
||||
<p>
|
||||
<strong>Financial → Period Close</strong> is a guided wizard that walks you through closing an accounting
|
||||
period for the fixed-asset subledger. It reviews the period's activity, <strong>posts the journal entries the
|
||||
close generates into the general ledger</strong>, reconciles the Fixed Asset Register to the GL, and then
|
||||
<strong>locks the period</strong> so it can't be changed after the fact. Every action is recorded in
|
||||
<a href="#" @click.prevent="select('admin')">Activity & Audit Trail</a>.
|
||||
</p>
|
||||
|
||||
<h3>Scope: which book, which period</h3>
|
||||
<ul>
|
||||
<li>A close runs against <strong>one book</strong> — pick it at the top of the screen; it defaults to your
|
||||
<strong>primary book</strong> (the one that feeds the financial GL). Run a separate close for each book you
|
||||
maintain (for example, close GAAP for the financial GL; tax books are reviewed at year-end).</li>
|
||||
<li>Choose <strong>Month</strong> (a fiscal year + month) or <strong>Year</strong> (a full fiscal year), then
|
||||
<strong>Start / Resume</strong>. A close in progress is saved, so you can leave and come back to it. The
|
||||
<strong>Recent closes</strong> list shows every period with its status and a 🔒 lock badge.</li>
|
||||
</ul>
|
||||
|
||||
<h3>The month-end checklist</h3>
|
||||
<p>Each step shows the live data for the period and lets you act, then mark it done:</p>
|
||||
<ol>
|
||||
<li><strong>Record additions</strong> — review capital purchases placed in service in the period. Any asset
|
||||
below your <strong>capitalization threshold</strong> is flagged so you can confirm it belongs on the register.</li>
|
||||
<li><strong>Record disposals</strong> — review assets sold, scrapped, or retired in the period (cost,
|
||||
accumulated depreciation, gain/loss). You can <strong>post the disposal journal entries</strong> here.</li>
|
||||
<li><strong>Capitalize WIP</strong> — review Construction-in-progress assets and <strong>capitalize</strong>
|
||||
completed ones, which moves them into service and begins depreciation.</li>
|
||||
<li><strong>Process & post depreciation</strong> — compute the period's depreciation and <strong>post it to
|
||||
the GL</strong>, allocated by department / cost center. Fully-depreciated assets that would still compute
|
||||
depreciation are flagged for review.</li>
|
||||
<li><strong>Check for impairments</strong> — review flagged assets and record any impairment loss (from the
|
||||
asset's Adjustments tab).</li>
|
||||
<li><strong>Reconcile subledger to GL</strong> — compare the register to the GL control accounts and clear or
|
||||
acknowledge any variance (see below). <em>Required to finalize.</em></li>
|
||||
<li><strong>Generate reports</strong> — run and file the Fixed Asset Register, Depreciation Summary, and
|
||||
Additions / Disposals reports for your records.</li>
|
||||
<li><strong>Finalize & lock</strong> — freeze the period.</li>
|
||||
</ol>
|
||||
|
||||
<h3>What gets posted to the GL</h3>
|
||||
<p>
|
||||
The close writes <strong>real, balanced journal entries</strong> into the stored GL layer (visible under
|
||||
<a href="#" @click.prevent="select('books')">Books → GL and Journals</a>). The accounts come from your
|
||||
<strong>GL posting defaults</strong> (per-asset overrides win where set):
|
||||
</p>
|
||||
<table class="manual-table">
|
||||
<thead><tr><th>Event</th><th>Debit</th><th>Credit</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>Period depreciation (by department)</td><td>Depreciation Expense</td><td>Accumulated Depreciation</td></tr>
|
||||
<tr><td>Capitalize WIP</td><td>Fixed Asset cost</td><td>Construction-in-progress (CWIP)</td></tr>
|
||||
<tr><td>Disposal</td><td>Accum. depreciation + Proceeds (+ Loss)</td><td>Asset cost (+ Gain)</td></tr>
|
||||
<tr><td>Year-end roll-forward</td><td>Retained Earnings</td><td>Depreciation Expense</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<v-alert type="info" variant="tonal" density="comfortable" class="manual-callout">
|
||||
Every close posting is <strong>tagged to its period</strong>, which makes posting <strong>idempotent</strong>:
|
||||
re-running a step (for example, re-posting depreciation after a correction) replaces the prior entries rather
|
||||
than duplicating them. Manage the accounts these entries use in
|
||||
<a href="#" @click.prevent="select('books')">Books → GL and Journals → Chart of accounts</a> (the
|
||||
<strong>posting defaults</strong>: asset cost, accumulated depreciation, depreciation expense, CWIP, gain/loss,
|
||||
proceeds clearing, and retained earnings).
|
||||
</v-alert>
|
||||
|
||||
<h3>Reconciling the subledger to the GL</h3>
|
||||
<p>
|
||||
The <strong>Reconcile</strong> step compares the Fixed Asset Register (the subledger) to the GL
|
||||
control-account balances — <strong>cost</strong>, <strong>accumulated depreciation</strong>, and
|
||||
<strong>net book value</strong> — and shows the variance per account. A green <em>Balanced</em> badge means
|
||||
they agree; otherwise the variance is shown so you can clear it (post an adjusting entry) or
|
||||
<strong>acknowledge</strong> it and finalize anyway.
|
||||
</p>
|
||||
<v-alert type="info" variant="tonal" density="comfortable" class="manual-callout">
|
||||
When you <em>first</em> adopt closes, the GL won't yet hold the asset's full history, so the register
|
||||
(life-to-date) and the GL (only what's been posted) will differ — that opening variance is expected. Post an
|
||||
opening-balance entry to clear it, or acknowledge it. Once you run closes forward period over period, the two
|
||||
stay in step.
|
||||
</v-alert>
|
||||
|
||||
<h3>Finalizing & the period lock</h3>
|
||||
<p>
|
||||
<strong>Finalize & lock</strong> becomes available once the required steps are done and the reconciliation
|
||||
has been run. Finalizing snapshots the period's totals and sets the period to <strong>Closed</strong>. A
|
||||
closed period is <strong>locked</strong>:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Any change whose effective date falls inside the closed period is <strong>rejected</strong> — including
|
||||
<strong>GL journal entries</strong> (for that book), and <strong>disposals, impairments, and asset
|
||||
financial-date / cost edits</strong> (gated by the <strong>primary</strong> book's locked periods, since
|
||||
that book is the subledger that feeds the GL).</li>
|
||||
<li>Edits to non-financial fields (location, custodian, notes, and so on) are <strong>not</strong> blocked.</li>
|
||||
</ul>
|
||||
|
||||
<h3>Year-end close</h3>
|
||||
<p>The year-end checklist includes everything above, plus:</p>
|
||||
<ul>
|
||||
<li><strong>Final month-end close</strong> — confirm the final month is closed and its depreciation posted.</li>
|
||||
<li><strong>Physical inventory</strong> — confirm you've verified high-value / mobile assets still exist.</li>
|
||||
<li><strong>Update tax books</strong> — recompute tax depreciation (MACRS / §179) and review book-vs-tax
|
||||
differences.</li>
|
||||
<li><strong>Roll-forward</strong> — finalizing a year close automatically posts the <strong>roll-forward</strong>
|
||||
that clears the year's depreciation expense into <strong>Retained Earnings</strong> and locks the year.</li>
|
||||
</ul>
|
||||
|
||||
<h3>Reopening a closed period</h3>
|
||||
<p>
|
||||
If you must correct a closed period, use <strong>Reopen</strong> (requires the close permission). Reopening
|
||||
<strong>reverses all of that close's posted entries</strong>, returns the period to <em>in progress</em>,
|
||||
and unlocks it so corrections can be made — then you re-run and finalize again. The reopen, the reversed
|
||||
entries, and the re-close are all audited.
|
||||
</p>
|
||||
<v-alert type="warning" variant="tonal" density="comfortable" class="manual-callout">
|
||||
Running the close needs the <strong>“Run month-end / year-end close & reopen periods”</strong> capability
|
||||
(included in the Finance role). Because closing posts to the GL and locks the books, keep this permission to
|
||||
the people responsible for the close.
|
||||
</v-alert>
|
||||
</section>
|
||||
|
||||
<!-- TAX RULES -->
|
||||
<section v-show="isShown('taxrules')" class="manual-section">
|
||||
<h2>Tax rule sets</h2>
|
||||
@@ -1067,6 +1187,7 @@ export default {
|
||||
{ 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: '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' },
|
||||
{ id: 'pm', title: 'Preventative maintenance', icon: 'mdi-wrench-clock', keywords: 'pm maintenance plan steps complete close signature ratings guidance photos schedule meter usage reading hours lifespan wear curve dynamic nightly recompute' },
|
||||
|
||||
@@ -161,6 +161,7 @@ export const navItems = [
|
||||
children: [
|
||||
{ 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: 'tax-rules', label: 'Tax rules', icon: 'mdi-scale-balance' }
|
||||
]
|
||||
},
|
||||
|
||||
383
src/views/CloseView.vue
Normal file
383
src/views/CloseView.vue
Normal file
@@ -0,0 +1,383 @@
|
||||
<template>
|
||||
<section class="content-grid">
|
||||
<!-- Launcher -->
|
||||
<v-card class="span-12 panel-pad">
|
||||
<div class="toolbar-row mb-1">
|
||||
<h2 class="section-title">Period Close</h2>
|
||||
</div>
|
||||
<p class="quiet text-body-2 mb-3">
|
||||
Guided month-end / year-end close for the fixed-asset subledger. Posts the period's journal entries to the GL,
|
||||
reconciles the register, and locks the period against backdated changes. Every action is recorded in
|
||||
Admin → Activity & Audit Trail.
|
||||
</p>
|
||||
<div class="toolbar-row" style="flex-wrap:wrap;gap:12px">
|
||||
<v-select v-model="form.book" :items="bookItems" label="Book" density="compact" hide-details style="max-width:200px" />
|
||||
<v-select v-model="form.period_type" :items="periodTypeItems" item-title="title" item-value="value" label="Period" density="compact" hide-details style="max-width:140px" />
|
||||
<v-text-field v-model.number="form.fiscal_year" type="number" label="Fiscal year" density="compact" hide-details style="max-width:130px" />
|
||||
<v-select v-if="form.period_type === 'month'" v-model="form.period_month" :items="monthItems" item-title="title" item-value="value" label="Month" density="compact" hide-details style="max-width:160px" />
|
||||
<v-btn color="primary" prepend-icon="mdi-play" :loading="starting" @click="startClose">Start / Resume</v-btn>
|
||||
</div>
|
||||
<v-alert v-if="error" type="error" density="compact" class="mt-3">{{ error }}</v-alert>
|
||||
</v-card>
|
||||
|
||||
<!-- Active close: the wizard -->
|
||||
<v-card v-if="period" class="span-8 panel-pad">
|
||||
<div class="toolbar-row mb-3">
|
||||
<div>
|
||||
<h2 class="section-title">
|
||||
{{ window.label }} · {{ period.book_code }}
|
||||
<v-chip size="small" :color="statusColor" variant="tonal" class="ml-2">{{ period.status }}</v-chip>
|
||||
<v-icon v-if="period.locked" icon="mdi-lock" color="error" class="ml-1" />
|
||||
</h2>
|
||||
<div class="quiet text-body-2">{{ period.period_type === 'year' ? 'Year-end close' : 'Month-end close' }}</div>
|
||||
</div>
|
||||
<v-spacer />
|
||||
<v-btn v-if="period.locked" color="warning" variant="tonal" size="small" prepend-icon="mdi-lock-open" :loading="busy" @click="reopen">Reopen</v-btn>
|
||||
<v-btn v-else color="success" prepend-icon="mdi-lock-check" :loading="busy" :disabled="!canFinalize" @click="finalize">Finalize & lock</v-btn>
|
||||
</div>
|
||||
<v-alert v-if="!period.locked && !canFinalize" type="info" density="compact" variant="tonal" class="mb-3">
|
||||
Complete the required steps to finalize: {{ missingRequired.join(', ') || 'run the reconciliation' }}.
|
||||
</v-alert>
|
||||
<v-alert v-if="message" type="success" density="compact" class="mb-3">{{ message }}</v-alert>
|
||||
|
||||
<!-- Step cards -->
|
||||
<div v-for="(step, index) in steps" :key="step.key" class="close-step" :class="{ done: isDone(step.key) }">
|
||||
<div class="toolbar-row">
|
||||
<div class="step-num">{{ index + 1 }}</div>
|
||||
<div>
|
||||
<div class="font-weight-medium">
|
||||
{{ step.title }}
|
||||
<v-chip v-if="step.required" size="x-small" color="primary" variant="tonal" class="ml-1">required</v-chip>
|
||||
<v-icon v-if="isDone(step.key)" icon="mdi-check-circle" color="success" size="small" class="ml-1" />
|
||||
</div>
|
||||
<div class="quiet text-caption">{{ step.description }}</div>
|
||||
</div>
|
||||
<v-spacer />
|
||||
<v-btn v-if="!period.locked" size="x-small" :variant="isDone(step.key) ? 'tonal' : 'outlined'" :color="isDone(step.key) ? 'success' : undefined" @click="toggleStep(step.key)">
|
||||
{{ isDone(step.key) ? 'Done' : 'Mark done' }}
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<div class="step-body">
|
||||
<!-- Additions -->
|
||||
<template v-if="step.key === 'additions'">
|
||||
<div class="quiet text-caption mb-1" v-if="data.threshold">Capitalization threshold: {{ currency(data.threshold) }}</div>
|
||||
<CloseTable :rows="data.additions" :columns="addCols" empty="No additions in this period.">
|
||||
<template #cell-below="{ row }"><v-chip v-if="row.below_threshold" size="x-small" color="warning" variant="tonal">below threshold</v-chip></template>
|
||||
</CloseTable>
|
||||
</template>
|
||||
|
||||
<!-- Disposals -->
|
||||
<template v-else-if="step.key === 'disposals'">
|
||||
<CloseTable :rows="data.disposals" :columns="dispCols" empty="No disposals in this period." />
|
||||
<v-btn v-if="!period.locked && data.disposals.length" size="x-small" variant="tonal" class="mt-2" prepend-icon="mdi-bank-transfer" :loading="busy" @click="postDisposals">Post disposal entries</v-btn>
|
||||
</template>
|
||||
|
||||
<!-- WIP -->
|
||||
<template v-else-if="step.key === 'wip'">
|
||||
<CloseTable :rows="data.wip" :columns="wipCols" empty="No work-in-progress assets.">
|
||||
<template #cell-action="{ row }">
|
||||
<v-btn v-if="!period.locked" size="x-small" color="primary" variant="tonal" :loading="busy" @click="capitalize(row)">Capitalize</v-btn>
|
||||
</template>
|
||||
</CloseTable>
|
||||
</template>
|
||||
|
||||
<!-- Depreciation -->
|
||||
<template v-else-if="step.key === 'depreciation'">
|
||||
<div class="ledger-summary mb-2">
|
||||
<div class="ledger-chip"><span class="quiet">Period depreciation</span><strong>{{ currency(data.depreciation.total) }}</strong></div>
|
||||
</div>
|
||||
<CloseTable :rows="data.depreciation.by_department" :columns="deptCols" empty="No depreciation this period." />
|
||||
<v-alert v-if="data.depreciation.fully_depreciated_still_running.length" type="warning" density="compact" variant="tonal" class="mt-2">
|
||||
{{ data.depreciation.fully_depreciated_still_running.length }} fully-depreciated asset(s) still computing depreciation — review.
|
||||
</v-alert>
|
||||
<v-btn v-if="!period.locked" size="small" color="primary" class="mt-2" prepend-icon="mdi-bank-plus" :loading="busy" @click="postDepreciation">Post depreciation</v-btn>
|
||||
</template>
|
||||
|
||||
<!-- Impairments -->
|
||||
<template v-else-if="step.key === 'impairments'">
|
||||
<CloseTable :rows="data.impairments" :columns="impairCols" empty="No impairment candidates flagged." />
|
||||
<div class="quiet text-caption mt-1">Record impairments from the asset's Adjustments tab; they post when this period is reconciled.</div>
|
||||
</template>
|
||||
|
||||
<!-- Reconcile -->
|
||||
<template v-else-if="step.key === 'reconcile'">
|
||||
<v-btn v-if="!period.locked" size="small" variant="tonal" prepend-icon="mdi-scale-balance" :loading="busy" @click="runReconcile">Run reconciliation</v-btn>
|
||||
<div v-if="reconciliation" class="mt-2">
|
||||
<v-chip size="small" :color="reconciliation.balanced ? 'success' : 'warning'" variant="tonal" class="mb-2">
|
||||
{{ reconciliation.balanced ? 'Balanced' : `Variance ${currency(reconciliation.max_variance)} — clear or acknowledge` }}
|
||||
</v-chip>
|
||||
<CloseTable :rows="reconciliation.lines" :columns="reconCols" empty="" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Reports -->
|
||||
<template v-else-if="step.key === 'reports'">
|
||||
<div class="quiet text-body-2">Run and file the final reports for the period:</div>
|
||||
<ul class="quiet text-body-2 mt-1">
|
||||
<li>Fixed Asset Register · Depreciation Summary · Additions / Disposals</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<!-- Year-only confirm steps -->
|
||||
<template v-else-if="['final_month_close', 'physical_inventory', 'tax_books'].includes(step.key)">
|
||||
<div class="quiet text-body-2">{{ step.description }} Mark done once verified.</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</v-card>
|
||||
|
||||
<!-- History -->
|
||||
<v-card :class="period ? 'span-4 panel-pad' : 'span-12 panel-pad'">
|
||||
<h3 class="section-title mb-2">Recent closes</h3>
|
||||
<v-list density="compact">
|
||||
<v-list-item
|
||||
v-for="p in periods"
|
||||
:key="p.id"
|
||||
:active="period && p.id === period.id"
|
||||
rounded="sm"
|
||||
@click="openPeriod(p.id)"
|
||||
>
|
||||
<v-list-item-title>{{ labelFor(p) }} · {{ p.book_code }}</v-list-item-title>
|
||||
<template #append>
|
||||
<v-icon v-if="p.status === 'closed'" icon="mdi-lock" color="error" size="small" />
|
||||
<v-chip v-else size="x-small" variant="tonal">{{ p.status }}</v-chip>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="!periods.length" class="quiet text-body-2">No closes yet.</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { apiRequest } from '../services/api';
|
||||
import { currency } from '../utils/format';
|
||||
import CloseTable from '../components/CloseTable.vue';
|
||||
|
||||
// Period Close wizard (Financial → Period Close). Walks the month/year close checklist, posts the
|
||||
// period's depreciation / WIP / disposal / roll-forward journal entries to the GL, reconciles the
|
||||
// register to the GL, and finalizes (locks) the period. Server enforces gating, scoping, and the lock.
|
||||
const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
|
||||
|
||||
export default {
|
||||
components: { CloseTable },
|
||||
props: { token: { type: String, required: true } },
|
||||
data() {
|
||||
return {
|
||||
books: [],
|
||||
form: { book: 'GAAP', period_type: 'month', fiscal_year: new Date().getFullYear(), period_month: new Date().getMonth() + 1 },
|
||||
periodTypeItems: [{ title: 'Month', value: 'month' }, { title: 'Year', value: 'year' }],
|
||||
monthItems: MONTHS.map((title, i) => ({ title, value: i + 1 })),
|
||||
period: null,
|
||||
steps: [],
|
||||
data: null,
|
||||
periods: [],
|
||||
starting: false,
|
||||
busy: false,
|
||||
error: '',
|
||||
message: '',
|
||||
addCols: [
|
||||
{ key: 'asset_id', label: 'Asset' }, { key: 'description', label: 'Description' },
|
||||
{ key: 'acquisition_cost', label: 'Cost', format: 'currency' }, { key: 'in_service_date', label: 'In service' }, { key: 'below', label: '' }
|
||||
],
|
||||
dispCols: [
|
||||
{ key: 'asset_id', label: 'Asset' }, { key: 'description', label: 'Description' }, { key: 'disposal_date', label: 'Date' },
|
||||
{ key: 'disposed_cost', label: 'Cost', format: 'currency' }, { key: 'accumulated_depreciation', label: 'Accum.', format: 'currency' }, { key: 'gain_loss', label: 'Gain/(loss)', format: 'currency' }
|
||||
],
|
||||
wipCols: [
|
||||
{ key: 'asset_id', label: 'Asset' }, { key: 'description', label: 'Description' }, { key: 'acquisition_cost', label: 'Cost', format: 'currency' }, { key: 'action', label: '' }
|
||||
],
|
||||
deptCols: [{ key: 'department', label: 'Department / cost center' }, { key: 'amount', label: 'Depreciation', format: 'currency' }],
|
||||
impairCols: [{ key: 'asset_id', label: 'Asset' }, { key: 'description', label: 'Description' }, { key: 'condition', label: 'Condition' }, { key: 'acquisition_cost', label: 'Cost', format: 'currency' }],
|
||||
reconCols: [
|
||||
{ key: 'account', label: 'Account' }, { key: 'subledger', label: 'Subledger', format: 'currency' },
|
||||
{ key: 'gl', label: 'General ledger', format: 'currency' }, { key: 'variance', label: 'Variance', format: 'currency' }
|
||||
]
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
bookItems() {
|
||||
return this.books.map((b) => b.code);
|
||||
},
|
||||
window() {
|
||||
return this.data?.window || { label: '' };
|
||||
},
|
||||
reconciliation() {
|
||||
return this.data?.reconciliation || this.period?.reconciliation || null;
|
||||
},
|
||||
statusColor() {
|
||||
return { open: 'grey', in_progress: 'info', closed: 'success' }[this.period?.status] || undefined;
|
||||
},
|
||||
requiredKeys() {
|
||||
return this.steps.filter((s) => s.required).map((s) => s.key);
|
||||
},
|
||||
missingRequired() {
|
||||
return this.requiredKeys.filter((key) => !this.isDone(key));
|
||||
},
|
||||
canFinalize() {
|
||||
return this.period && !this.period.locked && this.missingRequired.length === 0 && Boolean(this.reconciliation);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadBooks();
|
||||
this.loadPeriods();
|
||||
},
|
||||
methods: {
|
||||
currency,
|
||||
async api(path, options = {}) {
|
||||
return apiRequest(path, this.token, options);
|
||||
},
|
||||
labelFor(p) {
|
||||
return p.period_type === 'year' ? `FY ${p.fiscal_year}` : `${MONTHS[(p.period_month || 1) - 1].slice(0, 3)} ${p.fiscal_year}`;
|
||||
},
|
||||
isDone(key) {
|
||||
return Boolean(this.period?.steps?.[key]?.done);
|
||||
},
|
||||
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);
|
||||
if (primary) this.form.book = primary.code;
|
||||
} catch (error) {
|
||||
this.error = error.message;
|
||||
}
|
||||
},
|
||||
async loadPeriods() {
|
||||
try {
|
||||
this.periods = (await (await this.api('/api/close-periods')).json()).periods;
|
||||
} catch (error) {
|
||||
this.error = error.message;
|
||||
}
|
||||
},
|
||||
async startClose() {
|
||||
this.starting = true;
|
||||
this.error = '';
|
||||
this.message = '';
|
||||
try {
|
||||
const data = await (await this.api('/api/close-periods/start', { method: 'POST', body: JSON.stringify(this.form) })).json();
|
||||
await this.openPeriod(data.period.id);
|
||||
await this.loadPeriods();
|
||||
} catch (error) {
|
||||
this.error = error.message;
|
||||
} finally {
|
||||
this.starting = false;
|
||||
}
|
||||
},
|
||||
async openPeriod(id) {
|
||||
this.error = '';
|
||||
this.message = '';
|
||||
try {
|
||||
const data = await (await this.api(`/api/close-periods/${id}`)).json();
|
||||
this.period = data.period;
|
||||
this.steps = data.steps;
|
||||
this.data = data.data;
|
||||
} catch (error) {
|
||||
this.error = error.message;
|
||||
}
|
||||
},
|
||||
async refresh() {
|
||||
if (this.period) await this.openPeriod(this.period.id);
|
||||
},
|
||||
async act(path, body) {
|
||||
this.busy = true;
|
||||
this.error = '';
|
||||
this.message = '';
|
||||
try {
|
||||
const res = await (await this.api(path, { method: 'POST', body: JSON.stringify(body || {}) })).json();
|
||||
await this.refresh();
|
||||
return res;
|
||||
} catch (error) {
|
||||
this.error = error.message;
|
||||
return null;
|
||||
} finally {
|
||||
this.busy = false;
|
||||
}
|
||||
},
|
||||
async toggleStep(key) {
|
||||
await this.act(`/api/close-periods/${this.period.id}/steps/${key}`, { done: !this.isDone(key) });
|
||||
},
|
||||
async postDepreciation() {
|
||||
const res = await this.act(`/api/close-periods/${this.period.id}/post-depreciation`);
|
||||
if (res) this.message = `Posted ${currency(res.summary.debit)} of depreciation across ${res.summary.expense_lines} line(s).`;
|
||||
},
|
||||
async postDisposals() {
|
||||
const res = await this.act(`/api/close-periods/${this.period.id}/post-disposals`);
|
||||
if (res) this.message = `Posted entries for ${res.result.posted} disposal(s).`;
|
||||
},
|
||||
async runReconcile() {
|
||||
const res = await this.act(`/api/close-periods/${this.period.id}/reconcile`);
|
||||
if (res) this.message = res.reconciliation.balanced ? 'Subledger reconciles to the GL.' : `Variance of ${currency(res.reconciliation.max_variance)} — clear it or mark the step done to acknowledge.`;
|
||||
},
|
||||
async capitalize(row) {
|
||||
const date = window.prompt('In-service date (YYYY-MM-DD):', new Date().toISOString().slice(0, 10));
|
||||
if (!date) return;
|
||||
const res = await this.act(`/api/close-periods/${this.period.id}/wip/${row.id}/capitalize`, { in_service_date: date });
|
||||
if (res) this.message = `Capitalized ${row.asset_id} into service.`;
|
||||
},
|
||||
async finalize() {
|
||||
if (!window.confirm('Finalize and lock this period? Backdated changes will be blocked until it is reopened.')) return;
|
||||
const res = await this.act(`/api/close-periods/${this.period.id}/finalize`, { acknowledge_variance: true });
|
||||
if (res) {
|
||||
this.message = 'Period closed and locked.';
|
||||
await this.loadPeriods();
|
||||
}
|
||||
},
|
||||
async reopen() {
|
||||
if (!window.confirm('Reopen this closed period? Its posted close entries will be reversed.')) return;
|
||||
const res = await this.act(`/api/close-periods/${this.period.id}/reopen`);
|
||||
if (res) {
|
||||
this.message = 'Period reopened; close entries reversed.';
|
||||
await this.loadPeriods();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.close-step {
|
||||
border: 1px solid rgba(128, 128, 128, 0.2);
|
||||
border-radius: 8px;
|
||||
padding: 12px 14px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.close-step.done {
|
||||
border-color: rgba(76, 175, 80, 0.5);
|
||||
}
|
||||
.step-num {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 50%;
|
||||
background: rgba(var(--v-theme-primary), 0.12);
|
||||
color: rgb(var(--v-theme-primary));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.step-body {
|
||||
margin-top: 10px;
|
||||
margin-left: 32px;
|
||||
}
|
||||
.ledger-summary {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
.ledger-chip {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 160px;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid rgba(128, 128, 128, 0.25);
|
||||
border-radius: 8px;
|
||||
}
|
||||
.ledger-chip strong {
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user