Skip to content

Expense Documentation

Overview

The Expense model (expense) manages individual expense records submitted by employees. It supports an approval workflow, multi-currency transactions, and integration with expense claims for reimbursement processing.


Model Information

Model Name: expense Display Name: Expense Key Fields: None (no unique constraint defined)

Features

  • ❌ Audit logging enabled (_audit_log)
  • ✅ Multi-company support (company_id)
  • ❌ Full-text content search (_content_search)
  • ✅ Approval workflow integration
  • ✅ Multi-currency support

State Workflow

draft → wait_approve → approved
                    decline
State Code Description
Draft draft Initial state, expense can be edited
Awaiting Approval wait_approve Submitted for approval, pending review
Approved approved Expense has been approved
Declined decline Expense was rejected

Key Fields Reference

Header Fields

Field Type Required Description
merchant Char Name of the merchant/vendor
amount Decimal Total expense amount
date Date Date of the expense
employee_id Many2One Employee who incurred the expense
currency_id Many2One Currency of the expense
state Selection Current workflow state

Optional Fields

Field Type Description
description Text Detailed description of the expense
account_id Many2One GL account for posting
tax_id Many2One Applicable tax rate
currency_rate Decimal Exchange rate if foreign currency
project_id Many2One Related project
image File Receipt image attachment
categ_id Many2One Expense category
product_id Many2One Related product

Tracking Fields

Field Type Description
track_id Many2One Track-1 cost tracking category
track2_id Many2One Track-2 cost tracking category

Relationship Fields

Field Type Description
claim_id Many2One Parent expense claim
invoice_id Many2One Related invoice
company_id Many2One Company for multi-company support
related_id Reference Reference to related job

Approval Fields

Field Type Description
approvals One2Many Approval records
date_approve DateTime Date when expense was approved
show_approve Boolean Computed: Shows if current user can approve

Deprecated Fields

Field Type Description
qty Decimal ~~Quantity~~ (deprecated)
unit_price Decimal ~~Unit price~~ (deprecated)
supplier_id Many2One ~~Supplier~~ (deprecated)

Default Values

_defaults = {
    "date": lambda *a: time.strftime("%Y-%m-%d"),  # Today's date
    "employee_id": _get_employee,                   # Current user's employee record
    "currency_id": _get_currency,                   # Company default currency
    "state": "draft",                               # Initial state
}

API Methods

1. Create Expense

Method: create(vals, context)

Creates a new expense record.

Parameters:

vals = {
    "merchant": "Office Depot",        # Required: Vendor name
    "amount": 150.00,                  # Required: Expense amount
    "date": "2024-12-15",              # Required: Expense date
    "employee_id": employee_id,        # Required: Employee ID
    "currency_id": currency_id,        # Required: Currency ID
    "description": "Office supplies",  # Optional: Description
    "account_id": account_id,          # Optional: GL account
    "categ_id": categ_id,              # Optional: Category
    "image": file_path,                # Optional: Receipt image
}

Returns: int - New record ID

Example:

# Create a new expense
expense_id = get_model("expense").create({
    "merchant": "Starbucks",
    "amount": 25.50,
    "date": "2024-12-15",
    "description": "Client meeting coffee"
})


2. Submit for Approval

Method: submit_for_approval(ids, context)

Submits the expense for approval workflow.

Behavior: - Changes state from draft to wait_approve - Finds applicable approval workflow based on employee, company, and amount - Creates approval records for first-level approvers - Triggers notification to approvers

Example:

# Submit expense for approval
get_model("expense").submit_for_approval([expense_id])

Raises: - Exception("Missing user") - If employee_id is not set - Exception("Missing approval workflow") - If no workflow matches - Exception("Missing approval workflow steps") - If workflow has no steps


3. Approve Expense

Method: approve(ids, context)

Approves the expense (called by authorized approver).

Behavior: - Validates user has pending approval for this expense - Marks approval as approved - Cancels other pending approvals at same sequence level - Creates next-level approval if workflow continues - Sets state to approved if all approval levels complete

Example:

# Approve expense (as authorized approver)
get_model("expense").approve([expense_id])

Raises: - Exception("Invalid expense status") - If not in wait_approve state - Exception("User does not have permission...") - If user is not an approver


4. Reject Expense

Method: reject(ids, context)

Rejects the expense (called by authorized approver).

Behavior: - Validates user has pending approval for this expense - Marks approval as rejected - Sets expense state to decline - Cancels all other pending approvals - Triggers rejection notification

Example:

# Reject expense (as authorized approver)
get_model("expense").reject([expense_id])


5. Get Show Approve

Method: get_show_approve(ids, context)

Computed function that determines if the current user can approve the expense.

Returns: dict - Mapping of expense IDs to boolean values

Example:

# Check if current user can approve
result = get_model("expense").get_show_approve([expense_id])
can_approve = result[expense_id]  # True or False


UI Events (onchange methods)

onchange_product

Triggered when product is selected. Updates: - account_id - Sets to product's purchase account - tax_id - Sets to product's purchase tax rate

Usage:

data = {
    "product_id": product_id
}
result = get_model("expense").onchange_product(context={"data": data})
# result contains updated account_id and tax_id


Search Functions

Search by Employee

# Find all expenses for an employee
expenses = get_model("expense").search([["employee_id", "=", employee_id]])

Search by Date Range

# Find expenses in date range
expenses = get_model("expense").search([
    ["date", ">=", "2024-01-01"],
    ["date", "<=", "2024-12-31"]
])

Search by State

# Find pending approval expenses
expenses = get_model("expense").search([["state", "=", "wait_approve"]])

