Skip to content

Account Expense Line Documentation

Overview

The Account Expense Line model (account.expense.line) represents individual line items within an account expense record. Each line captures details about a specific item or service purchased, including quantity, unit price, account allocation, and tax rate.


Model Information

Model Name: account.expense.line Display Name: Expense Line Key Fields: None (no unique constraint defined)

Features

  • ❌ Audit logging enabled (_audit_log)
  • ❌ Multi-company support (company_id)
  • ❌ Full-text content search (_content_search)
  • ✅ Computed amount field
  • ✅ Cascade delete with parent expense

Key Fields Reference

All Fields

Field Type Required Description
expense_id Many2One Parent expense record
description Char Item description
qty Decimal Quantity purchased
unit_price Decimal Price per unit
amount Decimal Computed: qty × unit_price
account_id Many2One GL account for posting
tax_id Many2One Applicable tax rate

Computed Fields

get_amount(ids, context)

Calculates the line amount as quantity multiplied by unit price.

Formula:

amount = qty * unit_price

Example:

# Line: qty=5, unit_price=20.00
# Result: amount=100.00


API Methods

1. Create Line

Method: create(vals, context)

Creates a new expense line. Usually created via parent expense.

Parameters:

vals = {
    "expense_id": expense_id,      # Required: Parent expense
    "description": "Office chair", # Required: Item description
    "qty": 2,                      # Required: Quantity
    "unit_price": 150.00,          # Required: Unit price
    "account_id": furniture_acc_id, # Optional: GL account
    "tax_id": gst_rate_id,         # Optional: Tax rate
}

Returns: int - New record ID

Example (Standalone):

line_id = get_model("account.expense.line").create({
    "expense_id": expense_id,
    "description": "Wireless Mouse",
    "qty": 1,
    "unit_price": 45.00,
    "account_id": equipment_account_id,
})

Example (Via Parent):

# More common: Create lines when creating expense
expense_id = get_model("account.expense").create({
    "ref": "EXP-001",
    "contact_id": vendor_id,
    "date": "2024-12-15",
    "tax_type": "tax_in",
    "lines": [
        ("create", {
            "description": "Wireless Mouse",
            "qty": 1,
            "unit_price": 45.00,
        }),
        ("create", {
            "description": "USB Hub",
            "qty": 1,
            "unit_price": 25.00,
        })
    ]
})


2. Read Line

Method: browse(ids, context)

Retrieve line records for reading.

Example:

lines = get_model("account.expense.line").browse(line_ids)
for line in lines:
    print(f"{line.description}: {line.qty} x {line.unit_price} = {line.amount}")


3. Update Line

Method: write(ids, vals, context)

Update existing line records.

Example:

# Update quantity
get_model("account.expense.line").write([line_id], {
    "qty": 3
})


4. Delete Line

Method: delete(ids, context)

Delete line records. Lines are also automatically deleted when parent expense is deleted (cascade).

Example:

get_model("account.expense.line").delete([line_id])


5. Get Amount

Method: get_amount(ids, context)

Returns computed amount for each line.

Returns: Dict mapping line IDs to amounts

Example:

amounts = get_model("account.expense.line").get_amount(line_ids)
for line_id, amount in amounts.items():
    print(f"Line {line_id}: {amount}")


Model Relationship Description
account.expense Many2One (expense_id) Parent expense record
account.account Many2One (account_id) GL account for posting
account.tax.rate Many2One (tax_id) Tax rate for line

Common Use Cases

Use Case 1: Add Line to Existing Expense

# Add a new line to an existing expense
line_id = get_model("account.expense.line").create({
    "expense_id": expense_id,
    "description": "External Hard Drive",
    "qty": 1,
    "unit_price": 120.00,
    "account_id": equipment_account_id,
    "tax_id": gst_rate_id,
})

Use Case 2: Get All Lines for an Expense

# Get all lines for a specific expense
expense = get_model("account.expense").browse([expense_id])[0]
total = 0
for line in expense.lines:
    print(f"{line.description}: {line.amount}")
    total += line.amount
print(f"Total: {total}")

Use Case 3: Update Multiple Lines

# Update account for all lines in an expense
expense = get_model("account.expense").browse([expense_id])[0]
line_ids = [line.id for line in expense.lines]
get_model("account.expense.line").write(line_ids, {
    "account_id": new_account_id
})

Use Case 4: Calculate Line Totals with Tax

# Calculate line totals including tax (for tax-exclusive expenses)
expense = get_model("account.expense").browse([expense_id])[0]

