GuideOdoo AccountingMarch 13, 2026

Automating Expense Reports in Odoo 19:
OCR, Approval Workflows & Accounting

INTRODUCTION

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.

01

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 ProductGL AccountTaxRe-Invoice Policy
Hotel Accommodation620100 - Travel & LodgingVAT 20% (Purchases)At cost
Flight Tickets620100 - Travel & LodgingNo VATAt cost
Client Meals625000 - EntertainmentVAT 20% (Purchases)At sales price
Mileage625100 - Mileage ReimbursementNo VATNo
Office Supplies606000 - Office SuppliesVAT 20% (Purchases)No
Python — Expense product with default account
# 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,
})
Structure Matters

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:

XML — Mileage product data record
<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>
02

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, and description.
  • 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

Settings — Expenses OCR configuration
# 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 Services

Email 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:

Python — Email alias configuration
# 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 Tip

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.

03

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:

StageStatusWho ActsWhat Happens Next
1. DraftdraftEmployeeEmployee adds lines, attaches receipts, submits
2. SubmittedreportedManagerManager approves or refuses the report
3. ApprovedapprovedFinance / AccountantPost journal entries, schedule payment
4. PosteddoneSystemJournal 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:

Python — models/hr_expense_sheet.py
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

Python — Auto-assign approval 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
Audit Trail

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.

04

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

AccountLabelDebitCredit
620100 Travel & LodgingHotel - Paris client visit350.00
625000 EntertainmentClient dinner - Acme Corp120.00
445660 VAT DeductibleInput VAT on expenses94.00
421000 Employee PayableJohn Smith - Expense Report March564.00

Configuring the Expense Journal

Settings — Expense journal configuration
# 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:

Python — Programmatic payment registration
# 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()
Company Card Expenses

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.

05

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.
Python — Expense with analytic for re-invoicing
# 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 vs. Sales Price

"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.

06

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.
Configuration — Mobile-optimized setup
# 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"
Offline Receipts

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.

07

3 Expense Automation Mistakes That Create Accounting Headaches

1

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.

Our Fix

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.

2

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.

Our Fix

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.

3

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.

Our Fix

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.

BUSINESS ROI

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:

75%Less Processing Time

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.

2 daysFaster Month-End Close

No more chasing employees for missing receipts at close. Real-time submission means expenses are posted throughout the month, not batched at the end.

100%Receipt Compliance

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.

SEO NOTES

Optimization Metadata

Meta Desc

Complete guide to automating expense reports in Odoo 19. Configure OCR receipt scanning, build multi-level approval workflows, and automate accounting journal entries.

H2 Keywords

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"

Your Expense Process Should Run Itself

A properly automated expense workflow does more than save time. It gives your finance team accurate, real-time cost data instead of month-end surprises. It ensures every expense has a receipt, an approval, and a clean journal entry. It re-invoices billable costs to customers without anyone remembering to do it manually. And it lets employees submit expenses in 60 seconds from their phone instead of dreading the monthly paper chase.

If your team is still emailing spreadsheets and scanning receipts into shared folders, we should talk. We implement end-to-end expense automation in Odoo 19—from OCR configuration and approval workflows to accounting integration and re-invoicing setup. Most implementations take 3-5 days and pay for themselves within the first quarter.

Book a Free Expense Automation Audit