Blog

De factuur die zichzelf betaalde

Bij een B2B-webshop op WooCommerce kwam élke "achteraf betalen"-factuur in Billit binnen als reeds betaald, terwijl er nog niets op de rekening stond. De leverancier draaide in cirkels, de boekhouder wees naar het verkeerde veld. Uiteindelijk zat de oorzaak in een betaaldatum die WooCommerce stilletjes zelf invulde, en paste de oplossing in drie regels code. Dit is het volledige verhaal: het probleem, de omwegen en de fix.

Een klant van mij runt een webshop op WooCommerce. Zakelijke klanten bestellen daar en kunnen kiezen voor betalen achteraf op factuur, wat in de B2B-wereld doodnormaal is. Bestelling komt binnen, de factuur vertrekt automatisch via Peppol naar hun boekhoudpakket Billit, de klant betaalt later per overschrijving.

Een detail klopte niet. Elke WooCommerce factuur als betaald binnenkomen in Billit, terwijl er nog niets betaald was. Voor een boekhouding is dat geen schoonheidsfoutje. Je verliest gewoon het zicht op wie je nog moet betalen, en dat is nu net waarvoor je zo’n pakket gebruikt.

Hier is het volledige verhaal: het probleem, de omwegen, en de oplossing die uiteindelijk in drie regels code paste.

De setup

De keten ziet er zo uit. WooCommerce voor de webshop. Daarbovenop de plugin Webwinkelfacturen, die elke bestelling oppikt en als UBL-factuur via Peppol naar Billit stuurt. De betaalmethode “betalen achteraf” is technisch gewoon de ingebouwde overschrijvingsmethode van WooCommerce (BACS), herlabeld en enkel zichtbaar voor zakelijke klanten.

Op papier simpel. In de praktijk zitten er drie systemen aan elkaar geknoopt die elk hun eigen idee hebben over wat “betaald” betekent.

Waarom de WooCommerce factuur als betaald binnenkwam

Standaard zet WooCommerce een overschrijvingsbestelling op “in de wacht”. Logisch, want er is nog niet betaald. Maar Webwinkelfacturen stuurt zo’n order pas door naar Billit zodra die op “in verwerking” staat. Dus om de factuur überhaupt in Billit te krijgen, had ik de bestelling geforceerd naar “in verwerking”.

En daar zit de adder. Zodra een order in WooCommerce op “in verwerking” komt, zet WooCommerce er automatisch een betaaldatum op. Intern gebeurt dat via een functie die maybe_set_date_paid() heet, die bij het opslaan kijkt: heeft deze order een “betaald”-status? Zo ja, dan vul ik de betaaldatum in. Webwinkelfacturen leest precies die datum uit, ziet een ingevulde waarde, en stuurt de factuur door als betaald.

Met andere woorden: de fix die ervoor zorgde dat de factuur überhaupt vertrok, was exact de oorzaak waardoor hij als betaald binnenkwam. Twee vereisten die elkaar tegenwerken, in één status.

Werken in cirkels

Dit liep een tijd. De boekhouder van de klant wees naar het PaymentMeansCode-veld en domiciliëringsmandaten in de UBL. Een redelijke piste, maar een dwaalspoor: die klant gebruikt geen domiciliëring, en het probleem zat niet in de betaalwijze maar in de betaalstatus.

De support van Webwinkelfacturen draaide ondertussen in rondjes. Tot één antwoord alles veranderde, al besefte ik dat pas later. Ze schreven: wij lezen enkel uit wat WooCommerce ons aanlevert, een “Paid”-vlag en een betaaldatum, en als die velden leeg blijven, zien wij de order niet als betaald.

Dat was geen excuus. Dat was de oplossing, vermomd als een afwijzing. Als zij afgaan op de betaaldatum, dan moet ik die datum gewoon leeg houden. De order mag perfect op “in verwerking” blijven staan, want een achteraf-betalen-bestelling mag gewoon verwerkt worden. Hij hoeft alleen geen betaaldatum te hebben.

De eerste poging die faalde

Mijn eerste reflex: WooCommerce heeft een filter waarmee je de teruggegeven betaaldatum kan overschrijven, woocommerce_order_get_date_paid. Geef daar null terug voor deze bestellingen, dacht ik, en klaar.