for line in expense.lines:
    base_amount = line.amount
    if line.tax_id and expense.tax_type == "tax_ex":
        tax = get_model("account.tax.rate").compute_tax(
            line.tax_id.id,
            base_amount,
            tax_type="tax_ex"
        )
        total = base_amount + tax
    else:
        total = base_amount
    print(f"{line.description}: {base_amount} + tax = {total}")

Best Practices

1. Always Specify Account

# Good: Specify account for proper GL posting
line = ("create", {
    "description": "Office supplies",
    "qty": 1,
    "unit_price": 50.00,
    "account_id": supplies_account_id,  # Proper categorization
})

# Bad: Missing account
line = ("create", {
    "description": "Office supplies",
    "qty": 1,
    "unit_price": 50.00,
    # No account - may cause posting issues
})

2. Use Descriptive Line Items

# Good: Specific description
line = ("create", {
    "description": "HP LaserJet Toner Cartridge (Black)",
    "qty": 2,
    "unit_price": 85.00,
})

# Bad: Vague description
line = ("create", {
    "description": "Supplies",
    "qty": 2,
    "unit_price": 85.00,
})

3. Match Tax Rates to Account

# Good: Tax rate matches account's default tax
acc = get_model("account.account").browse([account_id])[0]
line = ("create", {
    "description": "Item",
    "qty": 1,
    "unit_price": 100.00,
    "account_id": account_id,
    "tax_id": acc.tax_id.id if acc.tax_id else None,
})

Troubleshooting

"Required field missing: description"

Cause: Creating line without description Solution: Always provide a description for each line item

"Required field missing: qty"

Cause: Creating line without quantity Solution: Provide qty field, even if it's 1

"Required field missing: unit_price"

Cause: Creating line without unit price Solution: Always specify unit_price

"Expense line has no parent"

Cause: expense_id not set or invalid Solution: Ensure valid expense_id is provided when creating standalone lines

"Amount shows as 0"

Cause: qty or unit_price is 0 or None Solution: Verify both qty and unit_price have valid positive values


Testing Examples

Unit Test: Line Amount Calculation

def test_expense_line_amount():
    # Create expense
    expense_id = get_model("account.expense").create({
        "ref": "TEST-001",
        "contact_id": vendor_id,
        "date": "2024-12-15",
        "tax_type": "no_tax",
        "lines": [
            ("create", {
                "description": "Test Item",
                "qty": 3,
                "unit_price": 25.00,
            })
        ]
    })

    # Get line
    expense = get_model("account.expense").browse([expense_id])[0]
    line = expense.lines[0]

    # Verify amount calculation
    assert line.qty == 3
    assert line.unit_price == 25.00
    assert line.amount == 75.00  # 3 x 25 = 75

    # Cleanup
    get_model("account.expense").delete([expense_id])

Unit Test: Multiple Lines

def test_multiple_expense_lines():
    # Create expense with multiple lines
    expense_id = get_model("account.expense").create({
        "ref": "TEST-002",
        "contact_id": vendor_id,
        "date": "2024-12-15",
        "tax_type": "no_tax",
        "lines": [
            ("create", {"description": "Item A", "qty": 2, "unit_price": 10.00}),
            ("create", {"description": "Item B", "qty": 1, "unit_price": 50.00}),
            ("create", {"description": "Item C", "qty": 5, "unit_price": 5.00}),
        ]
    })

    expense = get_model("account.expense").browse([expense_id])[0]

    # Verify line count
    assert len(expense.lines) == 3

    # Verify total
    total = sum(line.amount for line in expense.lines)
    assert total == 95.00  # (2*10) + (1*50) + (5*5) = 20 + 50 + 25 = 95

    # Cleanup
    get_model("account.expense").delete([expense_id])

Security Considerations

Permission Model

  • Lines inherit permissions from parent expense
  • Users who can edit expense can edit its lines

Data Access

  • Lines are accessed through parent expense
  • Cascade delete ensures orphan lines don't exist

Database Behavior

Cascade Delete

When a parent expense is deleted, all its lines are automatically deleted:

on_delete="cascade"

This ensures data integrity - no orphan lines remain.


Version History

Last Updated: December 2024 Model Version: account_expense_line.py Framework: Netforce


Additional Resources

  • Account Expense Documentation: account.expense
  • Account Documentation: account.account
  • Tax Rate Documentation: account.tax.rate

This documentation is generated for developer onboarding and reference purposes.