Skip to content

Account Expense Documentation

Overview

The Account Expense model (account.expense) manages expense receipts with detailed line items. It supports tax calculations, workflow approval, and integration with account claims for reimbursement processing. This model differs from expense by providing more structured line-item detail.


Model Information

Model Name: account.expense Display Name: Expense (implied) Name Field: ref Key Fields: None (no unique constraint defined)

Features

  • ❌ Audit logging enabled (_audit_log)
  • ❌ Multi-company support (company_id)
  • ❌ Full-text content search (_content_search)
  • ✅ Line item support
  • ✅ Tax calculation (inclusive/exclusive/no tax)
  • ✅ Approval workflow
  • ✅ UUID for unique identification

State Workflow

draft → waiting_approval → approved
                        declined
State Code Description
Draft draft Initial state, can be edited
Waiting Approval waiting_approval Submitted and pending review
Approved approved Expense has been approved
Declined declined Expense was rejected

Key Fields Reference

Header Fields

Field Type Required Description
ref Char Reference number (name field)
contact_id Many2One Contact/vendor
date Date Expense date
user_id Many2One Receipt owner
tax_type Selection Tax calculation method
state Selection Workflow state

Tax Types

Type Code Description
Tax Exclusive tax_ex Tax added on top of line amounts
Tax Inclusive tax_in Tax included in line amounts
No Tax no_tax No tax calculation

Computed Amount Fields

Field Type Description
amount_subtotal Decimal Sum of line amounts (before/after tax based on type)
amount_tax Decimal Total tax amount
amount_total Decimal Grand total (subtotal + tax)

Relationship Fields

Field Type Description
lines One2Many Expense line items
claim_id Many2One Parent account claim

Other Fields

Field Type Description
attach File Attachment/receipt file
uuid Char Unique identifier (auto-generated)

Default Values

_defaults = {
    "tax_type": "tax_in",                              # Default: Tax inclusive
    "uuid": lambda *a: str(uuid.uuid4()),              # Auto-generate UUID
    "user_id": lambda self, context: int(context["user_id"]),  # Current user
    "state": "draft",                                  # Initial state
}

API Methods

1. Create Expense

Method: create(vals, context)

Creates a new account expense record.

Parameters:

vals = {
    "ref": "EXP-001",                  # Required: Reference number
    "contact_id": vendor_id,           # Required: Vendor/contact
    "date": "2024-12-15",              # Required: Date
    "user_id": user_id,                # Required: Receipt owner
    "tax_type": "tax_in",              # Required: Tax calculation type
    "lines": [                          # Line items
        ("create", {
            "description": "Office supplies",
            "qty": 1,
            "unit_price": 100.00,
            "account_id": expense_account_id,
            "tax_id": tax_rate_id,
        })
    ]
}

Returns: int - New record ID

Example:

# Create expense with line items
expense_id = get_model("account.expense").create({
    "ref": "EXP-2024-001",
    "contact_id": vendor_id,
    "date": "2024-12-15",
    "tax_type": "tax_in",
    "lines": [
        ("create", {
            "description": "Printer paper",
            "qty": 5,
            "unit_price": 20.00,
            "account_id": supplies_account_id,
        }),
        ("create", {
            "description": "Ink cartridges",
            "qty": 2,
            "unit_price": 50.00,
            "account_id": supplies_account_id,
        })
    ]
})


2. Submit Expenses

Method: do_submit(ids, context)

Submits expenses for approval by creating an account claim.

Behavior: - Validates all expenses belong to the same user - Creates a new account.claim record - Links all expenses to the claim - Changes state to waiting_approval

Returns: Navigation to claim waiting approval view

Example:

# Submit expenses for approval
result = get_model("account.expense").do_submit([expense_id1, expense_id2])
# Returns: {"next": {"name": "claim_waiting_approval"}}


3. Approve Expense

Method: do_approve(ids, context)

Approves the expense.

Behavior: - Changes state to approved - Returns navigation to claim edit view

Example:

