Account Statement Line Documentation¶
Overview¶
The Account Statement Line module (account.statement.line) represents individual transactions within a bank or account statement. Each line captures a single debit or credit transaction from the bank, which must be matched and reconciled with journal entries in the accounting system.
Model Information¶
Model Name: account.statement.line
Display Name: Statement Line
Name Field: description
Key Fields: None (auto-increment ID)
Default Sort Order: date, id (chronological)
Features¶
- ✅ Individual transaction tracking
- ✅ Reconciliation status management
- ✅ Many-to-many linking with journal entries
- ✅ Running balance computation
- ✅ Cascade delete with parent statement
- ✅ Automatic reconciliation balance verification
Understanding Statement Lines¶
What is a Statement Line?¶
A Statement Line represents one transaction from a bank or account statement: - Bank deposits (received) - Bank withdrawals (spent) - Bank fees, interest, transfers
Statement Line Lifecycle¶
1. Import → 2. Link → 3. Reconcile → 4. Complete
(from (to journal (verify (state:
bank) entries) balances) reconciled)
Reconciliation Process¶
Each statement line must be matched to one or more journal entry lines: - One-to-one: Single deposit matches single receipt - Many-to-one: Multiple payments match one deposit - One-to-many: One payment matches multiple invoices
Key Fields Reference¶
Core Transaction Fields¶
| Field | Type | Required | Description |
|---|---|---|---|
statement_id |
Many2One | ✅ | Parent statement (cascade delete) |
date |
Date | ✅ | Transaction date from bank |
description |
Char(256) | ❌ | Transaction description/memo |
spent |
Decimal | ✅ | Debit amount (money out) |
received |
Decimal | ✅ | Credit amount (money in) |
balance |
Decimal | ✅ | Running balance (readonly) |
Reconciliation Fields¶
| Field | Type | Description |
|---|---|---|
state |
Selection | Reconciliation status (not_reconciled/reconciled) |
move_lines |
Many2Many | Linked journal entry lines (account.move.line) |
bank_reconcile_id |
Many2One | Link to bank reconciliation record |
account_balance |
Decimal | Computed sum of linked journal entries |
Related Fields¶
| Field | Type | Description |
|---|---|---|
account_id |
Many2One | Computed from statement's account |
Field Details¶
spent & received: - One should be zero, the other positive - Both cannot be positive (use separate lines) - Net movement = received - spent
balance: - Running balance after this transaction - Readonly - computed by statement - Used to verify statement integrity
state: - "not_reconciled": Needs to be matched - "reconciled": Successfully matched and verified - Default: "not_reconciled"
move_lines: - Many2Many relationship to journal entries - Multiple lines can be linked (partial payments) - Both sides must balance for reconciliation
account_balance: - Sum of debit - credit from linked move_lines - Should equal received - spent for proper reconciliation - Mismatch indicates adjustment needed
API Methods¶
1. Create Statement Line¶
Method: create(vals, context)
Creates a new statement line.
Parameters:
vals = {
"statement_id": statement_id, # Required
"date": "2025-01-15", # Required
"description": "Customer Payment",
"received": 1000.00, # Required (default 0)
"spent": 0, # Required (default 0)
"balance": 11000.00 # Required
}
Returns: int - New line ID
Example:
# Add line to existing statement
line_id = get_model("account.statement.line").create({
"statement_id": statement_id,
"date": "2025-01-15",
"description": "INV-001 Payment",
"received": 5000.00,
"spent": 0,
"balance": 55000.00
})
2. Get Reconcile Lines (Helper)¶
Method: get_reconcile_lines(ids)
Recursively finds all statement lines and journal lines that are interconnected.
Parameters:
- ids (list): Initial statement line IDs
Returns: tuple - (statement_line_ids, move_line_ids)
Behavior: Traverses the graph of linked lines: 1. Start with statement lines 2. Find all linked journal entries 3. Find all statement lines linked to those journal entries 4. Repeat until no new links found
Example:
# Find all interconnected lines
st_line_ids, move_line_ids = get_model("account.statement.line").get_reconcile_lines([line_id])
print(f"Statement lines: {len(st_line_ids)}")
print(f"Journal lines: {len(move_line_ids)}")
Use Case: - Handles complex reconciliations with multiple cross-links - Ensures all related lines are reconciled together
3. Reconcile Lines¶
Method: reconcile(ids, context={})
Reconciles statement lines with linked journal entries.
Parameters:
- ids (list): Statement line IDs to reconcile
- context (dict): Optional context
Returns: dict or None
Behavior:
1. Finds all interconnected lines via get_reconcile_lines()
2. Calculates total from statement lines (received - spent)
3. Calculates total from journal lines (debit - credit)
4. If balanced: Marks all as reconciled
5. If unbalanced: Returns adjustment wizard
Example:
# Reconcile statement line with journal entry
line_id = 123
move_line_id = 456
# Link them
get_model("account.statement.line").write([line_id], {
"move_lines": [("set", [move_line_id])]
})
# Reconcile
result = get_model("account.statement.line").reconcile([line_id])
if result:
print("Adjustment needed - opening wizard")
else:
print("✓ Reconciled successfully")
Adjustment Flow: If totals don't match, returns:
4. Unreconcile Lines¶
Method: unreconcile(ids, context={})
Removes reconciliation status from statement and journal lines.
Parameters:
- ids (list): Statement line IDs to unreconcile
- context (dict): Optional context
Behavior: 1. Finds all interconnected lines 2. Sets state to "not_reconciled" for statement lines 3. Sets state to "not_reconciled" for journal lines
Example:
# Undo reconciliation
get_model("account.statement.line").unreconcile([line_id])
print("✓ Unreconciled")
Use Case: - Correct reconciliation errors - Re-match with different journal entries - Remove incorrect links
5. Get Account Balance (Computed)¶
Method: get_account_balance(ids, context={})
Calculates total from linked journal entry lines.
Parameters:
- ids (list): Statement line IDs
- context (dict): Optional context
Returns: dict - Account balance for each line
Formula:
Example:
line = get_model("account.statement.line").browse([line_id])[0]
print(f"Statement amount: {line.received - line.spent}")
print(f"Accounting amount: {line.account_balance}")
print(f"Difference: {(line.received - line.spent) - line.account_balance}")
6. On Move Lines Change (UI Event)¶
Method: onchange_move_lines(context={})
Updates account_balance when journal entries are linked/unlinked.
Behavior: - Recalculates account_balance from selected move_lines - Updates in real-time during UI data entry
Search Functions¶
Find Unreconciled Lines¶
# Get all unreconciled statement lines
unreconciled = get_model("account.statement.line").search_browse([
["state", "=", "not_reconciled"]
])
print(f"Unreconciled transactions: {len(unreconciled)}")
for line in unreconciled:
print(f" {line.date} {line.description:40} "
f"{line.received - line.spent:12.2f}")
Find Lines by Date¶
# Get lines for specific date range
lines = get_model("account.statement.line").search_browse([
["date", ">=", "2025-01-01"],
["date", "<=", "2025-01-31"]
], order="date asc")
Find Large Transactions¶
# Find transactions over threshold
threshold = 10000
large_received = get_model("account.statement.line").search_browse([
["received", ">=", threshold]
])
large_spent = get_model("account.statement.line").search_browse([
["spent", ">=", threshold]
])
print(f"Large deposits: {len(large_received)}")
print(f"Large payments: {len(large_spent)}")
Search by Description¶
# Find transactions by description keyword
keyword = "payment"
lines = get_model("account.statement.line").search_browse([
["description", "ilike", keyword]
])
Best Practices¶
1. One Transaction per Line¶
# Good: Separate lines for debit and credit
line1 = {
"date": "2025-01-15",
"description": "Deposit",
"received": 1000,
"spent": 0
}
line2 = {
"date": "2025-01-16",
"description": "Withdrawal",
"received": 0,
"spent": 500
}
# Bad: Both received and spent in one line
line = {
"date": "2025-01-15",
"description": "Net transfer",
"received": 1000,
"spent": 500 # Confusing
}
2. Accurate Descriptions¶
# Good: Descriptive transaction details
description = "INV-2025-001 - ABC Corp Payment"
# Less helpful: Generic description
description = "Transfer"
3. Verify Before Reconciling¶
# Check balance before reconciling
line = get_model("account.statement.line").browse([line_id])[0]
stmt_amount = line.received - line.spent
acc_amount = line.account_balance
if abs(stmt_amount - acc_amount) > 0.01:
print(f"⚠ Warning: Amounts don't match!")
print(f" Statement: {stmt_amount}")
print(f" Accounting: {acc_amount}")
print(f" Adjustment needed: {stmt_amount - acc_amount}")
else:
# Safe to reconcile
get_model("account.statement.line").reconcile([line_id])
4. Handle Partial Payments¶
# Link multiple journal entries for partial payments
statement_line_id = 123 # $1000 deposit
invoice_payments = [
456, # $600 payment
457 # $400 payment
]
# Link all payments to statement line
get_model("account.statement.line").write([statement_line_id], {
"move_lines": [("set", invoice_payments)]
})
# Verify total
line = get_model("account.statement.line").browse([statement_line_id])[0]
if line.received == line.account_balance:
get_model("account.statement.line").reconcile([statement_line_id])
print("✓ Partial payments reconciled")
Database Constraints¶
Foreign Key Constraints¶
-- Statement reference with cascade delete
FOREIGN KEY (statement_id)
REFERENCES account_statement(id)
ON DELETE CASCADE
-- Bank reconcile reference
FOREIGN KEY (bank_reconcile_id)
REFERENCES account_bank_reconcile(id)
Cascade Behavior¶
- Deleting statement deletes all its lines
- Statement lines can exist independently of bank_reconcile_id
Related Models¶
| Model | Relationship | Description |
|---|---|---|
account.statement |
Many2One | Parent statement |
account.move.line |
Many2Many | Linked journal entry lines |
account.account |
Computed | Bank account from statement |
account.bank.reconcile |
Many2One | Automated reconciliation link |
Common Use Cases¶
Use Case 1: Manual Reconciliation¶
# Match statement line with journal entry manually
# 1. Find unreconciled statement line
st_line = get_model("account.statement.line").search_browse([
["description", "ilike", "INV-001"],
["state", "=", "not_reconciled"]
])[0]
# 2. Find corresponding journal entry
move_line = get_model("account.move.line").search_browse([
["move_id.number", "=", "PMT-001"],
["account_id", "=", st_line.account_id.id],
["state", "!=", "reconciled"]
])[0]
# 3. Verify amounts match
st_amount = st_line.received - st_line.spent
mv_amount = move_line.debit - move_line.credit
if abs(st_amount - mv_amount) < 0.01:
# 4. Link them
get_model("account.statement.line").write([st_line.id], {
"move_lines": [("set", [move_line.id])]
})
# 5. Reconcile
get_model("account.statement.line").reconcile([st_line.id])
print(f"✓ Reconciled: {st_line.description}")
else:
print(f"⚠ Amount mismatch: {st_amount} vs {mv_amount}")
Use Case 2: Bulk Auto-Reconciliation¶
# Automatically match statement lines to journal entries
def auto_reconcile_statement(statement_id):
stmt = get_model("account.statement").browse([statement_id])[0]
matched = 0
unmatched = 0
for st_line in stmt.lines:
if st_line.state == "reconciled":
continue
# Calculate statement amount
st_amount = st_line.received - st_line.spent
# Find matching journal entry
move_lines = get_model("account.move.line").search_browse([
["account_id", "=", stmt.account_id.id],
["date", "=", st_line.date],
["state", "!=", "reconciled"],
"|",
["debit", "=", abs(st_amount)] if st_amount > 0 else ["credit", "=", 0],
["credit", "=", abs(st_amount)] if st_amount < 0 else ["debit", "=", 0]
])
if len(move_lines) == 1:
# Exact match found
get_model("account.statement.line").write([st_line.id], {
"move_lines": [("set", [move_lines[0].id])]
})
result = get_model("account.statement.line").reconcile([st_line.id])
if not result: # No adjustment needed
matched += 1
else:
unmatched += 1
else:
unmatched += 1
print(f"Auto-reconciliation complete:")
print(f" Matched: {matched}")
print(f" Needs manual review: {unmatched}")
auto_reconcile_statement(statement_id)
Use Case 3: Handle Bank Fees¶
# Reconcile bank fee that wasn't in accounting
# Statement shows fee
fee_line = get_model("account.statement.line").search_browse([
["description", "ilike", "bank fee"],
["state", "=", "not_reconciled"]
])[0]
# Create journal entry for the fee
fee_account_id = get_model("account.account").search([
["code", "=", "6100"] # Bank fees expense
])[0]
move_id = get_model("account.move").create({
"journal_id": bank_journal_id,
"date": fee_line.date,
"narration": "Bank fee from statement",
"lines": [
("create", {
"account_id": fee_account_id,
"debit": fee_line.spent,
"credit": 0
}),
("create", {
"account_id": fee_line.account_id.id,
"debit": 0,
"credit": fee_line.spent
})
]
})
# Post the entry
get_model("account.move").post([move_id])
# Get the bank account line
bank_line = get_model("account.move.line").search_browse([
["move_id", "=", move_id],
["account_id", "=", fee_line.account_id.id]
])[0]
# Link and reconcile
get_model("account.statement.line").write([fee_line.id], {
"move_lines": [("set", [bank_line.id])]
})
get_model("account.statement.line").reconcile([fee_line.id])
print("✓ Bank fee reconciled")
Use Case 4: Reconciliation Report¶
# Generate detailed reconciliation report
def generate_reconciliation_report(statement_id):
stmt = get_model("account.statement").browse([statement_id])[0]
print(f"RECONCILIATION REPORT")
print(f"Statement: {stmt.date_start} to {stmt.date_end}")
print(f"Account: {stmt.account_id.name}")
print("=" * 100)
print(f"{'Date':12} {'Description':40} {'Amount':15} {'Status':15} {'Match':20}")
print("-" * 100)
for line in stmt.lines:
amount = line.received - line.spent
status = "✓ Reconciled" if line.state == "reconciled" else "⊙ Pending"
if line.move_lines:
match_info = f"{len(line.move_lines)} entries"
else:
match_info = "No match"
print(f"{line.date:12} {line.description[:40]:40} "
f"{amount:15.2f} {status:15} {match_info:20}")
print("=" * 100)
print(f"Opening Balance: {stmt.balance_start:15.2f}")
print(f"Closing Balance: {stmt.balance_end:15.2f}")
print(f"Net Change: {stmt.balance_end - stmt.balance_start:15.2f}")
reconciled = sum(1 for l in stmt.lines if l.state == "reconciled")
total = len(stmt.lines)
print(f"\nReconciliation: {reconciled}/{total} lines ({reconciled/total*100:.0f}%)")
generate_reconciliation_report(statement_id)
Use Case 5: Find Reconciliation Discrepancies¶
# Identify lines with reconciliation issues
def find_discrepancies():
# Find lines marked reconciled but with balance mismatch
all_lines = get_model("account.statement.line").search_browse([
["state", "=", "reconciled"]
])
discrepancies = []
for line in all_lines:
st_amount = line.received - line.spent
acc_amount = line.account_balance
if abs(st_amount - acc_amount) > 0.01:
discrepancies.append({
"line": line,
"difference": st_amount - acc_amount
})
if discrepancies:
print(f"⚠ Found {len(discrepancies)} reconciliation discrepancies:")
for disc in discrepancies:
line = disc["line"]
print(f" {line.date} {line.description[:40]:40} "
f"Diff: {disc['difference']:10.2f}")
else:
print("✓ No discrepancies found")
find_discrepancies()
Performance Tips¶
1. Index on Search Fields¶
CREATE INDEX idx_stmt_line_statement ON account_statement_line(statement_id);
CREATE INDEX idx_stmt_line_date ON account_statement_line(date);
CREATE INDEX idx_stmt_line_state ON account_statement_line(state);
CREATE INDEX idx_stmt_line_description ON account_statement_line(description);
2. Batch Reconciliation¶
# Process lines in batches
unreconciled = get_model("account.statement.line").search([
["state", "=", "not_reconciled"]
])
batch_size = 50
for i in range(0, len(unreconciled), batch_size):
batch = unreconciled[i:i+batch_size]
# Process batch
for line_id in batch:
try_auto_reconcile(line_id)
print(f"Processed {min(i+batch_size, len(unreconciled))} of {len(unreconciled)}")
Troubleshooting¶
"Cannot reconcile - amounts don't match"¶
Cause: Statement amount ≠ journal entry amount Solution: Use reconciliation adjustment or verify correct entries linked
line = get_model("account.statement.line").browse([line_id])[0]
print(f"Statement: {line.received - line.spent}")
print(f"Accounting: {line.account_balance}")
print(f"Difference: {(line.received - line.spent) - line.account_balance}")
"Line already reconciled"¶
Cause: Attempting to modify reconciled line Solution: Unreconcile first, make changes, then re-reconcile
get_model("account.statement.line").unreconcile([line_id])
# Make changes
get_model("account.statement.line").reconcile([line_id])
"Missing journal entry for statement line"¶
Cause: Transaction not recorded in accounting system Solution: Create corresponding journal entry
"Circular reconciliation links"¶
Cause: Complex web of interconnected lines
Solution: Use get_reconcile_lines() to visualize connections
st_ids, mv_ids = get_model("account.statement.line").get_reconcile_lines([line_id])
print(f"Connected statement lines: {st_ids}")
print(f"Connected journal lines: {mv_ids}")
Testing Examples¶
Unit Test: Line Reconciliation¶
def test_line_reconciliation():
# Create statement with line
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
})
line_id = get_model("account.statement.line").create({
"statement_id": stmt_id,
"date": "2025-01-15",
"description": "Test Payment",
"received": 1000.00,
"spent": 0,
"balance": 11000.00
})
# Create matching journal entry
move_id = get_model("account.move").create({
"journal_id": journal_id,
"date": "2025-01-15",
"lines": [
("create", {
"account_id": account_id,
"debit": 1000.00,
"credit": 0
}),
("create", {
"account_id": income_account_id,
"debit": 0,
"credit": 1000.00
})
]
})
get_model("account.move").post([move_id])
# Get bank line
bank_line = get_model("account.move.line").search_browse([
["move_id", "=", move_id],
["account_id", "=", account_id]
])[0]
# Link and reconcile
get_model("account.statement.line").write([line_id], {
"move_lines": [("set", [bank_line.id])]
})
result = get_model("account.statement.line").reconcile([line_id])
# Verify
assert result is None, "Should reconcile without adjustment"
line = get_model("account.statement.line").browse([line_id])[0]
assert line.state == "reconciled"
print("✓ Reconciliation test passed")
# Cleanup
get_model("account.statement").delete([stmt_id])
Security Considerations¶
Permission Model¶
- Line creation restricted to accounting users
- Reconciliation requires approval workflow in some organizations
- Unreconciling reconciled lines should be logged
Data Access¶
- Lines inherit access from parent statement
- Company-specific via statement's company_id
- Users can only access lines for their company's statements
Audit Trail¶
- Consider enabling audit log for sensitive accounts
- Track who reconciled and when
- Monitor bulk unreconciliation operations
Integration Points¶
Internal Modules¶
- account.statement: Parent statement container
- account.move.line: Journal entries for matching
- account.bank.reconcile: Automated reconciliation
- account.account: Bank and cash accounts
External Systems¶
- Banking APIs (transaction details)
- Payment processors (transaction feeds)
- CSV imports (manual entry)
Version History¶
Last Updated: 2025-12-16 Model Version: account_statement_line.py Framework: Netforce
Additional Resources¶
- Statement Documentation:
account.statement - Journal Entry Line Documentation:
account.move.line - Bank Reconciliation Guide
- Reconciliation Best Practices
This documentation is generated for developer onboarding and reference purposes.