Usage
On the order screen
Every Commerce order gets an Econz panel on its own edit screen. It shows:
- the status (pending, drafted, booked, failed, skipped or credited), with the draft, booked and credit note numbers
- the customer it went to, and the VAT zone Econz picked, with the reason
- the business details it found: CVR, EU VAT number, EAN
- a totals check: what e-conomic will compute next to what Commerce holds
- any VAT warning, when the zone would charge tax Commerce didn't (or the other way round)
- the payload, byte-for-byte what Econz would send to
/invoices/drafts
From there you can Send to e-conomic, Re-send a draft, Book invoice, Raise credit note, or Forget this order. Forgetting only clears Econz's record. The invoice in e-conomic stays as it is.
Econz → Orders lists every order Econz knows about, filtered by status. The order index also gets a Send to e-conomic element action. On Lite it sends one order at a time, and Pro sends a selection in bulk.
What goes on the invoice
e-conomic invoice lines carry net, ex-VAT unit prices. Commerce may or may not have VAT inside a line's price, so for each line Econz works out:
line net = subtotal + discount − included tax
The unit price is that divided by the quantity, carried to six decimals so that
3 × 33.333333 still lands on 100.00.
Shipping, order-level discounts and any other adjustments become lines of their own. Order-level included tax is spread across them in proportion, because Commerce records the amount but not which adjustment it was charged on.
Econz never sends excluded tax. e-conomic works VAT out from the zone and each product's sales account, so sending ours as well would charge it twice.
Nothing is sent twice
An order can hold exactly one draft. The order ID is the primary key of Econz's link table,
not just an indexed column, so two queue jobs, a bulk action and a console run racing each other
still end up with one invoice. Every write also carries an Idempotency-Key built from the
payload, and e-conomic honours it for an hour, which covers the whole window a queue job retries in.
Re-send replaces an unbooked draft. It won't touch a booked invoice. The only way to reverse one of those is a credit note.
Credit notes (Pro)
With Credit on refund on, a Commerce refund raises a credit note in e-conomic. It's built from the booked invoice's own lines, not from the order, which may have changed since. A credit note always arrives as a draft and Econz never books it. Reversing money in somebody's books is their bookkeeper's call.
Capturing a CVR at checkout
Craft 5 addresses already have an Organization Tax ID field, and Econz reads it. A value with
a country prefix (DE123456789) is treated as an EU VAT number, and a bare one as a CVR. If your
checkout already fills that field in, you're done.
Otherwise, add Econz's fields to the checkout:
<form method="post">
{{ csrfInput() }}
{{ actionInput('econz/checkout/save-business') }}
{{ craft.econz.businessFields(cart) }}
<button>Save</button>
</form>
businessFields() is plain and unstyled on purpose. It gets B2B invoicing into an existing checkout
with one line, and you can swap in your own markup whenever you like. The action checks the CVR's
modulus-11 digit, the VAT number's country format and the EAN's GS1 check digit. It replies with
JSON that includes the resolved zone, so you can tell the buyer what's going to happen:
{% set zone = craft.econz.vatZone(cart.billingAddress.countryCode, vatNumber) %}
{% if zone.zone == 'eu' %}
<p>VAT will be reverse-charged. {{ zone.reason }}</p>
{% endif %}
The action only ever writes to the requester's own cart. It takes no order ID, because accepting one would let anyone attach their CVR to somebody else's order.
Twig
craft.econz.status(order) | the link row, or null |
craft.econz.isSynced(order) | whether it reached e-conomic |
craft.econz.invoiceNumber(order) | the booked number if there is one, otherwise the draft |
craft.econz.vatZone(country, vatNumber) | {zone, reason, vatNumberValid} |
craft.econz.businessDetails(order) | {cvr, vatNumber, ean, attention} |
craft.econz.businessFields(order) | ready-made checkout inputs |
craft.econz.isValidCvr / isValidVatNumber / isValidEan | format checks |
craft.econz.counts() | how many orders are in each status |
Console
php craft econz/connection/test # call /self and print what came back
php craft econz/connection/catalog vatZones # list a reference resource
php craft econz/sync/preview <order> # the exact payload, without sending it
php craft econz/sync/order <order> # send one order (--force to re-send a draft)
php craft econz/sync/pending --limit=100 # send completed orders not yet sent
php craft econz/sync/retry # retry the failures (Pro)
php craft econz/sync/reconcile # read outstanding amounts back (Pro)
php craft econz/products/sync # dry run by default (Pro)
php craft econz/products/sync --dry-run=0 # …and for real
php craft econz/log/prune --days=30 # a good one for cron
<order> accepts an order number, a short number, a reference or an element ID.
catalog knows vatZones, paymentTerms, layouts, units, customerGroups, productGroups,
currencies, departments, projects and employees.
Events
Change the payload before it's sent:
use justinholtweb\econz\events\DraftPayloadEvent;
use justinholtweb\econz\services\Invoices;
use yii\base\Event;
Event::on(Invoices::class, Invoices::EVENT_MODIFY_DRAFT_PAYLOAD, function(DraftPayloadEvent $e) {
$e->payload['references']['other'] = $e->order->reference;
});
The preview goes through the same event, so it still shows exactly what will be sent.
Sync::EVENT_BEFORE_PUSH is cancelable. Set $event->isValid = false to skip an order and mark it
skipped. That's how you keep staff test orders out of a real ledger. Sync::EVENT_AFTER_PUSH
carries the SyncResult.