Skip to content

Account Reconciliation Documentation

Overview

The Account Reconciliation module (account.reconcile) manages the grouping and matching of journal entry lines that offset each other. Reconciliation is a critical accounting process used to match payments with invoices, bank statement entries with accounting entries, or any related transactions that should cancel each other out.


Model Information

Model Name: account.reconcile Display Name: Account Reconciliation Name Field: number Key Fields: None (auto-increment ID)

Features

  • ❌ Audit logging (not required for reconciliation groups)
  • ✅ Links multiple journal entry lines together
  • ✅ Computed balance to verify proper reconciliation
  • ✅ Visual indicator (*) for unbalanced reconciliations
  • ✅ Only includes posted journal entries

Understanding Account Reconciliation

What is Reconciliation?

Reconciliation is the process of matching and grouping related journal entry lines that offset each other. When lines are reconciled together, they form a reconciliation group that should have a zero balance (debits = credits).

Common Reconciliation Scenarios

1. Payment to Invoice Matching:

Invoice (Accounts Receivable - Debit)  $1,000
Payment (Cash - Credit)                $1,000
----------------------------------------
Reconciled Balance:                     $0 ✓

2. Bank Statement Reconciliation:

Bank Statement Entry (Debit)           $500
Accounting Entry (Credit)              $500
----------------------------------------
Reconciled Balance:                    $0 ✓

3. Partial Payments:

Invoice (A/R - Debit)                  $1,000
Payment 1 (Cash - Credit)              $600
Payment 2 (Cash - Credit)              $400
----------------------------------------
Reconciled Balance:                    $0 ✓

Reconciliation Number Format

Each reconciliation gets a computed number: - Balanced: R123 (debit = credit) - Unbalanced: R123* (asterisk indicates imbalance)


Key Fields Reference

Relationship Fields

Field Type Description
lines One2Many Journal entry lines (account.move.line) in this reconciliation group. Only includes posted entries.

Computed Fields

Field Type Description
number Char Reconciliation number: "R{id}" or "R{id}*" if unbalanced
debit Decimal Total debit amount from all lines in the group
credit Decimal Total credit amount from all lines in the group
balance Decimal Net balance (debit - credit). Should be 0 for proper reconciliation

Field Details

lines: - One2Many relationship to account.move.line - Condition: Only posted journal entries (move_id.state = 'posted') - Bidirectional: Lines have reconcile_id field pointing back - Cannot include draft or cancelled entries

number: - Format: R{reconcile_id} - Asterisk (*) appended if balance ≠ 0 - Examples: R1, R42*, R1503

debit, credit, balance: - All computed from summing related lines - Real-time calculation - Balance = debit - credit - Balanced reconciliation should have balance = 0


API Methods

1. Create Reconciliation

Method: create(vals, context)

Creates a new reconciliation group. Typically not called directly; instead use reconcile operations on journal lines.

Parameters:

vals = {
    "lines": [                         # Link existing lines
        ("set", [line_id1, line_id2, ...])
    ]
}

Returns: int - New reconciliation ID

Example:

# Create reconciliation group
reconcile_id = get_model("account.reconcile").create({
    "lines": [("set", [line1_id, line2_id, line3_id])]
})

Note: More commonly, use the reconcile() method on account.move.line model which handles reconciliation creation automatically.


2. Get Totals (Computed Method)

Method: get_total(ids, context={})

Calculates debit, credit, balance, and number for reconciliation records.

Parameters: - ids (list): Reconciliation record IDs - context (dict): Optional context

Returns: dict - Dictionary with computed values for each ID

{
    reconcile_id: {
        "debit": 1000.00,
        "credit": 1000.00,
        "balance": 0.00,
        "number": "R42"
    }
}

Behavior: - Sums debit/credit from all lines in the reconciliation - Calculates balance as debit - credit - Generates number with "*" suffix if unbalanced

Example:

# Get reconciliation totals
reconcile = get_model("account.reconcile").browse([reconcile_id])[0]
print(f"Reconciliation: {reconcile.number}")
print(f"Debit: {reconcile.debit}")
print(f"Credit: {reconcile.credit}")
print(f"Balance: {reconcile.balance}")
print(f"Balanced: {'Yes' if reconcile.balance == 0 else 'No'}")


Search Functions

Find Unbalanced Reconciliations

# Find all reconciliations with non-zero balance
# (These need attention)
unbalanced = get_model("account.reconcile").search_browse([
    ["balance", "!=", 0]
])

for rec in unbalanced:
    print(f"⚠ {rec.number} - Balance: {rec.balance}")