Niet dus. De factuur kwam nog steeds als betaald binnen.

Wat ik over het hoofd zag: die filter werkt enkel in “view”-context, de waarde die WooCommerce toont. De opgeslagen waarde bleef gewoon staan, en Webwinkelfacturen las die opgeslagen waarde. Ik maskeerde het symptoom, niet de bron.

Daar dook ook nog een tweede verrassing op. De site draait op HPOS, de nieuwe manier waarop WooCommerce orders opslaat. Daardoor staat de betaaldatum niet meer in de klassieke postmeta, maar in een aparte tabel, wp_wc_order_operational_data. Dus elke check die ik op de ruwe metadata deed, gaf vals comfort: leeg, terwijl de echte waarde elders gewoon ingevuld stond. Twee lagen waar de oude aanpak doorheen viel.

De fix

De oplossing was niet de datum achteraf verbergen, maar verhinderen dat hij ooit geschreven wordt.

maybe_set_date_paid() vult de betaaldatum alleen in als de order de status heeft die de filter woocommerce_payment_complete_order_status teruggeeft. Geef voor deze bestellingen een status terug die de order niet heeft, en de datum wordt nooit weggeschreven:

add_filter('woocommerce_payment_complete_order_status', function ($status, $order_id, $order) {
    if ($order instanceof WC_Order && 'bacs' === $order->get_payment_method()) {
        return 'on-hold'; // de order staat op 'processing', dus has_status() = false -> geen betaaldatum
    }
    return $status;
}, 20, 3);

Dat is het. De bestelling gaat gewoon naar “in verwerking”, de Webwinkelfacturen-trigger vuurt, de factuur vertrekt naar Billit. Maar er is geen betaaldatum, op geen enkel niveau: niet in de view, niet in de opgeslagen waarde, niet in de HPOS-tabel. Webwinkelfacturen leest geen betaaldatum en stuurt de factuur door als onbetaald.

Hoe ik het zeker wist voor het live ging

Ik wilde dit niet gokken op de productiesite. Op staging zette ik een klein stukje debug-code dat bij elke bestelling de betaalstatus wegschreef naar een logbestand: de status, en de betaaldatum in elke context. Zo kon ik bestelling na bestelling zien of de datum echt overal leeg bleef, zonder telkens iets richting de boekhouding te sturen.

Toen dat klopte, ging het naar live. Daar vergeleek ik twee echte bestellingen rechtstreeks in de database. De vorige order, met de oude aanpak, had nog een betaaldatum staan. De nieuwe order: leeg. Zelfde betaalmethode, enkel de fix verschilde. En als sluitstuk bevestigde Webwinkelfacturen dat die order bij hen binnenkwam als onbetaald.

Drie onafhankelijke controles, dan pas afvinken.

Wat ik hieruit meeneem

Dit soort koppelingen breekt zelden op de plek waar iedereen kijkt. De boekhouder keek naar de betaalwijze, de support keek naar hun eigen import, en het zat al die tijd in een betaaldatum die WooCommerce stilletjes zelf invulde.

Twee dingen hielden me op koers. Eén: blijven graven tot je de échte oorzaak hebt, niet stoppen bij het eerste symptoom dat je kan verbergen. De view-filter “werkte” op papier en deed in de praktijk niks. Twee: zelf eigenaar zijn van de koppeling. Als de leverancier en de boekhouder allebei naar elders wijzen, is het soms gewoon aan jou om de motorkap open te trekken.

Het uiteindelijke verschil tussen “weken vastzitten” en “opgelost” was drie regels code. Maar die drie regels vond je pas nadat je precies begreep wélk veld, in wélke laag, door wélk systeem werd uitgelezen. Dat begrip is het werk. De code is de samenvatting.

Gratis Website & AI Check

Benieuwd waar jouw website slimmer kan werken?

In een gesprek van 30 minuten kijken we samen waar je website, klantflow of opvolging slimmer kan. Je krijgt eerlijk advies over wat beter kan, waar AI-integraties zinvol zijn en wat je beter níét hoeft te bouwen.

Of stuur een bericht