
Een veelvoorkomende analyse bij verkooptransacties is nagaan of de verkoopfacturen volledig en juist aansluiten op de onderliggende verkooporders en leveringen.
Je wilt natuurlijk geen spookfacturen (omzet zonder order of levering), ongefactureerde leveringen (wel goederen weg, geen omzet) of afwijkingen in bedragen of hoeveelheden.
Deze 3-way match controle kan helpen in de audit bij zowel juistheid (klopt de omzet?) als volledigheid (is alles geleverd–gefactureerd?).
Wanneer zet je deze analyse in?
In omzetcontroles draait het vaak om cut-off en bestaan: is alles wat gefactureerd is ook daadwerkelijk door een klant besteld en geleverd? De 3-way match is daarom een goede eerste toets voor:
- Cut-off rond jaarafsluiting (levering na balansdatum, factuur ervoor)
- Inrichtingsrisico’s bij fast close processen met handmatige facturen
- Proces-compliance: worden uitzonderingen (creditnota’s, deelorders) netjes geregistreerd?
Door de basiscontrole vroeg uit te voeren, weet je meteen waar aanvullende detailtests nodig zijn.
Welke data hebben we nodig?
Voor deze analyse gebruiken we drie tabellen:
-
Verkoopfacturen (Sales Invoices) Bevat onder andere: InvoiceID, InvoiceDate, InvoiceAmount, InvoiceQuantity, CustomerID, OrderID (referentie naar de verkooporder) plus eventuele regelprijzen of BTW-componenten zodat aantals- en prijsverschillen traceerbaar zijn.
-
Verkooporders (Sales Orders) Bevat onder andere: OrderID, OrderDate, CustomerID, OrderAmount, OrderQuantity.
-
Leveringsbonnen (Delivery Notes) Bevat onder andere: DeliveryID, DeliveryDate, DeliveredQuantity, OrderID (referentie).
Met deze drie datasets kunnen we de volledige keten controleren.
Praktijktip: Werk bij voorkeur op regelniveau. Eén order kan meerdere regels (en leveringen) hebben. Zorg dus voor een unieke sleutel (bijv.
OrderID+OrderLine) en houd dezelfde sleutel in de factuur- en leveringsdata aan. Alleen zo kun je aantals- en prijsverschillen correct identificeren.
We gaan er vanuit dat de verkoopfacturen aansluiten op het grootboek, zo weet je zeker dat je de juiste dataset aan het controleren bent. Check daarnaast ook vooraf even of het bestand ook creditnota’s, retouren en handmatige correcties bevat.
Globale process flow van de Three way match
1. Bestanden inladen in Python
Goed, dan gaan we de bestanden inlezen. Hieronder lezen we een csv in, maar dit is vaak ook een excel of json bestand.Pas dan gewoon de read_csv aan naar read_excel of read_json.
import pandas as pd
## Inladen van datasets
invoices = pd.read_csv("dummy_sales_invoices.csv", parse_dates=["InvoiceDate"])
orders = pd.read_csv("dummy_sales_orders.csv", parse_dates=["OrderDate"])
deliveries = pd.read_csv("dummy_delivery_notes.csv", parse_dates=["DeliveryDate"])
print(invoices.head())
print(orders.head())
print(deliveries.head())
2. Facturen koppelen aan orders
We matchen facturen aan orders via OrderID. De outer join zorgt dat mismatches zichtbaar worden. Bij een 'outer join' nemen we alle regels van beide datasets mee. Ook als ze dus niet matchen.
df_order_invoice = orders.merge(
invoices,
on="OrderID",
how="outer",
suffixes=("_order", "_invoice")
)
print(df_order_invoice.head())
Heb je orderregels in plaats van orderheaders? Voeg dan eerst samen op regelniveau zodat het totaalbedrag en de totale aantallen per order beschikbaar zijn:
order_lines = orders.groupby("OrderID").agg({
"OrderAmount": "sum",
"OrderQuantity": "sum"
}).reset_index()
Koppel daarna de geaggregeerde data aan de facturen zoals hierboven.
Analyse 1: Orders zonder factuur
Begin met de orders die geen bijbehorende factuur hebben. Deze transacties moeten vaak alsnog gefactureerd worden of zijn geannuleerd zonder dat dit in de orderdata is verwerkt.
orders_zonder_factuur = df_order_invoice[df_order_invoice['InvoiceID'].isna()]
print(orders_zonder_factuur)
Dit toont orders die nooit gefactureerd zijn.
Analyse 2: Facturen zonder order
Controleer vervolgens het omgekeerde scenario: facturen zonder orderreferentie. Dit zijn potentiële spookfacturen, handmatige boekingen die buiten de normale orderflow om zijn verwerkt of verklaarbare facturen die geen order behoeven.
facturen_zonder_order = df_order_invoice[df_order_invoice['OrderDate'].isna()]
print(facturen_zonder_order)
3. Afwijkende bedragen detecteren
Na de basiscompleetheidschecks volgt een juistheidscontrole: sluit het gefactureerde bedrag aan op het orderbedrag? Je ziet snel of prijzen zijn aangepast, extra kosten zijn gefactureerd of er afrondingsverschillen zijn.
bedrag_afwijkingen = df_order_invoice[
(~df_order_invoice['OrderID'].isna()) &
(~df_order_invoice['InvoiceID'].isna()) &
(df_order_invoice['OrderAmount'] != df_order_invoice['InvoiceAmount'])
]
print(bedrag_afwijkingen)
Dit signaleert bijvoorbeeld:
- €400 besteld
- €450 gefactureerd
4. Leveringsbonnen aan orders koppelen
Koppel nu de orders aan de leveringen. Daarmee zie je of alle orders daadwerkelijk zijn uitgeleverd en kun je leveringen zonder order detecteren (bijvoorbeeld noodleveringen).
df_order_delivery = orders.merge(
deliveries,
on="OrderID",
how="outer",
suffixes=("_order", "_delivery")
)
print(df_order_delivery.head())
Analyse 3: Three way match
Deze lijst wijst je op orders die nog openstaan of nooit zijn uitgeleverd. Vaak vraagt dit om opvolging richting logistiek of ordermanagement.
orders_zonder_levering = df_order_delivery[df_order_delivery['DeliveryID'].isna()]
print(orders_zonder_levering)
Heb je de drie datasets compleet? Combineer dan alles in één 3-way dataset. Hiermee zie je per orderregel meteen alle drie de datapunten terug.
three_way = df_order_invoice.merge(
deliveries,
on="OrderID",
how="outer"
)
print(three_way.head())
Werk hier altijd met dezelfde unieke sleutel (bijv. OrderID + OrderLine). In deze gecombineerde tabel kun je direct filters zetten voor scenario’s zoals levering en factuur zonder order, levering zonder factuur of factuur voor een latere levering zonder dat je eerst aparte views hoeft te bouwen.
5. Verschillen in geleverde hoeveelheid
Vergelijk tenslotte de bestelde hoeveelheid met wat daadwerkelijk is geleverd. Hier komen deel- of overleveringen naar voren die later bij de facturatie voor afwijkingen kunnen zorgen.
hoeveelheid_afwijkingen = df_order_delivery[
(~df_order_delivery['OrderID'].isna()) &
(~df_order_delivery['DeliveryID'].isna()) &
(df_order_delivery['OrderQuantity'] != df_order_delivery['DeliveredQuantity'])
]
print(hoeveelheid_afwijkingen)
Bijvoorbeeld:
- 10 besteld
- 8 geleverd
6. Cut-off en timing controleren
Een klassiek risico bij omzet is dat facturen nog in het oude jaar worden geboekt terwijl de levering pas na balansdatum plaatsvindt.

