Skip to content
Finance · Expense management module

An expense management system that checks the policy so finance doesn't have to

Your expense policy is probably a PDF that new starters skim once. Finance then enforces it by hand, one receipt at a time, usually at month end. An expense management system inside Odoo or ERPNext can check caps, mileage and receipts when the claim is entered, so the awkward conversations happen less often and earlier.

ERPNext v16

Works with ERPNext v15 and v16, and Odoo 17, 18 and 19.

Features

What your expense management module can do

Caps per category, per day

Meals, hotels, client entertainment. Each category can carry its own daily or per-trip cap, and the error tells the employee how much headroom was left.

Mileage without the arithmetic

Employees enter distance and purpose. The rate comes from your settings, not from whatever the employee remembers from last year.

Receipt rules with a threshold

Small claims can go through on a description. Above your threshold, a receipt is required before the claim moves on.

Approvals that follow the org chart

Line manager first, then finance for anything unusual. Delegation covers holidays so claims don't sit for two weeks.

Out-of-policy reporting

A list of claims that were approved above policy, and by whom. Useful once a quarter, and slightly uncomfortable.

The output

What actually gets generated

Real files in a real repository. Here’s the typical output when someone asks for expense management.

ERPNext / Frappe app

  • Daily cap field on Expense Claim Type (fixture)
  • Expense Claim validate hook for caps and receipts
  • Mileage Log child table on Expense Claim
  • Workflow with department approver and finance step
  • Out of Policy Claims Script Report

Odoo addon

  • Daily cap field on expense products (product.template)
  • hr.expense constraint for per-day caps
  • Mileage rate setting on res.company
  • Approval activities for over-threshold claims
  • List view filter for out-of-policy expenses
expense_policy/models/hr_expense.py
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError


class ProductTemplate(models.Model):
    _inherit = "product.template"

    expense_daily_cap = fields.Float(string="Daily cap per employee", help="0 means no cap")


class HrExpense(models.Model):
    _inherit = "hr.expense"

    @api.constrains("total_amount", "product_id", "date", "employee_id")
    def _check_daily_cap(self):
        for expense in self:
            cap = expense.product_id.expense_daily_cap
            if not cap or not expense.date:
                continue
            same_day = self.search([
                ("employee_id", "=", expense.employee_id.id),
                ("product_id", "=", expense.product_id.id),
                ("date", "=", expense.date),
                ("state", "!=", "refused"),
            ])
            spent = sum(same_day.mapped("total_amount"))
            if spent > cap:
                earlier = spent - expense.total_amount
                raise ValidationError(_(
                    "%(category)s is capped at %(cap)s a day and %(earlier)s is already claimed for %(date)s.",
                    category=expense.product_id.name, cap=cap, earlier=earlier, date=expense.date,
                ))
Trimmed excerpt. The full module includes tests, fixtures and a README.

How it works

From a paragraph to a pull request

The long version
  1. 01

    Describe it

    In your own words. Paste the spreadsheet or a photo of the paper form if that's easier.

  2. 02

    Answer a couple of questions

    It asks only what it can't work out from your setup, like who's allowed to override.

  3. 03

    Try it on a sandbox

    A copy of your site with the module installed. Break it, then ask for changes.

  4. 04

    Merge when it's right

    Code lands as a pull request with tests. Your developer, or ours, reviews it first.

Expense policies live in a PDF on the shared drive

Almost every business has an expense policy. Very few have one that’s enforced where the claim is made. The usual setup is a document on a shared drive, a manager who approves on trust, and a finance person who spots the $112 dinner three weeks later and has to decide whether it’s worth an argument.

The timing is backwards. The employee knows the facts at the moment they enter the claim. The system knows the policy. Checking one against the other then is cheaper for everyone than an email thread at month end. It’s also fairer, because the rule applies the same way to the new starter and the sales director. Finance stops being the department that says no after the money is spent, which does wonders for how often people answer their emails.

Policy checks inside the expense management system

On Odoo, expense categories are products flagged as expensable, and each claim line is an hr.expense. The addon adds caps and rules to those categories and enforces them with constraints, so they apply whether the claim comes from the web, the mobile app or an import. Mileage rates sit in company settings, so finance can change them without a developer.

