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¶
| 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:
4. Decline Expense¶
Method: do_decline(ids, context)
Declines/rejects the expense.
Behavior:
- Changes state to declined
- Returns navigation to claim edit view
Example:
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
Related Models¶
| 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_idtracks 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.