Skip to content

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
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:

{
    "next": {
        "name": "reconcile_adjust",
        "context": {"line_id": line_id}
    }
}


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:

account_balance = sum(line.debit - line.credit for all linked move_lines)

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

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

# Create missing entry (see Use Case 3)

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.