← Admin

Bookkeeping handbook

How ledger intake forms, the expense app, Google Sheets (schedule / compliance / bank rec), and BI reporting combine into books that satisfy IRS record-keeping requirements — and how to run them day to day.

This is a working guide, not tax advice. It summarises published IRS rules as they apply to a small cash-basis business. Your entity type, state, and circumstances change the details. Have a CPA review your setup and returns.

How the pieces fit

Four systems, each doing one job. Day-to-day money in/out is captured in apps; Sheets keep the pieces that are schedules, tax rolls, or bank controls — not routine posting intake.

SystemHoldsWhy there
Ledger (intake + books) Daily sales, vendor payments, and card charges (forms → import to journal), plus the double-entry GL: accounts, dated entries, and pointers to source detail. Same pattern as expense: capture once, import as posted entries with source_app + source_record_id. Immutable, balanced, auditable.
Expense app Reimbursement requests with receipt images, approval chain, and payment details. Receipts attached to an approved record is exactly the substantiation §162 and §274 want.
Google Sheets Depreciation schedule, 1099 vendor tally, bank reconciliation — and any §274(d) meals/travel detail that must stay per-item. These are schedule / compliance / control worksheets, not simple posting forms. Keep them in Sheets until (if ever) built as full modules.
BI reporting Dashboards over the above. Analysis only. A dashboard is not a record — it is not archived and cannot substantiate anything.

What the IRS actually requires

IRC §6001 and Treas. Reg. §1.6001-1 are the whole of it: keep permanent books or records sufficient to establish the gross income, deductions, and credits shown on your return.

There is no prescribed chart of accounts, no mandated double-entry, no required subledger, and no approved software list. A spreadsheet and a folder of receipts is legally adequate if it substantiates the return. What gets scrutinised in practice:

  • §162 — expenses must be ordinary and necessary, and you must be able to show it.
  • §274(d) — heightened substantiation for travel, meals, gifts, and listed property. This is where small businesses actually lose in audit.
  • Rev. Proc. 97-22 — electronic records are fine, provided the system reproduces legible copies, provides indexing, and has reasonable controls to detect unauthorised alteration.

Retention

RecordKeep for
General books and supporting detail3 years from filing (§6501)
If >25% of gross income was omitted6 years
If no return was filed, or fraudIndefinitely
Employment tax records4 years
Asset records (basis, improvements)Until 3 years after the year of disposition
The practical standard Every number on the return should be reachable in three steps: return line → ledger account → source detail. If you can do that for any figure an examiner picks, the records are sufficient.

Why cash basis, and what would change it

These books are cash basis: income is recorded when money arrives, expenses when money leaves. That is available to you under the §448(c) small business taxpayer exception — if average annual gross receipts over the prior three years are below an inflation-indexed threshold (in the $30–32M range recently), the cash method is permitted regardless of entity type, and you are exempt from §471 inventory accounting, §263A UNICAP, and §460 long-term contracts.

Only three things force accrual:

  • Gross receipts above the threshold
  • A C corporation, or a partnership with a C corp partner, above the threshold
  • Tax shelters — never eligible at any size

What you file

EntityFormBalance sheet?
Single-member LLC (disregarded)Schedule C on Form 1040None exists — nowhere to report AR/AP
Multi-member LLCForm 1065Schedules L / M-1 / M-2 skippable under $250K receipts and $250K assets
LLC electing S corpForm 1120-SSame $250K / $250K skip
Switching basis is a filing, not a decision Changing accounting method requires IRS consent via Form 3115 under §446(e), with a §481(a) adjustment so income is neither double-counted nor skipped. Most small changes get automatic consent, but never just start recording differently.

Who holds which record

RecordLives inReaches the ledger as
Daily / weekly salesledger_intake (kind=sales)Import, source_app = sales
Vendor paymentsledger_intake (kind=vendor, keep payee/TIN)Import, source_app = vendor
Card chargesledger_intake (kind=card) + receipt folderImport, source_app = card
Employee reimbursementsExpense app (with receipts)Import from the Tools tab, source_app = expense
Bank reconciliationSheetNothing — it is a control, not a transaction
Depreciation scheduleSheetOne annual entry, source_app = sheet
1099 vendor totalsSheet (roll up from vendor payees)Nothing — feeds Form 1099-NEC directly
Meals & travel detailSheet, one row per itemSummary entry; detail must stay per-item
Opening balancesOne entry, source_app = opening

Never summarise these

Summary journal entries are fine and normal. Three categories are exceptions — the detail has to stay per-item and reachable no matter how the ledger records it.

1. Meals, travel, gifts, listed property

§274(d) requires five elements per item, not per month. Your sheet needs a column for each:

  • Amount
  • Time (date)
  • Place
  • Business purpose
  • Business relationship — who was present

Receipts are not required under $75 for travel and entertainment (Reg. §1.274-5(c)(2)(iii)), but the five written elements always are — and lodging requires a receipt at any amount. Affected accounts: 64300 Meals and Entertainment, 68600 Travel Expense.

2. Payments to contractors

Pay an unincorporated contractor $600 or more in a calendar year and you must file Form 1099-NEC with a W-9 on file. That needs per-payee annual totals, which a summarised expense line destroys. Keep a vendor tab with name, TIN, address, and a running total. Affected accounts: 62000 Contract Services, 66700 Professional Fees.

3. Deposits

Summarise no coarser than what ties to a bank statement line. If one ledger entry covers three deposits that appear separately on the statement, reconciliation stops working — and an unreconciled ledger is the first thing challenged in an examination.

Freeze the source when locking a date

The discipline that makes this whole arrangement work A live spreadsheet has no fixed state. You must retain the version that supports the return for 3 years (6 if income was materially understated). Google's revision history is not a substitute — it does not survive a copy, a re-creation, or an account change.

