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

Canonical: https://erpfly.com/modules/expense-management/
Last updated: August 17, 2026

Category: Finance

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.

### Features

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

### What gets generated

**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 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](https://erpfly.com/blog/odoo-inherit-vs-inherits/) 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](https://erpfly.com/modules/accounting/) take over. If you reimburse through salary, the claim needs to reach [payroll](https://erpfly.com/modules/hr-payroll/) cleanly. And if you bill travel back to clients, link each claim to a project through [project management](https://erpfly.com/modules/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](https://erpfly.com/odoo-customization/) page covers the lighter-touch options.

### FAQ

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