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:
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
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.
Related Models¶
| 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:
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.