OxifyOxify

Packing slips

Group a bundle's components under one header with its SKU and item count on your Shopify packing slip template.

On a packing slip, Shopify lists a merged box's components as separate rows and only stamps Part of: … on each one — there's no grouped block. This code rebuilds the grouping the packing slip doesn't do.

Where to edit: Shopify admin → Settings → Shipping and delivery → Packing slips → Edit packing slip template.

Back it up first

Select everything in the editor and paste it into a text file before you change anything. There's no version history for this template.

The result

Instead of six loose rows, the slip prints one bordered block per box: the bundle's name, its SKU and item count in the header, then the components indented beneath it. Non-bundle products in the same order print exactly as they did before.

Step 1 — Collect the bundles in this shipment

Put this above the line item loop. It walks the lines once and records each bundle's identity.

{% assign oxify_keys = "" %}
{% for line_item in line_items_in_shipment %}
  {% assign oxify_key = line_item.properties["_bundle_id"] %}
  {% if oxify_key != blank %}
    {% assign oxify_keys = oxify_keys | append: oxify_key | append: "~|~" %}
  {% endif %}
{% endfor %}
{% assign oxify_keys = oxify_keys | split: "~|~" | uniq %}

Liquid has no arrays you can push to, so the ids are glued into one string with a ~|~ separator and split back apart. uniq collapses the duplicates — every component of the same box contributes the same id.

The key is the app's _bundle_id property, which every Bundle Builder line carries in both cart line modes. Don't use the bundle's name as the key: two boxes from the same builder share a name and would collapse into one block.

Step 2 — Print the non-bundle lines

Your existing single loop stays exactly as it is, with one condition added inside it: skip any line that carries a _bundle_id.

{% if line_item.properties["_bundle_id"] == blank %}
  …your original line item markup…
{% endif %}

Keep the Part of: … line in this block. It still applies to bundles that don't come from Bundle Builder.

Step 3 — Print one block per bundle

Put this right after the loop from step 2. For each id collected in step 1, it scans the lines twice: once to work out the header (name, SKU, item count), once to render the components.

{% for oxify_bundle_key in oxify_keys %}
  {% assign oxify_title = "" %}
  {% assign oxify_sku = "" %}
  {% assign oxify_item_count = 0 %}
 
  {% for line_item in line_items_in_shipment %}
    {% if line_item.properties["_bundle_id"] == oxify_bundle_key %}
      {% assign oxify_item_count = oxify_item_count | plus: line_item.shipping_quantity %}
      {% if oxify_sku == blank %}
        {% assign oxify_sku = line_item.properties["Bundle SKU"] %}
      {% endif %}
      {% if oxify_title == blank %}
        {% for oxify_group in line_item.groups %}
          {% unless oxify_group.deliverable? %}
            {% assign oxify_title = oxify_group.title %}
          {% endunless %}
        {% endfor %}
      {% endif %}
      {% if oxify_title == blank and line_item.properties["_oxify_builder"] != blank %}
        {% assign oxify_parts = line_item.properties["_oxify_builder"] | split: '"n":"' %}
        {% if oxify_parts.size > 1 %}
          {% assign oxify_title = oxify_parts[1] | split: '"' | first %}
        {% endif %}
      {% endif %}
      {% if oxify_title == blank %}
        {% assign oxify_title = line_item.properties["_bundle_name"] %}
      {% endif %}
    {% endif %}
  {% endfor %}
 
  {% if oxify_title == blank %}{% assign oxify_title = "Bundle" %}{% endif %}
 
  <div class="ia-bundle-group">
    <div class="ia-bundle-group-header">
      <p class="ia-bundle-group-title">{{ oxify_title }}</p>
      <p class="ia-bundle-group-meta">
        {% if oxify_sku != blank %}SKU {{ oxify_sku }} &middot; {% endif %}
        {{ oxify_item_count }} item{% unless oxify_item_count == 1 %}s{% endunless %}
      </p>
    </div>
 
    {% for line_item in line_items_in_shipment %}
      {% if line_item.properties["_bundle_id"] == oxify_bundle_key %}
        <div class="ia-bundle-component">
          …your original line item markup…
        </div>
      {% endif %}
    {% endfor %}
  </div>
{% endfor %}

