Skip to main content
iVentureTeam

payment

Every Paid on line and every Outstanding credits box on an Odoo invoice is one widget: payment, a JSON-driven display with two faces, an info popover, and an Unreconcile button.

Siddharth JambukiyaSiddharth JambukiyaOdoo Techno-Functional Consultant
August 25, 2026Updated August 25, 20267 min read
Technical namepayment
Field typesbinary (JSON payload)
Viewsform
Moduleaccount, installed with Invoicing or Accounting
Used in core2 occurrences in 1 module: invoice_payments_widget and invoice_outstanding_credits_debits_widget on the invoice form
VersionsOdoo 20.0, Odoo 19.0, Odoo 18.0, Odoo 17.0, Odoo 16.0
No-code setupNo. It is wired into the invoice form; Studio cannot add or configure it
Alternativesopen_move_widget, account_document_state, one2many

What the payment info widget does

When an invoice is posted, two pieces of payment intelligence appear on the form. Below the totals, a Paid on line per reconciled payment. Above them, when the customer has unapplied credits, a boxed list of outstanding entries with an Add link each. Both are the payment widget rendering two different fields.

Neither field is a one2many. invoice_payments_widget and invoice_outstanding_credits_debits_widget are computed binary fields whose value is a JSON document: a content array of line dicts, a title, a move_id, and an outstanding boolean. That last flag is the switch: outstanding: true renders the credits box with Add links, outstanding: false renders the Paid on table with info icons. The widget itself holds no accounting logic; the server compute decides what is listed, the widget decides how it looks and which server method each click calls.

Each Paid on line carries an info icon opening a popover titled Journal Entry Info: amount (with the foreign currency in parentheses when relevant), memo, date, journal and payment method, and the branch in multi-company setups. Two buttons close it out: View opens the payment's business document, and Unreconcile detaches the payment from the invoice.

What this means for your team

This widget is where three everyday finance operations actually happen, so it pays to know its exact behavior.

Applying credits. The Add link on an outstanding credit calls js_assign_outstanding_line and reconciles that single entry against the invoice, then reloads the form. Teams that issue lots of credit notes live on this link; teams that never see the box usually have payments landing in a different partner or currency, which is a data problem, not a widget problem.

Undoing a wrong application. Unreconcile on the popover removes one partial reconciliation, not the payment itself. The money stays received; it just returns to outstanding status. Because the button only renders for users in the invoicing access group, junior roles literally cannot see the undo, which is a sensible default most companies never realize is protecting them.

Auditing what paid what. The popover's View button routes through action_open_business_doc, landing on the payment, the statement line or the entry, whichever the reconciled counterpart really is. That resolution logic is server side, so custom payment flows inherit it for free.

If your invoices come from a custom pipeline and the payment area behaves oddly, the JSON compute on the server is where to look, and adjusting it safely is standard Odoo development work.

Working examples

How core wires it (invoice form)

<field name="invoice_payments_widget" colspan="2" nolabel="1"
       widget="payment"
       invisible="not invoice_payments_widget"/>

The JSON contract the widget expects

{
    "title": "Outstanding credits",
    "outstanding": true,
    "move_id": 42,
    "content": [{
        "id": 7,                     // move line to assign
        "amount": 150.0,
        "currency_id": 1,
        "date": "2026-08-01",
        "journal_name": "Customer Invoices",
        "is_refund": false,
        "is_exchange": false
    }]
}

With outstanding: false the same rows also carry the popover fields: ref, journal_name, payment_method_name, amount_company_currency, amount_foreign_currency, partial_id and account_payment_id. Amounts are formatted in the browser with each line's currency_id, dates re-formatted to the user's locale.

Reusing it on a custom model

my_payments_json = fields.Binary(compute="_compute_my_payments_json")

Emit the same JSON shape and the rendering is free. The click handlers, however, call js_assign_outstanding_line, js_remove_outstanding_partial and action_open_business_doc on your model, so those three methods must exist there for the links to work.

Reloads, exchange rows and a dead attribute

Every action reloads the whole record. Both Add and Unreconcile finish with a full root reload rather than surgically updating one field, because reconciliation changes amounts, statuses and both JSON payloads at once. On heavily customized forms with expensive computes this reload is what users perceive as the click being slow.

Exchange differences are lines with special treatment. Rows flagged is_exchange render as an Exchange Difference label with the amount and, notably, no Unreconcile button in the popover: currency gain or loss entries are system-generated and removing the underlying reconciliation is what removes them.

The popover flips sides in RTL. The component picks left positioning, or bottom when the interface direction is right-to-left, one of the few core widgets making an explicit RTL layout decision in JavaScript.

A dead attribute ships in the template. The outstanding-credits link carries t-att-payment-id="account_payment_id", but no such variable exists in the rendering scope, so the attribute never renders. It has survived every refactor since 16 and is still on the development branch with a modernized spelling, equally dead.

Version drift is in the type declaration, not behavior. Through 16, 17 and 18 the descriptor declared supportedTypes: ["char"] while the server field was already binary; 19 corrected it to binary. Odoo 16 also shipped a parallel legacy-view implementation, removed in 17 along with the old popover API and cached value formatting.

Version compatibility

VersionStatusNotes
Odoo 20.0Partial / changedNot released. Field and widget switch to the json type, exchange rows become one aggregated clickable line. Re-verified after launch.
Odoo 19.0VerifiedBehavior verified against the shipped source; declared type corrected to binary.
Odoo 18.0VerifiedIdentical component; descriptor still declared supportedTypes char while rendering the binary JSON field.
Odoo 17.0VerifiedFunctionally identical to 19 apart from the char type declaration; first version on the modern popover API.
Odoo 16.0Partial / changedSame features, but with value caching, the old popover API and a parallel legacy-view implementation that was removed in 17.