Advancing locked_through_date is therefore not just a settings edit. Before locking through a day:

  1. Export every source tab referenced by entries on or before that day to CSV or PDF.
  2. Export the trial balance for that date range from the Reports tab.
  3. Archive both read-only, somewhere separate from the live sheet.
  4. Set locked_through_date in Admin → Settings (store accordion).

The source_url on each entry tells you exactly which tabs step 1 covers — filter the journal by date and collect the links.

Core concepts

Double entry, signed cents

Every entry has two or more lines. Amounts are stored as whole cents in amount_cents, signed: debit positive, credit negative. The entire correctness rule is therefore one check — the lines must sum to zero. The Debit and Credit columns you see are presentation only.

Integers, not decimals, because 0.1 + 0.2 ≠ 0.3 in floating point and "the trial balance is off by a penny" is miserable to chase.

Draft, then posted

A manual entry is first saved as draft. You can reopen and edit it as often as needed. Validate & post then reads the stored lines back, verifies the balance, source, and entry_date against store lock settings, and flips the entry to posted. Every report filters on posted, so an unfinished draft never affects the books.

Posted entries are permanent

You cannot edit or delete a posted entry. This is intentional. The API rules refuse it. Corrections are made by reversing entries, never by editing. That distinction is the line between a ledger and a list of transactions.

Period lock

There is no separate period record — the period is entry_date. You may draft anything at any time, but posting requires the date to be on or after books_start_date and after locked_through_date (the inclusive last closed day). Set both in Admin → Settings (store accordion).

Chart of accounts

Five-digit codes, grouped by leading digit:

RangeTypeExamples
1xxxxAsset10100 Checking, 17000 Furniture and Equipment
2xxxxLiability20100 Credit Card, 24000 Sales Tax Payable
3xxxxEquity30000 Opening Balance Equity, 39000 Retained Earnings
4xxxxIncome40000 Sales Revenue, 41000 Service Revenue
5xxxxCost of goods sold50000 Cost of Goods Sold
6xxxxOperating expense67100 Rent, 68700 Utilities
8xxxxOther80000 Ask My Accountant

Each account stores an explicit normal_balance rather than deriving it from type, because contra accounts break the rule: 17900 Accumulated Depreciation is an asset carrying a credit balance, and 33000 Owner's Draw is equity carrying a debit balance.

Deactivate, don't delete Deleting an account that has lines against it orphans history. The API rules require is_active = false before an admin can delete one at all. Prefer leaving it deactivated forever.

80000 Ask My Accountant exists for transactions you genuinely cannot classify. Use it rather than guessing, and clear it before year-end.

Books start & lock

There is no ledger_periods collection. The accounting period is each entry's entry_date. Two store settings control what dates may be posted:

  • books_start_date — when the books begin; BI history floor. Posting rejects earlier dates.
  • locked_through_date — inclusive last closed day. Posting requires a later entry_date.

Set both under Admin → Settings (store accordion). Closing a month means setting locked_through_date to that month's last day — after export and archive.

Locking is a ritual, not a date field alone See Freeze the source when locking a date. Export and archive first, then advance the lock date.

Post a manual entry

Journal tab → + New Entry.

  1. Date — the accounting date (entry_date), which is not necessarily today. Must be on or after books_start_date and after locked_through_date.
  2. Memo — what this entry is for, in words a stranger would understand a year from now.
  3. Source — leave on Manual.
  4. Fill the lines: account, and an amount in either the Debit or the Credit column.
  5. Save Draft. The entry remains editable and does not affect reports.
  6. Open the draft and make any corrections. Before posting, Balance must read 0.00 and turn green.
  7. Choose Validate & post, or Edit draft → Save & Post.

Common shapes:

Cash sale             DR 10100 Checking        CR 40000 Sales Revenue
Pay rent by check     DR 67100 Rent Expense    CR 10100 Checking
Card purchase         DR 64900 Office Supplies CR 20100 Credit Card
Pay the card          DR 20100 Credit Card     CR 10100 Checking
Owner takes money     DR 33000 Owner's Draw    CR 10100 Checking
If posting fails partway The entry is left as a draft and no report will count it. The error message includes the draft id. Do not retry from the middle — open the draft, finish it or delete it, then start again.

AI Quick Add

Journal tab → ✦ Quick AI Add, next to + New Entry.

Describe the transaction in plain language. The AI drafts a balanced entry — a simple transfer or a multi-line split — into the same New Entry form + New Entry would open. Nothing posts automatically: review the lines, fix anything wrong, then Save Draft or Save & Post exactly as you would for a manual entry. Memos in the draft follow your note_language setting (below).

What it does under the hood

  • The free text and the store's active chart of accounts are sent to myapi's /ai/chat endpoint — the AI sees only account codes/names/types, and the provider API keys stay server-side.
  • The model returns a memo, an optional date, and a list of lines (account code, debit or credit, memo, type) that balance to the penny.
  • Each line's account code is matched against the real chart of accounts. A code the AI invented or mis-typed is left blank on that line and flagged, rather than silently guessed — fix it in the split editor before saving.
  • Which backend/model would answer (or did answer) is shown under the text box — a resolved default when the panel opens, the actual one used once you've asked.
  • Entry and line memos are written in your note_language (see below). Account codes stay as in the chart.
  • In the split editor, hovering a Debit or Credit box explains what that number means for that specific account — e.g. debiting Checking Account reads "money in", crediting Unearned Revenue reads "you owe more" — not just the generic debit/credit rule.

Memo language (note_language)

Quick AI Add uses the signed-in user's users.settings.ledger_settings.note_language for every memo it drafts (entry memo and each line memo). Set it under Account → SettingsAI memo language.