# Approve expense
result = get_model("account.expense").do_approve([expense_id])


4. Decline Expense

Method: do_decline(ids, context)

Declines/rejects the expense.

Behavior: - Changes state to declined - Returns navigation to claim edit view

Example:

# Decline expense
result = get_model("account.expense").do_decline([expense_id])


5. Get Amount (Computed)

Method: get_amount(ids, context)

Calculates subtotal, tax, and total amounts.

Returns: Dict with amount_subtotal, amount_tax, amount_total

Example:

amounts = get_model("account.expense").get_amount([expense_id])
print(f"Subtotal: {amounts[expense_id]['amount_subtotal']}")
print(f"Tax: {amounts[expense_id]['amount_tax']}")
print(f"Total: {amounts[expense_id]['amount_total']}")


6. Update Amounts

Method: update_amounts(context)

Recalculates all line amounts and totals (used in UI).

Parameters:

context = {
    "data": {
        "tax_type": "tax_in",
        "lines": [
            {"qty": 1, "unit_price": 100, "tax_id": tax_id},
            ...
        ]
    }
}

Returns: Updated data dict with recalculated amounts

Example:

# Recalculate amounts after line changes
data = get_model("account.expense").update_amounts(context={"data": expense_data})


UI Events (onchange methods)

onchange_account

Triggered when account is selected on a line. Updates: - tax_id - Sets to account's default tax rate

Usage:

data = {
    "lines": [
        {"account_id": account_id}
    ]
}
result = get_model("account.expense").onchange_account(
    context={"data": data, "path": "lines.0"}
)


Write/Delete Overrides

write(ids, vals, **kw)

Custom write method that: - Tracks linked claim IDs before and after update - Calls parent write - Updates function store for expense - Recalculates linked claims

delete(ids, **kw)

Custom delete method that: - Tracks linked claim IDs before delete - Calls parent delete - Recalculates affected claims


Model Relationship Description
account.expense.line One2Many (lines) Expense line items
account.claim Many2One (claim_id) Parent claim for reimbursement
contact Many2One (contact_id) Vendor/supplier
base.user Many2One (user_id) Receipt owner

Common Use Cases

Use Case 1: Create Expense with Multiple Lines

# Create detailed expense receipt
expense_id = get_model("account.expense").create({
    "ref": "RECEIPT-001",
    "contact_id": office_depot_id,
    "date": "2024-12-15",
    "tax_type": "tax_in",
    "attach": receipt_file_path,
    "lines": [
        ("create", {
            "description": "A4 Paper (Box)",
            "qty": 10,
            "unit_price": 25.00,
            "account_id": office_supplies_acc,
            "tax_id": gst_rate_id,
        }),
        ("create", {
            "description": "Ballpoint Pens (Pack)",
            "qty": 5,
            "unit_price": 8.00,
            "account_id": office_supplies_acc,
            "tax_id": gst_rate_id,
        }),
        ("create", {
            "description": "Stapler",
            "qty": 2,
            "unit_price": 15.00,
            "account_id": office_supplies_acc,
            "tax_id": gst_rate_id,
        })
    ]
})

# Get totals
expense = get_model("account.expense").browse([expense_id])[0]
print(f"Subtotal: {expense.amount_subtotal}")
print(f"Tax: {expense.amount_tax}")
print(f"Total: {expense.amount_total}")

Use Case 2: Submit Multiple Expenses as Claim

# Create multiple expenses throughout the week
expense_ids = []

# Monday expense
expense_ids.append(get_model("account.expense").create({
    "ref": "MON-001",
    "contact_id": taxi_vendor_id,
    "date": "2024-12-09",
    "tax_type": "no_tax",
    "lines": [("create", {"description": "Taxi to client", "qty": 1, "unit_price": 25.00})]
}))

# Wednesday expense
expense_ids.append(get_model("account.expense").create({
    "ref": "WED-001",
    "contact_id": restaurant_id,
    "date": "2024-12-11",
    "tax_type": "tax_in",
    "lines": [("create", {"description": "Client lunch", "qty": 1, "unit_price": 85.00})]
}))