Find Reconciliations by Date Range

# Find reconciliations created in date range
from_date = "2025-01-01"
to_date = "2025-01-31"

reconciliations = get_model("account.reconcile").search_browse([
    ["lines.move_id.date", ">=", from_date],
    ["lines.move_id.date", "<=", to_date]
])

Find Reconciliations for Specific Account

# Find all reconciliations involving specific account
account_id = 123

reconciliations = get_model("account.reconcile").search_browse([
    ["lines.account_id", "=", account_id]
])

Find Reconciliations for Contact

# Find reconciliations for customer/supplier
contact_id = 456

reconciliations = get_model("account.reconcile").search_browse([
    ["lines.contact_id", "=", contact_id]
])

Computed Fields Functions

get_total(ids, context)

Computes all financial totals and the reconciliation number for the given reconciliation records.

Logic: 1. For each reconciliation record: - Sum debit from all related lines - Sum credit from all related lines - Calculate balance = debit - credit - Generate number = "R{id}" - Add "*" to number if balance ≠ 0

Returns: Dictionary with computed values


Best Practices

1. Always Reconcile Balanced Transactions

# Bad: Reconciling unbalanced entries
line1 = 1000  # Debit
line2 = 900   # Credit
# Total: Unbalanced by 100 ❌

# Good: Ensure debits = credits
line1 = 1000  # Debit
line2 = 1000  # Credit
# Total: Balanced ✓

2. Use Reconcile Helper Methods

# Bad: Manual reconciliation creation
reconcile_id = get_model("account.reconcile").create({
    "lines": [("set", [line1_id, line2_id])]
})
for line_id in [line1_id, line2_id]:
    get_model("account.move.line").write([line_id], {
        "reconcile_id": reconcile_id
    })

# Good: Use the reconcile method on lines
line_ids = [line1_id, line2_id]
get_model("account.move.line").reconcile(line_ids)

3. Monitor Unbalanced Reconciliations

# Regularly check for unbalanced reconciliations
unbalanced = get_model("account.reconcile").search_browse([
    ["balance", "!=", 0]
])

if unbalanced:
    print(f"⚠ Warning: {len(unbalanced)} unbalanced reconciliations found")
    for rec in unbalanced:
        print(f"  {rec.number}: Balance {rec.balance}")
else:
    print("✓ All reconciliations are balanced")

4. Only Reconcile Posted Entries

# The model automatically filters to posted entries
# But be aware when working with lines directly

# Get reconcilable lines (posted only)
lines = get_model("account.move.line").search_browse([
    ["account_id", "=", account_id],
    ["reconcile_id", "=", None],  # Not yet reconciled
    ["move_id.state", "=", "posted"]  # Posted only
])

Database Constraints

Referential Integrity

-- Cascade behavior from move lines
-- If reconciliation deleted, move lines' reconcile_id set to NULL

Note: This model has no explicit SQL constraints defined, relying on application logic and the computed balance field to ensure integrity.


Model Relationship Description
account.move.line One2Many Journal entry lines that are part of this reconciliation
account.move Indirect Parent journal entries of the lines
account.invoice Indirect Invoices linked to reconciled lines
account.payment Indirect Payments linked to reconciled lines

Common Use Cases

Use Case 1: Reconcile Invoice with Payment

# Scenario: Customer paid invoice in full

# 1. Find the invoice line (A/R Debit)
invoice_lines = get_model("account.move.line").search_browse([
    ["move_id.related_id", "=", f"account.invoice,{invoice_id}"],
    ["account_id.type", "=", "receivable"],
    ["reconcile_id", "=", None]  # Not yet reconciled
])

# 2. Find the payment line (Cash Credit)
payment_lines = get_model("account.move.line").search_browse([
    ["move_id.related_id", "=", f"account.payment,{payment_id}"],
    ["contact_id", "=", customer_id],
    ["reconcile_id", "=", None]
])

# 3. Reconcile them together
line_ids = [l.id for l in invoice_lines] + [l.id for l in payment_lines]
get_model("account.move.line").reconcile(line_ids)

# 4. Verify reconciliation
invoice_lines[0].reload()
reconcile = invoice_lines[0].reconcile_id
print(f"Reconciliation: {reconcile.number}")
print(f"Balance: {reconcile.balance}")  # Should be 0

Use Case 2: Reconcile Multiple Partial Payments

# Scenario: Invoice of $1000 paid in three installments

invoice_amount = 1000

# Find invoice A/R line
invoice_line = get_model("account.move.line").search_browse([
    ["move_id.related_id", "=", f"account.invoice,{invoice_id}"],
    ["account_id.type", "=", "receivable"],
    ["reconcile_id", "=", None]
])[0]

