VAT engine module

Module id: tax. Dependencies: checkout.

What it adds:

  • A VAT engine that decides, per order, which country's VAT applies and at what rate, instead of taking each product's tax class at face value. Built for a shop that ships from the Netherlands to customers in the Netherlands and Belgium.
  • A VAT screen (Sidebar → System, admins): the shop's VAT profile, the engine switch, the VAT return figures per quarter and the proof of transport for 0% sales to businesses abroad.
  • A Tax table (Sidebar → System, super admins only): the VAT rates per country and category every shop is charged from.
  • A VAT category on each product.
  • An optional VAT number field at checkout, checked against the EU register (VIES).

Turning the module on changes nothing by itself. Orders keep being taxed per product until someone switches the engine on for the shop, and that is only possible once a VAT profile is approved.

Turning it on for a shop

  1. Enable VAT engine on the website's Modules tab (checkout must be on).
  2. Check the products' VAT category (see Products). Existing products were given the category matching their tax class, confirmed for the Netherlands, so a Dutch-only shop usually has nothing to do here.
  3. Fill in and approve the VAT profile on the VAT screen (see below).
  4. Press Switch on on the VAT screen. From the next order on, the engine decides the VAT.

Switching off again is the same button; orders already placed keep the VAT they were charged. On the command line:

bin/console tax:engine-mode <website-id>            # show
bin/console tax:engine-mode <website-id> ACTIVE     # on
bin/console tax:engine-mode <website-id> OFF        # off

The VAT profile

The facts the engine decides from, as the shop or its tax adviser confirmed them. The engine never fills in a missing fact: an order it cannot decide is refused instead of guessed.

Most shops fill in three things:

Field What to enter
Seller's legal name The name on the invoices, e.g. "Winkel B.V."
Your VAT numbers The Dutch VAT number (NL…B01). It is printed on invoices and needed for 0% sales to businesses in other EU countries.
Where do consumers in other EU countries pay VAT? "Dutch VAT" when the shop's sales to consumers in other EU countries stay under €10,000 a year.

Applies from is the day the version takes effect. It cannot be earlier than the moment it is approved.

The €10,000 threshold. A shop whose sales to consumers in other EU countries (all countries together, excluding VAT, this year and last) stay under €10,000 may charge Dutch VAT on them. Above that, or by choice, it charges the customer's own country's VAT. The shop's other channels (bol.com, a physical shop) count too, which CeeMS cannot see, so this is the shop's or the adviser's answer, not a calculation.

