Add bank reconciliation workflow and templates

New capability for importing bank transactions (Schwab JSON), matching
against existing ledger entries, creating missing transactions, and
reconciling balances. Includes two new Grist tables (Reconciliations,
BankRules), two bank-import templates, and reference documentation.
This commit is contained in:
2026-02-19 20:26:48 -05:00
parent 357d1aadc8
commit fdb813d647
8 changed files with 630 additions and 4 deletions

View File

@@ -13,6 +13,7 @@ Double-entry accounting for sole proprietorship. Every transaction creates balan
|------|--------| |------|--------|
| Record vendor invoice | Use template from `templates/`, then audit | | Record vendor invoice | Use template from `templates/`, then audit |
| Record payment | Use template from `templates/`, then audit | | Record payment | Use template from `templates/`, then audit |
| Reconcile bank account | See [reconciliation.md](references/reconciliation.md) |
| Query balances | Use `sql_query` on Accounts table | | Query balances | Use `sql_query` on Accounts table |
| Generate reports | See [queries.md](references/queries.md) | | Generate reports | See [queries.md](references/queries.md) |
@@ -28,6 +29,8 @@ Double-entry accounting for sole proprietorship. Every transaction creates balan
| Invoice not yet paid | [bill-unpaid.json](templates/bill-unpaid.json) | | Invoice not yet paid | [bill-unpaid.json](templates/bill-unpaid.json) |
| Pay existing bill | [pay-existing-bill.json](templates/pay-existing-bill.json) | | Pay existing bill | [pay-existing-bill.json](templates/pay-existing-bill.json) |
| Direct expense (no bill) | [direct-expense.json](templates/direct-expense.json) | | Direct expense (no bill) | [direct-expense.json](templates/direct-expense.json) |
| Bank import - deposit | [bank-import-deposit.json](templates/bank-import-deposit.json) |
| Bank import - withdrawal | [bank-import-expense.json](templates/bank-import-expense.json) |
Templates contain only writable fields. See [templates.md](references/templates.md) for usage guide. Templates contain only writable fields. See [templates.md](references/templates.md) for usage guide.
@@ -38,6 +41,7 @@ Templates contain only writable fields. See [templates.md](references/templates.
| **Invoice/Bill from vendor** | Bill + BillLines + Transaction + TransactionLines | | **Invoice/Bill from vendor** | Bill + BillLines + Transaction + TransactionLines |
| **Receipt showing payment** | BillPayment + attach Receipt to existing Bill | | **Receipt showing payment** | BillPayment + attach Receipt to existing Bill |
| **Bank statement entry** | Transaction + TransactionLines only | | **Bank statement entry** | Transaction + TransactionLines only |
| **Bank export file** | Run bank reconciliation workflow (see below) |
| **Journal adjustment** | Transaction + TransactionLines only | | **Journal adjustment** | Transaction + TransactionLines only |
**Key Rule:** If there's a vendor invoice number, always create a Bill record. **Key Rule:** If there's a vendor invoice number, always create a Bill record.
@@ -114,6 +118,18 @@ Same as above but Cr Due to Owner (id=22) instead of Checking
For detailed code: see [workflows.md](references/workflows.md) For detailed code: see [workflows.md](references/workflows.md)
## Bank Reconciliation Workflow (4 Phases)
Import bank transactions, match against ledger, create missing entries, and verify balances.
**Phase 1 — Import:** Read bank export file (Schwab JSON), parse dates/amounts, present summary
**Phase 2 — Match:** Fetch TransactionLines for bank account via `get_records`, match by amount + date (±3 days)
**Phase 3 — Create:** For unmatched bank entries, check BankRules, suggest offset account by Type, create Transaction + TransactionLines (Status="Cleared")
**Phase 4 — Reconcile:** Create Reconciliation record, calculate cleared balance, compare to statement balance
For detailed reference: see [reconciliation.md](references/reconciliation.md)
For workflow code examples: see [workflows.md](references/workflows.md)
## Validation Checklist ## Validation Checklist
After entering bills: After entering bills:
@@ -195,3 +211,4 @@ For full audit queries and remediation: see [audit.md](references/audit.md)
| [references/workflows.md](references/workflows.md) | Detailed code examples | | [references/workflows.md](references/workflows.md) | Detailed code examples |
| [references/queries.md](references/queries.md) | SQL queries and financial reports | | [references/queries.md](references/queries.md) | SQL queries and financial reports |
| [references/audit.md](references/audit.md) | Audit queries and remediation | | [references/audit.md](references/audit.md) | Audit queries and remediation |
| [references/reconciliation.md](references/reconciliation.md) | Bank reconciliation workflow |

View File

@@ -5,10 +5,7 @@ Common queries and financial report templates.
## Contents ## Contents
- [Common Queries](#common-queries) - [Common Queries](#common-queries)
- [Financial Reports](#financial-reports) - [Financial Reports](#financial-reports)
- [Balance Sheet](#balance-sheet) - [Reconciliation Queries](#reconciliation-queries)
- [Income Statement](#income-statement)
- [Trial Balance](#trial-balance)
- [AP Aging](#accounts-payable-aging)
## Common Queries ## Common Queries
@@ -179,3 +176,54 @@ JOIN Vendors v ON b.Vendor = v.id
WHERE b.Status IN ('Open', 'Partial') WHERE b.Status IN ('Open', 'Partial')
ORDER BY b.DueDate ORDER BY b.DueDate
``` ```
## Reconciliation Queries
### Cleared Balance for Bank Account
Use `get_records` to avoid the `Transaction` reserved word issue:
```python
# Step 1: Get all TransactionLines for Checking (id=14)
lines = get_records("TransactionLines", filter={"Account": [14]})
# Step 2: Get cleared transaction IDs
txn_ids = list(set(line["Transaction"] for line in lines))
cleared = sql_query(f"SELECT id FROM Transactions WHERE Status = 'Cleared' AND id IN (...)")
cleared_ids = set(row["id"] for row in cleared)
# Step 3: Sum Debit - Credit for cleared lines
cleared_balance = sum(
line["Debit"] - line["Credit"]
for line in lines
if line["Transaction"] in cleared_ids
)
```
### Outstanding Items (Not Cleared by Bank)
```sql
SELECT t.id, t.Date, t.Description, t.Status,
tl.Debit, tl.Credit
FROM TransactionLines tl
JOIN Transactions t ON tl.Transaction = t.id
WHERE tl.Account = 14
AND t.Status != 'Cleared'
ORDER BY t.Date
```
### Reconciliation History
```sql
SELECT id, StatementDate, StatementBalance,
ClearedBalance, Difference, Status
FROM Reconciliations
WHERE Account = 14
ORDER BY StatementDate DESC
```
### Active Bank Rules
```python
get_records("BankRules", filter={"Account": [14], "IsActive": [true]})
```

View File

@@ -0,0 +1,242 @@
# Bank Reconciliation
Import bank transactions, match against ledger entries, create missing transactions, and reconcile balances.
## Contents
- [Bank File Parsing](#bank-file-parsing)
- [Transaction Matching](#transaction-matching)
- [Creating Missing Transactions](#creating-missing-transactions)
- [Balance Reconciliation](#balance-reconciliation)
- [Queries](#queries)
- [Edge Cases](#edge-cases)
## Bank File Parsing
### Schwab JSON Format
Schwab exports a JSON file with this structure:
```json
{
"FromDate": "MM/DD/YYYY",
"ToDate": "MM/DD/YYYY",
"PendingTransactions": [],
"PostedTransactions": [
{
"CheckNumber": null,
"Description": "Interest Paid",
"Date": "MM/DD/YYYY",
"RunningBalance": "$1,500.01",
"Withdrawal": "",
"Deposit": "$0.01",
"Type": "INTADJUST"
}
]
}
```
### Parsing Rules
1. **Date**: Convert `MM/DD/YYYY` → Unix timestamp. Example: `01/14/2026``1736812800`
2. **Amount**: Parse `$` strings, remove commas. `Deposit` = positive, `Withdrawal` = negative. Empty string = no amount.
3. **Sort**: By date ascending after parsing
4. **Zero-amount entries**: Flag for user review (e.g., `INTADJUST` with no Deposit/Withdrawal)
5. **Statement ending balance**: Extract from last entry's `RunningBalance` (chronologically latest)
### Date Conversion Reference
Use Jan 1, 2026 = 1767312000 as base, add `days * 86400`.
| Date String | Unix Timestamp | Calculation |
|-------------|---------------|-------------|
| 12/31/2025 | 1767225600 | Jan 1 - 1 day |
| 01/14/2026 | 1768435200 | Jan 1 + 13 days |
| 01/30/2026 | 1769817600 | Jan 1 + 29 days |
| 02/17/2026 | 1771372800 | Jan 1 + 47 days |
### Type Field Mapping
| Bank Type | Suggested Offset Account | Notes |
|-----------|-------------------------|-------|
| `INTADJUST` | Interest Income (4010, id=26) | Bank interest payments |
| `TRANSFER` | Owner's Investment (3001, id=23) | Incoming transfers — confirm with user |
| `ATM` | Owner's Draws (3002, id=24) | ATM withdrawals |
| `CHECK` | Prompt user | Check payments |
| `ACH` | Prompt user | ACH debits |
| `DEBIT` | Prompt user | Debit card transactions |
## Transaction Matching
### Algorithm
For each bank transaction, find a matching ledger entry:
1. Fetch all TransactionLines for the bank account using `get_records` (not `sql_query``Transaction` is a reserved word)
2. For each bank transaction with an amount:
- Find ledger entries where: **exact amount match** AND **date within ±3 days** AND **not already matched**
- Deposits match TransactionLines where `Debit > 0` (money into checking = debit to asset)
- Withdrawals match TransactionLines where `Credit > 0` (money out of checking = credit to asset)
3. Mark each bank entry: **Matched**, **Unmatched**, or **Skipped** (no amount)
4. Mark each ledger entry: **Matched** or **Outstanding**
### Matching with get_records
```python
# Fetch all TransactionLines for Checking Account (id=14)
get_records("TransactionLines", filter={"Account": [14]})
# Then fetch transaction details for date comparison
# Get unique transaction IDs from the lines, then:
sql_query("SELECT id, Date, Description, Status FROM Transactions WHERE id IN (id1, id2, ...)")
```
### Match Classification
| Bank Entry | Ledger Entry | Classification |
|------------|-------------|----------------|
| Has match | Has match | **Matched** — both confirmed |
| No match | — | **Unmatched (bank only)** — needs new ledger entry |
| No amount | — | **Skipped** — review manually |
| — | No match | **Outstanding** — in ledger but not cleared by bank |
## Creating Missing Transactions
For each unmatched bank transaction:
### 1. Check BankRules
```python
get_records("BankRules", filter={"Account": [14], "IsActive": [true]})
```
For each rule, check if the bank description matches the pattern:
- **Contains**: pattern is a substring of bank description
- **Starts With**: bank description starts with pattern
- **Exact**: bank description equals pattern
If a rule matches, auto-fill the offset account and description.
### 2. Determine Offset Account
Use the Type field mapping above as a suggestion. Always confirm with user for ambiguous types.
### 3. Create Transaction
Use the bank-import templates:
- **Deposits** → `bank-import-deposit.json` (Dr Checking, Cr Offset)
- **Withdrawals** → `bank-import-expense.json` (Dr Offset, Cr Checking)
All imported transactions use `Status = "Cleared"` since the bank confirms them.
### 4. Optionally Save BankRule
If user agrees, create a BankRule for future auto-categorization:
```python
add_records("BankRules", [{
"Account": 14,
"Pattern": "Interest Paid",
"MatchType": "Contains",
"OffsetAccount": 26,
"TransactionDescription": "Bank interest income",
"IsActive": true
}])
```
## Balance Reconciliation
### Calculate Cleared Balance
After matching and creating missing transactions:
```python
# Get all TransactionLines for Checking Account
lines = get_records("TransactionLines", filter={"Account": [14]})
# Get transaction IDs and filter for Cleared status
txn_ids = [unique transaction IDs from lines]
cleared_txns = sql_query("SELECT id FROM Transactions WHERE Status = 'Cleared' AND id IN (...)")
# Sum: Debit - Credit for cleared lines only
cleared_balance = sum(line.Debit - line.Credit for line in lines if line.Transaction in cleared_txn_ids)
```
### Reconciliation Record
```python
# Create reconciliation record
add_records("Reconciliations", [{
"Account": 14,
"StatementDate": statement_date_timestamp,
"StatementBalance": statement_balance,
"ClearedBalance": cleared_balance,
"Difference": statement_balance - cleared_balance,
"Status": "In Progress",
"StartedAt": today_timestamp,
"Notes": "Reconciling against Schwab export"
}])
```
### Finalization
- **Difference = $0**: Update Status to "Completed", set CompletedAt
- **Difference ≠ $0**: Report discrepancy, list outstanding items, offer options:
1. Review unmatched items
2. Create adjusting entry
3. Save progress and return later
## Queries
### Cleared Balance for Account
```python
# Use get_records to avoid Transaction reserved word issue
lines = get_records("TransactionLines", filter={"Account": [14]})
# Then filter by cleared transactions and sum Debit - Credit
```
### Outstanding Items (in ledger, not cleared)
```sql
SELECT t.id, t.Date, t.Description, t.Status,
tl.Debit, tl.Credit
FROM TransactionLines tl
JOIN Transactions t ON tl.Transaction = t.id
WHERE tl.Account = 14
AND t.Status != 'Cleared'
ORDER BY t.Date
```
### Reconciliation History
```sql
SELECT r.id, r.StatementDate, r.StatementBalance,
r.ClearedBalance, r.Difference, r.Status
FROM Reconciliations r
WHERE r.Account = 14
ORDER BY r.StatementDate DESC
```
### Unmatched Bank Rules
```python
get_records("BankRules", filter={"Account": [14], "IsActive": [true]})
```
## Edge Cases
### Zero-Amount Entries
Some bank entries (e.g., `INTADJUST` with empty Deposit and Withdrawal) have no monetary value. Skip these during matching and creation but report them to the user.
### Duplicate Amounts
When multiple bank transactions have the same amount, use date proximity as the tiebreaker. If still ambiguous, present options to the user.
### Re-imports
If the same bank file is imported again, the matching phase will find existing ledger entries for previously imported transactions. Only truly new entries will be unmatched.
### Partial Reconciliation
If the user can't resolve all differences in one session, save the Reconciliation with Status="In Progress". Resume later by loading the record and continuing from Phase 2.

View File

@@ -84,6 +84,29 @@ Complete table schemas for the Grist accounting system.
| Amount | Numeric | Payment amount | | Amount | Numeric | Payment amount |
| PaymentDate | Date | Unix timestamp | | PaymentDate | Date | Unix timestamp |
## Reconciliations
| Column | Type | Notes |
|--------|------|-------|
| Account | Ref:Accounts | Bank account reconciled |
| StatementDate | Date | Statement ending date (Unix timestamp) |
| StatementBalance | Numeric | Ending balance per bank statement |
| ClearedBalance | Numeric | Sum of cleared transactions in ledger |
| Difference | Numeric | StatementBalance - ClearedBalance |
| Status | Choice | "In Progress", "Completed", "Abandoned" |
| StartedAt | Date | Unix timestamp |
| CompletedAt | Date | Unix timestamp (null until done) |
| Notes | Text | |
## BankRules
| Column | Type | Notes |
|--------|------|-------|
| Account | Ref:Accounts | Bank account this rule applies to |
| Pattern | Text | Substring to match against bank description |
| MatchType | Choice | "Contains", "Starts With", "Exact" |
| OffsetAccount | Ref:Accounts | Account to categorize to |
| TransactionDescription | Text | Description template for created transactions |
| IsActive | Bool | |
## Formula Columns (Auto-Calculated) ## Formula Columns (Auto-Calculated)
| Table.Column | Description | | Table.Column | Description |

View File

@@ -12,6 +12,8 @@ JSON templates for common accounting scenarios. Templates contain only **writabl
| Vendor invoice received but not yet paid | `bill-unpaid.json` | | Vendor invoice received but not yet paid | `bill-unpaid.json` |
| Recording payment for previously entered bill | `pay-existing-bill.json` | | Recording payment for previously entered bill | `pay-existing-bill.json` |
| Bank fees, minor expenses without invoices | `direct-expense.json` | | Bank fees, minor expenses without invoices | `direct-expense.json` |
| Bank import — unmatched deposit | `bank-import-deposit.json` |
| Bank import — unmatched withdrawal | `bank-import-expense.json` |
## Template Structure ## Template Structure
@@ -185,3 +187,5 @@ get_records("TransactionLines", filter={"Transaction": [52]})
| [bill-unpaid.json](../templates/bill-unpaid.json) | Invoice recorded but not yet paid | | [bill-unpaid.json](../templates/bill-unpaid.json) | Invoice recorded but not yet paid |
| [pay-existing-bill.json](../templates/pay-existing-bill.json) | Payment for previously entered bill | | [pay-existing-bill.json](../templates/pay-existing-bill.json) | Payment for previously entered bill |
| [direct-expense.json](../templates/direct-expense.json) | Direct expense without vendor bill | | [direct-expense.json](../templates/direct-expense.json) | Direct expense without vendor bill |
| [bank-import-deposit.json](../templates/bank-import-deposit.json) | Unmatched bank deposit (interest, transfer in) |
| [bank-import-expense.json](../templates/bank-import-expense.json) | Unmatched bank withdrawal (ATM, check, ACH) |

View File

@@ -12,6 +12,7 @@ Detailed code examples for common accounting operations.
- [Pay Bill via Owner](#pay-bill-via-owner-reimbursement) - [Pay Bill via Owner](#pay-bill-via-owner-reimbursement)
- [Reimburse Owner](#reimburse-owner) - [Reimburse Owner](#reimburse-owner)
- [Batch Operations](#batch-operations) - [Batch Operations](#batch-operations)
- [Bank Reconciliation](#bank-reconciliation)
## Create a Vendor ## Create a Vendor
@@ -298,5 +299,124 @@ For bank fees, minor expenses without vendor invoices:
| 23 | 3001 | Owner's Investment | | 23 | 3001 | Owner's Investment |
| 24 | 3002 | Owner's Draws | | 24 | 3002 | Owner's Draws |
| 25 | 4001 | Service Revenue | | 25 | 4001 | Service Revenue |
| 26 | 4010 | Interest Income |
| 30 | 5020 | Bank & Merchant Fees | | 30 | 5020 | Bank & Merchant Fees |
| 36 | 5080 | Software & Subscriptions | | 36 | 5080 | Software & Subscriptions |
## Bank Reconciliation
Import bank transactions, match against ledger, create missing entries, and reconcile.
For full reference: see [reconciliation.md](reconciliation.md)
### Phase 1: Import Bank File (Schwab JSON)
```python
# Read and parse the bank export file
import json
with open("path/to/schwab_export.json") as f:
data = json.load(f)
# Parse each PostedTransaction
for txn in data["PostedTransactions"]:
# Date: "MM/DD/YYYY" -> Unix timestamp
# Amount: parse "$X,XXX.XX" strings; Deposit = positive, Withdrawal = negative
# Empty string = no amount (skip)
pass
```
### Phase 2: Match Against Ledger
```python
# Fetch existing TransactionLines for Checking (id=14)
# MUST use get_records, not sql_query (Transaction is reserved word)
get_records("TransactionLines", filter={"Account": [14]})
# Get transaction details for date matching
sql_query("SELECT id, Date, Description, Status FROM Transactions WHERE id IN (...)")
# Match criteria: exact amount AND date ±3 days AND not already matched
# Deposits match Debit > 0 (money into checking = debit to asset)
# Withdrawals match Credit > 0 (money out of checking = credit to asset)
```
### Phase 3: Create Missing Transactions
```python
# For unmatched deposits (e.g., interest income):
add_records("Transactions", [{
"Date": 1738195200,
"Description": "Bank interest income",
"Reference": "Interest Paid",
"Status": "Cleared",
"Memo": "Auto-imported from bank statement"
}])
# Returns: {"inserted_ids": [txn_id]}
# Dr Checking, Cr Interest Income
add_records("TransactionLines", [
{"Transaction": txn_id, "Account": 14, "Debit": 0.01, "Credit": 0, "Memo": "Bank interest"},
{"Transaction": txn_id, "Account": 26, "Debit": 0, "Credit": 0.01, "Memo": "Bank interest"}
])
# For unmatched withdrawals (e.g., ATM owner draw):
add_records("Transactions", [{
"Date": 1739750400,
"Description": "ATM withdrawal - owner draw",
"Reference": "P421164 88 ESSEX STREET NEW YORK",
"Status": "Cleared",
"Memo": "Auto-imported from bank statement"
}])
# Dr Owner's Draws, Cr Checking
add_records("TransactionLines", [
{"Transaction": txn_id, "Account": 24, "Debit": 102.00, "Credit": 0, "Memo": "ATM withdrawal"},
{"Transaction": txn_id, "Account": 14, "Debit": 0, "Credit": 102.00, "Memo": "ATM withdrawal"}
])
# Optionally save a BankRule for auto-categorization:
add_records("BankRules", [{
"Account": 14,
"Pattern": "Interest Paid",
"MatchType": "Contains",
"OffsetAccount": 26,
"TransactionDescription": "Bank interest income",
"IsActive": true
}])
```
### Phase 4: Reconcile
```python
# Update matched existing transactions to Cleared status
update_records("Transactions", [
{"id": existing_txn_id, "fields": {"Status": "Cleared"}}
])
# Calculate cleared balance
lines = get_records("TransactionLines", filter={"Account": [14]})
txn_ids = list(set(l["fields"]["Transaction"] for l in lines))
cleared = sql_query(f"SELECT id FROM Transactions WHERE Status = 'Cleared' AND id IN (...)")
cleared_ids = set(r["id"] for r in cleared)
cleared_balance = sum(
l["fields"]["Debit"] - l["fields"]["Credit"]
for l in lines
if l["fields"]["Transaction"] in cleared_ids
)
# Create Reconciliation record
add_records("Reconciliations", [{
"Account": 14,
"StatementDate": 1739750400, # date of last bank entry
"StatementBalance": 1398.01,
"ClearedBalance": cleared_balance,
"Difference": 1398.01 - cleared_balance,
"Status": "Completed", # if Difference == 0
"StartedAt": 1739923200, # today
"CompletedAt": 1739923200,
"Notes": "Reconciled against Schwab export"
}])
# Verify
sql_query("SELECT * FROM Transactions WHERE IsBalanced = false")
```

View File

@@ -0,0 +1,86 @@
{
"_meta": {
"name": "Bank Import - Deposit",
"description": "Record a deposit found in bank statement but missing from the ledger",
"scenario": "Unmatched bank deposit (interest, incoming transfer, revenue)",
"when_to_use": [
"Bank interest payments",
"Incoming transfers from owner",
"Revenue deposits",
"Any bank credit not yet in the ledger"
],
"when_not_to_use": [
"Deposits already recorded in the ledger",
"Vendor refunds with existing bill records"
]
},
"_variables": {
"date_timestamp": "integer - Unix timestamp for transaction date",
"amount": "number - Deposit amount (positive)",
"offset_account_id": "integer - Account to credit (e.g., 23=Owner's Investment, 25=Service Revenue)",
"description": "string - Transaction description",
"reference": "string - Bank reference or description from statement",
"memo": "string - Additional notes (optional)"
},
"_sequence": [
"1. Create Transaction header (Status='Cleared') -> get txn_id",
"2. Create TransactionLines (Dr Checking, Cr Offset Account)",
"3. Verify IsBalanced = true"
],
"records": {
"transaction": {
"_doc": "Step 1: Create transaction header. Status is Cleared since bank confirms it.",
"_table": "Transactions",
"_operation": "add_records",
"payload": {
"Date": "{{date_timestamp}}",
"Description": "{{description}}",
"Reference": "{{reference}}",
"Status": "Cleared",
"Memo": "{{memo}}"
}
},
"transaction_lines": {
"_doc": "Step 2: Debit Checking (asset increase), Credit offset account.",
"_table": "TransactionLines",
"_operation": "add_records",
"_requires": ["txn_id"],
"payload": [
{
"Transaction": "{{txn_id}}",
"Account": 14,
"Debit": "{{amount}}",
"Credit": 0,
"Memo": "{{description}}"
},
{
"Transaction": "{{txn_id}}",
"Account": "{{offset_account_id}}",
"Debit": 0,
"Credit": "{{amount}}",
"Memo": "{{description}}"
}
]
}
},
"journal_entries": {
"_doc": "Summary of journal entry created by this template",
"entry": {
"description": "Record bank deposit",
"debits": [{"account": "Checking Account (1001)", "amount": "{{amount}}"}],
"credits": [{"account": "Offset Account", "amount": "{{amount}}"}]
}
},
"examples": {
"interest_income": {
"description": "Interest Paid",
"offset_account_id": 26,
"offset_account_name": "Interest Income (4010)"
},
"owner_transfer": {
"description": "Transfer from owner",
"offset_account_id": 23,
"offset_account_name": "Owner's Investment (3001)"
}
}
}

View File

@@ -0,0 +1,86 @@
{
"_meta": {
"name": "Bank Import - Withdrawal/Expense",
"description": "Record a withdrawal found in bank statement but missing from the ledger",
"scenario": "Unmatched bank withdrawal (ATM, check, ACH debit, owner draw)",
"when_to_use": [
"ATM withdrawals (owner draws)",
"Check payments",
"ACH debits",
"Any bank debit not yet in the ledger"
],
"when_not_to_use": [
"Withdrawals already recorded in the ledger",
"Bill payments with existing bill records - use pay-existing-bill.json"
]
},
"_variables": {
"date_timestamp": "integer - Unix timestamp for transaction date",
"amount": "number - Withdrawal amount (positive, will be credited to Checking)",
"offset_account_id": "integer - Account to debit (e.g., 24=Owner's Draws, 30=Bank Fees)",
"description": "string - Transaction description",
"reference": "string - Bank reference or description from statement",
"memo": "string - Additional notes (optional)"
},
"_sequence": [
"1. Create Transaction header (Status='Cleared') -> get txn_id",
"2. Create TransactionLines (Dr Offset Account, Cr Checking)",
"3. Verify IsBalanced = true"
],
"records": {
"transaction": {
"_doc": "Step 1: Create transaction header. Status is Cleared since bank confirms it.",
"_table": "Transactions",
"_operation": "add_records",
"payload": {
"Date": "{{date_timestamp}}",
"Description": "{{description}}",
"Reference": "{{reference}}",
"Status": "Cleared",
"Memo": "{{memo}}"
}
},
"transaction_lines": {
"_doc": "Step 2: Debit offset account, Credit Checking (asset decrease).",
"_table": "TransactionLines",
"_operation": "add_records",
"_requires": ["txn_id"],
"payload": [
{
"Transaction": "{{txn_id}}",
"Account": "{{offset_account_id}}",
"Debit": "{{amount}}",
"Credit": 0,
"Memo": "{{description}}"
},
{
"Transaction": "{{txn_id}}",
"Account": 14,
"Debit": 0,
"Credit": "{{amount}}",
"Memo": "{{description}}"
}
]
}
},
"journal_entries": {
"_doc": "Summary of journal entry created by this template",
"entry": {
"description": "Record bank withdrawal",
"debits": [{"account": "Offset Account", "amount": "{{amount}}"}],
"credits": [{"account": "Checking Account (1001)", "amount": "{{amount}}"}]
}
},
"examples": {
"owner_draw": {
"description": "ATM withdrawal - owner draw",
"offset_account_id": 24,
"offset_account_name": "Owner's Draws (3002)"
},
"bank_fee": {
"description": "Bank service fee",
"offset_account_id": 30,
"offset_account_name": "Bank & Merchant Fees (5020)"
}
}
}