Skip to content

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:

  1. Period Definition - Start and end dates
  2. Opening Balance - Balance at start of period
  3. Transaction Lines - Individual debits and credits
  4. 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

{
    statement_id: "reconciled"  # or "not_reconciled"
}

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:

balance_end = balance_start + sum(line.received - line.spent for all lines)

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:

get_model("account.statement").delete([statement_id])

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:

# Called during statement entry
# Updates running balance for each line automatically


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

-- Statement deletion cascades to lines
-- See account.statement.line model

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.