# Find payment lines
payment1 = 400  # Payment 1
payment2 = 300  # Payment 2
payment3 = 300  # Payment 3

payment_line_ids = get_model("account.move.line").search([
    ["move_id.related_id", "in", [
        f"account.payment,{pay1_id}",
        f"account.payment,{pay2_id}",
        f"account.payment,{pay3_id}"
    ]],
    ["account_id.type", "=", "receivable"],
    ["credit", ">", 0],  # Payment lines are credits
    ["reconcile_id", "=", None]
])

# Reconcile all together
all_line_ids = [invoice_line.id] + payment_line_ids
get_model("account.move.line").reconcile(all_line_ids)

# Verify
invoice_line.reload()
print(f"Invoice fully reconciled: {invoice_line.reconcile_id.balance == 0}")

Use Case 3: Un-reconcile Transactions

# Scenario: Need to undo a reconciliation (e.g., payment was wrong)

# Find the reconciliation
reconcile_id = 42

# Get all lines in this reconciliation
reconcile = get_model("account.reconcile").browse([reconcile_id])[0]
line_ids = [line.id for line in reconcile.lines]

# Un-reconcile by clearing reconcile_id on all lines
get_model("account.move.line").write(line_ids, {
    "reconcile_id": None
})

# Delete the now-empty reconciliation
get_model("account.reconcile").delete([reconcile_id])

print("✓ Reconciliation undone")

Use Case 4: Bank Reconciliation Report

# Generate bank reconciliation report showing all reconciled items

account_id = bank_account_id  # Bank account
from_date = "2025-01-01"
to_date = "2025-01-31"

# Find all reconciliations for this account in date range
reconciliations = get_model("account.reconcile").search_browse([
    ["lines.account_id", "=", account_id],
    ["lines.move_id.date", ">=", from_date],
    ["lines.move_id.date", "<=", to_date]
])

print("BANK RECONCILIATION REPORT")
print(f"Account: {account_id}")
print(f"Period: {from_date} to {to_date}")
print("=" * 80)

for rec in reconciliations:
    status = "✓" if rec.balance == 0 else "⚠"
    print(f"\n{status} {rec.number} - Balance: {rec.balance}")
    for line in rec.lines:
        print(f"  {line.move_id.date} {line.move_id.number:20} "
              f"Dr: {line.debit:10.2f} Cr: {line.credit:10.2f}")
    print(f"  Total: Dr: {rec.debit:10.2f} Cr: {rec.credit:10.2f}")

Use Case 5: Find Outstanding (Unreconciled) Items

# Find all unreconciled receivable/payable lines for aging report

account_type = "receivable"  # or "payable"

unreconciled_lines = get_model("account.move.line").search_browse([
    ["account_id.type", "=", account_type],
    ["reconcile_id", "=", None],  # Not reconciled
    ["move_id.state", "=", "posted"]  # Posted only
])

print(f"OUTSTANDING {account_type.upper()} ITEMS")
print("=" * 80)

total = 0
for line in unreconciled_lines:
    amount = line.debit - line.credit
    total += amount
    print(f"{line.move_id.date} {line.contact_id.name:30} "
          f"{line.move_id.number:20} {amount:12.2f}")

print("=" * 80)
print(f"Total Outstanding: {total:12.2f}")

Performance Tips

1. Index on reconcile_id

Ensure account.move.line has an index on reconcile_id:

CREATE INDEX idx_move_line_reconcile
    ON account_move_line(reconcile_id);

2. Batch Reconciliation

When reconciling multiple items, do it in batches:

# Process reconciliations in batches
from itertools import islice

def batch_reconcile(line_groups, batch_size=100):
    for i in range(0, len(line_groups), batch_size):
        batch = line_groups[i:i+batch_size]
        for line_ids in batch:
            get_model("account.move.line").reconcile(line_ids)
        print(f"Processed {min(i+batch_size, len(line_groups))} "
              f"of {len(line_groups)} reconciliations")

3. Cache Computed Values

The computed fields (debit, credit, balance) are calculated on-demand. For reports that access many reconciliations, consider caching:

# Pre-load reconciliation data
reconcile_ids = [1, 2, 3, 4, 5]
reconciliations = get_model("account.reconcile").browse(reconcile_ids)

# Cache totals
totals = get_model("account.reconcile").get_total(reconcile_ids)
for rec_id, values in totals.items():
    print(f"R{rec_id}: {values['number']} - Balance: {values['balance']}")


Troubleshooting

