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:
- OUT — a Dolibarr customer invoice becomes an e-invoice and leaves for the access point (PA), plus the lifecycle statuses this instance emits (CDAR).
- IN — the synchronization pulls flows from the PA: received invoices become supplier invoices, received statuses land on the invoice they refer to.
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.
| Storage | One row / file = | Written by | Read by |
|---|---|---|---|
llx_einvoicing_document | one 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_extlinks | the PA state of one Dolibarr object: syncstatus, flow_id, routing override, precheck | EInvoicing::insertOrUpdateExtLink() | fetchLastknownInvoiceStatus(), isTransmittedLockActive(), the invoice list columns, the card blocks |
llx_einvoicing_lifecycle_msg | one lifecycle status message, in or out, with its PA validation | storeStatusMessage(), updated by updateStatusMessageValidation() | hasSentStatusMessage(), getSendableStatusesForReceivedInvoice(), fetchStatusMessages(), the supplier invoice card and list |
llx_einvoicing_routing | one routing identifier of a third party (routing_type='thirdparty'), or its default import product (='product') | addRouting(), setDefaultRouting(), setRoutingAsDefault(), and the COMPANY_CREATE/MODIFY trigger | getBuyerCommunicationURI(), generateCdarFile() (MDT-73), the import (default product) |
llx_einvoicing_extrafields | one named property the module keeps on a core object without using the core extrafields, which an admin could rename or delete | insertOrUpdateExtraField() | getExtraFieldValue(), the supplier invoice card |
llx_einvoicing_call | one API call, or one whole synchronization run, with its request and response, secrets redacted | AbstractPDPProvider::logCall() | call list and card, SupportExport |
| invoice output dir | <REF>_cii.xml or <REF>_facturx.pdf, the document actually transmitted | CIIProtocol::generateInvoice() / FacturXProtocol::generateInvoice() | getEInvoiceFilePath() (the file flag of the card), sendInvoice() |
| supplier invoice dir | the received document and the PA readable view | saveEInvoiceFileToSupplierInvoiceAttachment() | getXmlData(), the consistency check, the CDAR vendor identity |
2. OUT — a customer invoice becomes an e-invoice
The chain
- A USER VALIDATES AN INVOICE (or clicks Generate, or a mass action, or the API / cron)
- TRIGGER BILL_VALIDATE
- setInvoiceValidatedInThisRequest() — the marker the PDF hook reads
- fetchLastknownInvoiceStatus(), then setEInvoiceStatus()
- -> extlinks: NOT_GENERATED (in scope) or IGNORE (out of scope)
- the core then rebuilds the invoice PDF
- HOOK afterPDFCreation()
- the single entry point of the generation
- afterODTCreation() forwards to it
- 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
- non blocking: checkRecipientRoutableForSend() only warns
- ProtocolManager::getProtocol()
- EINVOICING_PROTOCOL -> CIIProtocol or FacturXProtocol
- protocol->generateInvoice($invoice, $outputlangs, $pdfPath)
- generateXML()
- buildinvoicelines.inc.php
- collects $invoiceData and $linesData
- reads Facture and its lines, mysoc, buyer, contacts, Account,
- Project, Expedition/Commande, VAT dictionary, einvoicing_routing
- checkBusinessRules()
- EN 16931 safety net, and: does the document claim
- the amount the invoice claims (warn, or abort in strict mode)
- the file that is actually transmittedFactur-X
- resolve the source PDF, merge,
- restore the metadata ->
- <REF>_facturx.pdf (PDF/A-3)
- then, still inside generateInvoice()
- hook afterEinvoiceCreation (other modules)
- setEInvoiceStatus(STATUS_GENERATED) -> extlinks
- checkFileSizeLimit() -> warnings
- back in afterPDFCreation()
- EINVOICING_AP_PRECHECK == 'auto' ? the PA validator, per providerSuperPDP
- 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
- 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
- provider->sendInvoice($invoice) — the deposit, per providerSuperPDP
- 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 point | Action | Joins at |
|---|---|---|
| Card button Generate | doActions('generate_einvoice') | generateOneEInvoice() → protocol->generateInvoice() |
| Card button Send | doActions('send_to_pdp') | sendOneInvoiceToAccessPoint() → provider->sendInvoice() |
| Card button Precheck | doActions('precheck_einvoice') | provider->validateEInvoiceFile() |
| Invoice list, mass action | doMassActions('einvoicing_generate') | generateOneEInvoice() |
| Invoice list, mass action | doMassActions('einvoicing_send_to_pdp') | sendOneInvoiceToAccessPoint() |
Function by function
| Function | Source | Role | Reads | Writes / core action |
|---|---|---|---|---|
TRIGGER BILL_CREATE | core/triggers/interface_98_modEInvoicing_EInvoicingTriggers.class.php:150 | Stamps 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_VALIDATE | core/triggers/interface_98_modEInvoicing_EInvoicingTriggers.class.php:172 | Opens 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_AP | setInvoiceValidatedInThisRequest() (request-scoped marker), setEInvoiceStatus() → extlinks |
TRIGGERS BILL_UNVALIDATE / DELETE / MODIFY | core/triggers/interface_98_modEInvoicing_EInvoicingTriggers.class.php:210 | The 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_id | nothing — returns a negative result, which aborts the core action |
HOOK afterPDFCreation() | class/actions_einvoicing.class.php:69 | The 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 below | calls 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:3630 | The 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:3551 | Which 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:1508 | The 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:2753 | Is 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:123 | Hands out the protocol named by EINVOICING_PROTOCOL: CIIProtocol or FacturXProtocol. | EINVOICING_PROTOCOL | — |
CIIProtocol::generateInvoice() | class/protocols/CIIProtocol.class.php:544 | Produces 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:338 | Data 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.php | lib/buildinvoicelines.inc.php:1 | The 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_routing | nothing persistent |
buildXML() | class/protocols/CIIProtocol.class.php:2065 | Arrays → 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:203 | The 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:81 | Same 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:1100 | Deposits 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 id | POST flows (multipart), GET flows/{id}?docType=Metadata; insertOrUpdateExtLink() twice; addEvent() in the agenda; logCall() → llx_einvoicing_call |
Esalink sendInvoice() | class/providers/EsalinkPDPProvider.class.php:431 | Deposits 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:1038 | The 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:412 | Esalink 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:1257 | The 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:1162 | The shared gate of the card button and of the mass action for the generation, so the two can never diverge. | fetchLastknownInvoiceStatus(), isTransmittedLockActive(), the generation options | protocol->generateInvoice(), then the optional precheck |
HOOK doMassActions() | class/actions_einvoicing.class.php:1073 | The 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 / BG | Source in Dolibarr | Note |
|---|---|---|
BT-1 documentno | Facture::$ref | |
BT-3 documenttypecode | _getTypeOfInvoice() | 380 / 381 / 384 / 386, from type and the situation |
BT-2 documentdate | Facture::$date | |
| BT-8 VAT point date code | einvoicingVatPointDateCode() | product / service mix, and einvoicingVatOnDebits() |
BT-10 buyerReference | array_options.options_d4d_promise_code, else the Chorus service code | not the same thing as BT-46 scheme 0224 |
BT-13 orderReference | Facture::$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 buyer | Facture::$thirdparty, getBuyerCommunicationURI() | BT-49 = the routing of llx_einvoicing_routing, or the invoice-level override |
| BG-9 buyer contact | the external BILLING contact of the invoice | BT-56 / 57 / 58 |
| BG-15 ship-to | einvoicingShipToFromContact() | 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 payment | Account (fk_account, else FACTURE_RIB_NUMBER), _getPaymentMeanNumber() | proprio / owner_name, falls back on mysoc->name |
BT-9 paymentDueDate | Facture::$date_lim_reglement | |
| BT-129 / BT-131 / BT-146 | FactureLigne 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 dictionary | one breakdown per category + rate + exemption, never per vat_src_code |
| BT-153 / BT-154 | line label and description | |
| BT-155 | product->ref | seller assigned id |
| BT-106…BT-115 totals | recomputed here, not read from the stored ones | checkBusinessRules() then confronts them with the invoice |
| business process id | getBillingProcessID() | 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
- who fires, and on which invoice212 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)
- provider->sendStatusMessage() — per provider, same pathSuperPDP
- 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
- 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
- POST flows (multipart, flowSyntax CDAR)
- the PA verdict comes back: ajax poller, or the next synchronization
- onOutboundStatusMessageValidated()
- the only side effect: a 210 accepted by the PA
- abandons the local supplier invoice
Function by function
| Function | Source | Role | Reads | Writes / core action |
|---|---|---|---|---|
TRIGGER PAYMENT_CUSTOMER_CREATE | core/triggers/interface_98_modEInvoicing_EInvoicingTriggers.class.php:277 | Reports 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 Facture | delegates to sendCashedInStatus() |
sendCashedInStatus() | core/triggers/interface_98_modEInvoicing_EInvoicingTriggers.class.php:488 | The 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_VALIDATE | core/triggers/interface_98_modEInvoicing_EInvoicingTriggers.class.php:302 | Three 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_PAYED | core/triggers/interface_98_modEInvoicing_EInvoicingTriggers.class.php:361 | Answers 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:597 | Whether 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:2975 | Checks 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:1798 | Checks 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 CDAR | storeStatusMessage(), updateStatusMessageValidation(), addEvent() |
getSendableStatusesForReceivedInvoice() | class/einvoicing.class.php:1020 | Narrows 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:271 | Builds 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 directory | the CDAR XML file; keeps recipientURIID and recipientURIIDOrigin for the error message |
getCashedAmountCharacteristics() | class/utils/CdarHandler.class.php:679 | The 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:561 | The 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:609 | Reads 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:3335 | One 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:3496 | The 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:639 | The only side effect of a status accepted by the PA: a 210 accepted abandons the local supplier invoice. | the lifecycle row, getReasonsByStatus() for the label | abandonRefusedSupplierInvoice() → core validate() then setCanceled() |
hasSentStatusMessage() | class/einvoicing.class.php:3398 | Whether 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
- Getting this wrong is what makes the platform answer "Electronic address (MDT-73) is invalid" or "no matching invoices found".
- status 212, our own invoice
- recipient = the BUYER, thirdpartyidprof(), scheme 0002
- 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'
- $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
- CRON "EInvoicing flows synchronization", hourly (declared in modEInvoicing::$cronjobs)
- Document::cronSyncFlows()
- provider = PDPProviderManager::getProvider(EINVOICING_PDP)
- syncFromDate = getLastSyncDate()
- maxflows = EINVOICING_FLOWS_SYNC_CALL_SIZE (100)
- provider->syncFlows() — the one real difference between the twoSuperPDP
- 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
- provider->syncFlow($flowId, $call_id) — same shape on bothSuperPDP
- 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
- 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
- $document->create($user)
- one row in einvoicing_document, always,
- whatever happened above
Two return shapes matter and are easy to confuse:
- error (
res < 0, nopostponeflow): something was stored, or the flow is unusable. It is reported as a business action for the operator. - postponed (
res < 0andpostponeflow = 1): nothing was stored, the flow stays pending and the next run retries it. Used when no readable document could be fetched, or when the invoice references a document Dolibarr does not hold yet. That flag is the reason one bad flow no longer stalls every invoice queued behind it.
A received document becomes a supplier invoice
- fetchImportableFlowDocument()
- tries docType=Converted, then Original, then ReadableView
- each through detectProtocolFromContent()
- none readable -> postponeflow + "set the AP conversion format"
- 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
- doCreateSupplierInvoiceFromSource()
- parseInvoiceHeader() / parseInvoiceLines()
- pure XPath over $invoiceTemplate and $lineTemplate
- _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
- === FROM HERE ON, ONE TRANSACTION FOR THE WHOLE IMPORT ===
- 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
- fill the FactureFournisseur, then
- createHeaderDiscounts() — document-level allowances -> DiscountAbsolute
- FactureFournisseur::create($user) <- CORE
- _saveImportedBuyerOrderReference() -> llx_einvoicing_extrafields
- _linkSupplierInvoiceToPurchaseOrder() -> core add_object_linked('order_supplier')
- createSupplierInvoiceLinesFromSource()
- per line: findProductFromEinvoiceLine()
- else _findOrCreateProductFromEinvoiceLine() -> Product::create()
- else the vendor's default routing product
- else a free line
- createSupplierInvoiceLinesIntoDatabase()
- INSERT a bare facture_fourn_det row, then core updateline()
- deliberately NOT addline()
- 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
- alignInvoiceTotalsWithDocument()
- every line exists now: confront the totals with BT-112 / BT-115
- core update_price()
Function by function
| Function | Source | Role | Reads | Writes / core action |
|---|---|---|---|---|
Document::cronSyncFlows() | class/document.class.php:1572 | The 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_SIZE | delegates to syncFlows(); the job output carries the manual actions to do |
getLastSyncDate() | class/providers/AbstractPDPProvider.class.php:831 | Where 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:1782 | Walks 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 stored | one Call row for the whole run; collects the business actions and the postponed flows |
Esalink syncFlows() | class/providers/EsalinkPDPProvider.class.php:856 | Its 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 returns | one Call row for the run; same postponeflow handling as SuperPDP |
SuperPDP syncFlow() | class/providers/SuperPDPProvider.class.php:2209 | One 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=Metadata | Document::create() → llx_einvoicing_document |
Esalink syncFlow() | class/providers/EsalinkPDPProvider.class.php:1223 | Reads 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=Metadata | Document::create() → llx_einvoicing_document |
fetchImportableFlowDocument() | class/providers/AbstractPDPProvider.class.php:460 | Retrieves 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:163 | Recognizes 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:677 | The 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_temp | temp files; commit or rollback of the import transaction |
doCreateSupplierInvoiceFromSource() | class/protocols/CIIProtocol.class.php:793 | The 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:406 | Extracts 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:1739 | Pure 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:1789 | Same, per IncludedSupplyChainTradeLineItem, over $lineTemplate. | the XML and the line template | — |
_syncOrCreateThirdpartyFromEInvoiceSeller() | class/protocols/CommonProtocol.class.php:456 | Resolves 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_NAME | Societe::fetch() by idprof1..6, then Societe::create() or Societe::update() |
findProductFromEinvoiceLine() | class/protocols/CommonProtocol.class.php:984 | Matches 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:1096 | Creates 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 vendor | Product::create() |
createSupplierInvoiceLinesFromSource() | class/protocols/CIIProtocol.class.php:1236 | Builds 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:726 | Writes 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->lines | FactureFournisseur::updateline() — so BT-131 is read and then dropped, the core recomputing from quantity x price |
alignInvoiceTotalsWithDocument() | class/protocols/CIIProtocol.class.php:3463 | Once 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 totals | FactureFournisseur::update_price(), and a message when they still disagree |
saveEInvoiceFileToSupplierInvoiceAttachment() | class/protocols/CIIProtocol.class.php:3205 | Files the received document and the PA readable view in the attachment directory of the supplier invoice. | the temp files | the documents directory of the invoice |
findIdByRef() | class/utils/SupplierInvoiceHelper.class.php:677 | Finds 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:397 | Whether 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:66 | Confronts 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:2812 | A 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_DELETE | core/triggers/interface_98_modEInvoicing_EInvoicingTriggers.class.php:394 | Only 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:548 | The 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 status | FactureFournisseur::delete(), syncFlow(), reuseDraftRef(), moveHistoryToInvoice(), adoptRecordWrittenByImport() |
Document::cleanXmlData() | class/document.class.php:1649 | Strips the attachments out of the received XML before it is stored, and checkXmlDataMaxSize() keeps it under the MEDIUMTEXT ceiling. | the raw XML | einvoicing_document.xml_data |
XML → Dolibarr, the field map of the import
| Parsed key (BT) | Written to | Note |
|---|---|---|
documentno (BT-1) | FactureFournisseur::$ref_supplier | also 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_source | credit 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_ttc | then recomputed and confronted by alignInvoiceTotalsWithDocument() |
orderReference (BT-13) | llx_einvoicing_extrafields.buyer_order_reference, and add_object_linked | kept even when it matches no order |
| the seller block | Societe, created or updated | the four steps above |
prodsellerid (BT-155) | $line->ref_supplier | |
prodname / proddesc (BT-153/154) | $line->desc | kept even when a real product is linked |
billedquantity (BT-129) | $line->qty | |
| net price, gross price, allowances | $line->subprice, $line->remise_percent | resolveLineUnitPrice(), resolveLineAmounts() |
rateApplicablePercent (BT-152) | $line->tva_tx | |
lineTotalAmount (BT-131) | read, then dropped | updateline() 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 XML | einvoicing_document.xml_data | attachments stripped, size checked |
A status arrives on one of our invoices
- flow_type "CustomerInvoiceLC"
- CdarHandler::readFromString()
- GET flows/{id}?docType=Original -> the CDAR XML
- AcknowledgementDocument.ReferenceReferencedDocument.IssuerAssignedID
- -> Facture::fetch(0, $ref), entity checked
- 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
- 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
| Lock | Function | Keys on | Effect |
|---|---|---|---|
| transmitted | isTransmittedLockActive() | extlinks.flow_id, never cleared | blocks unvalidate, delete, modification of the locked fields, re-send and regeneration; opt-out EINVOICING_ALLOW_RESEND_TRANSMITTED |
| transmitted (weak) | fetchLastknownInvoiceStatus()['transmitted'] | extlinks.syncstatus | reset to GENERATED by a regeneration — which is exactly why the auto-send carries both guards |
| generation re-entry | isEInvoiceGenerationInProgress() | a request-scoped static | stops the PDF hook from generating the document a second time while the module itself is rebuilding the PDF |
| validation origin | isInvoiceValidatedInThisRequest() | a request-scoped static, set by BILL_VALIDATE | restricts 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 sent | hasSentStatusMessage() | lifecycle_msg | one 205 and one 211 per invoice, whatever the path |
| flow already imported | SELECT flow_id | einvoicing_document | the synchronization never re-imports a flow it stored |
| invoice already imported | findIdByRef() | ref_supplier, per supplier | exact match by default; EINVOICING_TOLERANT_SUPPLIER_REF_MATCH adds a narrow fallback |
Who consumes what the two chains wrote
| Consumer | Where | Reads |
|---|---|---|
| customer invoice card block | EInvoiceCardBlock(), rendered by the formObjectOptions hook | extlinks (status, flow_id, precheck, routing override), the file on disk, lifecycle_msg |
| supplier invoice card block | supplierInvoiceCardBlock() | lifecycle_msg, einvoicing_document, einvoicing_extrafields |
| third party card block | thirdpartyCardBlock() | einvoicing_routing, the directory check |
| invoice list columns | printFieldListSelect/From/Where/GroupBy/Option/Title/Value | extlinks and lifecycle_msg, joined into the core list query |
| status pollers | ajax/checkinvoicestatus.php, ajax/checksupplierinvoicestatus.php | the PA, then write back into extlinks and lifecycle_msg |
| flow list and card | document_list.php, document_card.php | einvoicing_document |
| call list and card | call_list.php, call_card.php | einvoicing_call |
| support archive | class/utils/SupportExport.class.php | all of the above, secrets re-redacted |
| consistency check | checkDolInvoiceAndEInvoiceConsistency() | the stored XML against the local invoice, at validation (opt-in) |
The options that decide, by chain
| Constant | Chain | What it decides |
|---|---|---|
EINVOICING_PDP | both | which access point provider answers: SuperPDP, Esalink, the offline test one |
EINVOICING_ONLY_GENERATE | both | hidden: offers the generate-only provider, which talks to no platform, to a French company that files its documents by hand |
EINVOICING_DEBUG_MODE | both | keeps the temp XML and stores the raw PA response on the flow |
EINVOICING_DISABLE_SYNC_DOLI_TO_AP | OUT | switches the whole outbound side off |
EINVOICING_EINVOICE_IN_REAL_TIME | OUT | generate the e-invoice on validation |
EINVOICING_EINVOICE_CANCEL_IF_EINVOICE_FAILS | OUT | a failed pre-flight aborts, instead of only warning |
EINVOICING_AUTO_SEND_ON_GENERATION | OUT | deposit at the PA right after a validation |
EINVOICING_AP_PRECHECK | OUT | on auto, run the PA validator before sending |
EINVOICING_ENABLE_API_VALIDATION | OUT | adds the PA validation of the third party to the pre-flight checks |
EINVOICING_PRECHECK_DIRECTORY | OUT | the card checks the directory for an invoice not yet transmitted (with EINVOICING_LIVE) |
EINVOICING_PROTOCOL | OUT | CII or FACTURX: which file is produced and transmitted |
EINVOICING_XML_PROFILE | OUT | the profile buildXML() writes, falling back on the one of the protocol |
EINVOICING_BR_CHECK | OUT | nocheck, warning_only (default) or blocking for checkBusinessRules() |
EINVOICING_MAX_FILE_SIZE_MB | OUT | the size above which the generated file raises a warning |
EINVOICING_SKIP_B2C | OUT | a third party that is not a company leaves the e-invoicing scope |
EINVOICING_DEFAULT_EINVOICE_STATUS_FOR_POS, EINVOICING_NAME_OF_MODULESOURCE_THAT_ARE_POS | OUT | the status given to an invoice a point of sale module issued |
EINVOICING_BLOCK_INVOICE_NO_ROUTING_ID | OUT | no routing recorded = refuse, instead of falling back on the legal identifier |
EINVOICING_REQUIRE_ROUTABLE_RECIPIENT | OUT | directory gate: 1 = a confirmed unroutable recipient blocks, 2 = an undetermined answer blocks too |
EINVOICING_USE_CHORUS | OUT | the Chorus service code, and the checks that go with it |
EINVOICING_USE_BILLING_CONTACT_AS_BUYER | OUT | the billing contact becomes the buyer party, not only the buyer contact group |
EINVOICING_USE_DOLIBARR_ALREADY_CALCULATED_AMOUNTS | OUT | the line amounts are read from the invoice instead of being recomputed |
EINVOICING_PMT, EINVOICING_PMD, EINVOICING_AAB | OUT | the text of the three legal notes; empty falls back on the translation |
EINVOICING_ALLOW_RESEND_TRANSMITTED | OUT | disarms the transmitted lock |
EINVOICING_ALLOW_REGEN_TRANSMITTED | OUT | which actions stay open on a transmitted invoice |
EINVOICING_SEND_APPROVED_ON_VALIDATION | OUT status | 205 on supplier invoice validation |
EINVOICING_SEND_PAYMENT_SENT_STATUS | OUT status | 211 when a supplier invoice is classified paid |
EINVOICING_DISABLE_SYNC_AP_TO_DOLI | IN | hides the inbound side, screens included |
EINVOICING_FLOWS_SYNC_CALL_SIZE | IN | batch size of the flow search |
EINVOICING_FLOWS_SYNC_CALL_LIMIT | IN | the limit the manual synchronization form suggests |
EINVOICING_SYNC_MARGIN_TIME_HOURS | IN | how far back the manual synchronization form starts, before the last known flow |
EINVOICING_PREFER_ORIGINAL | IN | try the Original document before the Converted one |
EINVOICING_USE_EXTERNAL_FACTURX_READER | IN | read a received Factur-X with the external library instead of the CII parser |
EINVOICING_THIRDPARTIES_AUTO_GENERATION | IN | create the vendor when no identifier matches; without it the document is refused |
EINVOICING_THIRDPARTIES_COMPLETE_INFO | IN | complete an existing vendor with what the document says |
EINVOICING_THIRDPARTIES_MATCH_ON_NAME | IN | hidden: re-enables the fuzzy vendor match (step 3) |
EINVOICING_THIRDPARTIES_MATCH_ON_EMAIL | IN | hidden: adds the email address to the vendor lookup |
EINVOICING_PRODUCTS_AUTO_GENERATION | IN | create the product a line names |
EINVOICING_IMPORT_AS_FREE_LINES | IN | import every line as a free line, linked to no product |
EINVOICING_TOLERANT_SUPPLIER_REF_MATCH, EINVOICING_TOLERANT_SUPPLIER_REF_MIN_LENGTH | IN | hidden: narrow substring fallback on ref_supplier, and its floor (8) |
EINVOICING_SUPPLIER_INVOICE_CHECK_CONSISTENCY_ON_VALIDATION | IN | confront 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_MOVE | IN | the extra entities findIdByRef() may look into |
What actually differs between the two providers
| Step | SuperPDP | Esalink (Hubtimize) |
|---|---|---|
| host / auth | api.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=Metadata | POST 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 most | the 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 types | GET flows/{id}?docType=Metadata, then the switch over the five flow types |
sendStatusMessage() | CdarHandler::generateCdarFile(), then POST flows with flowSyntax CDAR | CdarHandler::generateCdarFile(), then POST flows with flowSyntax CDAR |
checkRecipientDirectory() | overridden: its own directory API, plus a legacy fallback | not overridden: keeps AbstractPDPProvider::checkRecipientDirectory() |
6. Where to look when it goes wrong
| Symptom | Start here |
|---|---|
| no e-invoice file after a validation | the five gates of afterPDFCreation(), then checkRequiredinformations() |
| a field is missing from the XML | lib/buildinvoicelines.inc.php — not buildXML() |
| the amounts of the document disagree with the invoice | checkBusinessRules() → checkDocumentClaimsTheInvoiceAmount() |
| the invoice was never deposited | the four conditions of the auto-send block, then sendInvoice() and llx_einvoicing_call |
| the card shows a status the PA does not have | fetchLastknownInvoiceStatus() — extlinks against lifecycle_msg |
| a status is refused with MDT-73 | the 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 run | it should be postponed, not failed — check the postponeflow returns |
| a received invoice landed on the wrong vendor | the four steps of _syncOrCreateThirdpartyFromEInvoiceSeller() |
| a received line has the wrong amount | resolveLineUnitPrice() and resolveLineAmounts(), then updateline() recomputing |
| the totals do not match the document | alignInvoiceTotalsWithDocument() |
| a received invoice cannot be deleted | BILL_SUPPLIER_DELETE: only a draft goes, and the flow is detached, not deleted |