Skip to content
Finance · Invoice management module

An invoice management system that catches mistakes before your customer does

Invoices rarely go unpaid because the customer forgot. They go unpaid because the PO number was missing, the site reference was wrong or someone disputed one line and parked the whole thing. An invoice management system should stop those problems before posting, and erpfly generates one inside ERPNext or Odoo.

ERPNext v16

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

Features

What your invoice management module can do

Customer requirements on the customer

PO required, site code required, invoice by email only. Set it once on the customer record and every invoice checks it before posting.

Duplicate checks that name the other invoice

A reused PO number or supplier reference stops posting and tells you exactly which invoice already has it.

Disputes you can report on

Log a dispute against a line, pick a reason, assign an owner. Disputed invoices get their own filter so they stop hiding inside the overdue list.

Review before it leaves the building

Invoices above a set amount, or for flagged customers, wait for a second pair of eyes before they're sent.

Printouts customers accept

Invoice layouts that put the PO number, site and contact where the customer's accounts team looks for them.

The output

What actually gets generated

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

ERPNext / Frappe app

  • Customer PO Required check field on Customer (fixture)
  • Sales Invoice validate hook for po_no and duplicates
  • Invoice Dispute DocType linked to Sales Invoice
  • Workflow for invoice review before submit
  • Sales Invoice Print Format with PO and site reference

Odoo addon

  • res.partner flag for PO-required customers
  • account.move _post override with posting checks
  • Constraint on duplicate customer references
  • Invoice dispute model with chatter (mail.thread)
  • QWeb invoice report extension
  • Search filter for disputed invoices
invoice_rules/models/account_move.py
from odoo import _, api, fields, models
from odoo.exceptions import UserError, ValidationError


class ResPartner(models.Model):
    _inherit = "res.partner"

    requires_po_number = fields.Boolean(string="Invoices need a PO number")


class AccountMove(models.Model):
    _inherit = "account.move"

    @api.constrains("ref", "partner_id", "move_type", "state")
    def _check_po_number_unique(self):
        for move in self.filtered(lambda m: m.move_type == "out_invoice" and m.ref):
            other = self.search([
                ("id", "!=", move.id),
                ("move_type", "=", "out_invoice"),
                ("commercial_partner_id", "=", move.commercial_partner_id.id),
                ("ref", "=", move.ref),
                ("state", "!=", "cancel"),
            ], limit=1)
            if other:
                raise ValidationError(_("PO %s is already on invoice %s.", move.ref, other.name))

    def _post(self, soft=True):
        for move in self.filtered(lambda m: m.move_type == "out_invoice"):
            partner = move.commercial_partner_id
            if partner.requires_po_number and not move.ref:
                raise UserError(_("%s needs their PO number in the Reference field.", partner.name))
        return super()._post(soft=soft)
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.

Most invoice problems start before the invoice

Ask a credit controller why an invoice is 60 days overdue and you’ll rarely hear “the customer has no money”. You’ll hear that it went to the wrong email, didn’t have a PO number, quoted last year’s price, or had one disputed line and the customer’s accounts team parked the whole thing.

Each of those is cheap to catch before posting and expensive to fix after. Once an invoice is out, the fix is a credit note, a reissue, a new payment term clock and an awkward email. Before posting, it’s a message on screen.

So we think about invoicing as a set of gates. What must be true about this customer, this invoice and this reference before it’s allowed to become a real document?

In practice the gates fall into three groups. Customer gates: this customer needs a PO, a site code, a named contact. Document gates: this reference hasn’t been used, the amount is within what was ordered, the tax template matches the customer’s country. Approval gates: this invoice is large or unusual enough that a second person looks at it. Most businesses need a handful of these, not dozens. The trick is writing down the ones your credit controller already checks in their head, then letting the system do the checking.

Inside the invoice management system we generate

On Odoo, invoices are account.move records. The addon adds customer-level requirements on res.partner, constraints on the invoice, and a check in _post so nothing is posted without passing. It uses the existing Reference field rather than inventing a new one, because the sales order’s customer reference already lands there. Disputes get their own model with chatter, so the conversation stays attached to the invoice.

On ERPNext, the Sales Invoice already has a customer PO field (po_no). The Frappe app adds a validate hook, a flag on Customer shipped as a fixture, an Invoice Dispute DocType and, if you want one, a review Workflow before submit. Print Formats get the PO and site reference placed where your customers expect them.

If you’re on Odoo and wondering whether Studio could do this instead, our comparison of Odoo Studio and a custom module shows where Studio runs out.

Example: a customer that bounces invoices

A facilities company cleans and maintains buildings for a hospital group with 14 sites. The hospital’s accounts payable team rejects any invoice without a valid PO number, and they don’t always say so quickly. Payment terms are 45 days, and a rejected invoice restarts the clock.

Last month the company raised 60 invoices to the group, averaging $3,200 each. Nine went out without a PO. Every one came back, some after three weeks. That’s roughly $28,800 of revenue arriving a month or more late, for a missing text field.

With the module, the hospital group is flagged as PO-required. An accounts clerk tries to post an invoice for the east wing contract without a reference and gets a clear message. Two days later, someone types last month’s PO number from the wrong email. The duplicate check stops it and names the earlier invoice. Nothing clever. Just two checks that someone used to do by eye at 5pm.

Things we won’t automate for you

  • Posting supplier bills straight from OCR. Reading a PDF into a draft saves typing. Posting it unreviewed is how you pay the same bill twice.
  • Editing posted invoices. Corrections go through credit notes. Both platforms are built around that, and so is every audit.
  • Your own tax engine. Tax rules belong in the localisation and tax configuration. We’ll add checks around them, never a parallel calculation.
  • Automatic reminders to disputed invoices. A customer who has raised a dispute and then gets a “your invoice is overdue” email will not be in a hurry to pay.

Where invoicing connects

Invoice quality depends on what came before. Clean sales orders make clean invoices, and vendor bills are far easier to check against disciplined purchase management. Once posted, everything lands in the ledger, and our page on a custom accounting module covers the controls that sit there.

Teams building on Odoo can read more about how addons are structured on our Odoo module development page.

Guides and terms for invoice management

Invoice management module questions

Something missing? Email hello@erpfly.com and a person will answer.

Does this replace e-invoicing for my country?

No. Where a maintained localisation module already handles e-invoicing for your country, use it. Our checks run before posting, so they sit comfortably in front of whatever e-invoicing or tax reporting you already have.

Can we still correct an invoice after it's sent?

Through credit notes, yes, the same way both platforms intend. We won't generate anything that edits posted invoices in place. Your auditors will prefer it that way.

Will it work with invoices created from sales orders and timesheets?

Yes. The checks run on the invoice itself, whatever created it. In Odoo, the customer reference from a sales order already flows into the invoice reference, so most invoices arrive with the PO filled in.

How do we handle vendor bills?

The same pattern works in reverse. Duplicate supplier references, bills without a matching purchase order and bills above the ordered amount can all be checked before posting. That usually lives alongside purchase management.

We send invoices from a separate tool today. Can we migrate?

We generate an import for open invoices with their balances, so receivables match on day one. Paid history can stay in the old tool as an archive unless you need it for reporting.

How long does this kind of module take?

The rules in the example above produce a working addon in minutes. Printouts and dispute reasons usually take a couple of rounds, because that's where the details are.

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.