On ERPNext, Expense Claim and Expense Claim Type come from the Frappe HR app. The generated app adds a cap on Expense Claim Type, a validate hook on Expense Claim and, if you want it, a Workflow that routes claims to the department approver and then to finance above a limit.

The difference between _inherit on an existing model and creating a new one matters here. We extend the standard expense objects rather than building a parallel “policy expense”, and our post on the two kinds of Odoo model inheritance shows why that keeps accounting and reporting intact.

Example: meals and mileage for a field sales team

A building products company has a field sales team visiting merchants and sites. Policy: meals are capped at $45 per person per day, mileage is paid at the company’s internal rate of $0.45 per km, and anything over $75 needs a receipt.

On a Tuesday, a rep logs lunch with a merchant at $28. That evening, stuck on the road, they add dinner at $31. The constraint adds the two, sees $59 against a $45 cap and stops the second line with a message saying $28 was already claimed that day. The rep can claim $17 of dinner, or ask their manager to approve an exception with a note. Either way, the decision happens that night, not three weeks later, and the manager approving the exception can see exactly what they’re agreeing to.

The same rep drives 212 km for site visits. They enter the distance and purpose. The claim comes out at $95.40, above the receipt threshold, but a mileage claim has no receipt, so the rule accepts a route description instead. Small detail. Get it wrong and every rep in the company will tell you about it by Friday.

Policies we’d rather you didn’t encode

Some rules are better as guidance than as code.

  • Hard blocks on everything. Caps on meals are fine. Blocking a hotel claim during a snowstorm because it’s $20 over is not. For some categories, flag and approve instead of stopping.
  • Five approval levels. Two is usually enough. Every extra layer mostly adds waiting.
  • Tax recoverability logic. Whether a given expense is claimable for tax purposes depends on local rules. Keep it in the tax configuration and ask your accountant.
  • Receipt checks that read the image. Matching amounts from photos is unreliable enough to create more work than it saves. Require the attachment and let a person look when it matters.

From approved claim to paid and posted

Approved claims turn into journal entries and payables, which is where the checks in our custom accounting module take over. If you reimburse through salary, the claim needs to reach payroll cleanly. And if you bill travel back to clients, link each claim to a project through project management so it shows up in project costs rather than general overhead.

If you’re on Odoo and the gap is mostly fields and views, our Odoo customization page covers the lighter-touch options.

Guides and terms for expense management

Terms used on this page

Expense management module questions

Something missing? Email [email protected] and a person will answer.

Does this work with the standard Odoo Expenses app and ERPNext Expense Claims?

Yes. In Odoo we extend hr.expense and the expense categories, which are products. In ERPNext, Expense Claim lives in the Frappe HR app, and we extend that. Employees use the same screens they already know.

Can employees snap receipts on their phones?

Both platforms already accept receipt photos from mobile. We don't rebuild that. We add the rules around it, such as requiring an attachment above a threshold before a claim can be submitted.

What about tax on expenses, like VAT recovery?

Tax on expense lines is handled by the platform's tax settings on the expense category. We'll make sure the right category carries the right tax, but whether a given expense is recoverable is a question for your accountant, not for code.

How are claims reimbursed?

Through the normal payment flow in each platform, or through payroll if that's how you pay expenses. If you use payroll, tell us early, because it changes where the posting happens.

We use a spreadsheet and email today. Is migration worth it?

Usually not for old claims. Start clean from a month end and archive the spreadsheet. Import only unpaid claims, so no claim is paid twice or missed.

Will the policy checks survive an upgrade?

They live in their own addon or Frappe app, so core upgrades don't overwrite them. Odoo has reworked the expenses app between major versions, so on Odoo we'd recheck the approval flow and the tests before moving to the next release.

Your next module is one paragraph away

Write it the way you’d explain it to a new hire. We’ll turn it into an app you can read, test and install.

ERPNext v16

Create your account

Free to start. No card needed.

By signing up you agree to our terms and privacy policy.