A black-box mock SAP endpoint. It speaks the shapes, not the business logic: SAP Gateway OData V2 and V4 services, BAPI/RFC calls over JSON and SOAP, and IDocs in XML and flat-file form, all backed by SQLite.
__metadata, /Date(…)/, EDMX $metadata, CSRF tokens, $batch changesets, BAPIRET2 tables.$apply aggregation, delta tokens, Fiori UI annotations, and OAuth 2.0 token flows a client can expire and refresh.INVOIC becomes a supplier invoice and money owed by you - an open item a payment run selects with one $filter, and the supplier's IBAN to pay it into. Post the bank's FINSTA01 statement back and the invoices it paid are cleared; a returned payment reopens one, and every line the statement cannot place is reported rather than guessed at.DELVRY, INVOIC or FINSTA01 keeps what it decided: which sales orders a delivery moved, the supplier invoice and accounting document an invoice created, which invoices a statement cleared and why it refused a line. Read the IDoc back and you get the same answer the POST gave, so a client that dropped the response can still find out; /_mock/idocs?settled= asks from the invoice's side, which is the question an integration actually has: what settled this one?PaymentRunID and PaymentRunDate, so a second run started before the statement comes back can leave it alone. And a credit line on a FINSTA01 has to say what it is: RET for a payment of ours coming back, RCV for money arriving. A credit that says neither reverses nothing and is listed as unprocessed with the reason, because either guess is wrong somewhere: RET pays an invoice twice, and RCV hides a payment that came back. Where two suppliers number an invoice the same and bill the same amount, the one a payment run claimed is the one a statement line clears.FINSTA01 clears the receivable it quotes, so order to cash runs end to end. One credit can settle several invoices whose amounts come to it exactly; a part payment is refused with both figures rather than posted. Payment terms can offer a cash discount, 0002 being 2% 10 net 30, and a payment short by exactly that is judged on the day it arrived: earned, the invoice clears; taken late, it clears and the discount stays open on the customer. A payment the bank takes back reopens what it had cleared. Wherever a line could be read two ways, nothing is cleared and both readings are named.REMADV from a payment document and you get a PEXR2002 naming every invoice that payment settled, with the number the supplier quotes and what came off it, and a total that is the sum of those rows. A statement that pays several invoices for one supplier posts one payment covering all of them, the way a payment run does, which is what makes the advice worth sending.$metadata declares read-only is refused rather than taken, a $search the mock cannot do is a 501 rather than every row, a V4 service reads V4 filter literals and a V2 service V2 ones, a POST to A_SupplierInvoice posts an invoice that can be paid, and BAPI_TRANSACTION_ROLLBACK answers E after a call it could not undo instead of claiming it did. A test that passes here should not fail against S/4 for a yes the mock made up.$metadata every service serves is validated against the CSDL schemas OASIS and Microsoft published, so the mock's output is checked by something it did not write.TYPE E in RETURN for a valid call. Both look like success from the sending side, which is why they are the ones worth testing against.
pip install mock-sap
mock-sap --port 8000
curl "http://127.0.0.1:8000/sap/opu/odata/sap/API_SALES_ORDER_SRV/A_SalesOrder?\$top=1&\$format=json"
Worked examples that use mock-sap: Procure to pay, PO bridge, Invoice check, E-invoice, Payment run and Remittance.