Voeg daarom een controle toe die order-, leverings- en factuurdata vergelijkt:
cutoff_issues = df_order_invoice.merge(
deliveries[['OrderID', 'DeliveryDate']],
on='OrderID',
how='left'
)
cutoff_issues = cutoff_issues[
(~cutoff_issues['InvoiceDate'].isna()) &
(~cutoff_issues['DeliveryDate'].isna()) &
(cutoff_issues['InvoiceDate'] < pd.Timestamp("2025-12-31")) &
(cutoff_issues['DeliveryDate'] > pd.Timestamp("2025-12-31"))
]
print(cutoff_issues[['OrderID','InvoiceDate','DeliveryDate','InvoiceAmount']])
Pas de datumgrenzen aan naar jouw boekjaar en onderzoek of deze transacties correct zijn verwerkt.
7. Snel overzicht van aantallen en bedragen
Losse lijsten zijn handig, maar een samenvattende tabel met aantallen en totaalbedragen maakt de communicatie met het auditteam of de klant makkelijker:
dashboard = {
"orders_zonder_factuur": {
"aantal": len(orders_zonder_factuur),
"bedrag": orders_zonder_factuur['OrderAmount'].sum()
},
"facturen_zonder_order": {
"aantal": len(facturen_zonder_order),
"bedrag": facturen_zonder_order['InvoiceAmount'].sum()
},
"orders_zonder_levering": {
"aantal": len(orders_zonder_levering),
"bedrag": orders_zonder_levering['OrderAmount'].sum()
},
"bedrag_afwijkingen": {
"aantal": len(bedrag_afwijkingen),
"verschil": (bedrag_afwijkingen['InvoiceAmount'] - bedrag_afwijkingen['OrderAmount']).sum()
},
"hoeveelheid_afwijkingen": {
"aantal": len(hoeveelheid_afwijkingen),
"verschil": (hoeveelheid_afwijkingen['DeliveredQuantity'] - hoeveelheid_afwijkingen['OrderQuantity']).sum()
}
}
dashboard = pd.DataFrame(dashboard).T
print(dashboard)
dashboard.to_excel("3way_match_dashboard.xlsx")
Zo heb je direct een Excel-output voor het controledossier. Bijvoorbeeld:
| Bevinding | Details |
|---|---|
| Factuur zonder order | INV9999 – OrderID onbekend |
| Orders zonder factuur | ORD1002, ORD1006 |
| Orders zonder levering | ORD1003, ORD1006 |
| Bedragafwijking | ORD1004: €400 vs €450 |
| Hoeveelheidsafwijking | ORD1005: 10 besteld / 8 geleverd |
8. Interpretatie: tolerantie, valuta en uitzonderingen
De lijsten die uit de 3-way match rollen zijn een startpunt voor vervolgonderzoek, geen automatisch oordeel. Houd rekening met:
- Toleranties: stel vooraf grenzen voor bedragen en aantallen (bijv. afrondingsverschillen tot €1 of 0,5% accepteren) en breng valuta naar één basisvaluta voordat je verschillen beoordeelt.
- BTW en bijkomende kosten: factureer je inclusief BTW of met transport-/verzekeringskosten? Normaliseer deze posten of splits ze uit zodat je appels met appels vergelijkt.
- Creditnota’s en geannuleerde orders: sluit geannuleerde of gekrediteerde orders uit of koppel ze aan de oorspronkelijke order zodat je geen dubbel alarmsignaal krijgt.
- Handmatige facturen of noodleveringen: documenteer vooraf welke processen buiten de standaard orderflow vallen en verwacht dus bewust afwijkingen. Markeer die al bij het inladen van de data met een indicatorveld.
Met deze context kun je bevindingen prioriteren en gericht opvolgen met het proces- of klantteam.
Conclusie
Een 3-way match op debiteuren geeft je als auditor een krachtig hulpmiddel. Met enkele Python-regels kun je een volledige analyse uitvoeren die normaal gesproken veel tijd kost in Excel en kan een grote hoeveelheid steekproeven verminderen.

