Files
grist-accounting/SKILL.md
Bill Ballou 70ac6be681 Add decision guide for when to create Bills vs Transactions
Adds a quick-reference section near the top of the skill that clarifies:
- Invoice/bill PDFs should always create Bill records
- Receipts attach to existing Bills via BillPayment
- Bank entries and journal adjustments are Transaction-only

Includes step-by-step checklist for common "vendor invoice paid by owner" workflow.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 21:46:34 -05:00

635 lines
19 KiB
Markdown

---
name: grist-accounting
description: Use when working with the Grist double-entry accounting system - recording transactions, entering bills, tracking vendors, querying account balances, or generating financial reports
---
# Grist Double-Entry Accounting System
## Overview
A complete double-entry accounting system for sole proprietorship service businesses. Every transaction creates balanced journal entries (debits = credits). Account balances roll up through parent-child hierarchy.
## Recording Transactions: Decision Guide
| Source Document | What to Create |
|-----------------|----------------|
| **Invoice/Bill PDF from vendor** | Bill + BillLines + Transaction + TransactionLines |
| **Receipt showing payment** | BillPayment + attach Receipt to existing Bill |
| **Bank statement entry** | Transaction + TransactionLines only |
| **Journal adjustment** | Transaction + TransactionLines only |
**Key Rule:** If there's a vendor invoice number, always create a Bill record—not just a transaction. Bills provide:
- Vendor tracking and AP aging
- Invoice/Receipt attachment storage
- Payment status tracking (Open/Partial/Paid)
### Quick Reference: Vendor Invoice Paid by Owner
When recording an invoice that was already paid by the owner:
1. Upload Invoice attachment → get attachment_id
2. Create Bill (Status: "Paid", Invoice: ["L", attachment_id])
3. Create BillLine(s) with expense account and amount
4. Create Transaction (debit Expense, credit Due to Owner)
5. Create TransactionLines
6. Update Bill.EntryTransaction to link to transaction
7. Create BillPayment record
8. Upload Receipt attachment if available
## MCP Tools Available
| Tool | Purpose |
|------|---------|
| `mcp__grist-accounting__list_documents` | List accessible Grist documents |
| `mcp__grist-accounting__list_tables` | List tables in a document |
| `mcp__grist-accounting__describe_table` | Get column schema for a table |
| `mcp__grist-accounting__get_records` | Fetch records (with optional filter, sort, limit) |
| `mcp__grist-accounting__add_records` | Insert new records, returns `{"inserted_ids": [...]}` |
| `mcp__grist-accounting__update_records` | Update existing records by ID |
| `mcp__grist-accounting__delete_records` | Delete records by ID |
| `mcp__grist-accounting__sql_query` | Run read-only SQL queries |
The document name is `accounting` for all operations.
## Date Handling
All date fields use **Unix timestamps** (seconds since 1970-01-01 UTC).
| Date | Timestamp |
|------|-----------|
| Oct 1, 2025 | 1759363200 |
| Nov 1, 2025 | 1762041600 |
| Dec 1, 2025 | 1764633600 |
| Jan 1, 2026 | 1767312000 |
Python: `int(datetime(2025, 10, 1).timestamp())`
## Complete Table Schemas
### Accounts
| Column | Type | Notes |
|--------|------|-------|
| Code | Text | Account number (e.g., "5080") |
| Name | Text | Account name |
| Type | Choice | "Asset", "Liability", "Equity", "Income", "Expense" |
| Parent | Ref:Accounts | Parent account for hierarchy (0 = top-level) |
| Description | Text | |
| IsActive | Bool | |
| Balance | Formula | Calculated from transactions |
### Vendors
| Column | Type | Notes |
|--------|------|-------|
| Name | Text | Vendor name |
| DefaultExpenseAccount | Ref:Accounts | Auto-fills on bill lines |
| PaymentTerms | Choice | "Due on Receipt", "Net 15", "Net 30" |
| Notes | Text | |
| IsActive | Bool | |
| Balance | Formula | Sum of unpaid bills |
### Items
| Column | Type | Notes |
|--------|------|-------|
| Name | Text | Item name (e.g., "Software Subscription") |
| DefaultAccount | Ref:Accounts | Expense account for this item |
| DefaultDescription | Text | Auto-fills on bill lines |
| IsActive | Bool | |
### Bills
| Column | Type | Notes |
|--------|------|-------|
| Vendor | Ref:Vendors | Required |
| BillNumber | Text | Invoice number from vendor |
| BillDate | Date | Unix timestamp |
| DueDate | Date | Unix timestamp |
| Status | Choice | "Open", "Partial", "Paid" |
| Memo | Text | |
| EntryTransaction | Ref:Transactions | Link to journal entry |
| Invoice | Attachments | Vendor invoice document (use `["L", id]` format) |
| Receipt | Attachments | Payment receipt/confirmation (use `["L", id]` format) |
| Amount | Formula | Sum of BillLines.Amount |
| AmountPaid | Formula | Sum of BillPayments.Amount |
| AmountDue | Formula | Amount - AmountPaid |
### BillLines
| Column | Type | Notes |
|--------|------|-------|
| Bill | Ref:Bills | Required |
| Item | Ref:Items | Optional - auto-fills Account/Description |
| Account | Ref:Accounts | Expense account |
| Description | Text | |
| Amount | Numeric | Line item amount |
### Transactions
| Column | Type | Notes |
|--------|------|-------|
| Date | Date | Unix timestamp |
| Description | Text | Transaction description |
| Reference | Text | Check number, invoice reference, etc. |
| Status | Choice | "Draft", "Posted", "Cleared" |
| Memo | Text | |
| Total | Formula | Sum of debits |
| IsBalanced | Formula | True if debits = credits |
### TransactionLines
| Column | Type | Notes |
|--------|------|-------|
| Transaction | Ref:Transactions | Required |
| Account | Ref:Accounts | Required |
| Debit | Numeric | Debit amount (or 0) |
| Credit | Numeric | Credit amount (or 0) |
| Memo | Text | |
### BillPayments
| Column | Type | Notes |
|--------|------|-------|
| Bill | Ref:Bills | Required |
| Transaction | Ref:Transactions | Payment journal entry |
| Amount | Numeric | Payment amount |
| PaymentDate | Date | Unix timestamp |
## Key Account IDs
| ID | Code | Name | Type |
|----|------|------|------|
| 4 | 2000 | Accounts Payable | Liability |
| 14 | 1001 | Checking Account | Asset |
| 22 | 2203 | Due to Owner | Liability |
| 36 | 5080 | Software & Subscriptions | Expense |
Query all accounts:
```sql
SELECT id, Code, Name, Type FROM Accounts WHERE IsActive = true ORDER BY Code
```
## Account Types
| Type | Normal Balance | Increases With | Examples |
|------|----------------|----------------|----------|
| Asset | Debit | Debit | Cash, AR, Prepaid |
| Liability | Credit | Credit | AP, Credit Cards, Due to Owner |
| Equity | Credit | Credit | Owner's Investment, Draws, Retained Earnings |
| Income | Credit | Credit | Service Revenue, Interest Income |
| Expense | Debit | Debit | Rent, Utilities, Office Supplies |
## Complete Workflows
### Create a Vendor
```python
add_records("Vendors", [{
"Name": "Acme Corp",
"DefaultExpenseAccount": 36, # Software & Subscriptions
"PaymentTerms": "Due on Receipt",
"Notes": "Software vendor",
"IsActive": True
}])
# Returns: {"inserted_ids": [vendor_id]}
```
### Create Items for Common Purchases
```python
add_records("Items", [{
"Name": "Monthly Software",
"DefaultAccount": 36,
"DefaultDescription": "Monthly SaaS subscription",
"IsActive": True
}])
```
### Complete Bill Entry (5 Steps)
**Step 1: Create Bill Header**
```python
add_records("Bills", [{
"Vendor": 1, # vendor_id
"BillNumber": "INV-001",
"BillDate": 1759708800, # Unix timestamp
"DueDate": 1759708800,
"Status": "Open",
"Memo": "October services"
}])
# Returns: {"inserted_ids": [bill_id]}
```
**Step 2: Create Bill Line(s)**
```python
add_records("BillLines", [{
"Bill": 1, # bill_id from step 1
"Item": 1, # optional - auto-fills Account/Description
"Account": 36, # expense account
"Description": "Monthly subscription",
"Amount": 100.00
}])
```
**Step 3: Create Journal Entry**
```python
# Transaction header
add_records("Transactions", [{
"Date": 1759708800,
"Description": "Acme Corp - October services",
"Reference": "INV-001",
"Status": "Posted"
}])
# Returns: {"inserted_ids": [txn_id]}
# Transaction lines: Debit expense, Credit AP
add_records("TransactionLines", [
{"Transaction": 1, "Account": 36, "Debit": 100.00, "Credit": 0, "Memo": "Monthly subscription"},
{"Transaction": 1, "Account": 4, "Debit": 0, "Credit": 100.00, "Memo": "Monthly subscription"}
])
```
**Step 4: Link Bill to Transaction**
```python
update_records("Bills", [{"id": 1, "fields": {"EntryTransaction": 1}}])
```
**Step 5: Upload Invoice (if available)**
If an invoice PDF is available, upload and link it to the Invoice field:
```bash
# Get session token, then upload to Invoice field
bash /path/to/scripts/upload-attachment.sh invoice.pdf Bills 1 $TOKEN Invoice
```
Or for batch uploads, use a script (see Batch Operations).
### Pay Bill from Checking Account
```python
# Step 1: Create payment transaction
add_records("Transactions", [{
"Date": 1760832000,
"Description": "Payment - Acme Corp INV-001",
"Reference": "Check #1001",
"Status": "Cleared"
}])
# Returns: {"inserted_ids": [txn_id]}
# Step 2: Debit AP, Credit Checking
add_records("TransactionLines", [
{"Transaction": 2, "Account": 4, "Debit": 100.00, "Credit": 0, "Memo": "Pay INV-001"},
{"Transaction": 2, "Account": 14, "Debit": 0, "Credit": 100.00, "Memo": "Pay INV-001"}
])
# Step 3: Create BillPayment record
add_records("BillPayments", [{
"Bill": 1,
"Transaction": 2,
"Amount": 100.00,
"PaymentDate": 1760832000
}])
# Step 4: Update bill status
update_records("Bills", [{"id": 1, "fields": {"Status": "Paid"}}])
# Step 5: Upload receipt (if available)
bash /path/to/scripts/upload-attachment.sh receipt.pdf Bills 1 $TOKEN Receipt
```
### Pay Bill via Owner Reimbursement
When the owner pays a business expense personally:
```python
# Step 1: Create payment transaction
add_records("Transactions", [{
"Date": 1760832000,
"Description": "Owner payment - Acme Corp INV-001",
"Reference": "Owner Reimb",
"Status": "Posted"
}])
# Step 2: Debit AP, Credit Due to Owner (not Checking)
add_records("TransactionLines", [
{"Transaction": 2, "Account": 4, "Debit": 100.00, "Credit": 0, "Memo": "Pay INV-001"},
{"Transaction": 2, "Account": 22, "Debit": 0, "Credit": 100.00, "Memo": "Owner paid"}
])
# Step 3: Create BillPayment record
add_records("BillPayments", [{
"Bill": 1,
"Transaction": 2,
"Amount": 100.00,
"PaymentDate": 1760832000
}])
# Step 4: Update bill status
update_records("Bills", [{"id": 1, "fields": {"Status": "Paid"}}])
# Step 5: Upload receipt (if available)
bash /path/to/scripts/upload-attachment.sh receipt.pdf Bills 1 $TOKEN Receipt
```
### Reimburse Owner
When business pays back the owner:
```python
add_records("Transactions", [{
"Date": 1762041600,
"Description": "Owner reimbursement",
"Reference": "Transfer",
"Status": "Cleared"
}])
add_records("TransactionLines", [
{"Transaction": 3, "Account": 22, "Debit": 500.00, "Credit": 0, "Memo": "Reimburse owner"},
{"Transaction": 3, "Account": 14, "Debit": 0, "Credit": 500.00, "Memo": "Reimburse owner"}
])
```
## Batch Operations
When entering multiple bills efficiently:
1. **Create all Bills first** → collect inserted IDs
2. **Create all BillLines** referencing bill IDs
3. **Create all Transactions** → collect inserted IDs
4. **Create all TransactionLines** referencing transaction IDs
5. **Update all Bills** with EntryTransaction links in one call
6. (If paying) Create payment transactions, lines, and BillPayments
7. **Upload invoice attachments** if files are available
### Batch Attachment Uploads
When invoice files are available, upload them after bill entry:
1. Request session token with write permission (1 hour TTL for batch work)
2. Create a mapping of bill_id → invoice file path
3. Loop: upload each file, link to corresponding bill
```bash
# Example batch upload pattern for invoices
TOKEN=$(request_session_token with write permission)
for each (bill_id, invoice_path):
curl -X POST -H "Authorization: Bearer $TOKEN" \
-F "file=@$invoice_path" \
https://grist-mcp.bballou.com/api/v1/attachments
# Returns attachment_id
update_records("Bills", [{"id": bill_id, "fields": {"Invoice": ["L", attachment_id]}}])
# For receipts (after payment):
update_records("Bills", [{"id": bill_id, "fields": {"Receipt": ["L", attachment_id]}}])
```
Example batch update:
```python
update_records("Bills", [
{"id": 1, "fields": {"EntryTransaction": 1}},
{"id": 2, "fields": {"EntryTransaction": 2}},
{"id": 3, "fields": {"EntryTransaction": 3}}
])
```
## Common Queries
### Unpaid Bills by Vendor
```sql
SELECT v.Name, b.BillNumber, b.BillDate, b.Amount, b.AmountDue
FROM Bills b
JOIN Vendors v ON b.Vendor = v.id
WHERE b.Status IN ('Open', 'Partial')
ORDER BY b.DueDate
```
### Bills Summary by Vendor
```sql
SELECT v.Name as Vendor, COUNT(b.id) as Bills, SUM(b.Amount) as Total, SUM(b.AmountDue) as Due
FROM Bills b
JOIN Vendors v ON b.Vendor = v.id
GROUP BY v.Name
ORDER BY Total DESC
```
### Account Balances (Non-Zero)
```sql
SELECT Code, Name, Type, Balance
FROM Accounts
WHERE Balance != 0
ORDER BY Code
```
### Owner Reimbursement Balance
```sql
SELECT Balance FROM Accounts WHERE Code = '2203'
```
### Expense Summary by Account
```sql
SELECT a.Code, a.Name, a.Balance
FROM Accounts a
WHERE a.Type = 'Expense' AND a.Balance != 0
ORDER BY a.Balance DESC
```
### Transaction History for Account
```sql
SELECT t.Date, t.Description, t.Reference, tl.Debit, tl.Credit
FROM TransactionLines tl
JOIN Transactions t ON tl.Transaction = t.id
WHERE tl.Account = 36
ORDER BY t.Date DESC
```
### Verify All Transactions Balance
```sql
SELECT id, Description, Total, IsBalanced
FROM Transactions
WHERE IsBalanced = false
```
## Financial Reports
### Balance Sheet
Shows Assets = Liabilities + Equity at a point in time.
**Important:** Parent accounts roll up child balances. Query only top-level parents (Parent = 0) to avoid double-counting.
```sql
-- Assets, Liabilities, Equity (top-level only)
SELECT Code, Name, Type, Balance
FROM Accounts
WHERE Type IN ('Asset', 'Liability', 'Equity')
AND Parent = 0
ORDER BY Type, Code
```
```sql
-- Net Income (for Equity section)
-- Query leaf expense accounts only (no children)
SELECT
COALESCE(SUM(CASE WHEN Type = 'Income' THEN Balance ELSE 0 END), 0) -
COALESCE(SUM(CASE WHEN Type = 'Expense' THEN Balance ELSE 0 END), 0) as NetIncome
FROM Accounts
WHERE Type IN ('Income', 'Expense')
AND id NOT IN (SELECT DISTINCT Parent FROM Accounts WHERE Parent != 0)
```
**Presentation:**
| **Assets** | |
|---|---:|
| Cash & Bank Accounts | $X.XX |
| Accounts Receivable | $X.XX |
| **Total Assets** | **$X.XX** |
| **Liabilities** | |
|---|---:|
| Accounts Payable | $X.XX |
| Due to Owner | $X.XX |
| **Total Liabilities** | **$X.XX** |
| **Equity** | |
|---|---:|
| Retained Earnings | $X.XX |
| Net Income (Loss) | $X.XX |
| **Total Equity** | **$X.XX** |
| **Total Liabilities + Equity** | **$X.XX** |
### Income Statement
Shows Revenue - Expenses = Net Income for a period.
```sql
-- All income and expense accounts (leaf accounts only)
SELECT Code, Name, Type, Balance
FROM Accounts
WHERE Type IN ('Income', 'Expense')
AND Balance != 0
AND id NOT IN (SELECT DISTINCT Parent FROM Accounts WHERE Parent != 0)
ORDER BY Type DESC, Code
```
**Presentation:**
| **Income** | |
|---|---:|
| Service Revenue | $X.XX |
| **Total Income** | **$X.XX** |
| **Expenses** | |
|---|---:|
| Software & Subscriptions | $X.XX |
| Professional Services | $X.XX |
| **Total Expenses** | **$X.XX** |
| **Net Income (Loss)** | **$X.XX** |
### Trial Balance
Lists all accounts with non-zero balances. Debits should equal Credits.
```sql
SELECT
Code,
Name,
Type,
CASE WHEN Type IN ('Asset', 'Expense') THEN Balance ELSE 0 END as Debit,
CASE WHEN Type IN ('Liability', 'Equity', 'Income') THEN Balance ELSE 0 END as Credit
FROM Accounts
WHERE Balance != 0
AND id NOT IN (SELECT DISTINCT Parent FROM Accounts WHERE Parent != 0)
ORDER BY Code
```
### Accounts Payable Aging
```sql
SELECT
v.Name as Vendor,
b.BillNumber,
b.BillDate,
b.DueDate,
b.AmountDue,
CASE
WHEN b.DueDate >= strftime('%s', 'now') THEN 'Current'
WHEN b.DueDate >= strftime('%s', 'now') - 2592000 THEN '1-30 Days'
WHEN b.DueDate >= strftime('%s', 'now') - 5184000 THEN '31-60 Days'
ELSE '60+ Days'
END as Aging
FROM Bills b
JOIN Vendors v ON b.Vendor = v.id
WHERE b.Status IN ('Open', 'Partial')
ORDER BY b.DueDate
```
## Validation Checklist
After entering bills, verify:
- [ ] Total bills match expected: `SELECT SUM(Amount) FROM Bills`
- [ ] All transactions balanced: `SELECT * FROM Transactions WHERE IsBalanced = false`
- [ ] AP balance correct: `SELECT Balance FROM Accounts WHERE Code = '2000'`
- [ ] Expense accounts increased appropriately
- [ ] Vendor balances reflect unpaid bills
- [ ] Invoices attached: `SELECT id, BillNumber FROM Bills WHERE Invoice IS NULL`
- [ ] Receipts attached for paid bills: `SELECT id, BillNumber FROM Bills WHERE Status = 'Paid' AND Receipt IS NULL`
## Common Mistakes
| Mistake | Fix |
|---------|-----|
| Transaction not balanced | Ensure SUM(Debit) = SUM(Credit) before saving |
| Wrong debit/credit direction | Assets/Expenses increase with debit; Liabilities/Equity/Income increase with credit |
| Posting to parent account | Post to leaf accounts (1001 Checking, not 1000 Cash) |
| Forgetting AP entry for bills | Bills need both the expense debit AND the AP credit |
| Missing EntryTransaction link | Always update Bill.EntryTransaction after creating journal entry |
| Bill status not updated | Manually set Status to "Paid" after full payment |
| Using string dates | Dates must be Unix timestamps (seconds), not strings |
| Missing invoice/receipt | Upload invoice after bill entry, receipt after payment |
## Uploading Attachments
Attachments (invoices, receipts) are uploaded via the HTTP proxy endpoint, not MCP tools. This is efficient for binary files.
### Workflow
1. **Request session token** with write permission via MCP
2. **Upload file** via `POST /api/v1/attachments` with multipart/form-data
3. **Link attachment** to record via `update_records`
### Upload Script
Use `scripts/upload-attachment.sh` in this skill directory:
```bash
# Get session token first (via MCP request_session_token tool)
# Then run:
bash scripts/upload-attachment.sh invoice.pdf Bills 13 # Invoice column (default)
bash scripts/upload-attachment.sh invoice.pdf Bills 13 $TOKEN # With token
bash scripts/upload-attachment.sh receipt.pdf Bills 13 $TOKEN Receipt # Receipt column
# Environment variable for custom endpoint:
GRIST_MCP_URL=https://custom.example.com bash scripts/upload-attachment.sh ...
```
Run `bash scripts/upload-attachment.sh` without arguments for full usage.
### Linking Attachments Manually
Grist attachment columns use format: `["L", attachment_id]`
```python
# Link invoice to bill
update_records("Bills", [{"id": 13, "fields": {"Invoice": ["L", 1]}}])
# Link receipt to bill (after payment)
update_records("Bills", [{"id": 13, "fields": {"Receipt": ["L", 2]}}])
```
## Formula Columns (Auto-Calculated)
| Table.Column | Description |
|--------------|-------------|
| Accounts.Balance | OwnBalance + ChildrenBalance |
| Transactions.IsBalanced | True if sum of debits = sum of credits |
| Transactions.Total | Sum of debit amounts |
| Bills.Amount | Sum of BillLines.Amount |
| Bills.AmountPaid | Sum of BillPayments.Amount |
| Bills.AmountDue | Amount - AmountPaid |
| Vendors.Balance | Sum of AmountDue for unpaid bills |