How each header value is resolved:

  • Name — the native group title (a merged box); for individual mode it's pulled out of the _oxify_builder property by string-splitting on "n":" (Liquid can't parse JSON), then _bundle_name (older merged lines), then literally "Bundle".
  • SKU — the visible Bundle SKU property, which is your parent product's SKU. It's shown once in the header instead of on every row, so the component rows should suppress it (see below).
  • Count — the components' shipping_quantity, summed. A box that ships in more than one parcel is counted per shipment: each slip lists only the components in its own shipment.

In the component markup, delete the Part of: … line (the header already says it) and suppress the SKU so it isn't repeated on every row. If the markup prints line item properties, use this loop for them — it skips Bundle SKU and every property whose name starts with an underscore:

{% for property in line_item.properties %}
  {% assign oxify_first_char = property.first | slice: 0 %}
  {% unless property.last == blank or oxify_first_char == '_' or property.first == 'Bundle SKU' %}
    {{ property.first }}: {{ property.last }}
  {% endunless %}
{% endfor %}

Step 4 — The CSS

Append these to the end of the template's existing <style> block. page-break-inside: avoid keeps a bundle from splitting across two printed pages.

.ia-bundle-group {
  border: 1px solid #d9d9d9;
  border-radius: 6px;
  padding: 10px 12px;
  margin-bottom: 14px;
  page-break-inside: avoid;
}
.ia-bundle-group-header {
  border-bottom: 1px solid #e6e6e6;
  padding-bottom: 6px;
  margin-bottom: 8px;
}
.ia-bundle-group-title {
  font-weight: bold;
  margin: 0;
}
.ia-bundle-group-meta {
  margin: 2px 0 0;
  font-size: 12px;
  color: #6b7177;
}
.ia-bundle-component {
  padding-left: 12px;
  border-left: 2px solid #e3e3e3;
  margin-bottom: 6px;
}

Everything else in the template — header, addresses, notes, footer, the original CSS — stays untouched.

Showing the bundle's image

Packing slips don't print product images by default, but if yours does, know what's actually available: the bundle's own image is not reliably on the order lines, only the component images are.

  • line_item.image on each component is the component's photo, and it's always there.
  • _bundle_image_url — the image you set in the builder's General Settings — is written to the line only on older, legacy-format lines. On current merged-mode orders it isn't present, because that value now lives in the app's Cart Transform configuration rather than on the cart line. Individual-mode lines never carry it.

So the safe recipe is: use the bundle image if the property happens to exist, and fall back to the first component's photo. Put this inside the {% for oxify_bundle_key in oxify_keys %} loop, above the <div class="ia-bundle-group">:

{% assign oxify_image_url = "" %}
{% for line_item in line_items_in_shipment %}
  {% if line_item.properties["_bundle_id"] == oxify_bundle_key and oxify_image_url == blank %}
    {% if line_item.properties["_bundle_image_url"] != blank %}
      {% assign oxify_image_url = line_item.properties["_bundle_image_url"] %}
    {% elsif line_item.image != blank %}
      {% assign oxify_image_url = line_item.image | img_url: 'small' %}
    {% endif %}
  {% endif %}
{% endfor %}
 
{% if oxify_image_url != blank %}
  <img src="{{ oxify_image_url }}" width="60" alt="{{ oxify_title }}">
{% endif %}

Drop the <img> into .ia-bundle-group-header next to the title.

Why there's no property for the real bundle image

The bundle image is kept on the hidden parent product in Shopify and in the app's Cart Transform configuration, not on the order lines, so there's nothing on a line for a template to read. Orders keep showing the component photo.

The preview lies

The preview in the packing slip editor doesn't include line item properties, so the SKU line looks missing there. Only a real print — Orders → More actions → Print packing slips — shows the true output. Always test with a real order that contains a bundle.

Next

Notification emails — the same idea for order confirmation and shipping emails.

On this page