Skip to content

l10n_fr_einvoicing: self-billed invoices — no way to exclude the mirror from the outbound flow, and incoming CustomerInvoice flows are never fetched #62

Description

@risto42

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:

  1. 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;
  2. 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;
  3. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions