Period close
Locking and closing a fiscal period: the close checklist, what a locked period stops, and how to reopen.
Bookkeeping happens continuously, but the books get finalized one period at a time. An is a dated window (a calendar month) that every posting falls into by its posting date. is the act of declaring that month's numbers final: no more postings land in it, and its balances are snapshotted for reporting. Carbon runs this as a three-state lifecycle with a NetSuite-style checklist gating the close.
The whole surface is gated on accounting enabled (companySettings.accountingEnabled). With accounting off, no journals are written, so there is nothing to close.
The close lifecycle
A period carries a close status (periodCloseStatus) that moves in one direction, then back only by explicit reopen. This is a separate axis from the legacy Active/Inactive flag, which just marks the current period for lazy auto-creation.
A period must be Locked before it can be Closed — closing an Open period is refused with "Period must be locked before closing." The transitions live in accounting.service.ts (lockAccountingPeriod, unlockAccountingPeriod, closeAccountingPeriod, reopenAccountingPeriod), each rejecting a move from the wrong starting state.
Locking is stricter than closing for one specific thing
A Locked period still accepts accounting adjustments — a manual journal posted with an accounting-adjustment intent goes through. It only blocks operational documents. A Closed period blocks everything, including reversals into it. Lock when operations are done for the month but the accountants aren't; close when the books are final.
What a locked or closed period stops
The gate lives in getOrCreateAccountingPeriod, which every posting path calls before it writes:
A database trigger (check_accounting_period_open) is the hard backstop underneath the service check: it rejects any INSERT, DELETE, or period-move of a into or out of a Closed period, so even a service-role job or edge function can't slip a posting into a closed month. (Locked semantics need actor identity, so they're enforced at the service layer; the trigger only guards the Closed invariant.) The one exception the trigger allows: a Posted → Reversed status flip, since the offsetting reversal lands in an open period.
The close checklist
Closing runs through a persisted checklist — a company-level template of task definitions (periodCloseTaskDefinition) plus per-period instances (periodCloseTask) materialized the first time you open a period's close screen. Nine system tasks seed with every company (they can be deactivated but never deleted). Each task has a type (Auto, Action, or Manual) and Auto tasks carry a severity.
Auto tasks fail closed: an Auto task whose evaluator can't be found is treated as failing, not silently passed, so the close stays gated and the reason stays visible.
Severities gate differently
An Auto task's severity (periodCloseTaskSeverity) decides whether a failing check blocks the close or merely warns:
The seeded checks, from computePeriodReadiness:
| Check | Severity | Fails when |
|---|---|---|
| Post pending operational documents | Blocker | Un-posted receipts/shipments/invoices dated in this period exist. |
| Post or re-date draft journal entries | Blocker | Draft journal entries are dated in this period. |
| Trial balance in balance for the period | Blocker | Any posted journal has unequal debits and credits. |
| Post depreciation runs covering the period | Warning | A draft depreciation run ends in this period. |
| Match & eliminate intercompany transactions | Warning | Unmatched intercompany transactions involve this company. |
A seventh seeded Auto task, Review negative on-hand inventory (Warning), has no live evaluator yet, so it fails closed and stays a standing Warning you resolve by skipping with a reason. The remaining three seeded tasks are the two Action steps (Lock the period, Close the period) and one Manual step (Review financial statements).
Completing and skipping tasks
An Action/Manual task is marked Done with completeTask (optional notes). Any non-Blocker task can be skipped with skipTask, which always requires a non-empty reason — and the service additionally refuses to skip a Blocker task. Auto tasks are never manually completed; their state is derived from the live readiness check every time the checklist loads.
The close itself (closePeriodWithChecklist) re-evaluates the whole checklist server-side. It refuses to close unless every required task resolves to Done or Skipped and no Blocker check is failing, surfacing the specific blocking task in the error. Never trust a disabled button: the gate is enforced on the server, and the DB trigger is the final backstop.
Sequential close and reopen
Periods close and reopen in order. Closing refuses if any earlier period isn't Closed ("Earlier periods must be closed first"). Reopening refuses if any later period is still Closed ("Later periods must be reopened first") — you reopen from the most recent close backwards.
Reopening
Reopening (reopenAccountingPeriod) flips a Closed period back to Open, clears its closedAt/closedBy, and deletes the balance snapshots the close wrote (its own and any later snapshot that embedded it, since snapshots are cumulative through their period's end date). After reopening, postings into the month resume and the balance reads fall back to a full-history scan until the period is closed again. A period that isn't Closed can't be reopened.
Unlocking (unlockAccountingPeriod) is the milder inverse: it moves Locked back to Open so operational documents can post again, clearing lockedAt/lockedBy. It writes no snapshots and touches no balances.
Fiscal periods and the calendar
Periods are generated per fiscal year — twelve monthly periods from the company's fiscal start month, via createFiscalYearPeriods (idempotent; it skips any period number already present). Each period carries a fiscalYear and periodNumber derived from that start month: a fiscal year is named for the calendar year it ends in, so a July-start FY beginning in 2025 is FY2026.
Once the calendar is committed — any posting exists, or any period is Locked/Closed — the fiscal start month locks (getFiscalCalendarCommitted), because re-labeling already-reported periods would rewrite history. An empty, Open period can still be deleted (deleteAccountingPeriod) and regenerated; a period with journals posted to it, or in any non-Open state, cannot.