# Find approved expenses
expenses = get_model("expense").search([["state", "=", "approved"]])

Search by Claim

# Find expenses linked to a claim
expenses = get_model("expense").search([["claim_id", "=", claim_id]])

Model Relationship Description
expense.categ Many2One (categ_id) Expense category
expense.claim Many2One (claim_id) Parent expense claim
hr.employee Many2One (employee_id) Employee who submitted
account.account Many2One (account_id) GL account for posting
account.tax.rate Many2One (tax_id) Tax rate
account.track.categ Many2One (track_id, track2_id) Cost tracking categories
currency Many2One (currency_id) Transaction currency
project Many2One (project_id) Related project
product Many2One (product_id) Related product
account.invoice Many2One (invoice_id) Related invoice
company Many2One (company_id) Company
approval One2Many (approvals) Approval records
approve.wkf Reference Approval workflow definition

Common Use Cases

Use Case 1: Create and Submit Expense

# 1. Create the expense
expense_id = get_model("expense").create({
    "merchant": "Singapore Airlines",
    "amount": 850.00,
    "date": "2024-12-10",
    "description": "Flight to KL for client meeting",
    "categ_id": travel_categ_id,
    "account_id": travel_expense_account_id,
})

# 2. Attach receipt image (optional)
get_model("expense").write([expense_id], {
    "image": "/path/to/receipt.pdf"
})

# 3. Submit for approval
get_model("expense").submit_for_approval([expense_id])

Use Case 2: Approve Multiple Expenses

# Find all pending expenses for current approver
user_id = access.get_active_user()
pending = get_model("approval").search([
    ["state", "=", "pending"],
    ["user_id", "=", user_id],
    ["related_id", "like", "expense,%"]
])

for app in get_model("approval").browse(pending):
    expense_id = int(app.related_id.split(",")[1])
    get_model("expense").approve([expense_id])

Use Case 3: Get Employee Expense Summary

# Get total expenses by category for an employee
employee_id = 123
expenses = get_model("expense").search_browse([
    ["employee_id", "=", employee_id],
    ["state", "=", "approved"],
    ["date", ">=", "2024-01-01"],
    ["date", "<=", "2024-12-31"]
])

by_category = {}
for exp in expenses:
    cat_name = exp.categ_id.name if exp.categ_id else "Uncategorized"
    by_category[cat_name] = by_category.get(cat_name, 0) + exp.amount

for cat, total in by_category.items():
    print(f"{cat}: {total:.2f}")

Best Practices

1. Always Set Required Fields

# Good: All required fields provided
expense_id = get_model("expense").create({
    "merchant": "Amazon",
    "amount": 99.00,
    "date": "2024-12-15",
    "employee_id": employee_id,
    "currency_id": currency_id
})

# Bad: Missing required fields (will fail)
expense_id = get_model("expense").create({
    "merchant": "Amazon"
})  # Error: Missing amount, date

2. Use Categories for Reporting

# Good: Categorize expenses for better reporting
expense_id = get_model("expense").create({
    "merchant": "Grab",
    "amount": 15.00,
    "date": "2024-12-15",
    "categ_id": transport_categ_id,  # Enables category-based reports
    "description": "Taxi to airport"
})

3. Attach Receipts

# Good: Always attach receipt for audit trail
expense_id = get_model("expense").create({
    "merchant": "Hotel ABC",
    "amount": 250.00,
    "date": "2024-12-15",
    "image": receipt_file_path  # Receipt for verification
})

Troubleshooting

"Missing user"

Cause: Employee ID is not set when submitting for approval Solution: Ensure employee_id is populated before calling submit_for_approval

"Missing approval workflow"

Cause: No approval workflow configured for the employee, company, or amount threshold Solution: Configure approval workflow in approve.wkf model matching the expense criteria

"User does not have permission to approve"

Cause: Current user is not an assigned approver for this expense Solution: Only users with pending approval records can approve/reject

"Invalid expense status"

Cause: Attempting to approve/reject an expense not in wait_approve state Solution: Only expenses in wait_approve state can be approved or rejected


Testing Examples

Unit Test: Expense Approval Flow

def test_expense_approval_flow():
    # 1. Create expense
    expense_id = get_model("expense").create({
        "merchant": "Test Vendor",
        "amount": 100.00,
        "date": time.strftime("%Y-%m-%d"),
    })

    # Verify draft state
    exp = get_model("expense").browse([expense_id])[0]
    assert exp.state == "draft"

    # 2. Submit for approval
    get_model("expense").submit_for_approval([expense_id])
    exp = get_model("expense").browse([expense_id])[0]
    assert exp.state == "wait_approve"

    # 3. Approve (as authorized user)
    get_model("expense").approve([expense_id])
    exp = get_model("expense").browse([expense_id])[0]
    assert exp.state == "approved"

Security Considerations

Permission Model

  • Users can typically only create/view their own expenses
  • Approvers can view and approve expenses in their approval queue
  • Admin/Accounting can view all expenses

Data Access

  • employee_id defaults to current user's employee record
  • Multi-company filtering via company_id
  • Approval records track who approved/rejected

Workflow Integration

Trigger Events

The expense model fires workflow triggers:

self.trigger("submit_approve")  # When submitted for approval

These can be configured in workflow automation for notifications.


Version History

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


Additional Resources

  • Expense Category Documentation: expense.categ
  • Expense Claim Documentation: expense.claim
  • Account Expense Documentation: account.expense
  • Approval Workflow Documentation: approve.wkf

This documentation is generated for developer onboarding and reference purposes.