Key Takeaways: The most useful finance automations are not complex — they are consistent. Each of the five workflows here follows the same JSON-RPC pattern: query an Odoo modelA mathematical function trained on data that maps inputs to outputs. In ML, a model is the artifact produced after training — it encapsulates learned patterns and is used to make predictions or…, apply business logic in a Code node, consolidate before sending. Invoice reminders should be batched by customer, not fired per invoice. Bank reconciliation alerts need a threshold — every minor discrepancy is noise. Budget notifications require the right comparison period or the numbers are meaningless. Expense routing is only as good as the approval matrix you define upfront. Month-end checklists are most useful when they check actual Odoo state, not just send a calendar reminder.
Finance teams spend a disproportionate share of their month on operational checks that could run automatically. Chasing overdue invoices, catching reconciliation gaps, approving expenses through email threads — these are repetitive tasks with clear logic, which makes them exactly the kind of work n8n handles well.
If you’ve read our post on alerting uninvoiced sales orders in Odoo using n8n, you already know the pattern: schedule a run, call Odoo’s JSON-RPC endpoint, apply your business filter, consolidate the results, send one useful notification. What changes across workflows is the Odoo modelA mathematical function trained on data that maps inputs to outputs. In ML, a model is the artifact produced after training — it encapsulates learned patterns and is used to make predictions or… you’re querying and the business logic that determines what counts as worth alerting on.
These five workflows extend that pattern. All of them are running in production at Trobz client sites. None of them are complicated — the complexity lives in defining the right filter logic, not in the automation itself.
Prerequisites
- Odoo 17 or 18 (Community or Enterprise)
- n8n v1.x (self-hosted or cloud)
- An Odoo API key and your user’s numeric ID (see the uninvoiced SO tutorial for setup steps)
- Your Odoo database name
Workflow 1: Consolidated Invoice Reminders
What to do: Query account.move for posted customer invoices past their due date, group them by customer, and send one batched reminder per customer — not one per invoice.
# Odoo JSON-RPC domain filter
[
["move_type", "=", "out_invoice"],
["state", "=", "posted"],
["payment_state", "in", ["not_paid", "partial"]],
["invoice_date_due", "<", "{{ $now.toISODate() }}"]
]
# Fields: name, partner_id, invoice_date_due, amount_residual
Why: The moment you connect the raw query output directly to an email node, you send one email per invoice. A customer with four overdue invoices gets four emails in the same minute. They either ignore all of them or call you annoyed. Grouping by partner_id first produces one email with a summary table — useful, not noisy.
// n8n Code node — group invoices by customer
const items = $input.all();
const byPartner = {};
items.forEach(item => {
const inv = item.json;
const pid = inv.partner_id[0];
const pname = inv.partner_id[1];
if (!byPartner[pid]) byPartner[pid] = { name: pname, invoices: [] };
byPartner[pid].invoices.push(inv);
});
return Object.values(byPartner).map(p => ({
json: {
customer: p.name,
count: p.invoices.length,
invoices: p.invoices,
total_due: p.invoices.reduce((sum, i) => sum + i.amount_residual, 0)
}
}));
Gotchas: Watch out for customers in dispute or on payment plans — you probably don’t want to automatically remind them. Add a boolean field on res.partner (e.g., x_exclude_reminder) and filter it out in your domain. Also, payment_state = "partial" means some payment exists but the balance is still open — include it in reminders, but flag it clearly in the email so whoever processes the response knows the history.
Workflow 2: Bank Reconciliation Mismatch Alerts
What to do: Check account.bank.statement.line for lines older than N days that are still unreconciled, and send a daily digest to the accounting team.
# Domain filter — unreconciled lines older than 3 days
[
["is_reconciled", "=", false],
["date", "<", "{{ $now.minus({days: 3}).toISODate() }}"]
]
# Fields: date, payment_ref, amount, journal_id
Why: A bank line that’s been sitting unreconciled for a week is usually a sign of a missing bill, a duplicate entry, or a payment applied to the wrong invoice. Catching these early — not at month-end — keeps the reconciliation workload manageable. A three-day threshold works for most clients. Some fast-moving teams prefer one day; some weekly-close businesses prefer a week.
Gotchas: Not every unreconciled line is a problem. Bank fees, payroll transfers, inter-company movements, and certain tax payments can take longer to match by design. Build an exclusion mechanism early — either by journal (e.g., skip the payroll journal), by amount range (e.g., amounts under 10,000 VND are noise), or by reference pattern. Otherwise the alert gets ignored because it’s full of expected exceptions.
Also: the is_reconciled field on account.bank.statement.line was introduced in Odoo 16. On older versions, filter on account_id = false instead — unreconciled lines have no counterpart account set.
Workflow 3: Budget Threshold Notifications
What to do: Query crossovered.budget.line to compare actual spend against the planned budget for each active line, and notify department heads when they cross 80% (or whatever threshold applies to your client).
# Odoo JSON-RPC call — active budget lines for current period
[
"crossovered.budget.line",
"search_read",
[[
["date_from", "<=", "{{ $now.toISODate() }}"],
["date_to", ">=", "{{ $now.toISODate() }}"]
]],
{
"fields": [
"name", "planned_amount", "practical_amount",
"theoritical_amount", "crossovered_budget_id"
]
}
]
Then in the Code node, calculate the burn rate and flag anything above your threshold:
// n8n Code node — flag budget lines above 80%
const lines = $input.all().map(i => i.json);
const alerts = lines
.filter(l => {
if (l.planned_amount === 0) return false;
const pct = Math.abs(l.practical_amount / l.planned_amount);
return pct >= 0.8;
})
.map(l => ({
json: {
line: l.name,
budget: l.planned_amount,
actual: l.practical_amount,
pct: Math.round(Math.abs(l.practical_amount / l.planned_amount) * 100)
}
}));
return alerts;
Why: Budget overruns rarely surprise anyone in the accounting team — they surprise the department heads who didn’t know they were close to the limit. A weekly notification at 80% and a daily one past 100% gives people time to respond before the damage is done.
Gotchas: The theoritical_amount field is Odoo’s name for the prorated expected spend (the typo is in the source code, not here). Use practical_amount for real spend. Budget lines are date-bound, so make sure your filter targets currently active periods — lines from a prior quarter that were never closed will appear in results if you’re not careful. On multi-company instances, always filter by company_id.
Workflow 4: Expense Approval Routing
What to do: Poll hr.expense.sheet for sheets in submit state, check the total amount, and route the approval request to the right approver based on amount thresholds.
# Domain filter — submitted expense sheets awaiting approval
[
["state", "=", "submit"]
]
# Fields: name, employee_id, total_amount, company_id, write_date
Then use a Switch node to route based on total_amount:
- Under 5,000,000 VND → direct manager
- 5,000,000 to 20,000,000 VND → finance manager
- Above 20,000,000 VND → CFO + finance manager
Add an escalation check that re-alerts if the sheet is still in submit state after 48 hours:
// n8n Code node — flag sheets unapproved after 48h
const sheets = $input.all();
const now = new Date();
const threshold = 48 * 60 * 60 * 1000;
return sheets.filter(s => {
const submitted = new Date(s.json.write_date);
return (now - submitted) > threshold;
});
Why: Expense sheets pile up in Odoo because approval loops live in email. Someone submits a report, a notification goes out, the manager forgets, the employee follows up a week later. The problem isn’t the process — it’s that it has no SLA enforcement. n8n can check every two hours and send a second nudge if the sheet is still unapproved after 48 hours.
Gotchas: Define your approval matrix before building this workflow. Who approves what, at what amount, for which company, and in which currency? If that’s not settled in writing before you start wiring nodes, you’ll rebuild this three times.
Also, hr.expense.sheet uses write_date to track the last update, not the submission time. If a manager adds a note, write_date resets and can suppress the escalation. A cleaner approach: add a custom field x_submitted_at in Odoo and set it via an automated action when the sheet moves to submit state. One extra field, much cleaner escalation logic.
Workflow 5: Month-End Checklist Triggers
What to do: On the first working day of each month, query Odoo to verify which closing tasks are still open, and send a structured status message to the finance team — with real counts from the database, not a generic reminder.
Instead of sending “don’t forget to close the month!”, build a workflow that actually checks:
# Check 1: Unreconciled bank lines from prior month
[
["is_reconciled", "=", false],
["date", "<", "{{ $now.startOf('month').toISODate() }}"]
]
# Check 2: Draft journal entries from prior month
[
["state", "=", "draft"],
["move_type", "in", ["entry"]],
["date", "<", "{{ $now.startOf('month').toISODate() }}"]
]
# Check 3: Uninvoiced confirmed sales orders (prior month)
[
["state", "=", "sale"],
["invoice_ids", "=", false],
["date_order", "<", "{{ $now.startOf('month').toISODate() }}"]
]
Run all three checks, consolidate the results, and send one message: “Heading into month-end — X unreconciled bank lines, Y draft entries, Z uninvoiced orders still need attention.”
Why: Finance teams know what closing tasks exist. What they lack is real-time visibility into which ones are actually done. A checklist email that shows live counts from Odoo is useful. “Remember to reconcile the bank” adds zero signal.
Gotchas: Month-end is when Odoo databases are under the most load. Don’t schedule four separate n8n workflows to fire simultaneously at 08:00 on the 1st. Stagger them by 5–10 minutes, or combine them into one workflow with sequential HTTP nodes.
Also, define what “prior month” means precisely in your filters. $now.startOf('month') in n8n uses the server’s local time. If n8n runs on UTC and your client closes fiscal months on Vietnamese time (UTC+7), offset explicitly. Test the boundary before relying on it for actual month-end reporting.
The Pattern Is Always the Same
Every one of these workflows uses the same skeleton:
- Schedule trigger — every 15 minutes, daily, or monthly
- HTTP Request node —
search_readon the right Odoo modelA mathematical function trained on data that maps inputs to outputs. In ML, a model is the artifact produced after training — it encapsulates learned patterns and is used to make predictions or… with the right domain - Code node — apply business logic, group records, compute thresholds
- IF or Switch node — branch on count, amount, or state
- HTTP Request node — create a
mail.mailrecord or post to Slack/Teams
The Odoo API call is rarely the hard part. Defining the business rules precisely enough that the automation isn’t worse than doing nothing — that’s the hard part. A flawed filter that catches too much trains people to ignore the notification. One that’s too narrow misses real problems.
Get the business logic right on paper first. Then it’s plumbing.
At Trobz, we configure and maintain n8n workflows like these as part of our process automation engagements — if your finance team is buried in manual checks, reach out and we can map out which workflows would have the most impact first.