"Reconciliation is unbalanced (R123*)"

Cause: Total debits don't equal total credits in the reconciliation group Solution: Review the lines and ensure they balance:

reconcile = get_model("account.reconcile").browse([reconcile_id])[0]
print(f"Debit: {reconcile.debit}")
print(f"Credit: {reconcile.credit}")
print(f"Difference: {reconcile.balance}")

# List all lines
for line in reconcile.lines:
    print(f"  {line.move_id.number}: Dr {line.debit} Cr {line.credit}")

"Cannot reconcile draft entries"

Cause: Trying to reconcile journal entries that haven't been posted Solution: Only reconcile posted entries:

# Check line states before reconciling
for line_id in line_ids:
    line = get_model("account.move.line").browse([line_id])[0]
    if line.move_id.state != "posted":
        print(f"Error: Move {line.move_id.number} is not posted")

"Reconciliation shows in UI but lines not linked"

Cause: Database inconsistency or incomplete transaction Solution: Verify and fix line relationships:

# Check line linkage
reconcile = get_model("account.reconcile").browse([reconcile_id])[0]
for line in reconcile.lines:
    if line.reconcile_id.id != reconcile_id:
        print(f"⚠ Line {line.id} has wrong reconcile_id")
        # Fix it
        get_model("account.move.line").write([line.id], {
            "reconcile_id": reconcile_id
        })

"Deleted reconciliation but lines still show as reconciled"

Cause: Lines weren't cleared before deletion Solution: Always clear lines before deleting reconciliation:

# Proper deletion
reconcile = get_model("account.reconcile").browse([reconcile_id])[0]
line_ids = [line.id for line in reconcile.lines]

# Clear reconcile_id from lines
get_model("account.move.line").write(line_ids, {"reconcile_id": None})

# Now delete reconciliation
get_model("account.reconcile").delete([reconcile_id])


Testing Examples

Unit Test: Basic Reconciliation

def test_reconciliation_balanced():
    # Create test journal entry with two lines
    move_id = get_model("account.move").create({
        "journal_id": journal_id,
        "date": "2025-01-15",
        "narration": "Test reconciliation",
        "lines": [
            ("create", {
                "account_id": account1_id,
                "debit": 1000,
                "credit": 0
            }),
            ("create", {
                "account_id": account2_id,
                "debit": 0,
                "credit": 1000
            })
        ]
    })

    # Post the move
    get_model("account.move").post([move_id])

    # Get line IDs
    lines = get_model("account.move.line").search_browse([
        ["move_id", "=", move_id]
    ])
    line_ids = [l.id for l in lines]

    # Reconcile
    get_model("account.move.line").reconcile(line_ids)

    # Verify
    lines[0].reload()
    reconcile = lines[0].reconcile_id

    assert reconcile is not None
    assert reconcile.debit == 1000
    assert reconcile.credit == 1000
    assert reconcile.balance == 0
    assert "*" not in reconcile.number

    print(f"✓ Test passed: {reconcile.number}")

Security Considerations

Permission Model

  • Reconciliation creation/deletion requires accounting permissions
  • Typically restricted to accounting staff and administrators
  • Un-reconciling should require supervisor approval in some cases

Data Access

  • Reconciliations are company-specific via journal entries
  • Users can only see reconciliations for entries they have access to
  • Sensitive reconciliations (e.g., bank) may need additional access controls

Audit Trail

  • Reconciliation changes are tracked via journal entry audit log
  • Un-reconciling important items should be logged and monitored
  • Consider requiring approval workflow for un-reconciliation

Integration Points

Internal Modules

  • account.move.line: Source of all reconciled lines
  • account.invoice: Invoices generate lines for reconciliation
  • account.payment: Payments generate lines for reconciliation
  • Banking Module: Bank statement reconciliation
  • Financial Reports: Aging reports use reconciliation status

Workflow Triggers

Reconciliation affects: - Invoice status (fully paid vs. partially paid) - Payment allocation status - Aging report calculations - Statement of account generation


Version History

Last Updated: 2025-12-16 Model Version: account_reconcile.py Framework: Netforce


Additional Resources

  • Journal Entry Documentation: account.move
  • Journal Line Documentation: account.move.line
  • Invoice Documentation: account.invoice
  • Payment Documentation: account.payment

Support & Feedback

For issues or questions about reconciliation: 1. Verify all entries are posted before attempting reconciliation 2. Check that debits equal credits in the reconciliation group 3. Review journal entry lines for accuracy 4. Consult aging reports to identify unreconciled items


This documentation is generated for developer onboarding and reference purposes.