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:
17
SKILL.md
17
SKILL.md
@@ -13,6 +13,7 @@ Double-entry accounting for sole proprietorship. Every transaction creates balan
|
||||
|------|--------|
|
||||
| Record vendor invoice | 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 |
|
||||
| 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) |
|
||||
| Pay existing bill | [pay-existing-bill.json](templates/pay-existing-bill.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.
|
||||
|
||||
@@ -38,6 +41,7 @@ Templates contain only writable fields. See [templates.md](references/templates.
|
||||
| **Invoice/Bill from vendor** | Bill + BillLines + Transaction + TransactionLines |
|
||||
| **Receipt showing payment** | BillPayment + attach Receipt to existing Bill |
|
||||
| **Bank statement entry** | Transaction + TransactionLines only |
|
||||
| **Bank export file** | Run bank reconciliation workflow (see below) |
|
||||
| **Journal adjustment** | Transaction + TransactionLines only |
|
||||
|
||||
**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)
|
||||
|
||||
## 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
|
||||
|
||||
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/queries.md](references/queries.md) | SQL queries and financial reports |
|
||||
| [references/audit.md](references/audit.md) | Audit queries and remediation |
|
||||
| [references/reconciliation.md](references/reconciliation.md) | Bank reconciliation workflow |
|
||||
|
||||
@@ -5,10 +5,7 @@ Common queries and financial report templates.
|
||||
## Contents
|
||||
- [Common Queries](#common-queries)
|
||||
- [Financial Reports](#financial-reports)
|
||||
- [Balance Sheet](#balance-sheet)
|
||||
- [Income Statement](#income-statement)
|
||||
- [Trial Balance](#trial-balance)
|
||||
- [AP Aging](#accounts-payable-aging)
|
||||
- [Reconciliation Queries](#reconciliation-queries)
|
||||
|
||||
## Common Queries
|
||||
|
||||
@@ -179,3 +176,54 @@ JOIN Vendors v ON b.Vendor = v.id
|
||||
WHERE b.Status IN ('Open', 'Partial')
|
||||
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]})
|
||||
```
|
||||
|
||||
242
references/reconciliation.md
Normal file
242
references/reconciliation.md
Normal 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.
|
||||
@@ -84,6 +84,29 @@ Complete table schemas for the Grist accounting system.
|
||||
| Amount | Numeric | Payment amount |
|
||||
| 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)
|
||||
|
||||
| Table.Column | Description |
|
||||
|
||||
@@ -12,6 +12,8 @@ JSON templates for common accounting scenarios. Templates contain only **writabl
|
||||
| Vendor invoice received but not yet paid | `bill-unpaid.json` |
|
||||
| Recording payment for previously entered bill | `pay-existing-bill.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
|
||||
|
||||
@@ -185,3 +187,5 @@ get_records("TransactionLines", filter={"Transaction": [52]})
|
||||
| [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 |
|
||||
| [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) |
|
||||
|
||||
@@ -12,6 +12,7 @@ Detailed code examples for common accounting operations.
|
||||
- [Pay Bill via Owner](#pay-bill-via-owner-reimbursement)
|
||||
- [Reimburse Owner](#reimburse-owner)
|
||||
- [Batch Operations](#batch-operations)
|
||||
- [Bank Reconciliation](#bank-reconciliation)
|
||||
|
||||
## Create a Vendor
|
||||
|
||||
@@ -298,5 +299,124 @@ For bank fees, minor expenses without vendor invoices:
|
||||
| 23 | 3001 | Owner's Investment |
|
||||
| 24 | 3002 | Owner's Draws |
|
||||
| 25 | 4001 | Service Revenue |
|
||||
| 26 | 4010 | Interest Income |
|
||||
| 30 | 5020 | Bank & Merchant Fees |
|
||||
| 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")
|
||||
```
|
||||
|
||||
86
templates/bank-import-deposit.json
Normal file
86
templates/bank-import-deposit.json
Normal 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)"
|
||||
}
|
||||
}
|
||||
}
|
||||
86
templates/bank-import-expense.json
Normal file
86
templates/bank-import-expense.json
Normal 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)"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user