ValueMemos written in
en (default)English
zh-twTraditional Chinese
zh-cnSimplified Chinese

Missing or invalid values fall back to English. This does not change the UI language toggle, and it does not rewrite memos on entries you type by hand.

Worked example

Typed:
received wire transfer in of 30784 for Dreamwear shipment 75872, we keep 2%
service revenue and calculate the rest to unearned client deposit

Drafted:
  DR 10100 Checking Account        30,784.00
  CR 41000 Service Revenue                       615.68
  CR 25000 Unearned Revenue                    30,168.32
Review before you post Treat the draft as a first pass, not a finished entry — the AI can mis-map an account or misjudge a split. That's why it lands as an editable draft in the ordinary New Entry form instead of posting itself: the same balance check, date lock, and review step apply as any manual entry.
Provenance An entry drafted this way remembers which model wrote it — stored on ledger_entries.ai_model (e.g. groq llama-3.1-8b-instant) and shown in the entry detail panel. Empty for entries entered by hand.

Post a spreadsheet summary

This is how sheet detail reaches the books. Same form, but set Source to Spreadsheet summary and four extra fields appear.

FieldRequiredWhat to put
Sheet URLyesDeep link to the exact tab, including the #gid= fragment. This is the indexing Rev. Proc. 97-22 asks for.
Batch keyyesA stable name for this batch, e.g. 2027-01-sales. Makes the entry idempotent.
RowsyesHow many source rows this entry stands for.
Sheet totaloptionalWhat the sheet says the batch totals. Cross-checked against the entry's debits.

What the checks do for you

  • Leave out the URL, batch key, or row count and the post is refused before touching the network. A summary that doesn't say what it summarises is unauditable.
  • Enter a Sheet total that disagrees with your lines and the post is refused with the exact difference. This catches a mis-keyed summary immediately rather than at year end.
  • The batch key can never post twice — a unique database index enforces it. Re-running a month is safe.

Worked example

January sales sheet: 31 rows, totalling 18,400.00

Date         2027-01-31
Memo         January sales summary
Source       Spreadsheet summary
Sheet URL    https://docs.google.com/spreadsheets/d/…#gid=0
Batch key    2027-01-sales
Rows         31
Sheet total  18400.00

  DR 10100 Checking Account     18,400.00
  CR 40000 Sales Revenue                    18,400.00
Naming batch keys Use YYYY-MM-what consistently — 2027-01-sales, 2027-01-cardcharges, 2027-depreciation. Because the key is what prevents double-posting, an inconsistent naming habit is what defeats it.

Corrections

Posted entries cannot be edited. To fix one, post two more:

  1. A reversing entry — same accounts, every amount negated, with reversal_of pointing at the original.
  2. A corrected entry with the right figures.

The audit trail then shows what you originally recorded, that you caught it, and what you replaced it with. That is a feature: an examiner seeing clean corrections sees a controlled process, whereas silently edited books raise the question of what else changed.

There is deliberately no reversed_by field, because setting it would mean updating a posted record. Find reversals by searching for entries whose reversal_of is the original's id.

If the entry date is locked You cannot post on or before locked_through_date. Post the reversal and correction with a later date, and note the reason in the memo.

Reports

All reports count posted entries only. Drafts are invisible everywhere.

ReportUse it for
Trial balance Every account with a balance, debits and credits totalled. The totals must be equal — the footer says so explicitly. This is what you export and archive when locking a date.
Account activity Every line hitting one account, in date order, with a running balance. Pick the account first. This is the drill-down when a figure looks wrong.
Cash snapshot Balances of bank and cash accounts as of a date. Ignores the "from" filter — it is a position, not a movement.
Integrity The audit of the books themselves. See below.

Integrity report reference

These are an audit, not a guardrail. The API rules and the posting protocol are the controls; this report confirms both worked. Anything appearing here means something bypassed the intended flow — most likely an admin acting directly on the database.

IssueMeansDo
Entry lines do not sum to zero A posted entry is unbalanced — impossible through the app. Serious. Investigate directly in PocketBase; the books are wrong until fixed.
line_count mismatch Lines were added or removed after the entry was posted. Compare against the archived export for that date range to see which is right.
total_debit_cents mismatch Line amounts changed after posting. Same — reconcile against the archive.
source_total_cents mismatch The entry no longer agrees with the total its source claimed at post time. Open the source_url. Either the sheet was edited after posting, or the entry was.
Sheet-sourced entry with no URL / row count A summary with no pointer back to its detail. Unauditable as-is. Find the source, and record it — reverse and repost if necessary.
Line references inactive account Usually benign — the account was deactivated after the entry was posted. Confirm the account was correct at the time. Usually no action.
Draft older than 7 days An abandoned or half-finished entry. Finish it or delete it. Drafts affect nothing, but they hide real work.
Booked expense not in ledger An expense marked booked that never reached the ledger. Run Import Booked Expenses on the Tools tab.
Run this before advancing the lock date An empty integrity report is part of what makes the archived trial balance trustworthy.

Tools tab

Pending import

Lists expense reimbursements marked booked that have not yet reached the ledger. Expand it to see what is waiting. If this is empty, expense and ledger agree.

Import expense reimbursements

Posts one ledger entry per booked expense, with source_app = expense and source_record_id set to the expense record id. That id is covered by a unique index, so running the import twice is safe — already-imported expenses are skipped, not duplicated.

Seed sample journal entries

Test data only. Creates example entries for trying the app out. Never run this against books you intend to file from. If you already have, find them by memo and reverse them.

Command line

Run from the ledger/ directory. All three need PocketBase credentials in the environment.

Seed the chart of accounts

Idempotent — accounts are matched on (store, code) and updated rather than duplicated. Run without --run first to preview.

PB_ADMIN=1 [email protected] PB_PASSWORD=… \
  node scripts/seed-accounts.js --store hilinkpro          # dry run

