Present for Craft CMS

Partial fulfilment

Commerce fulfils line items, not bundles. That is the right primitive — a box whose mug has shipped from one warehouse and whose tea has not shipped at all is genuinely two states of two SKUs — but it is not the question anyone asks about a box. Nobody asks how fulfilled line item 91 is. They ask how many boxes went out, and what is holding the rest up.

Present rolls Commerce's own committed and fulfilled inventory transactions back up into an answer about boxes. Nothing is stored: it is all derived at read time, so it cannot drift out of step with what the warehouse actually did.

How a box counts as shipped

A box is shipped when everything in it has shipped. So the number of whole boxes out of the door is the fewest any one component can cover:

2 boxes ordered, each holding 1 mug and 2 tea

Mugs: 2 of 2 fulfilled  → covers 2 boxes
Tea:  2 of 4 fulfilled  → covers 1 box
                          ─────────────
                          1 box shipped, 1 outstanding

Averaging components, or counting the container line, would report boxes that do not exist.

Statuses

StatusMeaning
UnfulfilledNothing in any box has moved
Partly fulfilledSomething has shipped, but not every box is complete
FulfilledEvery box has gone out in full

"Partial" deliberately means something has moved — a box one component short is not the same as a box nobody has touched, and a warehouse screen that shows them alike is useless.

Blockers

The actual deliverable is not "this order is 60% shipped". It is "you are four mugs short of finishing three boxes":

{% for key, fulfillment in craft.present.fulfillment(order) %}
  <h3>{{ fulfillment.qtyFulfilled }} of {{ fulfillment.qtyOrdered }} shipped</h3>

  {% for blocker in fulfillment.blockers %}
    <li>Waiting on {{ blocker.qtyOutstanding }} × {{ blocker.description }}
        ({{ blocker.slotName }})</li>
  {% endfor %}
{% endfor %}

Blockers come back worst-first, and only include components that are actually short.

In the control panel

The order edit screen gains a Bundles panel: one block per box, with every component's per-box quantity, ordered quantity, fulfilled quantity and outstanding quantity, plus the status and the blocker list. It also flags a group that has come apart.

Fulfilling

Fulfil through Commerce as normal — the components are ordinary line items and appear in Commerce's own fulfilment UI. Present reads the result; it does not intercept it. There is nothing extra to click and no separate bundle fulfilment state to keep in sync.

Restocking and refunds

A refunded or restocked component is a Commerce inventory movement like any other, and the rollup reflects it on the next read. A box that was complete and has had a component restocked goes back to partly fulfilled, which is accurate: that box is no longer out of the door in full.