Function map — the outbound and inbound chains, function by function

module 1.2.0, commit 05ffc589, generated 2026-09-07 · markdown version · lifecycle statuses

Memo, companion to LIFECYCLE-STATUSES.md. That one answers what the module does with each of the 14 lifecycle codes. This one answers who does it: which function is called, in which order, what it reads, what it writes, and which action of the Dolibarr core it triggers.

Two chains, read in opposite directions:

Provider examples use SuperPDPProvider; EsalinkPDPProvider implements the same abstract contract with its own API, and TestPDPProvider fakes it offline. Every file:line below is resolved against the source at build time, so it cannot rot: a renamed function makes the build report it instead of quietly lying.

1. Where everything is stored

Nothing in the two chains is stateless: each step leaves a trace, and knowing which table holds what is half of any diagnosis.

StorageOne row / file =Written byRead by
llx_einvoicing_documentone flow of the PA (invoice or status, in or out)Document::create() at the end of syncFlow()flow list and card, getLastSyncDate(), Document::reimport(), SupportExport
llx_einvoicing_extlinksthe PA state of one Dolibarr object: syncstatus, flow_id, routing override, precheckEInvoicing::insertOrUpdateExtLink()fetchLastknownInvoiceStatus(), isTransmittedLockActive(), the invoice list columns, the card blocks
llx_einvoicing_lifecycle_msgone lifecycle status message, in or out, with its PA validationstoreStatusMessage(), updated by updateStatusMessageValidation()hasSentStatusMessage(), getSendableStatusesForReceivedInvoice(), fetchStatusMessages(), the supplier invoice card and list
llx_einvoicing_routingone routing identifier of a third party (routing_type='thirdparty'), or its default import product (='product')addRouting(), setDefaultRouting(), setRoutingAsDefault(), and the COMPANY_CREATE/MODIFY triggergetBuyerCommunicationURI(), generateCdarFile() (MDT-73), the import (default product)
llx_einvoicing_extrafieldsone named property the module keeps on a core object without using the core extrafields, which an admin could rename or deleteinsertOrUpdateExtraField()getExtraFieldValue(), the supplier invoice card
llx_einvoicing_callone API call, or one whole synchronization run, with its request and response, secrets redactedAbstractPDPProvider::logCall()call list and card, SupportExport
invoice output dir<REF>_cii.xml or <REF>_facturx.pdf, the document actually transmittedCIIProtocol::generateInvoice() / FacturXProtocol::generateInvoice()getEInvoiceFilePath() (the file flag of the card), sendInvoice()
supplier invoice dirthe received document and the PA readable viewsaveEInvoiceFileToSupplierInvoiceAttachment()getXmlData(), the consistency check, the CDAR vendor identity

2. OUT — a customer invoice becomes an e-invoice

The chain

  1. A USER VALIDATES AN INVOICE (or clicks Generate, or a mass action, or the API / cron)
  2. TRIGGER BILL_VALIDATE
    • setInvoiceValidatedInThisRequest() — the marker the PDF hook reads
    • fetchLastknownInvoiceStatus(), then setEInvoiceStatus()
    • -> extlinks: NOT_GENERATED (in scope) or IGNORE (out of scope)
  3. the core then rebuilds the invoice PDF
  4. HOOK afterPDFCreation()
    • the single entry point of the generation
    • afterODTCreation() forwards to it
  5. the five gates, in order
    • 1. isEInvoiceGenerationInProgress() — we are the ones rebuilding: stop
    • 2. mustManageEInvoice() — out of scope (B2C, foreign third party...)
    • 3. isIgnoredStatus() and status != DRAFT
    • 4. EINVOICING_EINVOICE_IN_REAL_TIME, and not EINVOICING_DISABLE_SYNC_DOLI_TO_AP
    • 5. checkRequiredinformations() — -1 aborts or warns, per option
  6. non blocking: checkRecipientRoutableForSend() only warns
  7. ProtocolManager::getProtocol()
    • EINVOICING_PROTOCOL -> CIIProtocol or FacturXProtocol
  8. protocol->generateInvoice($invoice, $outputlangs, $pdfPath)
  9. buildinvoicelines.inc.php
    • collects $invoiceData and $linesData
    • reads Facture and its lines, mysoc, buyer, contacts, Account,
    • Project, Expedition/Commande, VAT dictionary, einvoicing_routing
  10. buildXML()
    • arrays -> CII DOM, gated by the profile
  11. checkBusinessRules()
    • EN 16931 safety net, and: does the document claim
    • the amount the invoice claims (warn, or abort in strict mode)
  12. the file that is actually transmitted
    CII
    • dol_copy() to
    • <output>/<REF>/<REF>_cii.xml
    Factur-X
    • resolve the source PDF, merge,
    • restore the metadata ->
    • <REF>_facturx.pdf (PDF/A-3)
  13. then, still inside generateInvoice()
    • hook afterEinvoiceCreation (other modules)
    • setEInvoiceStatus(STATUS_GENERATED) -> extlinks
    • checkFileSizeLimit() -> warnings
  14. back in afterPDFCreation()
  15. EINVOICING_AP_PRECHECK == 'auto' ? the PA validator, per provider
    SuperPDP
    • has_validator = 1
    • the file is validated at the platform
    • -> extlinks.ap_precheck_status / _result
    • a failure blocks the auto-send below
    Esalink (Hubtimize)
    • has_validator = 0
    • nothing is validated at the platform
    • -> no ap_precheck_* is ever written
    • by hand from the card: -1, no validator
  16. EINVOICING_AUTO_SEND_ON_GENERATION ? the four conditions
    • the option is on
    • isInvoiceValidatedInThisRequest() — this rebuild follows a validation
    • empty(status.transmitted) and not isTransmittedLockActive()
    • the precheck did not fail
  17. provider->sendInvoice($invoice) — the deposit, per provider
    SuperPDP
    • api.superpdp.tech/afnor-flow/v1/
    • POST flows (multipart), then
    • GET flows/{id}?docType=Metadata
    • extlink AWAITING_VALIDATION, then the ack
    Esalink (Hubtimize)
    • hubtimize.fr/api/orchestrator/v1/
    • POST flows (multipart), then
    • GET flows/{id}?docType=Metadata
    • extlink AWAITING_VALIDATION, then the ack

The manual paths join the same chain further down, through the two shared helpers, so their gates can never diverge from the automatic one:

