Small integrations between mock-sap, mock-edi, mock-bank and mock-einvoice, each with its tests. They live in mock-acme: ACME's middleware, one package with one copy of each, tested against the released mocks and every night against each mock's main. Its tests start the four mocks themselves, so there is nothing to run first.
Until 5 October 2026 each lived in the examples/ of the mock it was written beside. Each mock's examples/README.md says which file became which.
One purchase, carried the whole way: a purchase order in SAP becomes an X12 850, the supplier confirms it, ships it and invoices it, a three-way match posts the invoice into SAP as money owed, a payment run pays it in an ISO 20022 pain.001, the bank's statement clears it, and the supplier is told what was paid. Four of the six examples below are its individual legs; this is the one that runs end to end.
SAP ──850──▶ supplier a purchase order becomes an EDI order
◀──855/856/810── supplier confirmed, shipped, invoiced
SAP ◀──INVOIC── matched, posted, and now owed
──pain.001──▶ bank a payment run selects what is due
SAP ◀──FINSTA01◀──camt.053── the statement clears what was paid
SAP ──PEXR2002──▶ 820 ──▶ supplier and the supplier is told what for
INVOIC that named no supplier and so created nothing owed; a mock that answered "posted" for having posted nothing; an order placed in EUR that came back invoiced in dollars. All three had been passing for days.$filter, and it survives a restart because SAP is where the answer lives. Per supplier, because an invoice number is only unique within one.810 disagreeing with the purchase order rather than by a flag a test set.AC04 comes back: the item stays open and stays distinguishable from one that was paid and came back.
git clone https://github.com/rseufert/mock-acme && cd mock-acme
pip install -e ".[test]" # mock-acme, and the four mocks to test against
python3 -m unittest -v tests.test_procure_to_pay
A worked example of the two mocks together. po_bridge.py reads a purchase order from SAP over OData, sends it to the supplier as an X12 850, and posts the supplier's 855 back into SAP as an ORDRSP IDoc. Any line that isn't a clean confirmation comes back as an exception for a buyer to look at.
send: SAP PO (OData) ──▶ X12 850 ──▶ supplier
receive: SAP ◀── ORDRSP IDoc ◀── X12 855 ◀── supplier
git clone https://github.com/rseufert/mock-acme && cd mock-acme
pip install -e ".[test]" # mock-acme, and the four mocks to test against
python3 -m unittest -v tests.test_po_bridge
Walkthrough · Code · Tests
What happens after the goods ship. invoice_check.py is accounts-payable middleware: it takes a supplier's X12 810 invoice and posts it into SAP as an INVOIC IDoc only if it passes a three-way match against the purchase order and the 856 ship notice. Anything else is blocked, with the reasons.
PO (SAP) ──┐
856 ship ──┼──▶ match? ──▶ yes: INVOIC IDoc into SAP
810 bill ──┘ no: blocked, with reasons
CUR segment still passes.REF*SI), the number on the 856 (BSN02), and is then compared with that ship notice and no other. If the notice has not arrived the invoice is held: kept, reported on every run, and posted on the run after it comes. Before mock-acme 0.5.0 it was blocked for billing more than was shipped, and dropped, so it never posted when the notice did come.git clone https://github.com/rseufert/mock-acme && cd mock-acme
pip install -e ".[test]" # mock-acme, and the four mocks to test against
python3 -m unittest -v tests.test_invoice_check
Walkthrough · Code · Tests
The invoice check again, with the invoice arriving the new way. e_invoice.py collects a supplier's UBL e-invoice from mock-einvoice in place of an 810, reads it into what the 810 reader makes, and runs the same three-way match, the same INVOIC IDoc and the same duplicate question. Then it does what an 810 has no answer for: it tells the supplier what became of the invoice, in Peppol Invoice Responses. The order and the ship notice still go by EDI, with a supplier who sends no 810.
supplier ──UBL Invoice──▶ collected from the supplier's side
supplier ◀──AB── received
SAP ◀──INVOIC── matched, posted
supplier ◀──AP── or ◀──RE / UQ── accepted, or not and why
SAP clears the payable (payment run)
supplier ◀──PD── paid
RE; one that information could cure (an order SAP does not have, or none named) is queried with UQ. A block that is not the supplier's doing, SAP not answering among them, is not told.PD is said when the payable the invoice became is cleared in SAP, which is the fact the remittance advice rests on too. A payment sent and not yet on a statement is not paid.git clone https://github.com/rseufert/mock-acme && cd mock-acme
pip install -e ".[test]" # mock-acme, and the four mocks to test against
python3 -m unittest -v tests.test_e_invoice
The step after the invoice is approved: paying it. pay_invoices.py takes a supplier's EDIFACT INVOICs, pays each on its due date in an ISO 20022 pain.001, and then works out from what the bank sends back which invoices are actually paid.
INVOIC ──▶ pain.001 ──▶ bank
pain.002 ◀── accepted: scheduled, not paid
camt.053 ◀── on the statement: paid
MsgId, so the bank refuses the copy with DUPL, and that refusal does not undo the original.AC04, AM04) rather than retried.git clone https://github.com/rseufert/mock-acme && cd mock-acme
pip install -e ".[test]" # mock-acme, and the four mocks to test against
python3 -m unittest -v tests.test_pay_invoices
The same step, but paying what is in SAP rather than what arrived over EDI. payment_run.py does what SAP's F110 does: select the open supplier items that are due, pay them in one pain.001, and post each camt.053 back as a FINSTA01 so SAP clears what was paid - and reopens what came back.
select: SAP open items (OData) ──▶ pain.001 ──▶ bank
reconcile: SAP ◀── FINSTA01 ◀── camt.053 ◀── bank
cleared · reopened · unreconciled
ClearingIsReversed. Without it you either pay the returned invoice twice or never chase it.PaymentRunID and PaymentRunDate, and any other run, on any machine, skips the item and names the run that has it. SAP lets go when a statement clears the item or a returned payment reopens it; the run lets go when the bank refuses the payment. The run used to keep its own register of what it sent, and two installations kept two; a register is optional now.run.problems is neither raised nor swallowed - a bank that answers with an error is three sentences, not silence. A host that is down raises and stops the run instead - a deliberate difference, not an oversight.
git clone https://github.com/rseufert/mock-acme && cd mock-acme
pip install -e ".[test]" # mock-acme, and the four mocks to test against
python3 -m unittest -v tests.test_payment_run
Walkthrough · Code · Tests
Telling the supplier what was paid. mock-sap writes a payment advice from a payment it posted, a PEXR2002 naming every invoice the payment settled; remittance.py converts it into the X12 820 a supplier reads and sends it to mock-edi, which reads it as the payee.
SAP ──PEXR2002──▶ remittance ──820──▶ supplier
◀──997── and what it made of the advice
3570.00-; an amount that is not money out is refused rather than relabelled, and BPR01 says no money moves on this document.git clone https://github.com/rseufert/mock-acme && cd mock-acme
pip install -e ".[test]" # mock-acme, and the four mocks to test against
python3 -m unittest -v tests.test_remittance