What is changing in Odoo 20

Odoo 20 is expected at Odoo Experience in Brussels, 24 to 26 September 2026. Everything below is read from the public development branch, which is unstable until feature freeze; the page is re-verified against the shipped release.

The field becomes a real JSON type. Both server fields switch from fields.Binary to fields.Json, and the widget's declaration follows suit with supportedTypes: ["json"]. Custom fields feeding this widget will want the same migration.

Exchange differences get aggregated and clickable. Instead of one row per exchange line, the payload gains an exchange_info dict, and the widget renders a single Exchange Difference row whose info icon calls a new action_open_exchange_items method with the exchange line ids.

Cosmetics and labels shift. The outstanding-credit link shows the entry's move_name rather than the journal name, its tooltip is rebuilt from bank_label and ref, and the FontAwesome info icon moves to the new oi icon set.

If you customized the payload compute or the popover template, expect to touch it during your Odoo 20 migration; the JSON contract is changing shape for the first time since 17.

Common problems and fixes

SymptomCause and fix
No Unreconcile button in the popoverThe button is gated by the account.group_account_invoice group, and never renders on exchange difference lines. Grant invoicing access, or remove the reconciliation from the full entry for exchange rows.
Outstanding credits box never appears despite open creditsThe compute only lists entries matching the invoice's partner, company and eligible accounts; the visibility also depends on invoice_has_outstanding. Check partner and currency on the credit entry; confirm it sits on a receivable or payable account.
Clicking Add throws a discard-changes dialogAssigning reloads the whole record, and the form has unsaved edits. Save the invoice first, then apply credits.
Amounts show in the wrong currency formatEach line is formatted client side using the currency_id inside the JSON, not the invoice currency. Fix the currency_id emitted by the compute if you customized it.
Widget renders nothing at allThe field value is false; core hides it via invisible="not invoice_payments_widget" and the compute returns false for unposted or unpaid states. Post the invoice; the payload only exists in states where it is meaningful.
Custom model: clicks raise missing-method errorsThe widget calls js_assign_outstanding_line, js_remove_outstanding_partial and action_open_business_doc on the record's own model. Implement those methods or strip the interactive parts from a subclassed template.

Payment info widget vs the alternatives

WidgetBest forKey difference
paymentReconciliation state rendered inline on invoice formsRenders a computed JSON payload with actionable Add, Unreconcile and View links; not a relational field at all
open_move_widgetJumping from an entry number to its business documentPure navigation link on a char column, no payment logic
account_document_stateStatus fields with an explanatory popoverSelection label plus message popover; displays state instead of acting on reconciliations
one2manyListing payment records as an editable gridReal relational rows without assign or unreconcile shortcuts

Nothing else in core renders reconciliation state inline on the form. The alternatives below solve neighboring problems: raw entry navigation, status display, or listing the payments as records.

Frequently asked questions

What field type does the payment widget expect?+
In Odoo 19, a fields.Binary whose compute returns a JSON dict with content, title, move_id and outstanding keys. The declared widget type was char up to 18 even though the field was already binary, and the Odoo 20 branch moves both to a true json type.
What is the difference between the Paid on lines and the Outstanding credits box?+
Same widget, different payload. The outstanding flag in the JSON switches the template: false renders Paid on rows with info popovers, true renders the boxed list with an Add link per entry that reconciles it against the invoice.
What exactly does Unreconcile do?+
It calls js_remove_outstanding_partial with the partial reconciliation id from the popover, removing that one payment-to-invoice match and reloading the form. The payment itself is untouched; it simply becomes outstanding again. The button only renders for users in account.group_account_invoice.
Why does the whole invoice reload when I click Add?+
Reconciliation changes amounts due, payment status and both JSON payloads at once, so the widget reloads the root record rather than patching fields. On forms with heavy computes that reload is the delay you feel.
Can I use the payment widget on my own model?+
Yes: emit the same JSON shape from a binary compute and the rendering works as is. The interactive parts call js_assign_outstanding_line, js_remove_outstanding_partial and action_open_business_doc on your model, so implement those, or subclass the template to remove the actions.
How are refunds and exchange differences shown?+
Rows flagged is_refund render as Reversed on instead of Paid on. Rows flagged is_exchange render as an Exchange Difference amount without an Unreconcile button; on the Odoo 20 branch these collapse into one aggregated row with a dedicated action listing the exchange items.
Does the widget have any options?+
None. The descriptor declares only the supported type. All variation comes from the JSON payload, which makes the server compute the single place to customize behavior.

Payments not matching invoices the way they should?

Reconciliation UX is only as good as the data feeding it: partners, currencies, outstanding accounts and custom payment flows all end up in this one widget. We debug and extend Odoo payment and reconciliation flows across versions 16 to 19.

Sort out my reconciliation

How this page was produced

Verified by reading account_payment_field.js and account_payment.xml in the Odoo 19.0 account module, the fields.Binary definitions and compute methods in account_move.py, and both usages in the invoice form XML. Click flows, the popover, Add and Unreconcile were exercised on a clean Odoo 19 database, where the screenshot was captured. Version rows come from diffing the widget across 16.0 through the public development branch, including the binary-to-json field change. Report inaccuracies via our contact page.