Skip to content

Expense Claim Documentation

Overview

The Expense Claim model (expense.claim) manages expense reimbursement requests from employees. It aggregates multiple individual expenses, supports approval workflows, creates journal entries for accounting, and tracks payment status.


Model Information

Model Name: expense.claim Display Name: Expense Claim Name Field: number Key Fields: None (no unique constraint defined)

Features

  • ❌ Audit logging enabled (_audit_log)
  • ✅ Multi-company support (company_id)
  • ❌ Full-text content search (_content_search)
  • ✅ Automatic sequence numbering
  • ✅ Journal entry creation on approval
  • ✅ Tax calculation support
  • ✅ Cash advance tracking
  • ✅ Payment tracking

State Workflow

draft → waiting_approval → approved → paid
                        declined
State Code Description
Draft draft Initial state, can be edited
Waiting Approval waiting_approval Submitted for review
Approved approved Claim approved, journal entry created
Paid paid Reimbursement paid to employee
Declined declined Claim rejected

Note: State transitions between approved and paid are computed based on amount_due.


Key Fields Reference

Header Fields

Field Type Required Description
number Char Claim number (auto-generated)
employee_id Many2One Employee submitting claim
currency_id Many2One Currency for the claim
tax_type Selection Tax calculation method
state Selection Workflow state (computed/stored)

Optional Fields

Field Type Description
name Char Expense claim name/title
date_from Date Period start date
date_to Date Period end date
ref Char External reference
description Text Description/notes
project_id Many2One Related project
account_id Many2One Payment account for posting

Tax Types

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

Computed Fields

Field Type Description
amount_total Decimal Sum of all expense amounts
num_expenses Integer Count of expenses in claim
cash_remain Decimal Cash advance minus total expenses

Cash Advance Fields

Field Type Description
cash_advance Decimal Amount advanced to employee
cash_remain Decimal Remaining cash (advance - expenses)

Relationship Fields

Field Type Description
expenses One2Many Individual expense records
documents One2Many Attached documents
comments One2Many Comments/messages
payment_lines One2Many Payment allocation lines
move_id Many2One Generated journal entry
company_id Many2One Company
related_id Reference Related project or service order

Deprecated Fields

Field Type Description
contact_id Many2One ~~Supplier~~ (deprecated)

Default Values

_defaults = {
    "number": _get_number,                    # Auto-generated sequence
    "date": lambda *a: time.strftime("%Y-%m-%d"),  # Today
    "employee_id": _get_employee,             # Current user's employee
    "currency_id": _get_currency,             # Company currency
    "tax_type": "tax_in",                     # Default: Tax inclusive
    "state": "draft",                         # Initial state
    "company_id": lambda *a: get_active_company(),  # Current company
}

API Methods

1. Create Expense Claim

Method: create(vals, context)

Creates a new expense claim.

Parameters:

vals = {
    "name": "December Travel Expenses",     # Optional: Claim name
    "employee_id": employee_id,             # Required: Employee
    "currency_id": currency_id,             # Required: Currency
    "tax_type": "tax_in",                   # Required: Tax type
    "date_from": "2024-12-01",              # Optional: Period start
    "date_to": "2024-12-31",                # Optional: Period end
    "account_id": account_id,               # Required for approval: Payment account
    "expenses": [                           # Optional: Expense records
        ("create", {...expense_vals...})
    ]
}

Returns: int - New record ID

Example:

claim_id = get_model("expense.claim").create({
    "name": "Q4 Business Travel",
    "date_from": "2024-10-01",
    "date_to": "2024-12-31",
    "account_id": petty_cash_account_id,
})


2. Submit for Approval

Method: do_submit(ids, context)

Submits the expense claim for approval.

Behavior: - Changes state from draft to waiting_approval

Example:

get_model("expense.claim").do_submit([claim_id])


3. Approve Claim

Method: do_approve(ids, context)

Approves the expense claim and creates journal entry.

Behavior: 1. Validates claim has expenses 2. Gets purchase journal from settings 3. Creates journal entry with: - Debit lines for each expense (to expense accounts) - Debit lines for tax components - Credit line to payment account 4. Posts the journal entry 5. Updates state to approved

Prerequisites: - Claim must have at least one expense - Each expense must have an account_id - Claim must have account_id (payment account) - Purchase journal must be configured in settings

Example:

get_model("expense.claim").do_approve([claim_id])

Raises: - Exception("Expense claim is empty") - No expenses - Exception("Purchases journal not found") - Missing journal config - Exception("Missing line account") - Expense without account - Exception("Missing payment account") - Claim without account_id


4. Decline Claim

Method: do_decline(ids, context)

Declines/rejects the expense claim.

Behavior: - Changes state to declined

Example:

get_model("expense.claim").do_decline([claim_id])


5. Return to Draft

Method: do_to_draft(ids, context)

Reverts an approved claim back to draft.

Behavior: - Voids the journal entry - Deletes the journal entry - Changes state to draft

Example:

get_model("expense.claim").do_to_draft([claim_id])


6. View Journal Entry

Method: view_journal_entry(ids, context)

Returns navigation to view the claim's journal entry.

Returns:

{
    "next": {
        "name": "journal_entry",
        "mode": "form",
        "active_id": move_id,
    }
}

Example:

result = get_model("expense.claim").view_journal_entry([claim_id])
# Navigate to journal entry view


7. Get Amount

Method: get_amount(ids, context)

Calculates total amount from expenses.

Returns: Dict with amount_total

Example:

amounts = get_model("expense.claim").get_amount([claim_id])
total = amounts[claim_id]["amount_total"]


8. Get Number of Expenses

Method: get_num_expenses(ids, context)

Returns count of expenses in claim.

Example:

counts = get_model("expense.claim").get_num_expenses([claim_id])
num = counts[claim_id]  # e.g., 5


9. Get State (Computed)

Method: get_state(ids, context)

Computes state based on payment status.

Logic: - If approved and amount_due == 0: Returns paid - If paid and amount_due > 0: Returns approved - Otherwise: Returns stored state


10. Get Cash Remaining

Method: get_cash_remain(ids, context)

Calculates remaining cash from advance.

Formula:

cash_remain = (cash_advance or 0) - amount_total


UI Events (onchange methods)

onchange_account

Triggered when account is selected on expense line. Updates tax rate based on account's default.


Search Functions

Search by Employee

claims = get_model("expense.claim").search([["employee_id", "=", employee_id]])

Search by State

# Pending approval
pending = get_model("expense.claim").search([["state", "=", "waiting_approval"]])

# Approved but unpaid
unpaid = get_model("expense.claim").search([["state", "=", "approved"]])

Search by Date Range

claims = get_model("expense.claim").search([
    ["date_from", ">=", "2024-01-01"],
    ["date_to", "<=", "2024-12-31"]
])

Model Relationship Description
expense One2Many (expenses) Individual expense records
hr.employee Many2One (employee_id) Employee submitting claim
currency Many2One (currency_id) Claim currency
account.account Many2One (account_id) Payment account
account.move Many2One (move_id) Generated journal entry
account.payment.line One2Many (payment_lines) Payment allocations
company Many2One (company_id) Company
project Many2One (project_id) Related project
document One2Many (documents) Attached documents
message One2Many (comments) Comments

Common Use Cases

Use Case 1: Create and Submit Expense Claim

# 1. Create claim
claim_id = get_model("expense.claim").create({
    "name": "December Business Expenses",
    "date_from": "2024-12-01",
    "date_to": "2024-12-31",
    "account_id": petty_cash_account_id,
})

# 2. Add expenses (assuming they exist)
expense_ids = get_model("expense").search([
    ["employee_id", "=", employee_id],
    ["claim_id", "=", None],
    ["state", "=", "draft"]
])
get_model("expense").write(expense_ids, {"claim_id": claim_id})

# 3. Submit for approval
get_model("expense.claim").do_submit([claim_id])

Use Case 2: Approve Claim with Cash Advance

# Record cash advance given to employee
get_model("expense.claim").write([claim_id], {
    "cash_advance": 500.00
})

# After employee submits expenses...
claim = get_model("expense.claim").browse([claim_id])[0]
print(f"Total expenses: {claim.amount_total}")
print(f"Cash advance: {claim.cash_advance}")
print(f"Cash remaining: {claim.cash_remain}")

# If cash_remain is positive, employee returns money
# If cash_remain is negative, company owes employee

# Approve the claim
get_model("expense.claim").do_approve([claim_id])

Use Case 3: View Claim Journal Entry

# Get the journal entry for an approved claim
claim = get_model("expense.claim").browse([claim_id])[0]