Only when the customer's own country's VAT applies does the form ask for more:

  • Why: over the threshold, or chosen (a choice binds for two years), with the dates and a reference (the adviser's letter).
  • How you declare it: through OSS (the EU one-stop shop, filed with the Dutch tax office), or a VAT registration in the customer's country.
  • What a customer abroad pays: the price on the site (the shop's margin absorbs the rate difference), or the price at their country's VAT rate.

Not confirmed is a valid answer, with one consequence: checkout refuses orders from consumers in other EU countries until it is answered. Dutch orders and businesses with a valid VAT number are not affected.

Versions. An approved profile is never edited. A change is a draft, copied from the latest version and approved in its turn. Every order records the profile version it was taxed under, so an old invoice can always be explained.

What changes once the engine is on

Taxed per product (engine off) VAT engine on
Voucher Taken off after VAT Taken off before VAT, spread over the lines
Shipping No VAT VAT at the goods' rates, in proportion to the goods
Customer in Belgium Dutch rates Dutch or Belgian VAT, as the profile says
Business with a valid EU VAT number, abroad Full VAT 0%, intra-community supply
Receipts, emails, account Prices excl. VAT, VAT as a line Prices incl. VAT, "of which VAT" per rate

Gift cards are sold without VAT and remain a payment: they are taken off what is due after VAT, not off the price.

Refused orders. An order the engine cannot decide (a destination other than the Netherlands or Belgium, a product without an approved rate for the destination, consumers abroad while the profile says "Not confirmed") is refused before anything is reserved or charged. The customer sees "We cannot complete this order online. Please contact us and we will help you further."; the reasons are logged.

The order view in the admin shows the decision each order was charged under: the scheme, the VAT per rate, and the VIES check when there was one.

Business customers and VAT numbers

With the engine on, checkout shows an optional VAT number field, prefilled from the customer's default billing address or account (see customer.md).

The number only matters for a delivery abroad, to a VAT number of that country. Then CeeMS asks VIES, with the shop's own VAT number as requester, so the answer comes with a consultation number:

VIES says What happens
Valid 0% VAT, "intra-community supply" on the receipt and invoice with the buyer's VAT number.
Invalid An error on the field: check the number, or remove it to order with VAT.
Unavailable (timeout, outage) An error on the field: try again, or remove it to order with VAT. Never treated as invalid, never as valid.
A number from another country An error on the field: use a number of the delivery country.

Every check is stored with the answer, the time and the consultation number, and the order links to the check its 0% rests on. A clear answer (valid or invalid) is reused for 24 hours, an outage for 5 minutes.

A 0% sale also needs proof of transport, see below.

Products: VAT category

The product form has a VAT category field and the countries it was checked for. Left empty, it follows the product's tax class. The engine only charges a product at the rate of a country it was checked for: when Belgian VAT applies to an order, a product not checked for Belgium is refused rather than guessed at the Dutch rate.

Reduced rates are where this matters: the Netherlands has 9%, Belgium splits the same goods over 6% and 12%. Give such products a category per rate as your adviser says, and check them for Belgium.

The tax table (super admins)

Sidebar → System → Tax table. The VAT categories and the dated rate rules per country, the same for every shop.

  • Dutch rules for the three starting categories (standard 21%, reduced 9%, zero 0%) ship with the module. Belgian rules must be added before Belgian orders can be taxed.
  • A new rule is a draft until someone other than its author approves it. A wrong rate costs every shop money, so no one approves their own.
  • A rate change is a new rule from the day it applies; approving it ends the old one on that day. Any other overlap is refused, so the engine never meets two rules for one day.
  • Drafts can be deleted, approved rules only ended.

bol.com orders

When the engine is on, each new bol.com order is taxed by it too. bol's price is what the customer paid, so it stays exactly: the engine only decides where the sale is taxed and takes the VAT out of that price. An order the engine refuses is still imported (the sale happened), taxed per product, with a note on the order saying why. See ../bol-sync.md.

VAT report

VAT screen → VAT return. Pick a quarter (the last one that ended is preselected) to see what to declare:

  • Dutch VAT return: box 1a (standard rate), 1b (reduced rate), 1e (0% and exempt), 3b (supplies to businesses in other EU countries).
  • OSS and local registrations: the customer's-country VAT per country and rate, kept apart from the Dutch return.
  • Intra-community listing (ICP): the 0% sales per buyer VAT number.
  • Gift cards sold, counted apart: outside the scope of VAT.

Only orders the engine taxed are counted: sales by the day their VAT was settled (payment), less the orders refunded in the period. Orders taxed per product in the same period are not in the figures; a warning says how many there were.

CSV downloads the same figures for the accountant: semicolon separated, columns section;box;country;treatment;rate;base;vat, amounts with a dot as decimal separator.

Proof of transport

A 0% sale to a business in another EU country needs proof in the order file that the goods left the Netherlands. On such an order the admin order view shows a Proof of transport card with four pieces:

Piece Where it comes from
VIES check Automatic: the valid check from checkout.
Invoice Automatic: the issued invoice (see checkout.md).
Tracking code Automatic: the code entered under Shipment.
Delivery confirmation Recorded by someone once the carrier confirms delivery: the date, a carrier reference, a note, and optionally a file (the carrier's proof of delivery or a signed CMR; PDF, JPG or PNG, up to 10 MB).

Before the order ships the proof is not due yet; after, it is complete or missing. An admin can accept missing proof as an exception, with a reason; editors can record proof but not accept its absence. A refunded or cancelled order owes none. Every change is noted on the order with who made it.

The VAT screen lists these sales with the missing proof first.

Proof files are stored under CMS_NFS_TAX_EVIDENCE_ROOT (default /data/cms/tax-evidence, on the shared /data/cms mount), outside any web root, and are only served to logged-in admins through the admin API.

Things this module does NOT do

  • No other countries. It ships from the Netherlands to the Netherlands and Belgium. Any other destination is refused while the engine is on; leave the engine off for a shop that sells further.
  • No threshold monitoring. The €10,000 answer is the shop's; CeeMS does not count sales across channels.
  • No filing. The VAT report gives the figures; the return, the OSS return and the ICP listing are still filed by the shop or its accountant.
  • No partial refunds. A refund reverses the whole order, in the report and on the credit note.