PB_ADMIN=1 [email protected] PB_PASSWORD=… \
  node scripts/seed-accounts.js --store hilinkpro --run    # write

Test the posting protocol

No credentials and no network — pure logic. Run it after touching pb-ledger.js.

cd scripts && node test-posting.js

Covers money parsing, the balance invariant, the source-field rules, and every failure mode of the posting protocol — asserting in each case that a failed post is never flipped to posted.

Verify the API rules

Proves the security rules behave as the design assumes — in particular that an update rule matches the record's value before the update, which is what makes the two-phase post possible at all. Creates and cleans up real records.

PB_ADMIN=1 [email protected] PB_PASSWORD=… \
  node scripts/verify-rules.js

Monthly routine

  1. Reconcile the bank in the sheet — every statement line accounted for, ending balance agreed.
  2. Enter / import the month's intake — daily sales, vendor payments, and card charges in the ledger app (once those forms ship); until then, post summary entries from Sheets as today.
  3. Import booked expenses from the Tools tab.
  4. Check Pending import is empty (expenses and, later, other intake sources).
  5. Run the Integrity report and resolve anything it lists.
  6. Compare the Cash snapshot to the reconciled bank balance. They must agree. If they don't, stop and find out why before doing anything else.
  7. Confirm receipts are filed for the month — expense app for reimbursements, indexed folder for everything else.

Year-end routine

  1. Complete the monthly routine for the final month.
  2. Clear 80000 Ask My Accountant — reclassify everything sitting in it.
  3. Post depreciation from the schedule sheet, as one entry with source_app = sheet.
  4. Verify §274(d) detail is complete — every meals and travel row has all five elements. Fix gaps now, while you can still remember.
  5. Produce 1099-NEC totals from the vendor tab. Anyone unincorporated at $600+ needs a form, and you need their W-9. Due to recipients and the IRS by January 31.
  6. Run the Integrity report — it must be empty.
  7. Export and archive: every source tab, the trial balance, and account activity for any account you expect questions about.
  8. Set locked_through_date to year-end in Admin → Settings.
  9. Hand the archive to your CPA.
Archive before locking, not after Once a date is locked you cannot post into it, so a mistake found later becomes a current-date adjustment. Get the integrity report clean first.

What this system does not do

Known gaps, so you don't discover them in April:

MissingConsequenceWorkaround today
Daily sales / vendor / card intake forms Needs the ledger_intake collection created in PocketBase (LEDGER_POCKETBASE_SETUP.md Step 5). Use Intake tab → mark ready → Import Ready (or Tools).
Profit & loss report No categorised income statement to file from. Export the trial balance and group by account range in a sheet.
Year-end closing entry Net income is not rolled into 39000 Retained Earnings, so year two's trial balance carries year one's income and expense balances. Post it manually: debit each income account and credit each expense account to zero, with the difference to Retained Earnings.
Bank reconciliation No statement-matching UI (import statement, match groups). Mark lines with is_reconciled on journal detail or Account activity; compare to Cash snapshot / sheet.
AR / AP subledger No aging, no per-customer or per-vendor balances. Not required on cash basis. Track in a sheet if you need to know who owes you.
Fixed asset register No cost basis, method, or disposition tracking for Form 4562. Keep the depreciation schedule in a sheet; post one entry a year.
Payroll No payroll detail or tax liability tracking. Use a payroll provider; post summary entries to 66000 and 21000.

Schema and API rules are documented in ledger/LEDGER_POCKETBASE_SETUP.md.

記帳作業手冊

總帳進件表單、費用報銷、Google 試算表(折舊/1099/銀行對帳)與 BI 報表如何組合成一套 符合美國國稅局(IRS)帳簿保存要求的制度,以及日常該怎麼操作。

本文件為作業指南,非稅務意見。 內容係就已公布之 IRS 規定,針對採現金收付制之小型企業所做的整理。 您的實體型態、所在州別與個別情況都會改變細節,請務必請會計師(CPA)覆核您的制度與申報表。

四個系統如何分工

四個系統,各司其職。日常收付款由應用程式進件;試算表只保留「排程/稅務彙總/銀行控制」這類 工作表,而不是例行過帳的入口。

系統存放內容為何放這裡
總帳(進件+帳簿) 每日銷售、供應商付款、信用卡消費(表單 → 匯入分錄),以及複式簿記總帳:科目、依日期分錄與來源指標。 與費用報銷相同:先記錄、再以 source_app + source_record_id 匯入已過帳分錄。不可竄改、必然平衡、可供查核。
費用報銷系統 報銷申請單,含收據影像、簽核流程與付款資訊。 收據附在已核准的單據上,正是 §162 與 §274 所要求的佐證形式。
Google 試算表 折舊計算表、1099 供應商彙總、銀行對帳——以及必須逐筆保留的 §274(d) 餐飲/差旅明細。 這些是排程/合規/控制工作表,不是單純的過帳表單。在做成完整模組之前,繼續放在試算表。
BI 報表 建立在以上資料之上的儀表板。 僅供分析。儀表板不是帳簿記錄——它不會被封存,也無法作為任何佐證。

國稅局實際要求什麼

IRC §6001Treas. Reg. §1.6001-1 就是全部規定: 保存永久性的帳簿或記錄,足以證明申報表上所列的總收入、扣除額與抵減額。

沒有規定的會計科目表、沒有強制的複式簿記、沒有必備的明細帳,也沒有核准的軟體清單。 只要能佐證申報表,一份試算表加一疊收據在法律上就是充分的。實務上真正被檢視的是:

  • §162 —— 費用必須是「一般且必要」(ordinary and necessary),而且你要能證明。
  • §274(d) —— 對差旅、餐飲、禮品與特定財產(listed property)的加強佐證要求。小型企業在查核中失分,多半就在這裡。
  • Rev. Proc. 97-22 —— 電子記錄可以,前提是系統能重製清晰可讀的副本、提供索引,並具備偵測未授權竄改的合理控制。