if claim.move_id:
    move = claim.move_id
    print(f"Journal Entry: {move.number}")
    print(f"Date: {move.date}")
    for line in move.lines:
        print(f"  {line.account_id.name}: Dr {line.debit} Cr {line.credit}")

Use Case 4: Monthly Expense Report

# Get all approved claims for a month
claims = get_model("expense.claim").search_browse([
    ["state", "in", ["approved", "paid"]],
    ["date_from", ">=", "2024-12-01"],
    ["date_to", "<=", "2024-12-31"]
])

report = {}
for claim in claims:
    emp_name = claim.employee_id.name
    report[emp_name] = report.get(emp_name, 0) + claim.amount_total

print("December Expense Claims by Employee:")
for emp, total in sorted(report.items(), key=lambda x: -x[1]):
    print(f"  {emp}: {total:,.2f}")

Journal Entry Structure

When a claim is approved, a journal entry is created:

Date: [claim date]
Reference: [claim ref]
Narration: "Expense claim [number]"

Debit Lines:
  - Expense Account 1: [base_amount_1]
  - Expense Account 2: [base_amount_2]
  - Tax Account (if applicable): [tax_amount]

Credit Line:
  - Payment Account: [total_amount]

Best Practices

1. Set Payment Account Before Approval

# Good: Payment account set
claim = get_model("expense.claim").create({
    "name": "Travel Expenses",
    "account_id": petty_cash_account_id,  # Required for approval
})

# Bad: No payment account (approval will fail)
claim = get_model("expense.claim").create({
    "name": "Travel Expenses",
    # Missing account_id
})

2. Ensure All Expenses Have Accounts

# Before approval, verify all expenses have accounts
claim = get_model("expense.claim").browse([claim_id])[0]
for exp in claim.expenses:
    if not exp.account_id:
        raise Exception(f"Expense '{exp.description}' missing account")

3. Use Date Ranges for Organization

# Good: Clear period definition
claim = get_model("expense.claim").create({
    "name": "Q4 2024 Expenses",
    "date_from": "2024-10-01",
    "date_to": "2024-12-31",
})

# Bad: No date range (harder to track)
claim = get_model("expense.claim").create({
    "name": "Expenses",
})

Troubleshooting

"Expense claim is empty"

Cause: Trying to approve claim with no expenses Solution: Add expenses to the claim before approving

"Purchases journal not found"

Cause: Purchase journal not configured in settings Solution: Configure purchase_journal_id in Settings

"Missing line account"

Cause: One or more expenses don't have account_id set Solution: Set account_id on all expenses before approval

"Missing payment account"

Cause: Claim doesn't have account_id set Solution: Set account_id (payment account) on the claim

"Cannot return to draft after payment"

Cause: Trying to revert a claim that has payments Solution: Reverse payments first, then revert to draft


Testing Examples

Unit Test: Claim Approval Flow

def test_expense_claim_approval():
    # Create claim with expenses
    claim_id = get_model("expense.claim").create({
        "name": "Test Claim",
        "account_id": petty_cash_id,
        "expenses": [
            ("create", {
                "merchant": "Vendor A",
                "amount": 100.00,
                "date": "2024-12-15",
                "account_id": expense_account_id,
            })
        ]
    })

    # Submit
    get_model("expense.claim").do_submit([claim_id])
    claim = get_model("expense.claim").browse([claim_id])[0]
    assert claim.state == "waiting_approval"

    # Approve
    get_model("expense.claim").do_approve([claim_id])
    claim = get_model("expense.claim").browse([claim_id])[0]
    assert claim.state == "approved"
    assert claim.move_id is not None

    # Verify journal entry
    assert claim.move_id.state == "posted"

Security Considerations

Permission Model

  • Employees can create/view their own claims
  • Managers can approve claims for their team
  • Finance can view/process all claims

Data Access

  • Multi-company filtering via company_id
  • Employee filtering via employee_id

Configuration Requirements

Required Settings

Setting Location Description
purchase_journal_id Settings Journal for posting expense claims

Sequence Configuration

A sequence of type expense should be configured for automatic claim numbering.


Version History

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


Additional Resources

  • Expense Documentation: expense
  • Journal Entry Documentation: account.move
  • Payment Documentation: account.payment
  • Employee Documentation: hr.employee

This documentation is generated for developer onboarding and reference purposes.