Entry pointActionJoins at
Card button GeneratedoActions('generate_einvoice')generateOneEInvoice() → protocol->generateInvoice()
Card button SenddoActions('send_to_pdp')sendOneInvoiceToAccessPoint() → provider->sendInvoice()
Card button PrecheckdoActions('precheck_einvoice')provider->validateEInvoiceFile()
Invoice list, mass actiondoMassActions('einvoicing_generate')generateOneEInvoice()
Invoice list, mass actiondoMassActions('einvoicing_send_to_pdp')sendOneInvoiceToAccessPoint()

Function by function

FunctionSourceRoleReadsWrites / core action
TRIGGER BILL_CREATEcore/triggers/interface_98_modEInvoicing_EInvoicingTriggers.class.php:150Stamps a brand new invoice with the e-invoicing status it qualifies for, or with the one the creation form posted.needEInvoiceManagement(), GETPOST('seteinvoicestatus')setEInvoiceStatus() → extlinks
TRIGGER BILL_VALIDATEcore/triggers/interface_98_modEInvoicing_EInvoicingTriggers.class.php:172Opens the outbound chain. Leaves the request marker the PDF hook reads to know a validation is what caused the coming rebuild, then records the status the invoice qualifies for.Facture, llx_einvoicing_extlinks, EINVOICING_DISABLE_SYNC_DOLI_TO_APsetInvoiceValidatedInThisRequest() (request-scoped marker), setEInvoiceStatus() → extlinks
TRIGGERS BILL_UNVALIDATE / DELETE / MODIFYcore/triggers/interface_98_modEInvoicing_EInvoicingTriggers.class.php:210The three locks of a transmitted invoice. UNVALIDATE and DELETE refuse outright; MODIFY refuses only when one of the locked fields moved (ref, dates, currency, totals, third party, payment terms and mode).isTransmittedLockActive() → extlinks.flow_idnothing — returns a negative result, which aborts the core action
HOOK afterPDFCreation()class/actions_einvoicing.class.php:69The single orchestrator of the generation and of the auto-send. afterODTCreation() forwards to it, so an ODT invoice template follows the same path.$parameters['file'] and ['object'], extlinks, the five gates belowcalls the protocol then the provider; setEventMessages(); fills $this->warnings, which the core carries back onto the object from Dolibarr 23 on
mustManageEInvoice()class/einvoicing.class.php:3630The boolean question "is this invoice in scope". Never test needEInvoiceManagement() for truth: its codes meaning "out of scope" are truthy.needEInvoiceManagement()—
needEInvoiceManagement()class/einvoicing.class.php:3551Which e-invoicing status the invoice qualifies for: NOT_GENERATED (in scope), IGNORE or IGNORE_2 (out).Facture, Societe, country, EINVOICING_SKIP_B2C and friends—
checkRequiredinformations()class/einvoicing.class.php:1508The pre-flight: everything the document needs before it can be built (seller, buyer, identifiers, routing, invoice itself).validateMyCompanyConfiguration(), validatethirdpartyConfiguration(), validateInvoiceConfiguration(), validateChorusInformations()returns res −1 / 0 / 1 and a message; −1 aborts or only warns per EINVOICING_EINVOICE_CANCEL_IF_EINVOICE_FAILS
checkRecipientRoutableForSend()class/einvoicing.class.php:2753Is the recipient reachable in the directory. Opt-in, and fails open on every answer that cannot be trusted, so it never blocks unexpectedly.getBuyerCommunicationURI() — the very address the document will carry (BT-49) — then provider->checkRecipientDirectory()a warning at generation; blocks only the transmission, and only with EINVOICING_REQUIRE_ROUTABLE_RECIPIENT
ProtocolManager::getProtocol()class/protocols/ProtocolManager.class.php:123Hands out the protocol named by EINVOICING_PROTOCOL: CIIProtocol or FacturXProtocol.EINVOICING_PROTOCOL—
CIIProtocol::generateInvoice()class/protocols/CIIProtocol.class.php:544Produces the transmitted file: builds the XML, copies it to its final name, fires the afterEinvoiceCreation hook, records the status and checks the size.Facture, the temp XML<output>/<REF>/<REF>_cii.xml, setEInvoiceStatus(GENERATED), checkFileSizeLimit() → warnings
generateXML()class/protocols/CIIProtocol.class.php:338Data collection then serialization: includes buildinvoicelines.inc.php, calls buildXML(), runs the local business rules, writes the temp XML.$invoiceData and $linesData built by the include<temp>/<REF>/einvoice.xml
buildinvoicelines.inc.phplib/buildinvoicelines.inc.php:1The whole Dolibarr → EN 16931 mapping. Fills $invoiceData and $linesData. A field missing from the transmitted document is missing here, not in buildXML().Facture and its lines, mysoc, the buyer, the contacts, Account, Project, Expedition/Commande, the VAT dictionary, llx_einvoicing_routingnothing persistent
buildXML()class/protocols/CIIProtocol.class.php:2065Arrays → CII DOM, gated by the profile: MINIMUM, BASIC WL, EN 16931 or EXTENDED / EXTENDED-CTC-FR each allow a different set of terms.$invoiceData, $linesData, getBuildXmlProfile()the XML string
checkBusinessRules()class/protocols/AbstractProtocol.class.php:203The local safety net: a subset of the EN 16931 rules, plus checkDocumentClaimsTheInvoiceAmount(), which refuses to let a document announce an amount the invoice does not carry.the built XML and the invoice$this->warnings, or aborts in strict mode
FacturXProtocol::generateInvoice()class/protocols/FacturXProtocol.class.php:81Same XML, then the embedding: resolve the source PDF (the one the hook handed over, else the most recent one of the output dir, else regenerate it), copy it, merge the XML into a PDF/A-3, restore the metadata.the generated XML, the source PDF, DOL_VERSION (which merger to use)<output>/<REF>/<REF>_facturx.pdf; guards the rebuild with setEInvoiceGenerationInProgress()
SuperPDP sendInvoice()class/providers/SuperPDPProvider.class.php:1100Deposits the file at the PA, then immediately asks for the acknowledgement — the PA may still be analyzing, hence the two-step status.the file on disk, resolveFlowProfile() (declared from what the document carries, never hardcoded), a UUID correlation idPOST flows (multipart), GET flows/{id}?docType=Metadata; insertOrUpdateExtLink() twice; addEvent() in the agenda; logCall() → llx_einvoicing_call
Esalink sendInvoice()class/providers/EsalinkPDPProvider.class.php:431Deposits the file on hubtimize.fr/api/orchestrator/v1/: POST flows in multipart, then GET flows/{id}?docType=Metadata for the acknowledgement, with its own credentials and its own authentication.the generated file in the invoice output dir, resolveFlowProfile()insertOrUpdateExtLink() twice (AWAITING_VALIDATION, then the acknowledged status), addEvent(), logCall()
SuperPDP validateEInvoiceFile()class/providers/SuperPDPProvider.class.php:1038The PA-side validator, run before sending when EINVOICING_AP_PRECHECK is on auto, or from the card button.the file on disk, hasValidator()extlinks.ap_precheck_status / ap_precheck_result
Esalink validateEInvoiceFile()class/providers/EsalinkPDPProvider.class.php:412Esalink declares has_validator = 0, so hasValidator() is false: the automatic precheck never runs and no ap_precheck_* is ever written. Called by hand from the card it answers -1, NoAvailableValidatorforThisAccessPoint.$this->config['has_validator']nothing: no ap_precheck_* is ever written for this provider
sendOneInvoiceToAccessPoint()class/actions_einvoicing.class.php:1257The shared gate of the card button and of the mass action, so the two can never diverge.fetchLastknownInvoiceStatus(), isTransmittedLockActive(), checkRecipientRoutableForSend()provider->sendInvoice()
generateOneEInvoice()class/actions_einvoicing.class.php:1162The shared gate of the card button and of the mass action for the generation, so the two can never diverge.fetchLastknownInvoiceStatus(), isTransmittedLockActive(), the generation optionsprotocol->generateInvoice(), then the optional precheck
HOOK doMassActions()class/actions_einvoicing.class.php:1073The two mass actions of the invoice list, einvoicing_generate and einvoicing_send_to_pdp, looping on 'toselect' through the two helpers above.$parameters['toselect'], checkModulePrerequisites()a per-invoice recap of done / skipped / failed