保存年限

記錄類型保存期間
一般帳簿與佐證明細自申報日起 3 年(§6501)
漏報總收入超過 25%6 年
未申報或涉及詐欺永久
薪資稅相關記錄4 年
資產記錄(成本基礎、資本支出)至處分年度後 3 年
實務判準 申報表上的任何一個數字,都應該能在三步內追到底:申報表欄位 → 總帳科目 → 來源明細。 若查核人員隨便指一個數字你都做得到,這份記錄就是充分的。

為什麼採現金收付制,什麼情況會被迫改制

本帳簿採現金收付制:錢進來才認列收入,錢出去才認列費用。 這是依據 §448(c) 小型企業納稅人例外規定——若前三年平均年度總收入低於 經通膨調整的門檻(近年約在 3,000 萬至 3,200 萬美元區間), 則不論實體型態均可採用現金收付制,並免適用 §471 存貨會計、§263A 統一資本化規定 與 §460 長期工程合約規定。

只有三種情況會被迫改採權責發生制:

  • 總收入超過門檻
  • C 型公司,或合夥人中有 C 型公司的合夥組織,且超過門檻
  • 避稅安排(tax shelter)—— 任何規模皆不適用現金收付制

您需要申報什麼

實體型態申報表是否需附資產負債表
單一成員 LLC(視同無實體)Form 1040 Schedule C根本沒有這張表——無處可填應收/應付帳款
多成員 LLCForm 1065總收入與總資產雙雙低於 25 萬美元時,Schedule L/M-1/M-2 可免附
LLC 選擇按 S 型公司課稅Form 1120-S同樣的 25 萬/25 萬免附門檻
改變會計基礎是一項申報行為,不是自行決定 變更會計方法須依 §446(e) 提交 Form 3115 取得國稅局同意, 並做 §481(a) 調整,以免所得被重複計算或漏計。 多數小型變更可適用自動同意程序,但絕不可以就這樣開始換個方式記帳

哪份記錄放在哪裡

記錄存放於以何種形式進入總帳
每日/每週銷售ledger_intakekind=sales匯入,source_app = sales
供應商付款ledger_intakekind=vendor,保留收款人/TIN)匯入,source_app = vendor
信用卡消費ledger_intakekind=card)+ 收據資料夾匯入,source_app = card
員工報銷費用報銷系統(含收據)由「工具」頁籤匯入,source_app = expense
銀行對帳試算表不入帳——它是內部控制,不是交易
折舊計算表試算表每年一筆分錄,source_app = sheet
1099 供應商彙總試算表(由供應商收款人彙總)不入帳——直接用於填報 Form 1099-NEC
餐飲與差旅明細試算表,一筆一列以彙總分錄入帳;明細必須逐筆保留
期初餘額單筆分錄,source_app = opening

絕不可彙總的三類

彙總分錄本身完全正常。但有三類是例外——不論總帳怎麼記,明細都必須逐筆保留且可追查。

1. 餐飲、差旅、禮品、特定財產

§274(d) 要求每一筆都具備五項要素,不是每個月一次。您的試算表需要為每一項各設一欄:

  • 金額
  • 時間(日期)
  • 地點
  • 營業目的
  • 業務關係 —— 在場的人是誰

差旅與招待費用在 75 美元以下免附收據(Reg. §1.274-5(c)(2)(iii)), 但上述五項書面要素永遠必要——而且住宿費不論金額大小一律須附收據。 相關科目:64300 餐飲及招待68600 差旅費

2. 支付予承攬人的款項

若一個曆年內支付非公司組織的承攬人達 600 美元以上,即須申報 Form 1099-NEC,並須事先取得其 W-9。這需要按收款人彙總的年度金額, 而彙總分錄會把收款人資訊完全抹掉。請另開一個供應商工作表,記錄姓名、稅籍編號(TIN)、 地址與累計金額。相關科目:62000 承包服務66700 專業服務費

3. 存款

彙總的粗細程度,不可超過能對應銀行對帳單逐筆項目的程度。 若一筆總帳分錄涵蓋了對帳單上分別列示的三筆存款,銀行對帳就失效了—— 而未經對帳的總帳,是查核時第一個被質疑的對象。

鎖定日期前凍結來源

這是讓整套架構站得住腳的關鍵紀律 線上試算表沒有固定狀態。您必須保存「支持該年度申報表的那個版本」達 3 年 (若重大漏報所得則為 6 年)。Google 的版本記錄不能取代這件事—— 它無法在檔案被複製、重建或帳號變更後存活。

因此推進 locked_through_date 不只是改設定。在鎖定至某日之前:

  1. 將該日(含)以前分錄所引用的每一個來源工作表匯出成 CSV 或 PDF。
  2. 從「報表」頁籤匯出該日期區間的試算表。
  3. 兩者以唯讀方式封存,且存放位置與線上試算表分開。
  4. 在「管理 → 設定」(門市區塊)設定 locked_through_date

每筆分錄上的 source_url 會準確告訴您第 1 步該收哪些工作表—— 用日期篩選日記帳,把連結全部收集起來即可。

核心觀念

複式簿記,帶正負號的「分」

每筆分錄至少有兩行明細。金額以整數「分」儲存在 amount_cents, 並帶正負號:借方為正,貸方為負。因此整個正確性規則只有一條—— 明細加總必須等於零。畫面上看到的「借方」「貸方」兩欄純粹是呈現方式。

使用整數而非小數,是因為浮點數中 0.1 + 0.2 ≠ 0.3, 而「試算表差一分錢」這種問題追查起來極其痛苦。

先草稿,再過帳

手動分錄會先儲存為 draft(草稿),可不限次數重新開啟與修改。 按「驗證並過帳」後,系統會讀回已儲存的明細、核對借貸平衡、來源與 entry_date 是否符合門市鎖定設定,再切換為 posted(已過帳)。 所有報表只計算已過帳分錄,因此未完成草稿不會影響帳務。

已過帳的分錄永久不可更改

已過帳的分錄無法編輯、無法刪除。這是刻意的設計。 API 規則會直接拒絕。更正一律透過迴轉分錄,絕不透過修改。 這個差別,就是「總帳」與「一份交易清單」之間的分界線。

期間鎖定

沒有獨立的期間記錄——期間就是 entry_date。您隨時可以建立草稿,但過帳要求 日期不早於 books_start_date,且晚於 locked_through_date(含該日在內的最後已結帳日)。兩者都在「管理 → 設定」 (門市區塊)設定。

會計科目表

五位數科目代碼,以首位數字分類:

區間類別範例
1xxxx資產10100 支票帳戶、17000 傢俱與設備
2xxxx負債20100 信用卡、24000 應付銷售稅
3xxxx權益30000 期初餘額權益、39000 保留盈餘
4xxxx收入40000 銷售收入、41000 服務收入
5xxxx銷貨成本50000 銷貨成本
6xxxx營業費用67100 租金支出、68700 水電費
8xxxx其他80000 會計師諮詢

每個科目都明確儲存 normal_balance(正常餘額方向),而不是從類別推導, 因為抵銷科目會打破規則17900 累計折舊 是資產卻帶貸方餘額,33000 業主提款 是權益卻帶借方餘額。

停用,不要刪除 刪除已有明細掛在其下的科目會使歷史記錄成為孤兒。API 規則要求必須先設為 is_active = false,管理員才可能刪除。建議永遠只停用、不刪除。

80000 會計師諮詢 是給你真的分不出類別的交易用的。 寧可用它,也不要亂猜;並在年度結帳前把它清空。

帳簿起始與鎖定

沒有 ledger_periods 集合。會計期間就是每筆分錄的 entry_date。 兩個門市設定控制哪些日期可以過帳:

  • books_start_date —— 帳簿起始日;BI 歷史下限。早於此日的分錄無法過帳。
  • locked_through_date —— 含該日在內的最後已結帳日。過帳的 entry_date 必須更晚。

兩者都在「管理 → 設定」(門市區塊)設定。結束一個月,就是在匯出封存後, 把 locked_through_date 設為該月最後一天。

鎖定是一套流程,不只是改日期 請見鎖定日期前凍結來源。先匯出封存,再推進鎖定日期。

手動建立分錄

「日記帳」頁籤 → 「+ 新增分錄」

  1. 日期 —— 會計入帳日(entry_date),不一定是今天。須不早於 books_start_date,且晚於 locked_through_date
  2. 摘要 —— 這筆分錄的用途,用一年後的陌生人也看得懂的文字寫。
  3. 來源 —— 保持在「手動」。
  4. 填寫明細:選科目,並在「借方」或「貸方」其中一欄填入金額。
  5. 「儲存草稿」。草稿仍可編輯,也不會計入報表。
  6. 開啟草稿並完成修正。過帳前「差額」必須顯示 0.00 並轉為綠色。
  7. 「驗證並過帳」,或選擇「編輯草稿 → 儲存並過帳」

常見分錄型態:

現金銷貨       借 10100 支票帳戶      貸 40000 銷售收入
開票付房租     借 67100 租金支出      貸 10100 支票帳戶
刷卡採購       借 64900 辦公用品      貸 20100 信用卡
繳信用卡款     借 20100 信用卡        貸 10100 支票帳戶
業主提取現金   借 33000 業主提款      貸 10100 支票帳戶
過帳中途失敗時 分錄會停留在草稿狀態,任何報表都不會計入它。錯誤訊息會附上草稿 ID。 請不要從中間重試——打開該草稿,補完或刪除,然後重新開始。

AI 快速新增

「日記帳」頁籤 → 「✦ AI 快速新增」,就在「+ 新增分錄」旁邊。

用一般文字描述這筆交易,AI 會草擬一筆借貸平衡的分錄——可能是簡易轉帳,也可能是多筆明細的拆分—— 直接填入與「+ 新增分錄」相同的表單。不會自動過帳:請先檢視每一行明細、修正錯誤, 再按「儲存草稿」「儲存並過帳」,與手動輸入的分錄完全相同。 草擬出的摘要會依你的 note_language 設定撰寫(見下方)。

背後如何運作

  • 你輸入的文字與門市目前啟用中的會計科目表,會送到 myapi/ai/chat 端點——AI 只看得到科目代碼/名稱/類型,供應商的 API 金鑰全程留在伺服器端。
  • AI 會回傳摘要、(若文字中有提到)日期,以及一組借貸平衡到分的明細(科目代碼、借方或貸方、 明細摘要、類型)。
  • 每一行的科目代碼都會對照真實的會計科目表。若 AI 編造或打錯代碼,該行的科目會留空 並標示出來,而不是幫你亂猜——請在拆分編輯畫面中自行修正後再儲存。
  • 目前(或剛剛)使用的 AI 供應商/模型會顯示在輸入框下方——開啟面板時顯示預設會使用的模型, 解析完成後則顯示實際使用的模型。
  • 分錄摘要與每一行明細摘要會依你的 note_language 撰寫(見下方)。科目代碼維持科目表原樣。
  • 在拆分編輯畫面中,把滑鼠移到「借方」或「貸方」欄位上,會顯示這個金額對「這個科目」實際代表 的意義——例如借記支票帳戶顯示「存入」,貸記預收收入顯示「負債增加(欠更多)」,而不只是通用的 借貸規則。

