Prijs-datamodel met meerdere valuta voor belastingen en facturen
Leer een prijs-datamodel voor meerdere valuta dat wisselkoersen, afronding, belastingen en gelokaliseerde factuurweergave afhandelt zonder verrassingen.

Wat er meestal misgaat met facturen in meerdere valuta
Facturen in meerdere valuta falen op saaie, dure manieren. De cijfers lijken goed in de UI, maar zodra iemand een PDF exporteert, de administratie het importeert, en de totalen niet meer overeenkomen met de regelregels, ontstaat er paniek.
De wortel van het probleem is eenvoudig: rekenwerk met geld is niet alleen vermenigvuldigen met een wisselkoers. Belastingen, afronding en het exacte moment waarop je de koers vastlegt beïnvloeden het resultaat. Als je prijsdatamodel die keuzes niet expliciet maakt, zullen verschillende delen van je systeem "helpfully" opnieuw berekenen en verschillende antwoorden geven.
Drie weergaven moeten het eens zijn, ook als ze verschillende valuta tonen:
- Klantweergave: heldere prijzen in de klantvaluta, met totalen die optellen.
- Administratieweergave: consistente basisbedragen voor rapportage en reconciliatie.
- Auditweergave: een papieren spoor dat toont welke koers en afrondingsregels de factuur hebben opgeleverd.
Mismatchen komen meestal door kleine beslissingen die op verschillende plekken zijn genomen. Het ene team rondt elke regel af; een ander rondt alleen het totaal af. De ene pagina gebruikt de huidige koers; een andere de koers op factuurdatum. Sommige belastingen worden toegepast vóór kortingen, andere erna. Sommige belastingen zijn inbegrepen in de prijs; andere worden bovenop gezet.
Een concreet voorbeeld: je verkoopt een artikel van 19,99 EUR, factureert in GBP en rapporteert in USD. Als je per regel converteert en afrondt op 2 decimalen, kun je een ander belastingtotaal krijgen dan wanneer je eerst optelt en eenmaal converteert. Beide benaderingen kunnen redelijk zijn, maar slechts één kan je regel zijn.
Het doel is voorspelbare berekeningen en duidelijke opgeslagen waarden. Elke factuur moet zonder gokken kunnen antwoorden: welke bedragen zijn ingevoerd, in welke valuta, welke koers is gebruikt (en wanneer), wat is afgerond (en hoe), en welke belastingregels zijn toegepast. Die duidelijkheid zorgt ervoor dat totalen stabiel blijven in UI, PDF's, exports en audits.
Belangrijke termen om overeenstemming over te hebben voordat je het schema ontwerpt
Voordat je tabellen tekent, zorg dat iedereen dezelfde woorden gebruikt. De meeste bugs met meerdere valuta zijn niet technisch, maar "we bedoelden verschillende dingen"-bugs. Een schoon schema begint met definities die product-, finance- en engineeringteams allemaal accepteren.
Valutatermen die invloed hebben op je database
Voor elke geldstroom spreek je drie valuta af:
- Transactionele valuta: de valuta die de klant ziet en waar hij mee akkoord gaat (prijslijst, winkelwagen, factuurweergave).
- Settlement-valuta: de valuta waarin je daadwerkelijk betaald wordt (wat de payment provider of bank uitbetaalt).
- Rapportagevaluta: de valuta die gebruikt wordt voor dashboards en boekhoudsamenvattingen.
Definieer ook minor units. USD heeft 2 (cents), JPY heeft 0, KWD heeft 3. Dit is belangrijk omdat het opslaan van "12.34" als een float gaat schuiven, terwijl het opslaan als een integer in minor units (zoals 1234 centen) precies blijft en afronding voorspelbaar maakt.
Belastingtermen die totalen veranderen
Belastingen hebben hetzelfde niveau van overeenstemming nodig. Beslis of prijzen inclusief belasting zijn (de getoonde prijs bevat al belasting) of exclusief belasting (belasting wordt bovenop toegevoegd). Kies ook of belasting wordt berekend per regelregel (en daarna opgeteld) of per factuur (eerst optellen, dan belasting). Deze keuzes beïnvloeden afronding en kunnen het eindbedrag met een paar minor units veranderen.
Bepaal tenslotte wat je moet opslaan versus wat je kunt afleiden:
- Sla op wat juridisch en financieel belangrijk is: overeengekomen prijzen, toegepaste belastingtarieven, uiteindelijke afgeronde totalen en de gebruikte valuta.
- Leid af wat je veilig opnieuw kunt berekenen: opgemaakte strings, weergaveconversies en de meeste tussentijdse wiskunde.
Kernvelden voor geld: wat op te slaan en hoe
Begin met beslissen welke getallen feiten zijn die je opslaat en welke resultaten je kunt herberekenen. Het mengen van de twee is hoe facturen op het scherm één totaal laten zien en in exports een ander.
Sla geld op als integers in minor units (zoals centen) en sla altijd de valutacode erbij op. Een bedrag zonder valuta is onvolledige data. Integers vermijden ook kleine floating-fouten die zichtbaar worden bij het optellen van veel regels.
Een praktisch patroon is om zowel ruwe invoer als berekende output te bewaren. Invoer legt uit wat de gebruiker heeft ingevoerd. Output legt uit wat je gefactureerd hebt. Als iemand maanden later een factuur betwist, heb je beide nodig.
Voor factuurregels ziet een schoon, duurzaam veldset er zo uit:
unit_price_minor+unit_currencyquantity(enuomindien nodig)line_subtotal_minor(voor belasting/korting)line_discount_minorline_tax_minor(of opgesplitst per belastingtype)line_total_minor(eindbedrag voor de regel)
Afronding is niet alleen een UI-detail. Bewaar de gebruikte afrondingsmethode en precisie voor berekeningen, vooral als je valuta met verschillende minor units ondersteunt (JPY vs USD) of cash- afrondingsregels. Een klein "calculation context"-record kan calc_precision, rounding_mode en of afronding per regel gebeurt of alleen op het factuurtotaal vastleggen.
Houd weergaveformattering gescheiden van opgeslagen waarden. Opgeslagen waarden moeten platte nummers en codes zijn; formattering (valutasymbolen, scheidingstekens, gelokaliseerde getalformaten) hoort in de presentatie-laag. Sla bijvoorbeeld 12345 + EUR op, en laat de UI beslissen of het "€123.45" of "123,45 €" toont.
Wisselkoersen: tabellen, tijdstempels en audittrail
Behandel wisselkoersen als tijdgebaseerde data met een duidelijke bron. "De koers van vandaag" is niet iets dat je later veilig opnieuw kunt berekenen.
Een praktische wisselkoerstabel bevat meestal:
base_currency(waarvandaan converteren, zoals USD)quote_currency(waarnaartoe converteren, zoals EUR)rate(quote per 1 base, opgeslagen als een hoogprecisie decimaal)effective_at(timestamp waarvoor de koers geldig is)source(provider) ensource_ref(hun ID of een payload-hash)
Die broninformatie is belangrijk bij audits. Als een klant een bedrag betwist, kun je precies aanwijzen waar het getal vandaan kwam.
Kies daarna één regel wanneer een factuur de koers gebruikt, en houd je eraan. Veelvoorkomende opties zijn koers op het moment van bestelling, verzending of facturering. De beste keuze hangt af van je business. Het belangrijkste is consistentie en documentatie.
Wat je ook kiest, sla de exacte gebruikte koers op in de factuur (en vaak op elke factuurregel). Vertrouw er niet op dat je die later opnieuw opzoekt. Voeg velden toe zoals fx_rate, fx_rate_effective_at en fx_rate_source zodat de factuur exact reproduceerbaar is.
Voor ontbrekende koersen (weekenden, feestdagen, providerstoringen) maak fallback-gedrag expliciet. Typische benaderingen: gebruik de meest recente eerdere koers, blokkeer facturering totdat een koers beschikbaar is, of sta een handmatige koers toe met een goedkeuringsvlag.
Voorbeeld: een bestelling wordt geplaatst op zaterdag, verzonden op maandag en gefactureerd op maandag. Als je regel facturatietijd is maar je provider publiceert geen weekendkoersen, gebruik je mogelijk de koers van vrijdag en registreer je effective_at = Friday 23:59, samen met een source_ref voor spoorbaarheid.
Valutaconversie en afrondingsregels die consistent blijven
Afrondingsproblemen lijken zelden op duidelijke bugs. Ze verschijnen als 1-cent verschillen tussen het factuurtotaal en de som van de regels, of kleine belastingverschillen tussen wat je toont en wat je payment provider verwacht. Goede modellen maken afronding tot een regel die je kunt uitleggen, niet tot een verrassing die je later repareert.
Beslis precies waar afronding plaatsvindt
Kies de punten waar je afronding toestaat, en houd alle andere stappen op hogere precisie. Veelvoorkomende afrondingspunten zijn:
- Regelverlenging (quantity x unit price, na kortingen)
- Elk belastingbedrag (per regel of per factuur, afhankelijk van jurisdictie)
- Het uiteindelijke factuurtotaal
Als je deze punten niet definieert, zullen verschillende delen van je systeem afronden wanneer het hen uitkomt, en zullen totalen afdrijven.
Gebruik één afrondingsmodus, met duidelijke uitzonderingen voor belastingregels
Kies een afrondingsmodus (half-up of bankers rounding) en pas die consistent toe. Half-up is makkelijker uit te leggen aan klanten. Bankers rounding kan bias verminderen over grote volumes. Beide kunnen werken, maar je API, UI, exports en accounting-rapporten moeten dezelfde modus gebruiken.
Behoud extra precisie tijdens conversie en tussenstappen (bijvoorbeeld, sla FX-rates met veel decimalen op), en rond alleen af op de gekozen afrondingspunten.
Kortingen hebben ook één regel nodig: pas kortingen toe vóór belasting (gebruikelijk voor coupons) of na belasting (soms vereist voor specifieke fees). Leg het vast en codeer het één keer.
Sommige jurisdicties vereisen afronding per regel, per belasting of op het factuurtotaal. In plaats van veel eenmalige gevallen in je codebase te verankeren, sla een "rounding policy"-instelling op (per land/staat/belastingregime) en laat berekeningen dat beleid volgen.
Een eenvoudige controle: als je dezelfde factuur morgen opnieuw opbouwt met dezelfde opgeslagen koersen en beleid, moet je exact dezelfde centen krijgen.
Belastingvelden: patronen voor btw, sales tax en meerdere belastingen
Belastingen worden snel complex omdat ze afhangen van waar de koper is, wat je verkoopt en of prijzen netto of bruto worden getoond. Een schoon model houdt belastingen expliciet, niet impliciet.
Maak de belastingbasis ondubbelzinnig. Sla op of de prijs die je belast netto (belasting erbij) of bruto (belasting inbegrepen) is. Sla vervolgens zowel het toegepaste tarief als het berekende belastingbedrag op als snapshot, zodat latere wetswijzigingen de geschiedenis niet herschrijven.
Op elke factuurregel is een minimale set die jaren later nog duidelijk is:
tax_basis(NET of GROSS)tax_rate(decimaal, bijvoorbeeld 0.20)taxable_amount_minor(de basis die je daadwerkelijk belast)tax_amount_minortax_method(PER_LINE of ON_SUBTOTAL)
Als er meer dan één belasting kan gelden (bijvoorbeeld btw plus een lokale toeslag), voeg dan een aparte breakdown-tabel toe zoals InvoiceLineTax met één rij per toegepaste belasting. Elke rij moet een tax_code, tax_rate, taxable_amount_minor, tax_amount_minor, valuta en jurisdictie-hints bevatten die tijdens berekening zijn gebruikt (land, regio en postcode indien relevant).
Bewaar een snapshot van de toegepaste regeldetails op de factuur of factuurregel, zoals rule_version of een JSON-blok van beslisinputs (klantbelastingstatus, reverse charge, vrijstellingen). Als btw-regels volgend jaar veranderen, moeten oude facturen nog steeds overeenkomen met wat je daadwerkelijk in rekening bracht.
Voorbeeld: een SaaS-abonnement verkocht aan een klant in Duitsland kan 19% btw toepassen op een NET-regelprijs, plus 1% lokale belasting. Sla regeltotalen op zoals gefactureerd en bewaar een breakdown-rij voor elke belasting voor weergave en audit.
Hoe je tabellen stap voor stap ontwerpt
Dit gaat minder over slimme wiskunde en meer over het bevriezen van de juiste feiten op het juiste moment. Het doel is dat een factuur maanden later opnieuw geopend kan worden en nog steeds dezelfde cijfers toont.
Begin met beslissen waar de waarheid ligt voor productprijzen. Veel teams houden een prijs in basisvaluta per product en voegen optioneel overrides per markt toe (bijvoorbeeld aparte prijsregels voor USD en EUR). Wat je ook kiest, maak het expliciet in het schema zodat je "catalog price" en "converted price" niet door elkaar gebruikt.
Een eenvoudige volgorde die tabellen begrijpelijk houdt:
- Producten en prijzen:
product_id,price_amount_minor,price_currency,effective_from(als prijzen in de tijd veranderen). - Order- en factuurkoppen:
document_currency,customer_locale,billing_countryen tijdstempels (issued_at,tax_point_at). - Regelitems:
unit_price_amount_minor,quantity,discount_amount_minor,tax_amount_minor,line_total_amount_minoren valuta voor elk opgeslagen geldveld. - Wisselkoerssnapshot: de exacte gebruikte koers (
rate_value,rate_provider,rate_timestamp) waarnaar verwezen wordt vanuit order of factuur. - Belasting-breakdownrecords: één rij per belasting (
tax_type,rate_percent,taxable_base_minor,tax_amount_minor) plus eencalculation_method-vlag.
Vertrouw niet op later herberekenen. Wanneer je een factuur maakt, kopieer de uiteindelijke eenheidsprijzen, kortingen en totalen naar de factuurregels, zelfs als ze uit een order komen.
Voor traceerbaarheid voeg je een calculation_version (of calc_hash) toe op de factuur en een kleine calculation_log-tabel die vastlegt wie een herberekening heeft getriggerd en waarom (bijvoorbeeld "koers bijgewerkt vóór uitgifte").
Gelokaliseerde factuurweergave zonder de cijfers te breken
Lokalisatie moet veranderen hoe een factuur eruitziet, niet wat hij betekent. Doe alle berekeningen met opgeslagen numerieke waarden (minor units of fixed-precision decimals), en pas locale-formattering pas op het allerlaatste moment toe.
Houd factuurpresentatie-instellingen op de factuur zelf, niet alleen in het klantprofiel. Klanten veranderen van land, billing contact en voorkeuren in de tijd. Een factuur is een juridische snapshot. Sla dingen op zoals invoice_language, invoice_locale en formatteringsflags (bijvoorbeeld of je trailing nullen toont) bij het document zodat een herdruk over zes maanden overeenkomt met het origineel.
Valutasymbolen zijn een weergaveaangelegenheid. Sommige locales plaatsen het symbool vóór het bedrag, andere erna. Sommige vereisen een spatie, andere niet. Handel symboolplaatsing, spatiëring, decimale scheidingstekens en duizendtallen bij het renderen, gebaseerd op invoice-locale en valuta. Bak symbolen niet in opgeslagen geldvelden en parseer opgemaakte strings niet terug naar nummers.
Als je rapportage in een tweede valuta nodig hebt (vaak een thuisvaluta zoals USD of EUR), toon het expliciet als een secundair totaal, niet als vervanging. De documentvaluta blijft de juridische bron van waarheid.
Een praktische setup voor factuuropmaak:
- Toon regelitems en totalen in documentvaluta, met invoice-locale formattering.
- Toon optioneel een secundair rapportagetotaal, gelabeld met koersbron en timestamp.
- Toon belastingbreakdown als aparte regels (belastbare basis, elke belasting, totaal belasting), niet als één geblend bedrag.
- Render PDF's en e-mails vanaf dezelfde opgeslagen totalen zodat de nummers niet kunnen afwijken.
Voorbeeld: een Franse klant krijgt een factuur in CHF. De invoice-locale gebruikt komma-decimaal en plaatst de valuta achter het bedrag, maar berekeningen gebruiken nog steeds opgeslagen CHF-bedragen en opgeslagen belastingtotalen. De geformatteerde output verandert; de cijfers niet.
Veelvoorkomende fouten en valkuilen om te vermijden
De snelste manier om facturen in meerdere valuta te breken is geld behandelen als een gewoon getal. Float-typen voor prijzen, belasting en totalen creëren tiny fouten die later zichtbaar worden als "off by $0.01"-problemen. Sla bedragen op als integers in minor units (centen) of gebruik een fixed decimal-type met een duidelijke schaal, en gebruik dat consistent.
Een andere klassieke valkuil is per ongeluk de geschiedenis wijzigen. Als je een oude factuur opnieuw berekent met de koers van vandaag of met bijgewerkte belastingregels, heb je niet meer het document dat de klant zag en betaalde. Facturen moeten immutabel zijn: zodra uitgegeven, sla de exacte wisselkoers, afrondingsregels en belastingmethode op en herbereken opgeslagen totalen niet.
Valuta mengen binnen één regelitem is ook een stille schemasoort bug. Als de eenheidsprijs in EUR is, de korting in USD en de belasting in GBP, kun je de wiskunde later niet uitleggen. Kies één documentvaluta voor weergave en settlement, en één basisvaluta voor interne rapportage (indien nodig). Elk opgeslagen bedrag moet een expliciete valuta hebben.
Afrondingsfouten komen vaak doordat er te vaak wordt afgerond. Als je afrondt bij de eenheidsprijs, dan het regel totaal, dan belasting per regel, dan subtotaal opnieuw, kunnen totalen stoppen met overeenkomen met de som van de regels.
Veelvoorkomende valkuilen om op te letten:
- Floats gebruiken voor geld of wisselkoersen zonder vaste precisie
- Conversies opnieuw uitvoeren voor oude facturen in plaats van opgeslagen koersen gebruiken
- Eén regelitem toestaan met bedragen in meerdere valuta
- Te veel tussenstappen afronden in plaats van op duidelijk gedefinieerde punten
- Niet opslaan van ratetimestamp, afrondingsmodus en belastingmethode per document
Voorbeeld: je maakt een factuur in CAD, converteert een in EUR geprijsde dienst en werkt later je ratetable bij. Als je alleen het EUR-bedrag hebt opgeslagen en bij weergave converteert, verandert het CAD-totaal volgende week. Sla het EUR-bedrag, de toegepaste FX-rate (en tijd) en de uiteindelijke CAD-bedragen op de factuur.
Snelle checklist voordat je live gaat
Voordat je meervoudige-valuta facturen "klaar" noemt, doe een laatste check gericht op consistentie. De meeste bugs hier zijn niet complex. Ze ontstaan uit mismatchen tussen wat je opslaat, wat je toont en wat je optelt.
Gebruik dit als releaseregel:
- Elke factuur heeft precies één documentvaluta in de header, en elk opgeslagen totaal op de factuur is in die valuta.
- Elke geldwaarde die je opslaat is een integer in minor units, inclusief regeltotalen, belastingbedragen, kortingen en verzending.
- De factuur slaat de exacte wisselkoers op die is gebruikt (als precieze decimaal), plus timestamp en koersbron.
- Afrondingsregels zijn gedocumenteerd en geïmplementeerd op één gedeelde plek.
- Als meer dan één belasting kan gelden, sla je een belastingbreakdown per regel op (en optioneel per jurisdictie), niet alleen één belastingtotaal op de header.
Als het schema klopt, valideer dan de wiskunde zoals een auditor dat zou doen. Factuurtotalen moeten gelijk zijn aan de som van opgeslagen regeltotalen en opgeslagen belastingbedragen. Herbereken totalen niet uit opgemaakte waarden of strings.
Een praktische test: kies één factuur met minstens drie regels, pas een korting toe en voeg twee belastingen op één regel toe. Print het vervolgens in een andere locale (andere scheidingstekens en valutasymbool) en bevestig dat de opgeslagen nummers niet veranderen.
Voorbeeldscenario: één order, drie valuta's en belastingen
Een Amerikaanse klant wordt gefactureerd in USD, je EU-leverancier rekent in EUR en je finance-team rapporteert in GBP. Dit is het punt waar een model kalm blijft of verandert in een stapel 1-cent mismatches.
Order: 3 eenheden van een product.
- Klantprijs: $19.99 per stuk (USD)
- Korting: 10% op de regel
- US sales tax: 8.25% (belasting na korting)
- Leverancierskost: EUR 12.40 per stuk (EUR)
- Rapportagevaluta: GBP
Doorloop: wat er gebeurt en wanneer je converteert
Kies één conversiemoment en houd je daaraan. In veel facturatiesystemen is een veilige keuze: converteer bij uitgifte van de factuur en sla de exacte gebruikte koers op.
Bij het aanmaken van de factuur:
- Bereken de USD regel-subtotaal: 3 x 19.99 = 59.97 USD.
- Pas korting toe: 59.97 x 10% = 5.997, afgerond naar 6.00 USD.
- Netto regel: 59.97 - 6.00 = 53.97 USD.
- Belasting: 53.97 x 8.25% = 4.452525, afgerond naar 4.45 USD.
- Totaal: 53.97 + 4.45 = 58.42 USD.
Afronding gebeurt alleen op gedefinieerde punten (korting, elk belastingbedrag, regeltotalen). Sla die afgeronde resultaten op en tel altijd opgeslagen waarden op. Dat voorkomt het klassieke probleem waarbij je PDF 58.42 toont maar een export 58.43 herberekent.
Wat je opslaat zodat je de factuur later kunt reproduceren
Op de factuur (en factuurregels) sla je de valutacode (USD), bedragen in minor units (centen), belastingbreakdown per type en de ID's van wisselkoersrecords die gebruikt zijn om USD naar GBP te converteren voor rapportage. Voor leverancierskosten sla je de EUR-kost op en ook een koersrecord als je die kosten ook naar GBP converteert.
De klant ziet een nette USD-factuur (prijzen, korting, belasting, totaal). Finance exporteert USD-bedragen plus de ingevroren GBP-equivalenten en de exacte koerstijdstempels, zodat maandafsluitingscijfers nog steeds overeenkomen ook als koersen morgen veranderen.
Volgende stappen: implementeren, testen en onderhoudbaar houden
Schrijf je minimale schema als een korte overeenkomst: welke bedragen worden opgeslagen (origineel, geconverteerd, belasting), in welke valuta elk bedrag is, welke afrondingsregel geldt en welke timestamp een wisselkoers voor een factuur vastzet. Houd het saai en specifiek.
Bouw vóór je UI-schermen tests. Test niet alleen normale facturen. Voeg edge cases toe die klein genoeg zijn om afrondingsruis bloot te leggen en groot genoeg om aggregatieproblemen te vinden.
Een starterset testgevallen:
- Heel kleine eenheidsprijzen (zoals 0.01) met hoge hoeveelheden
- Kortingen die repeterende decimalen creëren na conversie
- Wisselkoerswijzigingen tussen orderdatum en factuurdatum
- Gemengde belastingregels (belasting inbegrepen vs belasting exclusief) op hetzelfde factuurtype
- Teruggaven en creditnota's die precies moeten overeenkomen met de originele afronding
Om supporttickets kort te houden, voeg een auditweergave toe die elk getal op een factuur uitlegt: opgeslagen bedragen, valutacodes, wisselkoers-ID en timestamp, en de gebruikte afrondingsmethode. Als iemand vraagt "waarom is dit totaal anders?", kun je uit de opgeslagen feiten antwoorden.
Als je een interne billing-tool bouwt, kan een no-code platform zoals AppMaster (appmaster.io) je helpen dit consistent te houden door het schema op één plek te zetten en de calculatielogica in één herbruikbare workflow te plaatsen, zodat web- en mobiele schermen niet elk hun eigen versie van de wiskunde doen.
Tot slot: wijs eigenaarschap toe. Beslis wie wisselkoersen bijwerkt, wie belastingregels bijwerkt en wie wijzigingen goedkeurt die uitgegeven facturen beïnvloeden. Stabiliteit is een proces, geen alleen maar een schema.