What the document says, and where it comes from

buildinvoicelines.inc.php is the whole Dolibarr → EN 16931 mapping. Anything it does not fill simply does not exist in the transmitted document — which is why an audit of a missing field always starts in this file, never in buildXML().

BT / BGSource in DolibarrNote
BT-1 documentnoFacture::$ref
BT-3 documenttypecode_getTypeOfInvoice()380 / 381 / 384 / 386, from type and the situation
BT-2 documentdateFacture::$date
BT-8 VAT point date codeeinvoicingVatPointDateCode()product / service mix, and einvoicingVatOnDebits()
BT-10 buyerReferencearray_options.options_d4d_promise_code, else the Chorus service codenot the same thing as BT-46 scheme 0224
BT-13 orderReferenceFacture::$ref_client / ref_customer, else the first linked order
BT-25 / BT-26 (BG-3)preceding situation invoice, replaced invoice, deposit invoices_depositlines, invoiceRefDocs
BT-27…BT-34 seller$mysoc, einvoicingSellerTaxRegistrations(), getSellerCommunicationURI()BT-31 or BT-32 per VAT regime
BT-44…BT-49 buyerFacture::$thirdparty, getBuyerCommunicationURI()BT-49 = the routing of llx_einvoicing_routing, or the invoice-level override
BG-9 buyer contactthe external BILLING contact of the invoiceBT-56 / 57 / 58
BG-15 ship-toeinvoicingShipToFromContact()names a party, not a person
BT-73 / BT-74 (BG-14)einvoicingInvoicingPeriodFromLines()earliest start, latest end of the line periods
BT-81…BT-85 paymentAccount (fk_account, else FACTURE_RIB_NUMBER), _getPaymentMeanNumber()proprio / owner_name, falls back on mysoc->name
BT-9 paymentDueDateFacture::$date_lim_reglement
BT-129 / BT-131 / BT-146FactureLigne and calcul_price_total()the progress of a situation invoice is applied to the price
BT-118…BT-121 (BG-23)getCategoryRate() and the VAT dictionaryone breakdown per category + rate + exemption, never per vat_src_code
BT-153 / BT-154line label and description
BT-155product->refseller assigned id
BT-106…BT-115 totalsrecomputed here, not read from the stored onescheckBusinessRules() then confronts them with the invoice
business process idgetBillingProcessID()A1 / A2 … decided by the invoice, not by an option

3. OUT — a lifecycle status leaves Dolibarr (CDAR)

The three ways a status is emitted

  1. who fires, and on which invoice
    212 Encaissee — on OUR OWN invoice (we are the seller)
    • for each invoice paid by this payment, amount > 0
    • -> sendCashedInStatus()
    • AUTOMATIC, no option: the reform wants every
    • cash-in, partial ones included
    205 Approuvee — on a SUPPLIER invoice (we are the buyer)
    • shouldSendApprovedOnValidation()
    • (EINVOICING_SEND_APPROVED_ON_VALIDATION, off by default)
    • or the card button, which validates the draft first
    • then sends only if the validation did not already
    211 Paiement transmis — on a SUPPLIER invoice
    • EINVOICING_SEND_PAYMENT_SENT_STATUS
    • + getSommePaiement() > 0 + isEInvoice()
    • + not hasSentStatusMessage(211)
    210 Refusee, and 205/206/207/208 by hand
    • doActions('confirm_sendStatusMessage')
    • -> sendStatusMessage($object, code, reasonCode)
  2. provider->sendStatusMessage() — per provider, same path
    SuperPDP
    • is the code sendable for that object ?
    • CdarHandler::generateCdarFile()
    • POST flows, flowSyntax CDAR
    • storeStatusMessage(), then the verdict
    Esalink (Hubtimize)
    • is the code sendable for that object ?
    • CdarHandler::generateCdarFile()
    • POST flows, flowSyntax CDAR
    • storeStatusMessage(), then the verdict
  3. CdarHandler::generateCdarFile()
    • who issues (SE or BY), who receives, the MDT-73 ladder below,
    • and the MDG-43 blocks: MEN for a 212, MPA for a 211
  4. POST flows (multipart, flowSyntax CDAR)
  5. storeStatusMessage()
    • direction OUT, validation left pending
  6. the PA verdict comes back: ajax poller, or the next synchronization
  7. updateStatusMessageValidation()
    • updates the row, then dispatches
  8. onOutboundStatusMessageValidated()
    • the only side effect: a 210 accepted by the PA
    • abandons the local supplier invoice

Function by function

