Expense Reports Are Where Finance Automation Goes to Die
Every company has the same story. Sales reps collect paper receipts in their wallets for three weeks, then dump a stack of crumpled paper on the finance team's desk the day before month-end close. The finance team manually enters each line, chases managers for approvals via email, and then re-keys everything into the accounting system. The process takes days, errors are constant, and nobody is happy.
Odoo 19's Expenses module changes this completely. Receipts are scanned with OCR that extracts amounts, dates, and vendors automatically. Approval workflows route expense reports to the right manager based on department and amount thresholds. Approved expenses post journal entries directly into your chart of accounts—no manual re-entry. And billable expenses can be re-invoiced to customers with a single click.
This guide walks through every layer of expense automation in Odoo 19: how to configure expense categories, how OCR receipt scanning works, how to build multi-level approval workflows, how journal entries are generated, how to re-invoice expenses to customers, and how employees submit expenses from their phones.
Configuring Expense Categories and Products for Clean Reporting
Before any receipt hits the system, you need a well-structured set of expense categories. In Odoo, every expense line is linked to an expense product—a special product type that maps to a specific GL account. Getting this structure right determines whether your P&L breakdown is useful or whether everything lands in a generic "Employee Expenses" bucket.
Creating Expense Products
Navigate to Expenses > Configuration > Expense Products. Each product represents a category of spend:
| Expense Product | GL Account | Tax | Re-Invoice Policy |
|---|---|---|---|
| Hotel Accommodation | 620100 - Travel & Lodging | VAT 20% (Purchases) | At cost |
| Flight Tickets | 620100 - Travel & Lodging | No VAT | At cost |
| Client Meals | 625000 - Entertainment | VAT 20% (Purchases) | At sales price |
| Mileage | 625100 - Mileage Reimbursement | No VAT | No |
| Office Supplies | 606000 - Office Supplies | VAT 20% (Purchases) | No |
# Creating expense products via code (e.g., data migration)
expense_product = env['product.product'].create({
'name': 'Hotel Accommodation',
'type': 'service',
'can_be_expensed': True,
'property_account_expense_id': env.ref(
'l10n_generic_coa.1_a620100'
).id,
'supplier_taxes_id': [(6, 0, [tax_20_purchase.id])],
'expense_policy': 'cost', # 'cost' or 'sales_price'
'default_code': 'EXP-HOTEL',
'list_price': 0.0,
'standard_price': 0.0,
}) Create separate expense products even when two categories share the same GL account. "Hotel" and "Flight" may both hit 620100, but having them as distinct products gives you pivot-ready reporting—you can see travel spend broken down by type, not just as a single line on the trial balance.
Setting Up Mileage Allowances
Mileage expenses require a special configuration. Instead of a receipt amount, employees enter the number of kilometers driven, and Odoo calculates the reimbursement using a per-km rate:
<odoo>
<record id="expense_product_mileage" model="product.product">
<field name="name">Mileage Reimbursement</field>
<field name="type">service</field>
<field name="can_be_expensed">True</field>
<field name="uom_id" ref="uom.product_uom_km"/>
<field name="uom_po_id" ref="uom.product_uom_km"/>
<field name="standard_price">0.67</field>
<!-- EUR 0.67/km - adjust per local tax authority rates -->
<field name="property_account_expense_id"
ref="l10n_generic_coa.1_a625100"/>
</record>
</odoo>OCR Receipt Scanning: How Odoo 19 Reads Your Receipts Automatically
Odoo 19 includes built-in OCR powered by its IAP (In-App Purchase) OCR service. When an employee uploads a photo or PDF of a receipt, the system extracts the vendor name, date, total amount, and currency—then pre-fills the expense form. No manual data entry required.
How the OCR Pipeline Works
The extraction follows a specific sequence:
- Upload — Employee attaches a receipt image (JPEG, PNG) or PDF to a new expense line. This can happen from the web UI, email alias, or mobile app.
- IAP Request — Odoo sends the image to the Odoo IAP OCR endpoint. Each scan consumes one IAP credit (approximately €0.08 per scan).
- Field Extraction — The OCR engine returns structured data:
vendor,date,total,currency, anddescription. - Auto-Fill — Odoo maps the extracted data onto the expense form fields. The employee reviews and corrects if needed before saving.
Enabling OCR for Expenses
# Navigate to: Settings > Expenses
# Enable: "Digitization" (under the Expenses section)
#
# Options:
# - "Do not digitize" : OCR disabled
# - "Digitize on demand" : Employee clicks a button to trigger OCR
# - "Digitize automatically" : OCR runs on every attachment upload
#
# For most companies, "Digitize automatically" is the right choice.
# Each scan costs ~EUR 0.08 via Odoo IAP credits.
# To check your IAP credit balance:
# Settings > In-App Purchases > View My ServicesEmail Alias for Receipt Submission
Employees can forward receipts to an email alias (e.g., expenses@yourcompany.odoo.com), and Odoo creates an expense line automatically. Configure this under Expenses > Configuration > Settings > Incoming Emails:
# The expense email alias creates hr.expense records
# from incoming emails. The attachment triggers OCR.
#
# Matching logic:
# 1. Sender email > matched to employee's work email
# 2. Attachment > sent to OCR for field extraction
# 3. Subject line > used as expense description fallback
#
# If the sender email doesn't match any employee,
# the expense is created but assigned to the catch-all
# user defined in Settings > Expenses > Default Employee.OCR accuracy depends on image quality. Train your team to photograph receipts on a flat, dark surface with good lighting. Crumpled receipts photographed at an angle inside a wallet produce extraction rates below 60%. Flat, well-lit receipts hit 90%+ accuracy on amount, date, and vendor extraction.
Building Multi-Level Expense Approval Workflows in Odoo 19
Small companies can get by with a single manager approval. But once you have multiple departments, spending tiers, or compliance requirements, you need a multi-level approval workflow. Odoo 19 supports this through a combination of expense report approval rules, department-based routing, and activity-based escalation.
The Default Approval Flow
Out of the box, Odoo routes expense reports through this sequence:
| Stage | Status | Who Acts | What Happens Next |
|---|---|---|---|
| 1. Draft | draft | Employee | Employee adds lines, attaches receipts, submits |
| 2. Submitted | reported | Manager | Manager approves or refuses the report |
| 3. Approved | approved | Finance / Accountant | Post journal entries, schedule payment |
| 4. Posted | done | System | Journal entry created, payment registered |
Adding Amount-Based Approval Tiers
For companies that require director-level approval above a certain threshold, you can extend the expense model with a custom approval step:
from odoo import api, fields, models
class HrExpenseSheet(models.Model):
_inherit = "hr.expense.sheet"
x_requires_director_approval = fields.Boolean(
string="Requires Director Approval",
compute="_compute_requires_director",
store=True,
)
x_director_approved = fields.Boolean(
string="Director Approved",
tracking=True,
)
x_director_id = fields.Many2one(
"res.users",
string="Approving Director",
tracking=True,
)
@api.depends("total_amount")
def _compute_requires_director(self):
threshold = float(
self.env["ir.config_parameter"].sudo().get_param(
"expense.director_approval_threshold", "5000.0"
)
)
for sheet in self:
sheet.x_requires_director_approval = (
sheet.total_amount > threshold
)
def action_approve_director(self):
"""Called by the director approval button."""
self.ensure_one()
self.write({
"x_director_approved": True,
"x_director_id": self.env.uid,
})
# Trigger the standard manager approval next
self.action_approve_expense_sheets()Department-Based Routing with Activities
def action_submit_sheet(self):
"""Override submit to route to the correct approver."""
res = super().action_submit_sheet()
for sheet in self:
approver = sheet.employee_id.department_id.manager_id.user_id
if not approver:
approver = sheet.employee_id.parent_id.user_id
if approver:
sheet.activity_schedule(
"hr_expense.mail_act_expense_approval",
user_id=approver.id,
note=f"Expense report {{sheet.name}} "
f"({{sheet.total_amount}} "
f"{{sheet.currency_id.symbol}}) "
f"requires your approval.",
)
return res Every approval action in Odoo is logged in the chatter with timestamps and user IDs. For SOX or ISO compliance, this built-in audit trail is usually sufficient. If you need additional controls, add tracking=True to your custom approval fields—every change is recorded as a tracked value in mail.tracking.value.
How Approved Expenses Become Journal Entries in Odoo 19
This is where the real automation happens. When a finance user clicks "Post Journal Entries" on an approved expense report, Odoo generates a complete accounting entry—debiting the expense accounts and crediting the payable account. No manual journal entry required.
The Generated Journal Entry Structure
| Account | Label | Debit | Credit |
|---|---|---|---|
620100 Travel & Lodging | Hotel - Paris client visit | 350.00 | |
625000 Entertainment | Client dinner - Acme Corp | 120.00 | |
445660 VAT Deductible | Input VAT on expenses | 94.00 | |
421000 Employee Payable | John Smith - Expense Report March | 564.00 |
Configuring the Expense Journal
# Navigate to: Accounting > Configuration > Journals
#
# Create or configure a journal for expenses:
# Name: "Employee Expenses"
# Type: Purchase
# Short Code: EXP
# Default Account: 421000 (Employee Payable)
#
# Then link it in: Settings > Expenses > Journal
#
# Important: The credit side (employee payable) uses the
# journal's default account. The debit side uses the
# account from each expense product's
# "property_account_expense_id" field.Payment Registration
After the journal entry is posted, the employee has an open payable balance. To reimburse them, register a payment just like paying a vendor bill:
# After posting, the expense sheet creates an account.move.
# To register payment programmatically:
for sheet in expense_sheets.filtered(lambda s: s.state == 'done'):
move = sheet.account_move_id
payment_vals = {
'payment_type': 'outbound',
'partner_type': 'supplier',
'partner_id': sheet.employee_id.address_home_id.id,
'amount': sheet.total_amount,
'journal_id': bank_journal.id,
'ref': f"Expense reimbursement: {{sheet.name}}",
}
payment = env['account.payment'].create(payment_vals)
payment.action_post()
# Reconcile with the expense journal entry
(move.line_ids + payment.move_id.line_ids).filtered(
lambda l: l.account_id.account_type == 'liability_payable'
).reconcile() If the expense was paid with a company credit card, the accounting flow changes. Instead of crediting Employee Payable (421000), Odoo credits the company's credit card liability account. Enable this by setting "Paid By" = "Company" on the expense line. The journal entry skips the employee payable entirely—the employee doesn't need reimbursement because the company already paid.
Re-Invoicing Expenses to Customers: Billing Travel and Project Costs
Consulting firms, agencies, and professional services companies regularly bill travel and project expenses back to clients. Odoo handles this through the re-invoice policy on expense products, linked to analytic accounts tied to sales orders.
The Re-Invoicing Flow
- Step 1: Set the expense product's Re-Invoice Policy to "At cost" or "At sales price."
- Step 2: When creating the expense, the employee selects the Analytic Account linked to the customer's sales order.
- Step 3: After the expense report is approved and posted, the expense lines appear as "Expenses to Re-Invoice" on the linked sales order.
- Step 4: When you create the next invoice from the sales order, these expense lines are automatically included.
# Employee creates an expense linked to a customer project
expense = env['hr.expense'].create({
'name': 'Flight to Paris - Acme Corp project',
'product_id': flight_product.id,
'total_amount_currency': 450.00,
'employee_id': employee.id,
'analytic_distribution': {
str(acme_analytic_account.id): 100.0,
},
# The analytic account must be linked to a sales order
# for re-invoicing to work.
})
# After approval + posting, check the sales order:
# sale_order.expense_ids will include this expense
# sale_order._create_invoices() will add it to the invoice"At cost" re-invoices the exact amount the employee spent. "At sales price" re-invoices using the product's list price—useful for mileage where you charge clients a premium rate (e.g., €0.90/km) while reimbursing employees at the government rate (€0.67/km). The margin goes to your bottom line.
Mobile Expense Submission: From Receipt Photo to Posted Entry in 60 Seconds
The mobile experience is where expense automation delivers the most visible ROI. Employees no longer collect receipts for weeks—they photograph each receipt immediately after the transaction. The Odoo mobile app (iOS and Android) provides a streamlined expense submission flow:
- Open the Odoo app — Navigate to Expenses and tap the "Scan" button.
- Photograph the receipt — The app uses the phone's camera. OCR processes the image immediately.
- Review pre-filled fields — Amount, date, vendor, and currency are auto-populated. The employee selects the expense category and adds notes.
- Submit — The expense is added to a draft expense report. The employee can submit the report immediately or batch multiple expenses.
- Manager notification — The manager receives a push notification and can approve directly from the mobile app.
# Recommended mobile-friendly settings:
#
# 1. Expenses > Settings > Digitization = "Digitize automatically"
# (Employees don't need to trigger OCR manually)
#
# 2. Set default expense products per department:
# HR > Departments > [Dept] > Expense Products
# This reduces the number of taps on mobile.
#
# 3. Enable push notifications for approvers:
# Settings > Discuss > Push Notifications = Enabled
# Managers get instant alerts for pending approvals.
#
# 4. Consider allowing "single expense" submission
# (vs. grouped reports) for real-time processing:
# Settings > Expenses > Expense Report Grouping = "None"The Odoo mobile app supports offline receipt capture. If the employee is in a location without connectivity (e.g., underground parking, airplane), the photo is stored locally and synced when the device reconnects. The OCR processing happens server-side after sync. Train your team to photograph receipts immediately—even without signal.
3 Expense Automation Mistakes That Create Accounting Headaches
Not Configuring Employee Home Addresses as Partners
The expense journal entry credits Employee Payable, which requires a partner record for the employee. If the employee's address_home_id (private address) is not set, the journal entry either fails to post or posts without a partner—making reconciliation impossible. You'll discover this when the payment wizard says "no matching payable lines found" and your finance team spends an hour debugging.
Before going live with expense automation, run a data quality check: env['hr.employee'].search([('address_home_id', '=', False)]). Every employee who submits expenses needs a private address with a valid partner record. Add this as a mandatory field in your HR onboarding checklist.
Analytic Account Mismatch Breaks Re-Invoicing Silently
An employee selects the wrong analytic account on an expense. The expense is approved, posted, and the journal entry is correct. But the expense never appears on the sales order for re-invoicing because the analytic account doesn't match any active SO line. The finance team discovers the gap weeks later during a client billing review—after the invoice has already been sent without the expense lines.
Restrict the analytic account selection on expenses using a domain filter. Create a scheduled action that runs weekly to flag posted expenses where the analytic account exists but no matching sales order line is found. The SQL check is simple: join hr_expense on sale_order_line via the analytic distribution and report any orphans.
Multi-Currency Expenses Without Rate Lock-In
An employee travels to the US and incurs expenses in USD. They submit the expense two weeks later. By default, Odoo converts the USD amount to the company currency using today's exchange rate—not the rate on the date of the expense. If the rate moved significantly, the reimbursement amount doesn't match what the employee actually paid, and the journal entry records the wrong cost.
Always populate the expense date field with the actual transaction date, not the submission date. Odoo uses the expense date to fetch the exchange rate from res.currency.rate. If you use an automatic rate feed (ECB, Banxico, etc.), ensure rates are fetched daily so the historical rate is available. For large multi-currency volumes, consider adding a x_locked_rate field that captures the rate at submission time.
What Expense Automation Saves Your Business
Expense automation isn't about making life easier for employees (though it does). It's about recovering finance team hours and closing the books faster:
OCR plus automatic journal entries eliminate manual data entry. A report that took 15 minutes to process now takes under 4 minutes—including the approval click.
No more chasing employees for missing receipts at close. Real-time submission means expenses are posted throughout the month, not batched at the end.
Every expense has an attached receipt image stored in Odoo. Audit-ready documentation without filing cabinets or shared drives of scanned PDFs.
For a 200-person company where 50 employees submit monthly expense reports averaging 8 lines each, automation saves approximately 65 finance team hours per month. At a fully loaded cost of €45/hour, that's €2,925/month or €35,100 per year—from a module that's included in your Odoo Enterprise license.
Optimization Metadata
Complete guide to automating expense reports in Odoo 19. Configure OCR receipt scanning, build multi-level approval workflows, and automate accounting journal entries.
1. "Configuring Expense Categories and Products for Clean Reporting"
2. "OCR Receipt Scanning: How Odoo 19 Reads Your Receipts Automatically"
3. "Building Multi-Level Expense Approval Workflows in Odoo 19"
4. "How Approved Expenses Become Journal Entries in Odoo 19"
5. "3 Expense Automation Mistakes That Create Accounting Headaches"