← all projects

mock-sap

Two supplier invoices are sent to mock-sap as INVOIC IDocs for one purchase order. The first posts and leaves EUR 1190.00 payable. The second is refused with status 51, "Posting period 10 2026 is not open". The capture script told the mock to refuse it; mock-sap itself does not check posting periods.
two invoices, one refused on request drawn by mock-films from a real run

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.

  • Zero dependencies. Python 3.8+ standard library and SQLite.
  • Real wire shapes. __metadata, /Date(…)/, EDMX $metadata, CSRF tokens, $batch changesets, BAPIRET2 tables.
  • V2 and V4 over the same rows. $apply aggregation, delta tokens, Fiori UI annotations, and OAuth 2.0 token flows a client can expire and refresh.
  • Both sides of the document chain. A sales order becomes a delivery, an invoice and money owed to you; an inbound 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.
  • Posting is remembered, not just answered. A posted 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?
  • An invoice is not paid twice behind your back. An open item says which payment run has it in flight, as 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.
  • Customers pay, too. A customer's payment on a 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.
  • A supplier can be told what was paid. Generate a 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.
  • It does not say yes when SAP would not. A property $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.
  • The numbers are SAP's numbers. Every amount is a decimal, rounded half up the way a tax authority rounds it: 19% on 2.50 is 0.48, not the 0.47 a binary float gives. And the $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.
  • Deterministic demo data. Business partners, products, sales and purchase orders, the same every run for a given seed.
  • Failure on demand. Latency, 500s, locked documents, expired CSRF tokens - and the two that answer success and mean no: an inbound IDoc SAP accepts and then declines to post, and a BAPI that returns HTTP 200 with 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.
A client asks mock-sap to create a purchase order with BAPI_PO_CREATE1 and then commits it. Each call is answered by a RETURN, the first carrying the new order's number. The same create is then refused with message E SR 000. The capture script posted the rule that told the mock to refuse it.
a purchase order created, then refused on request drawn by mock-films from a real run
Two supplier invoices of EUR 1190.00 post to mock-sap and are owed. The capture script then posts a bank statement with two lines: one pays the first invoice exactly, and mock-sap clears it. The script wrote the other line for 1000.00 against the second invoice, and mock-sap refused it itself, saying the item is for 1190.00 but the line is for 1000.00; nothing told it to.
a statement clears one invoice, refuses a line drawn by mock-films from a real run
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.