Account Statement Documentation¶
Overview¶
The Account Statement module (account.statement) manages bank and account statements, tracking imported transactions and facilitating bank reconciliation. Statements represent a period of activity on an account, containing individual transaction lines that need to be matched with accounting entries.
Model Information¶
Model Name: account.statement
Display Name: Statement
Name Field: date_start
Key Fields: None (auto-increment ID)
Default Sort Order: date_start desc (newest first)
Features¶
- ✅ Multi-company support
- ✅ Audit logging enabled
- ✅ Statement period tracking (start/end dates)
- ✅ Opening and closing balance computation
- ✅ Automatic bank reconciliation trigger
- ✅ Reconciliation status tracking
- ✅ Statement line management
Understanding Account Statements¶
What is an Account Statement?¶
An Account Statement represents a period of transactions on an account (typically a bank account). It contains:
- Period Definition - Start and end dates
- Opening Balance - Balance at start of period
- Transaction Lines - Individual debits and credits
- Closing Balance - Computed balance at end of period
Statement Workflow¶
1. Import Statement → 2. Match Transactions → 3. Reconcile → 4. Complete
(date range, (link to journal (verify (all lines
balance, lines) entries) balances) reconciled)
Reconciliation States¶
| State | Description |
|---|---|
| Not Reconciled | Some or all lines not yet matched with accounting entries |
| Reconciled | All lines successfully matched and reconciled |
Key Fields Reference¶
Header Fields¶
| Field | Type | Required | Description |
|---|---|---|---|
date_imported |
Date | ❌ | When statement was imported (default: today) |
date_start |
Date | ✅ | Start date of statement period |
date_end |
Date | ✅ | End date of statement period |
balance_start |
Decimal | ✅ | Opening balance at period start |
account_id |
Many2One | ✅ | Bank/cash account for this statement |
company_id |
Many2One | ❌ | Company (default: active company) |
Computed Fields¶
| Field | Type | Description |
|---|---|---|
state |
Selection | Reconciliation status (reconciled/not_reconciled) |
balance_end |
Decimal | Closing balance (start + line movements) |
Relationship Fields¶
| Field | Type | Description |
|---|---|---|
lines |
One2Many | Transaction lines (account.statement.line) |
Field Details¶
date_start & date_end: - Define the statement period - Typically one month (e.g., 2025-01-01 to 2025-01-31) - Can be any period (weekly, monthly, quarterly)
balance_start: - Opening balance from bank statement - Should match previous statement's ending balance - Used to verify statement integrity
balance_end (computed):
- Calculated as: balance_start + sum(received - spent)
- Should match bank statement's closing balance
- Mismatch indicates missing or incorrect transactions
state (computed): - "reconciled" when all lines are reconciled - "not_reconciled" when any line is not reconciled - Updated automatically when lines change state
account_id: - Typically a bank or cash account - Account type should be "receivable" or "payable" for reconciliation - Each statement belongs to one account
API Methods¶
1. Create Statement¶
Method: create(vals, context)
Creates a new statement and triggers automatic bank reconciliation.
Parameters:
vals = {
"account_id": account_id, # Required
"date_start": "2025-01-01", # Required
"date_end": "2025-01-31", # Required
"balance_start": 10000.00, # Required
"lines": [ # Optional
("create", {
"date": "2025-01-15",
"description": "Customer Payment",
"received": 1000.00,
"spent": 0,
"balance": 11000.00
})
]
}
Returns: int - New statement ID
Example:
# Create statement with lines
statement_id = get_model("account.statement").create({
"account_id": bank_account_id,
"date_start": "2025-01-01",
"date_end": "2025-01-31",
"balance_start": 50000.00,
"lines": [
("create", {
"date": "2025-01-05",
"description": "INV-001 Payment",
"received": 5000.00,
"spent": 0,
"balance": 55000.00
}),
("create", {
"date": "2025-01-10",
"description": "Supplier Payment",
"received": 0,
"spent": 3000.00,
"balance": 52000.00
})
]
})
print(f"Created statement {statement_id}")
Behavior:
- Auto-fills date_imported to today
- Auto-fills company_id to active company
- Triggers auto_bank_reconcile() on the account
- Computes initial state based on lines
2. Get State (Computed)¶
Method: get_state(ids, context={})
Determines reconciliation status based on line states.
Parameters:
- ids (list): Statement IDs
- context (dict): Optional context
Returns: dict - State for each statement
Logic: - Returns "reconciled" if ALL lines are reconciled - Returns "not_reconciled" if ANY line is not reconciled
3. Get End Balance (Computed)¶
Method: get_end_balance(ids, context={})
Calculates closing balance from opening balance and line movements.
Parameters:
- ids (list): Statement IDs
- context (dict): Optional context
Returns: dict - Ending balance for each statement
Formula:
Example:
statement = get_model("account.statement").browse([statement_id])[0]
print(f"Opening: {statement.balance_start}")
print(f"Closing: {statement.balance_end}")
print(f"Change: {statement.balance_end - statement.balance_start}")
4. Update Statement¶
Method: write(ids, vals, context)
Updates statement and triggers auto-reconciliation.
Example:
# Update statement dates
get_model("account.statement").write([statement_id], {
"date_end": "2025-02-01",
"balance_start": 51000.00
})
Behavior:
- Triggers auto_bank_reconcile() on affected accounts
- Updates computed fields automatically
- Maintains audit log
5. Delete Statement¶
Method: delete(ids, context)
Deletes statement after unreconciling all lines.
Example:
Behavior: 1. Collects all statement lines 2. Unreconciles all lines (clears move_lines links) 3. Deletes statement (cascades to lines)
Important: Cannot be undone - use with caution
6. On Account Change (UI Event)¶
Method: onchange_account(context={})
Auto-fills statement dates and opening balance when account is selected.
Behavior:
- Finds most recent statement for selected account
- Sets date_start to day after previous statement's date_end
- Sets date_end to end of current month
- Sets balance_start to previous statement's balance_end
- If no previous statement, uses current month defaults
Example:
# Triggered in UI when user selects account
# Automatically populates:
# - date_start: 2025-02-01 (if last statement ended 2025-01-31)
# - date_end: 2025-02-28
# - balance_start: 52000.00 (from previous ending)
7. Update Balance (UI Event)¶
Method: update_balance(context={})
Recalculates running balances for all lines.
Behavior:
- Starts with balance_start
- For each line, adds received - spent
- Updates line's balance field
- Sets statement's balance_end
Example:
Search Functions¶
Find Unreconciled Statements¶
# Get all statements needing reconciliation
unreconciled = get_model("account.statement").search_browse([
["state", "=", "not_reconciled"]
])
for stmt in unreconciled:
print(f"Statement {stmt.id}: {stmt.date_start} to {stmt.date_end}")
print(f" Account: {stmt.account_id.name}")
print(f" Lines needing reconciliation: "
f"{sum(1 for l in stmt.lines if l.state != 'reconciled')}")
Find Statements by Account¶
# Get all statements for specific bank account
statements = get_model("account.statement").search_browse([
["account_id", "=", bank_account_id]
], order="date_start desc")
print(f"Statements for {statements[0].account_id.name}:")
for stmt in statements:
print(f" {stmt.date_start} - {stmt.date_end}: "
f"{stmt.balance_start} → {stmt.balance_end}")
Find Statements by Date Range¶
# Get statements overlapping with date range
from_date = "2025-01-01"
to_date = "2025-03-31"
statements = get_model("account.statement").search_browse([
"|",
["date_start", ">=", from_date],
["date_end", "<=", to_date]
])
Find Recent Statements¶
# Get last 3 months of statements
statements = get_model("account.statement").search_browse([
["date_imported", ">=", "2024-10-01"]
], order="date_imported desc")
Computed Fields Functions¶
get_state(ids, context)¶
Determines if statement is fully reconciled by checking all line states.
Returns: "reconciled" or "not_reconciled"
get_end_balance(ids, context)¶
Calculates closing balance by summing line movements from opening balance.
Formula: start_balance + Σ(received - spent)
Best Practices¶
1. Sequential Statement Periods¶
# Good: Sequential, non-overlapping periods
stmt1: 2025-01-01 to 2025-01-31 ✓
stmt2: 2025-02-01 to 2025-02-28 ✓
# Bad: Overlapping periods
stmt1: 2025-01-01 to 2025-01-31 ❌
stmt2: 2025-01-25 to 2025-02-25 ❌ Overlap!
2. Verify Balance Continuity¶
# Check balance continuity between statements
statements = get_model("account.statement").search_browse([
["account_id", "=", account_id]
], order="date_start asc")
for i in range(1, len(statements)):
prev = statements[i-1]
curr = statements[i]
if prev.balance_end != curr.balance_start:
print(f"⚠ Balance mismatch between statements!")
print(f" {prev.date_end} ending: {prev.balance_end}")
print(f" {curr.date_start} starting: {curr.balance_start}")
3. Reconcile Regularly¶
# Don't let unreconciled statements accumulate
unreconciled = get_model("account.statement").search([
["state", "=", "not_reconciled"],
["date_end", "<", "2025-01-01"] # Older than this year
])
if len(unreconciled) > 5:
print(f"⚠ Warning: {len(unreconciled)} old unreconciled statements")
print(" Action: Prioritize reconciliation backlog")
4. Import Complete Statements¶
# Good: Complete statement with all transactions
statement_id = get_model("account.statement").create({
"account_id": account_id,
"date_start": "2025-01-01",
"date_end": "2025-01-31",
"balance_start": 50000.00,
"lines": [
# All transactions for the period
("create", {...}),
("create", {...}),
# ...
]
})
# Verify completeness
stmt = get_model("account.statement").browse([statement_id])[0]
if stmt.balance_end != expected_end_balance:
print("⚠ Statement may be incomplete - balance mismatch")
Database Constraints¶
Foreign Key Constraints¶
-- Account reference
FOREIGN KEY (account_id)
REFERENCES account_account(id)
-- Company reference
FOREIGN KEY (company_id)
REFERENCES company(id)
Cascade Behavior¶
Related Models¶
| Model | Relationship | Description |
|---|---|---|
account.statement.line |
One2Many | Individual transaction lines |
account.account |
Many2One | Bank/cash account |
account.move.line |
Indirect | Journal entries matched to statement lines |
account.bank.reconcile |
Indirect | Bank reconciliation records |
company |
Many2One | Multi-company support |
Common Use Cases¶
Use Case 1: Import Bank Statement¶
# Import monthly bank statement from CSV or bank feed
statement_id = get_model("account.statement").create({
"account_id": bank_account_id,
"date_start": "2025-01-01",
"date_end": "2025-01-31",
"balance_start": 50000.00,
"lines": []
})
# Add lines from bank feed
import csv
with open("bank_statement.csv") as f:
reader = csv.DictReader(f)
running_balance = 50000.00
for row in reader:
received = float(row["credit"]) if row["credit"] else 0
spent = float(row["debit"]) if row["debit"] else 0
running_balance += received - spent
get_model("account.statement.line").create({
"statement_id": statement_id,
"date": row["date"],
"description": row["description"],
"received": received,
"spent": spent,
"balance": running_balance
})
# Verify
stmt = get_model("account.statement").browse([statement_id])[0]
print(f"Imported {len(stmt.lines)} transactions")
print(f"Ending Balance: {stmt.balance_end}")
Use Case 2: Reconcile Statement¶
# Reconcile statement lines with journal entries
statement_id = 123
stmt = get_model("account.statement").browse([statement_id])[0]
print(f"Statement: {stmt.date_start} to {stmt.date_end}")
print(f"Status: {stmt.state}")
print(f"Lines: {len(stmt.lines)} total, "
f"{sum(1 for l in stmt.lines if l.state != 'reconciled')} unreconciled")
# Process each unreconciled line
for line in stmt.lines:
if line.state == "reconciled":
continue
# Find matching journal entries
amount = line.received - line.spent
move_lines = get_model("account.move.line").search_browse([
["account_id", "=", stmt.account_id.id],
["date", ">=", line.date],
["date", "<=", line.date],
["debit" if amount > 0 else "credit", "=", abs(amount)],
["state", "!=", "reconciled"]
])
if len(move_lines) == 1:
# Auto-match
get_model("account.statement.line").write([line.id], {
"move_lines": [("set", [move_lines[0].id])]
})
get_model("account.statement.line").reconcile([line.id])
print(f" ✓ Matched: {line.description}")
else:
print(f" ⊙ Manual review needed: {line.description} "
f"({len(move_lines)} candidates)")
# Check final status
stmt.reload()
print(f"\nFinal Status: {stmt.state}")
Use Case 3: Statement Balance Verification¶
# Verify statement integrity
def verify_statement(statement_id):
stmt = get_model("account.statement").browse([statement_id])[0]
# Calculate balance manually
computed_balance = stmt.balance_start
for line in stmt.lines:
computed_balance += (line.received or 0) - (line.spent or 0)
# Compare
if abs(computed_balance - stmt.balance_end) > 0.01:
print(f"⚠ Balance mismatch!")
print(f" Computed: {computed_balance}")
print(f" Recorded: {stmt.balance_end}")
return False
print(f"✓ Statement balanced correctly")
return True
verify_statement(statement_id)
Use Case 4: Generate Reconciliation Report¶
# Generate reconciliation status report for all accounts
accounts = get_model("account.account").search_browse([
["type", "in", ["bank", "cash"]]
])
print("BANK RECONCILIATION STATUS REPORT")
print("=" * 80)
for account in accounts:
statements = account.statements
if not statements:
continue
total_stmts = len(statements)
reconciled = sum(1 for s in statements if s.state == "reconciled")
unreconciled = total_stmts - reconciled
print(f"\n{account.code} - {account.name}")
print(f" Total Statements: {total_stmts}")
print(f" Reconciled: {reconciled} ({reconciled/total_stmts*100:.0f}%)")
print(f" Unreconciled: {unreconciled}")
if unreconciled > 0:
# Show oldest unreconciled
oldest = min(
(s for s in statements if s.state == "not_reconciled"),
key=lambda s: s.date_start
)
print(f" ⚠ Oldest unreconciled: {oldest.date_start}")
Use Case 5: Correct Statement Error¶
# Fix incorrect statement entry
statement_id = 123
# Option 1: Add missing transaction
get_model("account.statement.line").create({
"statement_id": statement_id,
"date": "2025-01-15",
"description": "Missing Transaction",
"received": 500.00,
"spent": 0,
"balance": 0 # Will be recalculated
})
# Recalculate all balances
stmt = get_model("account.statement").browse([statement_id])[0]
running_balance = stmt.balance_start
for line in sorted(stmt.lines, key=lambda l: (l.date, l.id)):
running_balance += (line.received or 0) - (line.spent or 0)
get_model("account.statement.line").write([line.id], {
"balance": running_balance
})
print(f"✓ Statement updated")
# Option 2: Delete and reimport if major errors
# get_model("account.statement").delete([statement_id])
# # Then reimport correct data
Performance Tips¶
1. Index on Common Search Fields¶
CREATE INDEX idx_statement_account ON account_statement(account_id);
CREATE INDEX idx_statement_dates ON account_statement(date_start, date_end);
CREATE INDEX idx_statement_state ON account_statement(state);
CREATE INDEX idx_statement_company ON account_statement(company_id);
2. Limit Statement Period Size¶
# Good: Monthly statements (manageable line count)
statement_period = "monthly" # ~30-100 lines
# Avoid: Yearly statements (too many lines)
statement_period = "yearly" # Could be 1000+ lines
3. Batch Reconciliation¶
# Process statements in batches
unreconciled = get_model("account.statement").search([
["state", "=", "not_reconciled"]
])
# Process 10 at a time
batch_size = 10
for i in range(0, len(unreconciled), batch_size):
batch = unreconciled[i:i+batch_size]
for stmt_id in batch:
# Process reconciliation
process_statement_reconciliation(stmt_id)
print(f"Processed {min(i+batch_size, len(unreconciled))} of {len(unreconciled)}")
Troubleshooting¶
"Balance mismatch between statements"¶
Cause: Opening balance doesn't match previous closing balance Solution: Verify continuity and correct discrepancy
# Find the gap
stmt = get_model("account.statement").browse([statement_id])[0]
prev_stmts = get_model("account.statement").search_browse([
["account_id", "=", stmt.account_id.id],
["date_end", "<", stmt.date_start]
], order="date_end desc", limit=1)
if prev_stmts:
if prev_stmts[0].balance_end != stmt.balance_start:
print(f"Gap: {stmt.balance_start - prev_stmts[0].balance_end}")
"Auto bank reconcile not working"¶
Cause: Matching logic unable to find exact matches Solution: Review matching criteria and consider manual reconciliation
"Cannot delete statement"¶
Cause: Statement lines may be reconciled Solution: Unreconcile first, then delete
stmt = get_model("account.statement").browse([statement_id])[0]
line_ids = [l.id for l in stmt.lines]
get_model("account.statement.line").unreconcile(line_ids)
get_model("account.statement").delete([statement_id])
"Computed balance doesn't match bank"¶
Cause: Missing or incorrect transactions Solution: Review line by line
stmt = get_model("account.statement").browse([statement_id])[0]
running = stmt.balance_start
print(f"Opening: {running}")
for line in stmt.lines:
movement = (line.received or 0) - (line.spent or 0)
running += movement
print(f"{line.date} {line.description:30} {movement:10.2f} → {running:12.2f}")
print(f"Expected: {stmt.balance_end}")
print(f"Actual from bank: {actual_bank_balance}")
Testing Examples¶
Unit Test: Statement Creation¶
def test_statement_creation():
# Create test account
account_id = get_model("account.account").create({
"code": "TEST-BANK",
"name": "Test Bank Account",
"type": "bank"
})
# Create statement
stmt_id = get_model("account.statement").create({
"account_id": account_id,
"date_start": "2025-01-01",
"date_end": "2025-01-31",
"balance_start": 10000.00,
"lines": [
("create", {
"date": "2025-01-15",
"description": "Test Transaction",
"received": 1000.00,
"spent": 0,
"balance": 11000.00
})
]
})
# Verify
stmt = get_model("account.statement").browse([stmt_id])[0]
assert stmt.balance_start == 10000.00
assert stmt.balance_end == 11000.00
assert len(stmt.lines) == 1
assert stmt.state == "not_reconciled"
print("✓ Statement test passed")
# Cleanup
get_model("account.statement").delete([stmt_id])
get_model("account.account").delete([account_id])
Security Considerations¶
Permission Model¶
- Statement creation requires accounting permissions
- Reconciliation requires approval in some organizations
- Deletion should be restricted to supervisors
Data Access¶
- Statements are company-specific
- Users can only access statements for their company's accounts
- Multi-company setups should verify access controls
Audit Trail¶
- Audit logging enabled for all statement changes
- Track who imported and reconciled statements
- Monitor deletion of reconciled statements
Integration Points¶
Internal Modules¶
- account.statement.line: Individual transactions
- account.account: Bank and cash accounts
- account.move.line: Journal entries for matching
- account.bank.reconcile: Automated reconciliation
External Systems¶
- Banking APIs (for automated import)
- CSV/Excel imports (manual entry)
- Payment processors (transaction feeds)
Version History¶
Last Updated: 2025-12-16 Model Version: account_statement.py Framework: Netforce
Additional Resources¶
- Statement Line Documentation:
account.statement.line - Bank Reconciliation Guide
- Account Documentation:
account.account - Journal Entry Documentation:
account.move.line
This documentation is generated for developer onboarding and reference purposes.