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¶
| 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:
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:
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:
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:
6. View Journal Entry¶
Method: view_journal_entry(ids, context)
Returns navigation to view the claim's journal entry.
Returns:
Example:
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:
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:
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¶
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"]
])
Related Models¶
| 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.