摘要語言(note_language

AI 快速新增會讀取登入使用者的 users.settings.ledger_settings.note_language,用來撰寫它草擬的所有摘要 (分錄摘要與每一行明細摘要)。請到「帳號 → 設定」「AI 摘要語言」設定。

摘要語言
en(預設)英文
zh-tw繁體中文
zh-cn簡體中文

若未設定或值無效,則回退為英文。這與介面語言切換無關,也不會改寫你手動輸入的摘要。

範例

輸入:
收到 Dreamwear 出貨單 75872 的電匯 30784,其中 2% 認列服務收入,
其餘記為預收客戶款項

草擬結果:
  借 10100 支票帳戶            30,784.00
  貸 41000 服務收入                          615.68
  貸 25000 預收收入                        30,168.32
儲存前務必檢查 請把 AI 草擬的結果當成初稿,而非定稿——AI 可能會配錯科目,或誤判拆分比例。正因如此, 它會落地成一筆可編輯的草稿,停留在一般的「新增分錄」表單中,而不是直接過帳: 與手動輸入的分錄一樣,仍要通過借貸平衡檢查、期間鎖定與人工複核。
可追溯性 以此方式草擬的分錄會記住是哪個模型草擬的——存放在 ledger_entries.ai_model (例如 groq llama-3.1-8b-instant),並顯示於分錄詳細資訊面板。手動輸入的分錄則此欄位留空。

試算表彙總分錄

這是試算表明細進入帳簿的方式。表單相同,但把「來源」切換為 「試算表彙總」後,會多出四個欄位。

欄位必填填什麼
試算表連結必填指向該工作表分頁的完整連結,含 #gid= 片段。這就是 Rev. Proc. 97-22 所要求的索引。
批次代碼必填這一批的固定名稱,例如 2027-01-sales。用來確保不會重複入帳。
列數必填這筆分錄代表了多少列來源資料。
試算表合計選填試算表自己算出的這批總額。系統會與分錄的借方合計交叉核對。

這些檢查為你做了什麼

  • 少填連結、批次代碼或列數,在連上伺服器之前就會被拒絕。說不出自己彙總了什麼的分錄無法查核。
  • 填入的試算表合計與明細不符時,過帳會被拒絕並顯示確切差額。這能當下抓到打錯的彙總,而不是拖到年底。
  • 同一個批次代碼永遠不可能過帳兩次——由資料庫唯一索引強制執行。整個月重跑一次是安全的。

實例

一月銷售試算表:31 列,合計 18,400.00

日期           2027-01-31
摘要           一月銷售彙總
來源           試算表彙總
試算表連結     https://docs.google.com/spreadsheets/d/…#gid=0
批次代碼       2027-01-sales
列數           31
試算表合計     18400.00

  借 10100 支票帳戶      18,400.00
  貸 40000 銷售收入                  18,400.00
批次代碼的命名 請一致地使用 YYYY-MM-用途——2027-01-sales2027-01-cardcharges2027-depreciation。 因為防止重複入帳靠的就是這個代碼,命名習慣不一致正是讓這道防線失效的原因。

更正錯誤

已過帳的分錄不能編輯。要更正,請再過帳兩筆:

  1. 一筆迴轉分錄 —— 科目相同、每個金額取負值,並將 reversal_of 指向原分錄。
  2. 一筆更正後分錄,填入正確數字。

如此軌跡上便清楚呈現:你原本記了什麼、你發現了錯誤、你用什麼取代它。 這是優點:查核人員看到乾淨的更正軌跡,看到的是一套受控的流程; 而被悄悄改過的帳簿,只會讓人開始想「還有什麼被改過」。

系統刻意不設 reversed_by 欄位,因為要寫入它就必須更新一筆已過帳的記錄。 要找迴轉分錄,請改以「reversal_of 等於原分錄 ID」來查詢。

若分錄日期已鎖定 您無法在 locked_through_date 當日或更早的日期過帳。請以較晚的日期過帳 迴轉與更正分錄,並在摘要中註明原因。

報表

所有報表只計算已過帳的分錄。草稿在任何地方都不會出現。

報表用途
試算表 列出所有有餘額的科目及借貸方合計。兩邊合計必須相等,表尾會明確標示。這是結帳時要匯出封存的報表。
科目明細 單一科目的所有異動,依日期排序並顯示累計餘額。需先選定科目。數字看起來不對時,就用這張表往下追。
現金概況 指定日期的銀行與現金科目餘額。會忽略「起日」篩選——它呈現的是時點餘額,不是期間變動。
完整性檢查 對帳簿本身的稽核。詳見下節。

完整性檢查對照

這些是稽核,不是防護欄。API 規則與過帳協定才是控制機制, 這份報表只是確認兩者都生效了。任何出現在這裡的項目,都代表有東西繞過了既定流程—— 最可能是管理員直接對資料庫動手。

問題代表該怎麼做
分錄明細加總不為零 一筆已過帳分錄不平衡——透過應用程式不可能發生。 嚴重。請直接到 PocketBase 調查;在修正前帳是錯的。
line_count 不符 分錄過帳之後有明細被新增或刪除。 與該日期區間的封存檔比對,判斷哪一邊才是正確的。
total_debit_cents 不符 過帳後明細金額被更動。 同上——與封存檔核對。
source_total_cents 不符 分錄與其來源在過帳當下所宣稱的合計已不一致。 打開 source_url。不是試算表在過帳後被改,就是分錄被改。
試算表來源分錄缺少連結/列數 一筆沒有指回明細的彙總。 就現狀無法查核。請找出來源並補記——必要時迴轉後重新過帳。
明細引用已停用科目 通常無害——科目是在分錄過帳之後才被停用。 確認當時使用該科目是正確的。通常不需處理。
草稿超過 7 天 被遺忘或做到一半的分錄。 補完或刪除。草稿不影響帳務,但會掩蓋真正待辦的事。
已入帳費用未進入總帳 某筆標記為 booked 的報銷從未進到總帳。 到「工具」頁籤執行「匯入已入帳費用」
推進鎖定日期前都先跑一次 一份沒有任何問題的完整性報表,是讓封存的試算表值得信賴的一部分。

工具頁籤

待匯入

列出已標記為 booked 但尚未進入總帳的報銷單。展開即可看到待處理清單。 若這裡是空的,代表費用系統與總帳一致。

匯入已入帳費用

為每一筆已入帳的報銷單各建立一筆總帳分錄,source_app = expensesource_record_id 設為該報銷單的記錄 ID。該 ID 受唯一索引保護, 所以重複執行匯入是安全的——已匯入的會被略過,不會重複產生。

建立範例分錄

僅供測試。 它會建立用來試玩的範例分錄。絕對不要在正式申報用的帳簿上執行。 若已誤執行,請依摘要找出這些分錄並以迴轉分錄沖銷。

命令列工具

請在 ledger/ 目錄下執行。三個腳本都需要在環境變數中提供 PocketBase 帳密。

建立會計科目表

可重複執行——科目以 (store, code) 比對後更新,不會重複建立。建議先不加 --run 預覽。

PB_ADMIN=1 [email protected] PB_PASSWORD=… \
  node scripts/seed-accounts.js --store hilinkpro          # 預覽,不寫入

PB_ADMIN=1 [email protected] PB_PASSWORD=… \
  node scripts/seed-accounts.js --store hilinkpro --run    # 實際寫入

測試過帳協定

不需帳密、不連網路——純邏輯測試。修改 pb-ledger.js 後請執行。

cd scripts && node test-posting.js

涵蓋金額解析、平衡不變式、來源欄位規則,以及過帳協定的每一種失敗情境—— 並在每種情境下都斷言失敗的分錄絕不會被切換為 posted

驗證 API 規則

證明安全規則的行為與設計假設一致——特別是「更新規則比對的是更新的欄位值」 這一點,正是兩階段過帳得以成立的前提。此腳本會建立並清除真實記錄。

PB_ADMIN=1 [email protected] PB_PASSWORD=… \
  node scripts/verify-rules.js

每月流程

  1. 在試算表完成銀行對帳 —— 對帳單每一筆都有著落,期末餘額相符。
  2. 登錄/匯入當月進件 —— 每日銷售、供應商付款、信用卡消費(表單上線後於總帳完成);在此之前,仍依現行方式自試算表建立彙總分錄。
  3. 從「工具」頁籤匯入已入帳費用
  4. 確認「待匯入」為空(費用,以及之後其他進件來源)。
  5. 執行完整性檢查,並處理列出的每一項。
  6. 比對「現金概況」與已對帳的銀行餘額。兩者必須相符。若不符,先停下來查明原因,再做其他事。
  7. 確認當月收據都已歸檔 —— 報銷放在費用系統,其餘放在已建立索引的資料夾。

年度結帳流程

  1. 先完成最後一個月的每月流程。
  2. 清空 80000 會計師諮詢 —— 把裡面的東西全部重新分類。
  3. 過帳折舊 —— 依折舊計算表建立一筆分錄,source_app = sheet
  4. 檢查 §274(d) 明細完整 —— 每一列餐飲與差旅都具備五項要素。趁還記得的時候補齊。
  5. 產出 1099-NEC 彙總 —— 由供應商工作表產生。非公司組織且達 600 美元以上者都需要一份表,而你需要對方的 W-9。1 月 31 日前須送達收款人與國稅局。
  6. 執行完整性檢查 —— 必須是空的。
  7. 匯出並封存:所有來源工作表、試算表,以及任何你預期會被追問的科目明細。
  8. 在「管理 → 設定」將 locked_through_date 設為年度最後一天
  9. 把封存資料交給你的會計師。
先封存,再鎖定 一旦日期被鎖定,就無法再過帳到該日,之後才發現的錯誤只能以較晚日期的分錄調整。 請先讓完整性檢查乾淨無誤。

本系統做不到的事

已知的缺口,先講清楚,免得四月才發現:

缺少後果目前的替代做法
每日銷售/供應商/信用卡進件表單 需先在 PocketBase 建立 ledger_intake 集合(LEDGER_POCKETBASE_SETUP.md 步驟 5)。 使用「進件」頁籤 → 標為就緒 → 匯入就緒項目(或「工具」)。
損益表 沒有可直接用於申報的分類損益表。 匯出試算表,在試算表軟體中依科目區間分組彙總。
年度結帳分錄 本期損益不會結轉至 39000 保留盈餘,因此第二年的試算表會帶著第一年的收入與費用餘額。 手動過帳:借記各收入科目、貸記各費用科目使其歸零,差額轉入保留盈餘。
銀行對帳 沒有對帳單匯入/自動勾銷介面。 在分錄明細或科目明細以 is_reconciled 手動勾選;再與「現金概況」/試算表比對。
應收/應付明細帳 沒有帳齡分析,也沒有客戶別或供應商別餘額。 現金收付制下並非必要。若需要知道誰欠你錢,用試算表追蹤。
固定資產帳 沒有成本基礎、折舊方法或處分記錄可供填報 Form 4562。 折舊計算表放在試算表;每年過帳一筆分錄。
薪資 沒有薪資明細或稅務負債追蹤。 使用薪資服務商;將彙總分錄過帳至 6600021000

資料表結構與 API 規則記載於 ledger/LEDGER_POCKETBASE_SETUP.md(英文)。