この記事は現在英語版のみです。翻訳は近日公開予定です。
By the end of this recipe, your Odoo 19 instance will produce auditor-grade Accounts Receivable and Accounts Payable aging reports with custom bucket definitions (Current/1-30/31-60/61-90/90+), drill-down to invoice level, multi-currency translation, and exportable PDF for monthly close. Skill required: accounting + Odoo administrator + basic SQL. Time required: 90 minutes setup. ECOSIRE has built this for clients running tight collection cycles and complex multi-currency AP, and the recipe below is the playbook.
The trap most teams fall into: using Odoo's stock aging report which buckets by invoice date, not by due date. The two are different — invoice date is when the invoice was issued; due date is when it's actually overdue. The recipe below uses due date and respects the customer's payment terms.
What you will need
- Odoo version: 17, 18, or 19. The Aging report ships with
account_reports(Enterprise) oraccount_financial_report(OCA Community). - Properly configured payment terms: every invoice must have a
invoice_date_dueset. - Posted invoices: drafts don't count toward AR/AP.
- User access: Accounting Manager.
- Time: 90 minutes.
Step-by-step
1. Configure aging buckets
Go to Accounting > Configuration > Settings > Aging Periods. Default is 30/60/90/120 days. Customize per industry — short cycle (services): 15/30/45/60. Long cycle (construction): 30/60/90/180.
Verification: the aging report's column headers reflect the new buckets.
2. Build the AR aging SQL view
CREATE OR REPLACE VIEW bi.ar_aging AS
SELECT
am.partner_id,
rp.name AS partner_name,
am.id AS invoice_id,
am.name AS invoice_number,
am.invoice_date,
am.invoice_date_due,
am.amount_total_signed AS total,
am.amount_residual_signed AS residual,
am.currency_id,
am.company_id,
CASE
WHEN CURRENT_DATE <= am.invoice_date_due THEN 'Current'
WHEN CURRENT_DATE <= am.invoice_date_due + INTERVAL '30 days' THEN '1-30 days'
WHEN CURRENT_DATE <= am.invoice_date_due + INTERVAL '60 days' THEN '31-60 days'
WHEN CURRENT_DATE <= am.invoice_date_due + INTERVAL '90 days' THEN '61-90 days'
ELSE '90+ days'
END AS bucket,
CURRENT_DATE - am.invoice_date_due AS days_overdue
FROM account_move am
JOIN res_partner rp ON am.partner_id = rp.id
WHERE am.state = 'posted'
AND am.move_type IN ('out_invoice', 'out_refund')
AND am.payment_state IN ('not_paid', 'partial');
Verification: SELECT bucket, SUM(residual) FROM bi.ar_aging GROUP BY bucket matches Odoo's stock AR aging report total.
3. Build the AP aging view
Same query with move_type IN ('in_invoice', 'in_refund'). Save as bi.ap_aging.
4. Build a pivot in Odoo
Expose bi.ar_aging as a non-editable Odoo model (see the COGS recipe pattern). Add a Pivot view:
- Rows:
partner_name - Columns:
bucket - Measure:
residual
In bi/views/aging_views.xml:
<record id="ar_aging_pivot" model="ir.ui.view">
<field name="name">bi.ar.aging.pivot</field>
<field name="model">bi.ar.aging</field>
<field name="arch" type="xml">
<pivot string="AR Aging" sample="1">
<field name="partner_name" type="row"/>
<field name="bucket" type="col"/>
<field name="residual" type="measure"/>
</pivot>
</field>
</record>
Verification: the pivot renders with totals matching the SQL.
5. Add multi-currency support
Translate foreign-currency invoices to company currency at period-end FX rate:
SELECT
am.partner_id,
am.invoice_id,
am.residual AS residual_company_ccy,
aml.amount_residual_currency AS residual_in_invoice_ccy,
rc.name AS invoice_currency
FROM bi.ar_aging am
JOIN account_move_line aml ON aml.move_id = am.invoice_id AND aml.account_id IN (
SELECT id FROM account_account WHERE account_type = 'asset_receivable'
)
JOIN res_currency rc ON aml.currency_id = rc.id;
Show both columns: residual in invoice currency (for collection conversation with customer) and in company currency (for financial reporting).
Verification: USD invoices show in USD on the customer-facing collection notice and EUR-equivalent on the CFO board pack.
6. Drill-down to invoice level
In the pivot, click any cell to open the underlying invoices. From the invoice form, see the full payment history.
7. Schedule weekly collection report
Cron emails the AR aging report to the credit-control team every Monday at 9 AM. Each row is a customer with at least $1,000 in 30+ days bucket:
@api.model
def _cron_email_collection_report(self):
self.env.cr.execute("""
SELECT partner_id, partner_name, SUM(residual) AS overdue
FROM bi.ar_aging
WHERE bucket != 'Current'
GROUP BY partner_id, partner_name
HAVING SUM(residual) >= 1000
ORDER BY overdue DESC
""")
body = '<h3>Customers with $1,000+ overdue:</h3><ul>'
for row in self.env.cr.fetchall():
body += f'<li>{row[1]}: ${row[2]:,.2f}</li>'
body += '</ul>'
self.env['mail.mail'].create({
'subject': 'Weekly Collection Report',
'body_html': body,
'email_to': '[email protected]',
}).send()
Verification: the email arrives Monday morning with actionable list.
8. Add an "expected receipts" forecast column
Project cash inflows for the next 30, 60, 90 days based on due dates and historical payment behavior:
SELECT
SUM(residual) FILTER (WHERE invoice_date_due BETWEEN CURRENT_DATE AND CURRENT_DATE + 30) AS receipts_30d,
SUM(residual) FILTER (WHERE invoice_date_due BETWEEN CURRENT_DATE AND CURRENT_DATE + 60) AS receipts_60d,
SUM(residual) FILTER (WHERE invoice_date_due BETWEEN CURRENT_DATE AND CURRENT_DATE + 90) AS receipts_90d
FROM bi.ar_aging;
Verification: the forecast feeds into your 13-week cash flow projection.
Common mistakes
- Bucketing by invoice_date instead of invoice_date_due. Customers on Net 60 look like they're paying late even when they're current.
- Including drafts. AR is only posted invoices.
- Ignoring credit notes. They reduce AR — sum should be net.
- Single-currency total when invoices are in multiple currencies. Always translate to company currency for totals.
- Sending the entire report to the entire company. Aging is sensitive — restrict to credit/collections team.
Going further
Predictive payment scoring: pair aging with historical payment data to predict which 30-day-overdue invoices will cure vs go to write-off. Train a logistic regression on features like customer industry, invoice amount, days since last on-time payment, and current bucket. Score every invoice nightly; the top decile of high-risk invoices gets human follow-up while the rest get automated email cadences.
Auto-dunning sequences: trigger reminder emails at +1 day, +7 days, +14 days, +30 days overdue. Configure in Accounting > Configuration > Follow-up Levels. The default Odoo follow-up engine reads payment_term_id.line_ids[0].delay_type and triggers reminders relative to due date. Customize the email templates per customer segment — a Fortune 500 customer gets a polite nudge, a long-overdue SMB gets escalation language.
Risk-weighted AR: weight overdue balances by customer credit score. A $10k overdue from a A-rated customer is less concerning than $1k from a D-rated one. Pull credit scores from Experian/Dun & Bradstreet APIs and store as a custom field. Build a "risk-adjusted DSO" KPI that multiplies straight DSO by the average risk weight.
Aging of customer credit limits: track customers approaching their credit limit alongside aging. When AR + open-orders gets within 90 percent of credit limit, flag the customer for credit review. Block new orders until credit team approves an increase.
Multi-entity consolidated aging: for groups with multiple Odoo companies, build a consolidated aging that sums across all entities while showing which entity originated each invoice. The recipe extends easily — add am.company_id to the GROUP BY.
Cash application matching: pair aging with bank statement parsing so customer payments auto-clear the right invoices. The aging report then shows real open positions, not stale receivables waiting for manual application.
Top 20 customer focus: 80 percent of receivables typically concentrate in the top 20 customers. Build a special view showing only those, with named-account-manager assignments and weekly call cadence. Most ECOSIRE finance clients run this dashboard daily.
For full credit-management automation including payment-prediction ML and customer credit scoring, ECOSIRE custom Odoo development ships fixed-price engagements. Pair this with how to track sales rep commission.
Frequently Asked Questions
Why does my aging report show negative residuals?
Customer overpayments. Treat them as deferred revenue or refund-due, depending on policy. Build a "Customer Credit Balance" report that surfaces these for collections review.
How do I handle disputed invoices?
Add a custom field dispute_state (none / under_review / disputed). Filter disputed out of normal aging and into a separate "disputes" bucket. Add an SLA on dispute resolution (e.g., 14 days) and escalate unresolved disputes to senior management.
Can I include partial payments?
Yes — residual is post-partial-payment. The aging shows the unpaid remainder, not the original total. The original invoice total is in amount_total; the difference is what's been paid.
How does this differ from cash flow forecasting?
Aging looks backward (invoices issued, not yet collected). Cash flow forecast looks forward (expected receipts based on aging + sales pipeline + recurring contracts). Combine them for a complete liquidity picture.
What about prepayments / advance payments?
Prepayments show as customer credit balance (negative receivable). Apply them to future invoices as those are issued. The aging report should net them out at customer level.
How do I calculate DSO from this view?
DSO = AR Total / (Daily Revenue × 30). Daily revenue from monthly invoiced revenue / 30. Tracks how fast you're collecting. Healthy DSO depends on industry: SaaS 25-45 days, B2B services 30-60 days, manufacturing 60-90 days.
Can I auto-generate dunning letters?
Yes — Odoo's Follow-up Levels feature ships with dunning letter templates. Configure per overdue threshold. Pair with mail templates for branded customer communication.
How do I split aging by sales rep?
Add am.invoice_user_id to the GROUP BY of the aging view. Each rep sees their own AR. Drives accountability for collections.
What about credit notes and returns?
Credit notes are negative invoices. They reduce AR. The move_type IN ('out_invoice', 'out_refund') filter includes both. The total residual is the net.
How frequently should I run this?
Daily during the close week, weekly otherwise. The collection team needs fresh data; the CFO sees monthly trends.
Can I integrate with credit insurance?
Yes — for invoices above a threshold, push to your credit insurer's API for risk scoring. Build a wizard that bulk-submits and stores the credit insurance status on the invoice.
For automated dunning sequences and credit-management workflows, ECOSIRE accounting services build the entire AR/AP automation stack. Or read how to build a cash flow statement.
執筆者
ECOSIRE TeamTechnical Writing
The ECOSIRE technical writing team covers Odoo ERP, Shopify eCommerce, AI agents, Power BI analytics, GoHighLevel automation, and enterprise software best practices. Our guides help businesses make informed technology decisions.
関連記事
Odoo フォーム ビューにカスタム ボタンを追加する方法 (2026)
Odoo 19 フォーム ビューにカスタム アクション ボタンを追加します: Python アクション メソッド、ビューの継承、条件付き可視性、確認ダイアログ。製造テスト済み。
Studio を使用せずに Odoo にカスタムフィールドを追加する方法 (2026)
Odoo 19 のカスタム モジュールを介してカスタム フィールドを追加します: モデル継承、ビュー拡張、計算フィールド、ストア/非ストアの決定。コードファースト、バージョン管理。
外部レイアウトを使用して Odoo にカスタム レポートを追加する方法
web.external_layout: QWeb テンプレート、paperformat、アクション バインディングを使用して、Odoo 19 でブランド化された PDF レポートを構築します。印刷ロゴ + フッターのオーバーライド付き。