# Submit all as single claim
result = get_model("account.expense").do_submit(expense_ids)

Use Case 3: Tax Calculation Examples

# Tax Exclusive - tax added on top
expense_ex = get_model("account.expense").create({
    "ref": "TAX-EX-001",
    "contact_id": vendor_id,
    "date": "2024-12-15",
    "tax_type": "tax_ex",  # Tax exclusive
    "lines": [("create", {
        "description": "Item",
        "qty": 1,
        "unit_price": 100.00,  # Base price
        "tax_id": gst_7_percent_id,
    })]
})
# Result: subtotal=100, tax=7, total=107

# Tax Inclusive - tax included in price
expense_in = get_model("account.expense").create({
    "ref": "TAX-IN-001",
    "contact_id": vendor_id,
    "date": "2024-12-15",
    "tax_type": "tax_in",  # Tax inclusive
    "lines": [("create", {
        "description": "Item",
        "qty": 1,
        "unit_price": 107.00,  # Price includes tax
        "tax_id": gst_7_percent_id,
    })]
})
# Result: subtotal=100, tax=7, total=107

Best Practices

1. Use Proper Tax Types

# Good: Match tax type to receipt format
# If receipt shows "Price: $100 + GST: $7 = Total: $107"
expense = get_model("account.expense").create({
    "tax_type": "tax_ex",  # Tax shown separately
    ...
})

# If receipt shows "Total: $107 (incl. GST)"
expense = get_model("account.expense").create({
    "tax_type": "tax_in",  # Tax included
    ...
})

2. Always Attach Receipts

# Good: Attach receipt for audit trail
expense = get_model("account.expense").create({
    "ref": "EXP-001",
    "attach": "/uploads/receipts/exp001.pdf",  # Receipt image
    ...
})

3. Use Meaningful References

# Good: Descriptive, traceable reference
expense = get_model("account.expense").create({
    "ref": "TRAVEL-KL-20241215",  # Type-Location-Date
    ...
})

# Bad: Generic reference
expense = get_model("account.expense").create({
    "ref": "001",  # Not meaningful
    ...
})

Troubleshooting

"Expenses belong to different users"

Cause: Trying to submit expenses from multiple users as single claim Solution: Only submit expenses belonging to the same user together

"Tax calculation mismatch"

Cause: Incorrect tax_type selected for the receipt Solution: Verify if receipt shows tax-inclusive or tax-exclusive prices

"Missing required field: contact_id"

Cause: Creating expense without specifying vendor Solution: Always provide a valid contact_id

"Amount total is zero"

Cause: No line items added to expense Solution: Add at least one line item with qty and unit_price


Testing Examples

Unit Test: Create Expense with Tax Calculation

def test_expense_tax_calculation():
    # Create expense with tax-inclusive pricing
    expense_id = get_model("account.expense").create({
        "ref": "TEST-001",
        "contact_id": test_vendor_id,
        "date": "2024-12-15",
        "tax_type": "tax_in",
        "lines": [
            ("create", {
                "description": "Test item",
                "qty": 1,
                "unit_price": 107.00,
                "tax_id": gst_7_id,  # 7% GST
            })
        ]
    })

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

    # Verify calculations
    assert expense.amount_total == 107.00
    # For 7% tax inclusive: base = 107/1.07 = 100, tax = 7
    assert abs(expense.amount_subtotal - 100.00) < 0.01
    assert abs(expense.amount_tax - 7.00) < 0.01

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

Security Considerations

Permission Model

  • Users typically can only create/view their own expenses
  • Approval permissions controlled by workflow
  • Finance/Admin can view all expenses

Data Access

  • user_id tracks expense ownership
  • UUID provides unique external reference

Version History

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


Additional Resources

  • Account Expense Line Documentation: account.expense.line
  • Account Claim Documentation: account.claim
  • Tax Rate Documentation: account.tax.rate

This documentation is generated for developer onboarding and reference purposes.