Journal Entry Line Model (account.move.line)¶
Overview¶
The Journal Entry Line model (account.move.line) represents individual debit and credit entries within a journal entry. It is the most granular level of accounting data in Netforce, forming the actual ledger entries that comprise the general ledger. Each line belongs to a journal entry (account.move) and posts to a specific account. This model handles reconciliation, tracking categories, tax components, and foreign currency amounts. It serves as the foundation for all account balances, financial reports, and reconciliation processes.
Model Information¶
Model Name: account.move.line
Display Name: Ledger Entry
Key Fields: None (lines are identified by move_id and sequence)
Source File: netforce_account/models/account_move_line.py
Features¶
- ✅ Cascade deletion with parent move (
on_delete="cascade") - ✅ Indexed for performance (
account_id,move_date) - ✅ Reconciliation support via
reconcile_id - ✅ Multi-currency support via
amount_cur - ✅ Tracking/analytics integration via
track_id,track2_id
Understanding Journal Entry Lines¶
What are Journal Entry Lines?¶
Journal entry lines are the individual debit and credit entries that make up a complete accounting transaction. In double-entry bookkeeping:
- Every transaction has at least 2 lines (one debit, one credit)
- Total debits must equal total credits for the parent move to balance
- Each line posts to one account and affects that account's balance
Example Transaction:
Cash Sale of $1,000
Line 1: Cash Account (101) DR: $1,000 CR: $0
Line 2: Revenue Account (401) DR: $0 CR: $1,000
Line Relationship Structure¶
account.move (Journal Entry)
└── account.move.line (Line 1) → account.account (Cash)
└── account.move.line (Line 2) → account.account (Revenue)
└── account.move.line (Line 3) → account.account (Tax)
Reconciliation States¶
| State | Code | Description |
|---|---|---|
| Not Reconciled | not_reconciled |
Line has not been matched with offsetting entries |
| Reconciled | reconciled |
Line has been matched and balanced with other entries |
Reconciliation is used primarily for: - Receivables/Payables: Matching invoices with payments - Bank Accounts: Matching transactions with bank statements - Clearing Accounts: Ensuring temporary accounts balance to zero
Key Fields Reference¶
Core Fields¶
| Field | Type | Required | Description |
|---|---|---|---|
move_id |
Many2One | ✅ | Parent journal entry |
sequence |
Integer | ❌ | Line order within entry |
description |
Text | ✅ | Line description (defaults to move narration) |
account_id |
Many2One | ✅ | General ledger account |
debit |
Decimal | ✅ | Debit amount (must be ≥ 0) |
credit |
Decimal | ✅ | Credit amount (must be ≥ 0) |
state |
Selection | ❌ | Reconciliation state |
Parent Move Fields (Computed/Related)¶
| Field | Type | Description |
|---|---|---|
move_date |
Date | Transaction date (from move) |
move_state |
Selection | Move state (draft/posted/voided) |
move_narration |
Char | Move narration |
move_type |
Selection | Move type (invoice/payment/transfer/etc.) |
move_ref |
Char | Move reference |
move_number |
Char | Move/journal entry number |
Account Fields (Computed/Related)¶
| Field | Type | Description |
|---|---|---|
account_name |
Char | Account name |
account_code |
Char | Account code |
Tax Fields¶
| Field | Type | Description |
|---|---|---|
tax_comp_id |
Many2One | Tax component (VAT, WHT, etc.) |
tax_base |
Decimal | Base amount for tax calculation |
tax_no |
Char | Tax invoice number |
tax_date |
Date | Tax invoice date |
Tracking/Analytics Fields¶
| Field | Type | Description |
|---|---|---|
track_id |
Many2One | Primary tracking category (e.g., department, project) |
track2_id |
Many2One | Secondary tracking category |
Relationship Fields¶
| Field | Type | Description |
|---|---|---|
contact_id |
Many2One | Related contact (customer/supplier) |
product_id |
Many2One | Related product |
invoice_id |
Many2One | Related invoice |
stock_move_id |
Many2One | Related stock movement |
due_date |
Date | Payment due date (for AR/AP) |
Reconciliation Fields¶
| Field | Type | Description |
|---|---|---|
reconcile_id |
Many2One | Reconciliation record |
statement_lines |
Many2Many | Related bank statement lines |
statement_line_id |
Many2One | Statement line (deprecated) |
statement_line_search |
Many2One | Statement line search helper |
bank_reconcile_id |
Many2One | Bank reconciliation (deprecated) |
is_account_reconciled |
Boolean | Whether reconciliation is balanced (computed) |
Additional Fields¶
| Field | Type | Description |
|---|---|---|
qty |
Decimal | Quantity (for inventory tracking) |
amount_cur |
Decimal | Amount in foreign currency |
empty_contact |
Boolean | Search helper for lines without contact |
API Methods¶
1. Create Line¶
Method: create(vals, context)
Creates a new journal entry line. Usually called as part of journal entry creation.
Parameters:
vals = {
"move_id": 123, # Required: Parent move ID
"account_id": 101, # Required: Account ID
"description": "Cash received", # Optional (defaults to move narration)
"debit": 1000.00, # Required: Debit amount
"credit": 0.00, # Required: Credit amount
"contact_id": 456, # Optional: Contact ID
"track_id": 10, # Optional: Tracking category
"tax_comp_id": 5, # Optional: Tax component
"tax_base": 1000.00, # Optional: Tax base amount
"due_date": "2025-02-15" # Optional: Due date
}
Returns: int - New line ID
Example:
# Usually created as part of journal entry
move_id = get_model("account.move").create({
"journal_id": journal_id,
"date": "2025-01-15",
"narration": "Cash sale",
"lines": [
("create", {
"account_id": cash_account_id,
"debit": 1000.00,
"credit": 0
}),
("create", {
"account_id": revenue_account_id,
"debit": 0,
"credit": 1000.00
})
]
})
2. Reconcile Lines¶
Method: reconcile(ids, context)
Reconciles multiple lines together, typically matching invoices with payments.
Parameters:
- ids (list): Line IDs to reconcile together
Behavior: - Creates new reconciliation record - Includes lines from existing reconciliations (merges) - Validates all lines use same account - Links all lines to reconciliation - Updates related invoice states
Returns: None
Example:
# Reconcile invoice with payment
invoice_line_id = 123 # From invoice move
payment_line_id = 456 # From payment move
get_model("account.move.line").reconcile([invoice_line_id, payment_line_id])
# Verify reconciliation
line = get_model("account.move.line").browse(invoice_line_id)
print(f"Reconciled: {line.reconcile_id is not None}")
print(f"Balanced: {line.is_account_reconciled}")
Validation Errors: - "Can only reconcile transactions of same account" - Lines must be from same account
3. Unreconcile Lines¶
Method: unreconcile(ids, context)
Removes reconciliation from lines, typically used for bank statement lines.
Parameters:
- ids (list): Line IDs to unreconcile
Behavior: - Finds related bank statement lines - Unreconciles statement lines - Sets line state to not_reconciled
Returns: None
Example:
4. Unreconcile Manually¶
Method: unreconcile_manual(ids, context)
Removes reconciliation manually, including all lines in the reconciliation.
Parameters:
- ids (list): Line IDs to unreconcile
Behavior: - Finds all lines in same reconciliation(s) - Removes reconcile_id from all lines - Does not update statement lines
Returns: None
Example:
# Remove reconciliation from invoice/payment match
get_model("account.move.line").unreconcile_manual([invoice_line_id])
5. Remove from All Reconciliations¶
Method: reconcile_remove_from_all(ids, context)
Removes lines from all bank statement reconciliations.
Parameters:
- ids (list): Line IDs
Behavior: - Clears all statement_lines relationships
Returns: None
Example:
6. View Transaction¶
Method: view_transaction(ids, context)
Navigates to the parent journal entry.
Parameters:
- ids (list): Line IDs
Returns: dict with next navigation action
Example:
# Navigate to journal entry from line
result = get_model("account.move.line").view_transaction([line_id])
# Redirects to journal entry form
7. Delete Line¶
Method: delete(ids, context)
Deletes journal entry lines.
Parameters:
- ids (list): Line IDs to delete
Behavior: - Validates parent move is not posted - Updates related reconciliations - Deletes lines
Returns: None
Example:
Validation Errors: - "Can not delete line of posted journal entry"
Search Functions¶
Search by Account¶
Search by Date Range¶
# Find lines in date range (uses indexed field)
condition = [
["move_date", ">=", "2025-01-01"],
["move_date", "<=", "2025-01-31"]
]
Search by Contact¶
Search by Move Number¶
Search Unreconciled Lines¶
# Find unreconciled lines for an account
condition = [
["account_id", "=", account_id],
["state", "=", "not_reconciled"]
]
Search Lines Without Contact¶
Search by Statement Line¶
# Find lines linked to statement line
condition = [["statement_line_search", "=", statement_line_id]]
Computed Fields Functions¶
_is_account_reconciled(ids, context)¶
Checks if the reconciliation this line belongs to is fully balanced:
- Returns True if abs(reconcile_id.balance) == 0
- Returns False if no reconciliation or unbalanced
_get_related(ids, context)¶
Generic function to retrieve related field values: - Retrieves values from parent move (date, state, narration, etc.) - Retrieves values from account (name, code)
_search_empty_contact(clause, context)¶
Search helper for finding lines with/without contacts: - Used to locate lines requiring contact assignment
Database Indexes¶
The model has composite indexes for performance:
This makes queries filtering by account and date range very fast, which is critical for: - Account balance calculations - Trial balance generation - Aged receivables/payables reports - Period-based financial reports
Best Practices¶
1. Always Set Either Debit or Credit (Never Both)¶
# Bad: Both debit and credit set
line_vals = {
"account_id": account_id,
"debit": 1000.00,
"credit": 500.00 # Wrong!
}
# Good: Only one side set
line_vals = {
"account_id": account_id,
"debit": 1000.00,
"credit": 0 # Correct
}
2. Use Description for Line-Level Detail¶
# Bad: Generic description
line_vals = {
"description": "Transaction", # Not helpful
"account_id": account_id,
...
}
# Good: Specific description
line_vals = {
"description": "Payment received for Invoice INV-2025-001",
"account_id": account_id,
...
}
3. Set amount_cur for Foreign Currency Accounts¶
# Good: Include currency amount
line_vals = {
"account_id": usd_bank_account_id,
"debit": 1350.00, # Base currency (THB)
"credit": 0,
"amount_cur": 1000.00 # Foreign currency (USD)
}
4. Use Tracking Categories for Analytics¶
# Good: Set tracking for analytics
line_vals = {
"account_id": expense_account_id,
"debit": 5000.00,
"credit": 0,
"track_id": department_id, # Department tracking
"track2_id": project_id, # Project tracking
"contact_id": vendor_id
}
5. Reconcile Related Transactions¶
# Good: Reconcile invoice with payment
invoice = get_model("account.invoice").browse(invoice_id)
payment = get_model("account.payment").browse(payment_id)
# Find AR/AP lines
invoice_line = invoice.move_id.lines[0] # First line is usually AR/AP
payment_lines = [l for l in payment.move_id.lines
if l.account_id.id == invoice_line.account_id.id]
if payment_lines:
get_model("account.move.line").reconcile([
invoice_line.id,
payment_lines[0].id
])
Related Models¶
| Model | Relationship | Description |
|---|---|---|
account.move |
Many2One | Parent journal entry |
account.account |
Many2One | General ledger account |
account.reconcile |
Many2One | Reconciliation record |
account.tax.component |
Many2One | Tax component |
account.track.categ |
Many2One | Tracking category |
contact |
Many2One | Customer/supplier |
product |
Many2One | Product |
account.invoice |
Many2One | Related invoice |
stock.move |
Many2One | Related stock movement |
account.statement.line |
Many2Many | Bank statement lines |
Common Use Cases¶
Use Case 1: Query Account Balance¶
# Calculate account balance for a date range
account_id = 101 # Cash account
start_date = "2025-01-01"
end_date = "2025-01-31"
lines = get_model("account.move.line").search_browse([
["account_id", "=", account_id],
["move_date", ">=", start_date],
["move_date", "<=", end_date],
["move_state", "=", "posted"]
])
total_debit = sum(line.debit for line in lines)
total_credit = sum(line.credit for line in lines)
balance = total_debit - total_credit
print(f"Account Balance: {balance}")
Use Case 2: Find Unreconciled Receivables¶
# Find outstanding customer invoices
ar_account_id = 120 # Accounts Receivable
unreconciled_lines = get_model("account.move.line").search_browse([
["account_id", "=", ar_account_id],
["state", "=", "not_reconciled"],
["move_state", "=", "posted"]
])
for line in unreconciled_lines:
amount = line.debit - line.credit
print(f"Invoice: {line.move_number}")
print(f"Customer: {line.contact_id.name if line.contact_id else 'N/A'}")
print(f"Amount: {amount}")
print(f"Due Date: {line.due_date or 'N/A'}")
print("---")
Use Case 3: Reconcile Invoice with Payment¶
# Match invoice with payment
invoice_id = 123
payment_id = 456
# Get invoice AR line
invoice = get_model("account.invoice").browse(invoice_id)
invoice_move = invoice.move_id
invoice_ar_line = None
for line in invoice_move.lines:
if line.account_id.type in ("receivable", "payable"):
invoice_ar_line = line
break
# Get payment AR line
payment = get_model("account.payment").browse(payment_id)
payment_move = payment.move_id
payment_ar_line = None
for line in payment_move.lines:
if line.account_id.id == invoice_ar_line.account_id.id:
payment_ar_line = line
break
# Reconcile
if invoice_ar_line and payment_ar_line:
get_model("account.move.line").reconcile([
invoice_ar_line.id,
payment_ar_line.id
])
print("Invoice and payment reconciled")
Use Case 4: Generate Aged Receivables Report¶
from datetime import datetime, timedelta
# Define aging periods
today = datetime.now().date()
periods = [
("Current", 0),
("1-30 days", 30),
("31-60 days", 60),
("61-90 days", 90),
("Over 90 days", 999)
]
ar_account_id = 120
# Get all unreconciled AR lines
lines = get_model("account.move.line").search_browse([
["account_id", "=", ar_account_id],
["state", "=", "not_reconciled"],
["move_state", "=", "posted"]
])
# Group by customer and age
customer_aging = {}
for line in lines:
customer_id = line.contact_id.id if line.contact_id else None
customer_name = line.contact_id.name if line.contact_id else "Unknown"
amount = line.debit - line.credit
if amount <= 0:
continue
# Calculate age
invoice_date = datetime.strptime(line.move_date, "%Y-%m-%d").date()
age_days = (today - invoice_date).days
# Determine period
period_name = "Over 90 days"
for name, max_days in periods:
if age_days <= max_days:
period_name = name
break
# Aggregate
if customer_id not in customer_aging:
customer_aging[customer_id] = {
"name": customer_name,
"periods": {p[0]: 0 for p in periods}
}
customer_aging[customer_id]["periods"][period_name] += amount
# Print report
print("AGED RECEIVABLES REPORT")
print("=" * 80)
for customer_id, data in customer_aging.items():
print(f"\nCustomer: {data['name']}")
for period in periods:
amount = data["periods"][period[0]]
if amount > 0:
print(f" {period[0]}: {amount:,.2f}")
Use Case 5: Find Lines Requiring Contact Assignment¶
# Find lines where account requires contact but none assigned
lines = get_model("account.move.line").search_browse([
["empty_contact", "=", True],
["move_state", "=", "posted"]
])
print("Lines requiring contact assignment:")
for line in lines:
if line.account_id.require_contact:
print(f"Move: {line.move_number}")
print(f"Account: {line.account_code} - {line.account_name}")
print(f"Amount: DR {line.debit} CR {line.credit}")
print("---")
Use Case 6: Track Expenses by Department and Project¶
# Analyze expenses with dual tracking
expense_account_id = 501
lines = get_model("account.move.line").search_browse([
["account_id", "=", expense_account_id],
["move_date", ">=", "2025-01-01"],
["move_date", "<=", "2025-01-31"],
["move_state", "=", "posted"]
])
# Group by department and project
tracking = {}
for line in lines:
dept = line.track_id.name if line.track_id else "Unassigned"
proj = line.track2_id.name if line.track2_id else "Unassigned"
key = (dept, proj)
if key not in tracking:
tracking[key] = 0
tracking[key] += line.debit
# Print analysis
print("EXPENSE ANALYSIS BY DEPARTMENT AND PROJECT")
print("=" * 80)
for (dept, proj), amount in sorted(tracking.items()):
print(f"{dept:30} | {proj:30} | {amount:>15,.2f}")
Performance Tips¶
1. Use Indexed Fields for Queries¶
# Good: Uses indexed fields (account_id, move_date)
lines = get_model("account.move.line").search_browse([
["account_id", "=", account_id],
["move_date", ">=", start_date],
["move_date", "<=", end_date]
])
2. Filter by Posted State¶
# Good: Exclude draft entries
lines = get_model("account.move.line").search_browse([
["account_id", "=", account_id],
["move_state", "=", "posted"] # Only posted entries
])
3. Use Direct SQL for Large Reports¶
# For very large datasets, use direct SQL
from netforce import database
db = database.get_connection()
sql = """
SELECT
account_id,
SUM(debit) as total_debit,
SUM(credit) as total_credit
FROM account_move_line l
JOIN account_move m ON l.move_id = m.id
WHERE m.state = 'posted'
AND m.date >= %s
AND m.date <= %s
GROUP BY account_id
"""
res = db.query(sql, start_date, end_date)
Troubleshooting¶
"Can not delete line of posted journal entry"¶
Cause: Attempting to delete line from posted entry
Solution: Unpost the journal entry first (to_draft), then delete
"Can only reconcile transactions of same account"¶
Cause: Attempting to reconcile lines from different accounts
Solution: Only reconcile lines that post to the same account (e.g., same AR account)
Lines not appearing in balance queries¶
Cause: Parent move not posted
Solution: Ensure move_state = 'posted' in search conditions
Reconciliation not clearing invoice¶
Cause: Reconciliation not fully balanced
Solution: Check reconcile_id.balance = 0, may need additional lines
Missing currency amount error¶
Cause: Foreign currency account line without amount_cur
Solution: Always set amount_cur for multi-currency accounts
Testing Examples¶
Unit Test: Create and Query Lines¶
def test_create_and_query_lines():
# Create journal entry with lines
move_id = get_model("account.move").create({
"journal_id": 1,
"date": "2025-01-15",
"narration": "Test entry",
"lines": [
("create", {
"account_id": 101,
"description": "Test debit",
"debit": 500.00,
"credit": 0
}),
("create", {
"account_id": 401,
"description": "Test credit",
"debit": 0,
"credit": 500.00
})
]
})
# Query lines
lines = get_model("account.move.line").search_browse([
["move_id", "=", move_id]
])
# Verify
assert len(lines) == 2
assert lines[0].debit == 500.00
assert lines[1].credit == 500.00
# Cleanup
move = get_model("account.move").browse(move_id)
move.delete(force=True)
Unit Test: Reconcile Lines¶
def test_reconcile_lines():
# Create two offsetting entries
move1_id = get_model("account.move").create({
"journal_id": 1,
"date": "2025-01-15",
"narration": "Invoice",
"lines": [
("create", {
"account_id": 120, # AR
"debit": 1000.00,
"credit": 0
}),
("create", {
"account_id": 401, # Revenue
"debit": 0,
"credit": 1000.00
})
]
})
get_model("account.move").post([move1_id])
move2_id = get_model("account.move").create({
"journal_id": 1,
"date": "2025-01-20",
"narration": "Payment",
"lines": [
("create", {
"account_id": 101, # Cash
"debit": 1000.00,
"credit": 0
}),
("create", {
"account_id": 120, # AR
"debit": 0,
"credit": 1000.00
})
]
})
get_model("account.move").post([move2_id])
# Find AR lines
move1 = get_model("account.move").browse(move1_id)
move2 = get_model("account.move").browse(move2_id)
ar_line1 = [l for l in move1.lines if l.account_id.id == 120][0]
ar_line2 = [l for l in move2.lines if l.account_id.id == 120][0]
# Reconcile
get_model("account.move.line").reconcile([ar_line1.id, ar_line2.id])
# Verify
ar_line1 = ar_line1.browse()[0] # Refresh
assert ar_line1.reconcile_id is not None
assert ar_line1.is_account_reconciled == True
# Cleanup
get_model("account.move").void([move1_id, move2_id])
Security Considerations¶
Permission Model¶
Lines inherit permissions from parent moves. Users need:
- move_view: View lines
- move_create: Create lines (via move creation)
- move_edit: Edit lines (on draft moves)
- move_delete: Delete lines
Data Access¶
- Lines are visible based on parent move company
- Reconciliation requires appropriate permissions
- Sensitive fields (amounts, contacts) protected by field-level security
Integration Points¶
Internal Modules¶
- Journal Entries: Parent container for lines
- Accounts: Posts to general ledger
- Reconciliation: Matches transactions
- Bank Statements: Links to statement lines
- Invoices: Creates AR/AP lines
- Payments: Creates payment lines
- Tracking: Creates analytics entries
- Reports: Source for all financial reports
Version History¶
Last Updated: November 7, 2025
Model File: netforce_account/models/account_move_line.py
Framework: Netforce
Document Version: 1.0.0
Complexity: ⭐⭐⭐⭐⭐ (Very High - Foundation of accounting)
Additional Resources¶
- Journal Entry Documentation:
account.move - Account Documentation:
account.account - Reconciliation Documentation:
account.reconcile - Balance Documentation:
account.balance
Support & Feedback¶
For issues or questions about this module: 1. Check parent journal entry is posted 2. Verify account types and requirements 3. Check reconciliation balance 4. Review indexed field usage for performance 5. Test queries in development environment
This documentation is generated for developer onboarding and reference purposes.