The problem. fr_ctc_send_out_invoice is the only lever deciding
whether a posted customer invoice is sent: fr_einvoicing_required is a
plain compute on (company setting × partner entity type), and _post()
creates the outbound flow with no other condition — nothing at invoice,
journal or contract level can exclude a document.
The real-world case: self-billing (mandat de facturation, CGI art. 289
I-2 — e.g. EDF OA self-billing photovoltaic producers; XP Z12-014 use case
19b, and its § 4.1.4.5.3 notes the practice is "souvent mis en œuvre").
The customer issues and deposits the invoice
(BT-3 = 389) through its platform; the seller only keeps a mirror
customer invoice in Odoo for accounting, usually in a sale journal
reserved to that mandatary. That mirror must never be sent — but with
send_out='b2b' (mandatory from September 2027 for SMEs) it is.
Verified on the SUPER PDP sandbox — two separate Odoo 18.0 databases,
fr-einvoicing 2bfc243:
| deposit |
platform verdict |
| nominal: the buyer (mandatary) deposits the self-billed invoice, BT-3 389 |
accepted — served to the seller as an incoming CustomerInvoice flow |
the bug: the seller's Odoo posts the accounting mirror (plain 380, Odoo's own number) with send_out='b2b' |
accepted silently — the buyer receives a second invoice for the same supply, no error anywhere (numbers differ, so the platform cannot match them) |
There is no seller-side fix: we also verified that a 389 deposited through
the seller's channel is refused outright — self-billed documents must
enter on the buyer's side, so blocking the mirror is the only correct
behaviour.
The twin gap: the flow import never requests incoming
CustomerInvoice flows (types_to_get), so the legally authoritative
self-billed invoice stays on the platform. Once we booked it manually
(invoice name = the mandatary's number), the existing
CustomerInvoiceLC machinery worked as-is (_match_invoice_from_event
matched by name and attached the ap_received event) — status-return for
customer invoices is already implemented; only fetching/booking the
invoice itself is missing.
Real errors observed (nominal workflow only — a mandatary deposits a
self-billed invoice, the seller's Odoo runs its regular flow import).
Every lifecycle status of the self-billed invoice arrives as an LC flow
whose matching invoice can never exist in Odoo, and ends in
state = 'error':
fr.einvoicing.flow ie_963185 CustomerInvoiceLC in error
odoo_error_details: No customer invoice/refund found with number F20260819_AUTOFACT_389_T6
fr.einvoicing.flow ie_974733 CustomerInvoiceLC in error
odoo_error_details: No customer invoice/refund found with number AUTOF-2026-0002
This repeats for each status event of each self-billed invoice, and the
errored flows cannot be retried from the UI: _process() only accepts
state == 'downloaded', so the button skips them silently. After booking
the invoice manually we had to reset the state by hand — reprocessing
then matched by name and attached the ap_received event correctly.
Suggested fixes, simplest first:
- a one-line guard in
_post(): and not move.fr_einvoicing_flow_id —
an invoice already carrying a flow must never spawn an outbound one;
- a journal-level flag ("invoices in this journal are issued by the
customer under a billing mandate — never send outbound") checked at the
same creation lock;
- later: fetch incoming
CustomerInvoice flows and book them as customer
invoices (we can contribute — a working proof-of-concept exists). One
design point: this fetch (and emitting the seller-side lifecycle
statuses on those invoices) must not be gated by
fr_ctc_send_out_invoice — self-billing is an exception where a sale
behaves as an incoming flow: a seller with send_out='none' (no
emission obligation before September 2027) still receives self-billed
invoices the moment it is registered in the directory. A dedicated
receive-side opt-in fits better, following the existing
fr_ctc_get_in_invoice pattern. Second design point: booking must be
preceded by a check that a real and still-valid billing mandate
exists for that mandatary (validity dates, mandatary identity matched
on the buyer block). The platform does check a mandate at deposit
time, but that check lives on the buyer's platform — the seller
cannot rely on it for a mandate it may have terminated: a self-billed
invoice with no active mandate must never be booked silently (alert
instead).
Full demonstration details (flow identifiers, step-by-step reproduction, side findings) are available on request.
The problem.
fr_ctc_send_out_invoiceis the only lever decidingwhether a posted customer invoice is sent:
fr_einvoicing_requiredis aplain compute on (company setting × partner entity type), and
_post()creates the outbound flow with no other condition — nothing at invoice,
journal or contract level can exclude a document.
The real-world case: self-billing (mandat de facturation, CGI art. 289
I-2 — e.g. EDF OA self-billing photovoltaic producers; XP Z12-014 use case
19b, and its § 4.1.4.5.3 notes the practice is "souvent mis en œuvre").
The customer issues and deposits the invoice
(BT-3 = 389) through its platform; the seller only keeps a mirror
customer invoice in Odoo for accounting, usually in a sale journal
reserved to that mandatary. That mirror must never be sent — but with
send_out='b2b'(mandatory from September 2027 for SMEs) it is.Verified on the SUPER PDP sandbox — two separate Odoo 18.0 databases,
fr-einvoicing
2bfc243:CustomerInvoiceflowsend_out='b2b'There is no seller-side fix: we also verified that a 389 deposited through
the seller's channel is refused outright — self-billed documents must
enter on the buyer's side, so blocking the mirror is the only correct
behaviour.
The twin gap: the flow import never requests incoming
CustomerInvoiceflows (types_to_get), so the legally authoritativeself-billed invoice stays on the platform. Once we booked it manually
(invoice
name= the mandatary's number), the existingCustomerInvoiceLCmachinery worked as-is (_match_invoice_from_eventmatched by name and attached the
ap_receivedevent) — status-return forcustomer invoices is already implemented; only fetching/booking the
invoice itself is missing.
Real errors observed (nominal workflow only — a mandatary deposits a
self-billed invoice, the seller's Odoo runs its regular flow import).
Every lifecycle status of the self-billed invoice arrives as an LC flow
whose matching invoice can never exist in Odoo, and ends in
state = 'error':This repeats for each status event of each self-billed invoice, and the
errored flows cannot be retried from the UI:
_process()only acceptsstate == 'downloaded', so the button skips them silently. After bookingthe invoice manually we had to reset the state by hand — reprocessing
then matched by name and attached the
ap_receivedevent correctly.Suggested fixes, simplest first:
_post():and not move.fr_einvoicing_flow_id—an invoice already carrying a flow must never spawn an outbound one;
customer under a billing mandate — never send outbound") checked at the
same creation lock;
CustomerInvoiceflows and book them as customerinvoices (we can contribute — a working proof-of-concept exists). One
design point: this fetch (and emitting the seller-side lifecycle
statuses on those invoices) must not be gated by
fr_ctc_send_out_invoice— self-billing is an exception where a salebehaves as an incoming flow: a seller with
send_out='none'(noemission obligation before September 2027) still receives self-billed
invoices the moment it is registered in the directory. A dedicated
receive-side opt-in fits better, following the existing
fr_ctc_get_in_invoicepattern. Second design point: booking must bepreceded by a check that a real and still-valid billing mandate
exists for that mandatary (validity dates, mandatary identity matched
on the buyer block). The platform does check a mandate at deposit
time, but that check lives on the buyer's platform — the seller
cannot rely on it for a mandate it may have terminated: a self-billed
invoice with no active mandate must never be booked silently (alert
instead).
Full demonstration details (flow identifiers, step-by-step reproduction, side findings) are available on request.