FunctionSourceRoleReadsWrites / core action
TRIGGER PAYMENT_CUSTOMER_CREATEcore/triggers/interface_98_modEInvoicing_EInvoicingTriggers.class.php:277Reports every cash-in as a 212, per payment line and not once when the invoice is fully paid: the reform expects the date and the amount of every payment, partial ones included.Paiement::$amounts, then each Facturedelegates to sendCashedInStatus()
sendCashedInStatus()core/triggers/interface_98_modEInvoicing_EInvoicingTriggers.class.php:488The gates of the 212: in scope, cash-in owed, invoice actually transmitted, and not sitting on STATUS_ERROR (a refused deposit holds no invoice at the PA to attach a cash-in to).mustManageEInvoice(), needCashedInStatus(), fetchLastknownInvoiceStatus()provider->sendStatusMessage($invoice, 212, '', ['amount' => …])
TRIGGER BILL_SUPPLIER_VALIDATEcore/triggers/interface_98_modEInvoicing_EInvoicingTriggers.class.php:302Three jobs on a received invoice being validated: the optional consistency check against its XML, closing the invoice a replacement supersedes, and answering 205 — validating a received invoice is the act of accepting it.SupplierInvoiceHelper::isEInvoice(), checkDolInvoiceAndEInvoiceConsistency(), shouldSendApprovedOnValidation()closeReplacedSupplierInvoice() (core), sendStatusMessage($object, 205). A send failure never rolls back the validation.
TRIGGER BILL_SUPPLIER_PAYEDcore/triggers/interface_98_modEInvoicing_EInvoicingTriggers.class.php:361Answers 211 once per invoice when it is classified paid. Opt-in, and guarded against a payment deleted then recorded anew, which makes Dolibarr classify the invoice paid a second time.EINVOICING_SEND_PAYMENT_SENT_STATUS, getSommePaiement(), isEInvoice(), hasSentStatusMessage()sendStatusMessage($object, 211, '', ['amount', 'date'])
shouldSendApprovedOnValidation()class/utils/SupplierInvoiceHelper.class.php:597Whether the validation of a received invoice must answer 205: the option, the invoice really coming from the platform, and no 205 already sent.EINVOICING_SEND_APPROVED_ON_VALIDATION, isEInvoice(), hasSentStatusMessage()—
SuperPDP sendStatusMessage()class/providers/SuperPDPProvider.class.php:2975Checks the code is sendable for that object type, builds the CDAR, posts it as a flow of syntax CDAR, records it and reads back the platform verdict. Names the platform when it refuses (issue #799).getEinvoiceStatusOptions($onlySendable), getStatusLabel()CdarHandler::generateCdarFile(), POST flows, storeStatusMessage(), updateStatusMessageValidation(), addEvent()
Esalink sendStatusMessage()class/providers/EsalinkPDPProvider.class.php:1798Checks the code is sendable for that object type, builds the CDAR with CdarHandler::generateCdarFile(), posts it as a flow of syntax CDAR, records it and reads back the platform verdict.getEinvoiceStatusOptions($onlySendable), the generated CDARstoreStatusMessage(), updateStatusMessageValidation(), addEvent()
getSendableStatusesForReceivedInvoice()class/einvoicing.class.php:1020Narrows the sendable list for one received invoice, from what has already been sent and from the state of the invoice.getEinvoiceStatusOptions(), lifecycle_msg—
CdarHandler::generateCdarFile()class/utils/CdarHandler.class.php:271Builds the CDAR on disk. Decides who issues the status and who receives it — get those two the wrong way round and the platform answers "no matching invoices found".mysoc, the third party, the stored received XML, the routing, the directorythe CDAR XML file; keeps recipientURIID and recipientURIIDOrigin for the error message
getCashedAmountCharacteristics()class/utils/CdarHandler.class.php:679The MEN blocks of a 212: an amount and a VAT rate per rate. BR-FR-CDV-14 makes them mandatory, and the platform answers 400 without them.the invoice, its VAT breakdown, the payment amount—; an empty result fails the CDAR here rather than at the platform
getPaymentSentCharacteristics()class/utils/CdarHandler.class.php:561The MPA block of a 211: what was paid and when. No rule makes it mandatory, so a status with no known amount is still sent, just bare.the supplier invoice and the payment data—
getVendorIdentityFromReceivedInvoice()class/utils/CdarHandler.class.php:609Reads the stored received XML for the vendor GlobalID and its BT-34. The platform indexed the incoming flow under those, which may differ from the Dolibarr third-party SIREN.SupplierInvoiceHelper::getXmlData()—
storeStatusMessage()class/einvoicing.class.php:3335One row per lifecycle status message, in or out, with its reason code and, for an outbound one, a validation left pending.—llx_einvoicing_lifecycle_msg
updateStatusMessageValidation()class/einvoicing.class.php:3496The PA verdict on a status we sent comes back here, from the ajax poller or from the synchronization. Reads the row first, then updates it, then dispatches.the lifecycle_msg row (element_type, element_id, lc_status, lc_reason_code)lifecycle_msg, then SupplierInvoiceHelper::onOutboundStatusMessageValidated()
onOutboundStatusMessageValidated()class/utils/SupplierInvoiceHelper.class.php:639The only side effect of a status accepted by the PA: a 210 accepted abandons the local supplier invoice.the lifecycle row, getReasonsByStatus() for the labelabandonRefusedSupplierInvoice() → core validate() then setCanceled()
hasSentStatusMessage()class/einvoicing.class.php:3398Whether a given status was already sent for an object — the idempotency of the 205 and of the 211, whatever the path that sent it.llx_einvoicing_lifecycle_msg—

The MDT-73 ladder — who the CDAR is addressed to

  1. Getting this wrong is what makes the platform answer "Electronic address (MDT-73) is invalid" or "no matching invoices found".
  2. status 212, our own invoice
    • recipient = the BUYER, thirdpartyidprof(), scheme 0002
  3. every other status (supplier invoice, we are the buyer): first rung that answers wins
    • 1. fetchDefaultRouting($vendor) — a routing recorded in Dolibarr — origin 'routing'
    • 2. BT-34 of the received invoice — the vendor said where it is — origin 'einvoice' (its own scheme is kept, not relabelled 0225)
    • 3. provider->checkRecipientDirectory(SIREN) — the national directory — origin 'directory'
    • 4. getBuyerCommunicationURI($thirdparty) — the SIREN, as a guess — origin 'thirdpartyid'
  4. $cdarHandler->recipientURIID and ->recipientURIIDOrigin keep the answer, so the caller can name the address and the vendor in the error it shows.

4. IN — the synchronization

The chain

  1. CRON "EInvoicing flows synchronization", hourly (declared in modEInvoicing::$cronjobs)
  2. Document::cronSyncFlows()
    • provider = PDPProviderManager::getProvider(EINVOICING_PDP)
    • syncFromDate = getLastSyncDate()
    • maxflows = EINVOICING_FLOWS_SYNC_CALL_SIZE (100)
  3. provider->syncFlows() — the one real difference between the two
    SuperPDP
    • api.superpdp.tech/afnor-flow/v1/
    • POST flows/search, no total in the answer
    • walks the updatedAfter cursor
    • MAX_SYNC_BATCHES batches at most
    • discards the flows already stored
    Esalink (Hubtimize)
    • hubtimize.fr/api/orchestrator/v1/
    • POST flows/search, the answer has a total
    • one single call sized on that total
    • no cursor, no batch loop
    • discards the flows already stored
  4. provider->syncFlow($flowId, $call_id) — same shape on both
    SuperPDP
    • api.superpdp.tech/afnor-flow/v1/
    • GET flows/{id}?docType=Metadata
    • fills a Document: flow_type, direction,
    • syntax, profile, ack_*, submittedAt
    • then the switch below
    Esalink (Hubtimize)
    • hubtimize.fr/api/orchestrator/v1/
    • GET flows/{id}?docType=Metadata
    • fills a Document: flow_type, direction,
    • syntax, profile, ack_*, submittedAt
    • then the switch below
  5. switch ($document->flow_type)
    "CustomerInvoice"
    • an invoice WE sent, seen from the PA
    • Facture::fetch(0, trackingId)
    • -> the flow is only linked
    "SupplierInvoice"
    • a RECEIVED invoice -> the import below
    • direction must be 'In': an outgoing one
    • is not a received invoice, the flow is
    • marked processed and nothing is booked
    "CustomerInvoiceLC"
    • a status ON ONE OF OUR INVOICES
    • -> the CDAR section below
    "SupplierInvoiceLC"
    • direction 'In': a status the VENDOR issues
    • direction 'Out': OUR status coming back validated
    • -> fetchStatusMessages(flowId) then
    • updateStatusMessageValidation()
    "" (empty)
    • a late validation answer for a sent invoice
    • -> extlinks STATUS_ERROR + agenda event
  6. $document->create($user)
    • one row in einvoicing_document, always,
    • whatever happened above

Two return shapes matter and are easy to confuse:

A received document becomes a supplier invoice

  1. fetchImportableFlowDocument()
    • tries docType=Converted, then Original, then ReadableView
    • each through detectProtocolFromContent()
    • none readable -> postponeflow + "set the AP conversion format"
  2. createSupplierInvoiceFromSource()
    • per-call temp file in_<uid>.xml (two concurrent syncs
    • must not parse each other's invoice)
    • finally: commit or rollback the import transaction
  3. doCreateSupplierInvoiceFromSource()
  4. parseInvoiceHeader() / parseInvoiceLines()
    • pure XPath over $invoiceTemplate and $lineTemplate
  5. _syncOrCreateThirdpartyFromEInvoiceSeller()
    • OWN TRANSACTION, committed before the import starts
    • 1. Societe::fetch() by idprof1..6 from the seller GlobalIDs
    • 2. else by VAT number (several matches = error)
    • 3. else, only with EINVOICING_THIRDPARTIES_MATCH_ON_NAME, findNearest()
    • 4. else Societe::create(), or refuse if auto-creation is off
  6. === FROM HERE ON, ONE TRANSACTION FOR THE WHOLE IMPORT ===
  7. the two refusals that come before anything is written
    • already imported for this supplier ? findIdByRef(ref_supplier, socid) -> only (re)attach the files
    • every referenced document present ? no -> postponeflow, with a link to the screen that creates the missing invoice
  8. fill the FactureFournisseur, then
    • createHeaderDiscounts() — document-level allowances -> DiscountAbsolute
    • FactureFournisseur::create($user) <- CORE
    • _saveImportedBuyerOrderReference() -> llx_einvoicing_extrafields
    • _linkSupplierInvoiceToPurchaseOrder() -> core add_object_linked('order_supplier')
  9. createSupplierInvoiceLinesFromSource()
    • per line: findProductFromEinvoiceLine()
    • else _findOrCreateProductFromEinvoiceLine() -> Product::create()
    • else the vendor's default routing product
    • else a free line
  10. createSupplierInvoiceLinesIntoDatabase()
    • INSERT a bare facture_fourn_det row, then core updateline()
    • deliberately NOT addline()
  11. then everything that hangs off the invoice
    • deposits: getOrCreateDepositDiscount() -> DiscountAbsolute::create()
    • Societe::update() to flag the third party as a supplier if needed
    • createHeaderChargeLines() — document-level charges (BG-21) as lines
    • ProductFournisseur::update_buyprice() — supplier prices of the imported products
    • insertOrUpdateExtLink($supplierInvoiceId, 'invoice_supplier', $flowId)
    • saveEInvoiceFileToSupplierInvoiceAttachment() x2 — document and readable view
  12. alignInvoiceTotalsWithDocument()
    • every line exists now: confront the totals with BT-112 / BT-115
    • core update_price()

Function by function

FunctionSourceRoleReadsWrites / core action
Document::cronSyncFlows()class/document.class.php:1572The scheduled job. Loads the provider itself — the cron runner includes this class file and nothing else — then reports the run in $this->output.EINVOICING_PDP, getLastSyncDate(), EINVOICING_FLOWS_SYNC_CALL_SIZEdelegates to syncFlows(); the job output carries the manual actions to do
getLastSyncDate()class/providers/AbstractPDPProvider.class.php:831Where the synchronization starts from: the highest updatedAt already stored for this provider and this entity. Never getEntity() here — one entity at a time.MAX(updatedat) of llx_einvoicing_document—
SuperPDP syncFlows()class/providers/SuperPDPProvider.class.php:1782Walks the flow window in batches. This search endpoint ignores offset and page, caps a batch and reports no total, so the only cursor is updatedAfter and the batches walk it.POST flows/search, then SELECT flow_id FROM einvoicing_document to discard what is already storedone Call row for the whole run; collects the business actions and the postponed flows
Esalink syncFlows()class/providers/EsalinkPDPProvider.class.php:856Its flow search answers a total, so the run asks for that many rows in one call: no updatedAfter cursor to walk and no batch ceiling. Flows already stored are discarded, and an unreadable one is postponed.POST flows/search, the total it returnsone Call row for the run; same postponeflow handling as SuperPDP
SuperPDP syncFlow()class/providers/SuperPDPProvider.class.php:2209One flow. Reads its metadata into a Document object, then dispatches on flow_type. Whatever happens, the Document row is written at the end.GET flows/{id}?docType=MetadataDocument::create() → llx_einvoicing_document
Esalink syncFlow()class/providers/EsalinkPDPProvider.class.php:1223Reads the flow metadata into a Document, dispatches on flow_type over the five flow types, and writes the Document row at the end whatever happened.GET flows/{id}?docType=MetadataDocument::create() → llx_einvoicing_document
fetchImportableFlowDocument()class/providers/AbstractPDPProvider.class.php:460Retrieves the invoice in whichever shape this module can read: Converted, then Original, then the readable view. Asking only for the Converted one makes the import depend on a setting that lives on the PA account.fetchFlowData() x3, detectProtocolFromContent()—; when nothing is readable the flow is postponed, never dropped
detectProtocolFromContent()class/protocols/ProtocolManager.class.php:163Recognizes what an incoming payload actually is, whatever the PA said it was: a CII XML, a Factur-X PDF, or nothing this module can read.the raw content—
createSupplierInvoiceFromSource()class/protocols/CIIProtocol.class.php:677The public wrapper. Owns the per-call temp files — a fixed file slot would let two concurrent syncs parse each other's invoice — and closes in a finally block the transaction the import opened, so no early return and no exception can leave one open.conf->einvoicing->dir_temptemp files; commit or rollback of the import transaction
doCreateSupplierInvoiceFromSource()class/protocols/CIIProtocol.class.php:793The import itself, from the parsed document to the booked invoice with its lines, discounts, charges, links and attachments.parseInvoiceHeader(), parseInvoiceLines()FactureFournisseur::create() and everything that hangs off it
FacturXProtocol::doCreateSupplierInvoiceFromSource()class/protocols/FacturXProtocol.class.php:406Extracts the XML out of the received PDF, then runs the same CII import path.extractXmlFromFileContent(), checkFacturxStructure()FactureFournisseur::create(), its lines, discounts, charges, links and attachments
parseInvoiceHeader()class/protocols/CIIProtocol.class.php:1739Pure XPath over $invoiceTemplate: every header term of the document, plus the date and amount normalisation.the XML and the template declared in the constructor—
parseInvoiceLines()class/protocols/CIIProtocol.class.php:1789Same, per IncludedSupplyChainTradeLineItem, over $lineTemplate.the XML and the line template—
_syncOrCreateThirdpartyFromEInvoiceSeller()class/protocols/CommonProtocol.class.php:456Resolves the vendor. Its own transaction, committed before the import starts: a business error further down must not roll back the third party the operator is precisely being asked to complete.sellerGlobalIds merged with SpecifiedLegalOrganization, VAT number, EINVOICING_THIRDPARTIES_MATCH_ON_NAMESociete::fetch() by idprof1..6, then Societe::create() or Societe::update()
findProductFromEinvoiceLine()class/protocols/CommonProtocol.class.php:984Matches a line to a product of the base, by the vendor reference first.the parsed line, the supplier product references—
_findOrCreateProductFromEinvoiceLine()class/protocols/CommonProtocol.class.php:1096Creates the product when the setup allows it, guessing its type from what the line says. Otherwise the line falls back on the vendor's default routing product, or stays a free line._detectProductTypeFromEinvoiceLine(), the 'product' routing of the vendorProduct::create()
createSupplierInvoiceLinesFromSource()class/protocols/CIIProtocol.class.php:1236Builds the lines in memory: product resolution, description, discount, quantity, unit price, VAT, period, deposit handling and the line-level referenced documents.the parsed lines, resolveLineUnitPrice(), resolveLineAmounts(), resolveLinePeriod()$supplierInvoice->lines, plus the supplier price entries to create afterwards
createSupplierInvoiceLinesIntoDatabase()class/protocols/CIIProtocol.class.php:726Writes the lines: a bare facture_fourn_det row, then the core updateline(). Deliberately not addline(), which does not re-price, overwrites ref_supplier and refuses a line with no supplier price.$supplierInvoice->linesFactureFournisseur::updateline() — so BT-131 is read and then dropped, the core recomputing from quantity x price
alignInvoiceTotalsWithDocument()class/protocols/CIIProtocol.class.php:3463Once every line exists, confronts the totals of the invoice with the ones the document announces, and picks the rounding mode that reconciles them when one does.the parsed header totalsFactureFournisseur::update_price(), and a message when they still disagree
saveEInvoiceFileToSupplierInvoiceAttachment()class/protocols/CIIProtocol.class.php:3205Files the received document and the PA readable view in the attachment directory of the supplier invoice.the temp filesthe documents directory of the invoice
findIdByRef()class/utils/SupplierInvoiceHelper.class.php:677Finds the supplier invoice of a vendor by its reference. Exact match by default — a wrong match silently drops an invoice or links the new one to the wrong document. EINVOICING_TOLERANT_SUPPLIER_REF_MATCH adds a narrow, delimited substring fallback, and reports an ambiguity instead of guessing it.llx_facture_fourn.ref_supplier, per supplier—
SupplierInvoiceHelper::isEInvoice()class/utils/SupplierInvoiceHelper.class.php:397Whether a supplier invoice came from the platform, and whether two flows claim it (a duplicate, which makes every comparison meaningless).llx_einvoicing_document—
checkDolInvoiceAndEInvoiceConsistency()class/utils/SupplierInvoiceHelper.class.php:66Confronts the local invoice with the XML it came from: amounts, VAT, dates. Opt-in, run at validation.getXmlData(), getVatDetails()—; a mismatch refuses the validation
processIncomingSupplierInvoiceStatus()class/providers/SuperPDPProvider.class.php:2812A status the vendor issues about one of its own invoices — a 212 answering our 211, typically. The mirror of what the CustomerInvoiceLC case does for our customers' statuses.GET flows/{id}?docType=Original, CdarHandler, findSupplierInvoiceByVendorReference()storeStatusMessage(direction IN); never a negative result, so one unattachable status cannot stall the run
TRIGGER BILL_SUPPLIER_DELETEcore/triggers/interface_98_modEInvoicing_EInvoicingTriggers.class.php:394Only a draft may go: a draft is a local booking, holds no accounting entry and says nothing to the platform. The incoming flow is kept and detached, so the document stays in the flow list and can be imported again.the invoice re-read through the core class (->status, not the deprecated ->statut)detachEInvoicingRecordsOfSupplierInvoice() → einvoicing_document.fk_element_id = 0
Document::reimport()class/document.class.php:548The way out of a bad import: delete the draft, sync the flow again, give the new invoice the reference of the draft it replaces, move the lifecycle history onto it, and adopt the row the import wrote so the flow keeps its line in the list.flow_direction, fk_element_type, the invoice statusFactureFournisseur::delete(), syncFlow(), reuseDraftRef(), moveHistoryToInvoice(), adoptRecordWrittenByImport()
Document::cleanXmlData()class/document.class.php:1649Strips the attachments out of the received XML before it is stored, and checkXmlDataMaxSize() keeps it under the MEDIUMTEXT ceiling.the raw XMLeinvoicing_document.xml_data

XML → Dolibarr, the field map of the import

Parsed key (BT)Written toNote
documentno (BT-1)FactureFournisseur::$ref_supplieralso the duplicate key, per supplier
documenttypecode (BT-3)$type, via getDolibarrInvoiceType()an unknown code refuses the import
documentdate (BT-2)$date
invoiceRefDocs[0] (BT-25)$fk_facture_sourcecredit note and replacement; left empty rather than linked wrong
invoiceCurrency (BT-5)$multicurrency_code
payment terms, due date, means_applyPaymentInfoToSupplierInvoice()
taxBasisTotalAmount / taxTotalAmount / grandTotalAmount$total_ht / $total_tva / $total_ttcthen recomputed and confronted by alignInvoiceTotalsWithDocument()
orderReference (BT-13)llx_einvoicing_extrafields.buyer_order_reference, and add_object_linkedkept even when it matches no order
the seller blockSociete, created or updatedthe four steps above
prodsellerid (BT-155)$line->ref_supplier
prodname / proddesc (BT-153/154)$line->desckept even when a real product is linked
billedquantity (BT-129)$line->qty
net price, gross price, allowances$line->subprice, $line->remise_percentresolveLineUnitPrice(), resolveLineAmounts()
rateApplicablePercent (BT-152)$line->tva_tx
lineTotalAmount (BT-131)read, then droppedupdateline() recomputes from quantity x price
linePeriodStart / End (BT-134/135)$line->date_start / date_end
headerAllowancesCharges (BG-20/21)discounts and charge lines
the whole XMLeinvoicing_document.xml_dataattachments stripped, size checked

A status arrives on one of our invoices

  1. flow_type "CustomerInvoiceLC"
  2. CdarHandler::readFromString()
    • GET flows/{id}?docType=Original -> the CDAR XML
    • AcknowledgementDocument.ReferenceReferencedDocument.IssuerAssignedID
    • -> Facture::fetch(0, $ref), entity checked
  3. the three writes
    • insertOrUpdateExtLink(facture, flowId, ProcessConditionCode, ...) — the card and the list now show the PA status
    • storeStatusMessage(..., direction IN, reasonCode) — the status history of the invoice
    • addEvent('STATUS', ...) — the agenda of the invoice
  4. The switch on the lifecycle code is, today, deliberately empty: no Dolibarr status is derived from an incoming code.

5. Cross-cutting

The locks, and what they key on

LockFunctionKeys onEffect
transmittedisTransmittedLockActive()extlinks.flow_id, never clearedblocks unvalidate, delete, modification of the locked fields, re-send and regeneration; opt-out EINVOICING_ALLOW_RESEND_TRANSMITTED
transmitted (weak)fetchLastknownInvoiceStatus()['transmitted']extlinks.syncstatusreset to GENERATED by a regeneration — which is exactly why the auto-send carries both guards
generation re-entryisEInvoiceGenerationInProgress()a request-scoped staticstops the PDF hook from generating the document a second time while the module itself is rebuilding the PDF
validation originisInvoiceValidatedInThisRequest()a request-scoped static, set by BILL_VALIDATErestricts the auto-send to the rebuild that follows a validation, not to every later PDF rebuild (a payment, the Generate button, a mass rebuild)
status already senthasSentStatusMessage()lifecycle_msgone 205 and one 211 per invoice, whatever the path
flow already importedSELECT flow_ideinvoicing_documentthe synchronization never re-imports a flow it stored
invoice already importedfindIdByRef()ref_supplier, per supplierexact match by default; EINVOICING_TOLERANT_SUPPLIER_REF_MATCH adds a narrow fallback

Who consumes what the two chains wrote

ConsumerWhereReads
customer invoice card blockEInvoiceCardBlock(), rendered by the formObjectOptions hookextlinks (status, flow_id, precheck, routing override), the file on disk, lifecycle_msg
supplier invoice card blocksupplierInvoiceCardBlock()lifecycle_msg, einvoicing_document, einvoicing_extrafields
third party card blockthirdpartyCardBlock()einvoicing_routing, the directory check
invoice list columnsprintFieldListSelect/From/Where/GroupBy/Option/Title/Valueextlinks and lifecycle_msg, joined into the core list query
status pollersajax/checkinvoicestatus.php, ajax/checksupplierinvoicestatus.phpthe PA, then write back into extlinks and lifecycle_msg
flow list and carddocument_list.php, document_card.phpeinvoicing_document
call list and cardcall_list.php, call_card.phpeinvoicing_call
support archiveclass/utils/SupportExport.class.phpall of the above, secrets re-redacted
consistency checkcheckDolInvoiceAndEInvoiceConsistency()the stored XML against the local invoice, at validation (opt-in)

The options that decide, by chain

ConstantChainWhat it decides
EINVOICING_PDPbothwhich access point provider answers: SuperPDP, Esalink, the offline test one
EINVOICING_ONLY_GENERATEbothhidden: offers the generate-only provider, which talks to no platform, to a French company that files its documents by hand
EINVOICING_DEBUG_MODEbothkeeps the temp XML and stores the raw PA response on the flow
EINVOICING_DISABLE_SYNC_DOLI_TO_APOUTswitches the whole outbound side off
EINVOICING_EINVOICE_IN_REAL_TIMEOUTgenerate the e-invoice on validation
EINVOICING_EINVOICE_CANCEL_IF_EINVOICE_FAILSOUTa failed pre-flight aborts, instead of only warning
EINVOICING_AUTO_SEND_ON_GENERATIONOUTdeposit at the PA right after a validation
EINVOICING_AP_PRECHECKOUTon auto, run the PA validator before sending
EINVOICING_ENABLE_API_VALIDATIONOUTadds the PA validation of the third party to the pre-flight checks
EINVOICING_PRECHECK_DIRECTORYOUTthe card checks the directory for an invoice not yet transmitted (with EINVOICING_LIVE)
EINVOICING_PROTOCOLOUTCII or FACTURX: which file is produced and transmitted
EINVOICING_XML_PROFILEOUTthe profile buildXML() writes, falling back on the one of the protocol
EINVOICING_BR_CHECKOUTnocheck, warning_only (default) or blocking for checkBusinessRules()
EINVOICING_MAX_FILE_SIZE_MBOUTthe size above which the generated file raises a warning
EINVOICING_SKIP_B2COUTa third party that is not a company leaves the e-invoicing scope
EINVOICING_DEFAULT_EINVOICE_STATUS_FOR_POS, EINVOICING_NAME_OF_MODULESOURCE_THAT_ARE_POSOUTthe status given to an invoice a point of sale module issued
EINVOICING_BLOCK_INVOICE_NO_ROUTING_IDOUTno routing recorded = refuse, instead of falling back on the legal identifier
EINVOICING_REQUIRE_ROUTABLE_RECIPIENTOUTdirectory gate: 1 = a confirmed unroutable recipient blocks, 2 = an undetermined answer blocks too
EINVOICING_USE_CHORUSOUTthe Chorus service code, and the checks that go with it
EINVOICING_USE_BILLING_CONTACT_AS_BUYEROUTthe billing contact becomes the buyer party, not only the buyer contact group
EINVOICING_USE_DOLIBARR_ALREADY_CALCULATED_AMOUNTSOUTthe line amounts are read from the invoice instead of being recomputed
EINVOICING_PMT, EINVOICING_PMD, EINVOICING_AABOUTthe text of the three legal notes; empty falls back on the translation
EINVOICING_ALLOW_RESEND_TRANSMITTEDOUTdisarms the transmitted lock
EINVOICING_ALLOW_REGEN_TRANSMITTEDOUTwhich actions stay open on a transmitted invoice
EINVOICING_SEND_APPROVED_ON_VALIDATIONOUT status205 on supplier invoice validation
EINVOICING_SEND_PAYMENT_SENT_STATUSOUT status211 when a supplier invoice is classified paid
EINVOICING_DISABLE_SYNC_AP_TO_DOLIINhides the inbound side, screens included
EINVOICING_FLOWS_SYNC_CALL_SIZEINbatch size of the flow search
EINVOICING_FLOWS_SYNC_CALL_LIMITINthe limit the manual synchronization form suggests
EINVOICING_SYNC_MARGIN_TIME_HOURSINhow far back the manual synchronization form starts, before the last known flow
EINVOICING_PREFER_ORIGINALINtry the Original document before the Converted one
EINVOICING_USE_EXTERNAL_FACTURX_READERINread a received Factur-X with the external library instead of the CII parser
EINVOICING_THIRDPARTIES_AUTO_GENERATIONINcreate the vendor when no identifier matches; without it the document is refused
EINVOICING_THIRDPARTIES_COMPLETE_INFOINcomplete an existing vendor with what the document says
EINVOICING_THIRDPARTIES_MATCH_ON_NAMEINhidden: re-enables the fuzzy vendor match (step 3)
EINVOICING_THIRDPARTIES_MATCH_ON_EMAILINhidden: adds the email address to the vendor lookup
EINVOICING_PRODUCTS_AUTO_GENERATIONINcreate the product a line names
EINVOICING_IMPORT_AS_FREE_LINESINimport every line as a free line, linked to no product
EINVOICING_TOLERANT_SUPPLIER_REF_MATCH, EINVOICING_TOLERANT_SUPPLIER_REF_MIN_LENGTHINhidden: narrow substring fallback on ref_supplier, and its floor (8)
EINVOICING_SUPPLIER_INVOICE_CHECK_CONSISTENCY_ON_VALIDATIONINconfront the local invoice with its XML at validation (offered in the setup only when EINVOICING_SUPPLIER_INVOICE_CHECK_CONSISTENCY_ON_VALIDATION_AVAILABLE is set)
EINVOICING_ALLOW_MULTICOMPANY_INVOICE_MOVEINthe extra entities findIdByRef() may look into

What actually differs between the two providers

StepSuperPDPEsalink (Hubtimize)
host / authapi.superpdp.tech/afnor-flow/v1/, OAuth (client credentials or authorization code)hubtimize.fr/api/orchestrator/v1/, username and password
sendInvoice()POST flows in multipart, then GET flows/{id}?docType=MetadataPOST flows in multipart, then GET flows/{id}?docType=Metadata
validateEInvoiceFile()has_validator = 1: validates the file at the platform and writes ap_precheck_*has_validator = 0: never validates, writes no ap_precheck_*, answers −1 when called by hand
syncFlows()the search answers no total: the run walks the updatedAfter cursor, MAX_SYNC_BATCHES batches at mostthe search answers a total: one single call sized on it, no cursor and no batch loop
syncFlow()GET flows/{id}?docType=Metadata, then the switch over the five flow typesGET flows/{id}?docType=Metadata, then the switch over the five flow types
sendStatusMessage()CdarHandler::generateCdarFile(), then POST flows with flowSyntax CDARCdarHandler::generateCdarFile(), then POST flows with flowSyntax CDAR
checkRecipientDirectory()overridden: its own directory API, plus a legacy fallbacknot overridden: keeps AbstractPDPProvider::checkRecipientDirectory()

6. Where to look when it goes wrong

SymptomStart here
no e-invoice file after a validationthe five gates of afterPDFCreation(), then checkRequiredinformations()
a field is missing from the XMLlib/buildinvoicelines.inc.php — not buildXML()
the amounts of the document disagree with the invoicecheckBusinessRules() → checkDocumentClaimsTheInvoiceAmount()
the invoice was never depositedthe four conditions of the auto-send block, then sendInvoice() and llx_einvoicing_call
the card shows a status the PA does not havefetchLastknownInvoiceStatus() — extlinks against lifecycle_msg
a status is refused with MDT-73the ladder of generateCdarFile(), and recipientURIIDOrigin in the error message
the synchronization reports "skipped, 0 new"the updatedAfter cursor and getLastSyncDate()
one flow blocks the whole runit should be postponed, not failed — check the postponeflow returns
a received invoice landed on the wrong vendorthe four steps of _syncOrCreateThirdpartyFromEInvoiceSeller()
a received line has the wrong amountresolveLineUnitPrice() and resolveLineAmounts(), then updateline() recomputing
the totals do not match the documentalignInvoiceTotalsWithDocument()
a received invoice cannot be deletedBILL_SUPPLIER_DELETE: only a draft goes, and the flow is detached, not deleted