justinholtweb / craft-palletizr
Palletization for Craft Commerce — consolidates an order's cartons onto standard, custom and multiple shipping pallets, derives freight class and handling units, and feeds the result to Postie's freight carriers.
Package info
github.com/justinholtweb/craft-palletizr
Type:craft-plugin
pkg:composer/justinholtweb/craft-palletizr
Requires
- php: ^8.2
- ext-json: *
- craftcms/cms: ^5.3.0
- craftcms/commerce: ^5.0.0
Suggests
- verbb/postie: Live freight rates from FedEx Freight, UPS/TForce Freight and other carriers, rated on the pallets Palletizr builds.
README
Palletization for Craft Commerce. Palletizr consolidates an order's cartons onto shipping pallets — standard footprints, your own custom ones, and as many as the shipment needs — then hands the finished pallets to Postie so your freight carrier quotes handling units instead of boxes.
Craft 5.3+ · Craft Commerce 5+ · PHP 8.2+ · $59
The problem
A freight carrier does not price forty cartons. It prices the two pallets those cartons are strapped to. But a Craft store quoting LTL through Postie hands the carrier its cartons, because that is what Postie's box packer produces — and the carrier either rates forty parcels, refuses, or returns a number that has nothing to do with the bill that arrives three weeks later.
Palletizr closes that gap. It sits between Postie's packing and Postie's rate request, builds the pallets your warehouse would actually build, and gives the carrier the shipment as it will really turn up on its dock.
What it does
- Builds real pallet loads. Layer by layer, bottom to top, the way a person does — not free-space 3D bin packing that produces an arrangement nobody can strap or wrap.
- Finds patterns block loading misses. A 500 × 300 case on a 1200 × 800 Euro pallet blocks at four per layer whichever way round you lay it. Palletizr's optimised solver finds six. On a 24-case order that is the difference between two pallets and one, and between class 110 and class 92.5. Both numbers come from the test suite, not from marketing.
- Standard and custom pallets. Twelve standard footprints seeded in your store's own units — GMA, EPAL 1/2/3/6, Asia T11, Australian, half and quarter pallets — plus anything else you build on.
- Multiple pallets, deliberately. Fill each pallet before opening the next, or spread the layers evenly so a carrier is not charging you a full handling unit for three cartons. Let a shipment finish on a half pallet rather than opening a second full one.
- Freight class from density. Gross weight over gross cube, pallet included, against the NMFTA density scale — with per-pallet overrides for commodities that carry their own NMFC number.
- Carrier limits that are checked, not assumed. Fifteen carriers' published per-handling-unit limits for weight, height and length, plus the linear-foot threshold past which LTL stops being LTL. A load that breaks one is quoted with a warning, never silently.
- Your own tariff, when there is no live rate. A base charge, a charge per handling unit, per hundredweight by freight class, fuel surcharge and accessorials — appearing at checkout as an ordinary Commerce shipping method.
Installation
composer require justinholtweb/craft-palletizr php craft plugin/install palletizr
Postie is optional. Without it, Palletizr still builds pallets for the simulator, for your templates and for its own freight tariffs — it simply has no carrier to send them to.
Getting started
-
Seed some pallets. Palletizr → Pallets offers the standard footprints, or:
php craft palletizr/pallets/seed gma-48x40
Then weigh one and correct its empty weight. You are billed for it on every shipment.
-
Write a rule. Palletizr → Rules. The usual first one is "palletize anything over 150 lb": one rule, action Palletize, a minimum order weight, and the pallets it may build on.
-
Try it. Palletizr → Simulator takes made-up cartons through the same load builder a real order uses, and draws every layer looking down at the deck.
-
Check a real order.
php craft palletizr <order-number> --explain
How rules work
Rules are read top to bottom, and the first decisive one wins. This is the opposite of how a rate table works, and deliberately: palletizing is a single decision about a single shipment, and summing partial answers from four rules would produce a plan no warehouse could follow.
require + missed -> the order does not palletize, stop
never + matched -> the order does not palletize, stop
palletize + matched -> the order palletizes on this rule's terms, stop
nothing decisive -> the "palletize when no rule matches" setting decides (off by default)
require is the gate. "Only ever palletize inside the contiguous US" is one require rule with a
destination condition, sitting above everything else.
A matching palletize rule settles everything downstream: which pallets are eligible, how one is
chosen, what happens when the load spills onto a second, how cartons are stacked, whether overhang
is allowed, and which deck-pattern solver runs.
How the Postie integration works
Palletizr attaches to Provider::EVENT_AFTER_PACK_ORDER — the moment Postie has finished packing and
has not yet asked anybody for a price — and replaces the carton list with a pallet list. Everything
downstream is unchanged: Postie serialises the pallets, converts them into the provider's units and
sends them. No changes to Postie are needed.
Two things it is careful about:
- Carton tare does not evaporate. A Postie packed box weighs its empty box plus its contents. Rebuilding a pallet from the items alone would silently lose every carton's own weight — several kilograms on a full pallet, which the carrier still charges for. The pallet's empty weight carries the pallet's tare plus every carton's tare, and the shipment weighs exactly what it weighed before. There is a check in the test suite that asserts precisely this.
- Parcel providers are left alone by default. A parcel carrier asked to rate a one-tonne pallet either refuses or returns nonsense, so only freight providers are palletized unless you say otherwise.
If anything in there throws, the error is logged and Postie's original packing stands. The worst case is a shipment quoted as loose cartons — which is what would have happened anyway.
Templating
{% set load = craft.palletizr.palletize(cart) %}
{% if load.palletized %}
<p>
Shipping on {{ load.palletCount }} pallet{{ load.palletCount == 1 ? '' : 's' }},
{{ load.grossWeight|round }} g gross, freight class {{ load.freightClass }}.
</p>
{% for warning in load.allWarnings %}
<p class="warning">{{ warning }}</p>
{% endfor %}
{% endif %}
craft.palletizr.quote(cart, 'ltl') prices a shipment under one of your own tariffs, and
craft.palletizr.freightClassForDensity(18.4) answers the "what class is my freight" question on
its own.
Price
Palletizr is $59, with a $49/year renewal for continued updates and support. There is no feature-gated tier: one price, everything switched on — the optimised deck-pattern solver, per-class hundredweight rates, condition-builder rules, the Postie bridge, the simulator and the console.
Gating the optimised solver behind a tier was considered and rejected. It is the feature that turns two pallets into one, which means the cheaper edition would be the one that quietly costs the store money on every shipment.
What it will not pretend
- A carton with no measurements cannot be placed. Guessing its size would produce a quote the carrier will not honour. It ships loose, and the control panel names it.
- The layer model is a real restriction. It will not find the very tightest arrangement a full 3D solver could, because it insists every course be flat and every carton sit on something. That restriction is the point: a load that cannot be strapped and wrapped is not a load.
- Carrier limits are advisory. They are each carrier's general published limits, not your contract. A load that breaks one is quoted with a warning, because a store with a negotiated tariff has different numbers — but it is never quoted silently.
- The density scale is a guideline, not a classification. A commodity with its own NMFC item number is classed by that number. That is what a pallet's freight class override is for.
Console
php craft palletizr <order-number> # build sheet for a real order php craft palletizr <order-number> --explain # …and every rule that was evaluated php craft palletizr/pallets # the catalogue php craft palletizr/pallets/standards # footprints available to seed php craft palletizr/pallets/seed [keys] # seed them php craft palletizr/pallets/export --file=p.json php craft palletizr/pallets/import --file=p.json
Export and import move pallets, rules and tariffs between environments. Nothing is ever deleted by an import.
Licence
The Craft License. See LICENSE.md.