OxifyOxify

Order Printer

Group bundles on invoices, pick lists, and return forms in Shopify's free Order Printer app.

Shopify's Order Printer app has its own templates, separate from the built-in packing slip. They're plain Liquid with access to the same line item properties, so the same grouping technique applies.

Where to edit: Shopify admin → Apps → Order Printer → Templates → pick a template → Edit.

The differences from packing slips

Packing slipOrder Printer
Loop variableline_items_in_shipmentline_items
Quantity fieldshipping_quantityquantity
line_item.groupsAvailableListed in Shopify's reference but not described — don't rely on it
PricesNot shownAvailable (line_item.final_line_price)

Because you can't rely on groups here, grouping relies entirely on the app's _bundle_id property — which is present in both cart line modes, so nothing is lost. (The packing slip snippet groups by it too, and only uses groups to read the bundle's name.)

Use final_line_price for money, not line_price: Shopify's Order Printer reference marks line_price as deprecated because it leaves out automatic discounts and discount codes, so a bundle total built from it would be the pre-discount amount.

The snippet

Replace the template's {% for line_item in line_items %} loop with this.

{% assign bundle_ids = "" %}
{% for line_item in line_items %}
  {% if line_item.properties._bundle_id != blank %}
    {% assign bundle_ids = bundle_ids | append: line_item.properties._bundle_id | append: "~|~" %}
  {% endif %}
{% endfor %}
{% assign bundle_ids = bundle_ids | split: "~|~" | uniq %}
 
{% comment %} 1. Everything that isn't part of a bundle {% endcomment %}
{% for line_item in line_items %}
  {% if line_item.properties._bundle_id == blank %}
    <tr>
      <td>{{ line_item.quantity }} x</td>
      <td>{{ line_item.title }}{% if line_item.sku != blank %}<br><small>{{ line_item.sku }}</small>{% endif %}</td>
      <td>{{ line_item.final_line_price | money }}</td>
    </tr>
  {% endif %}
{% endfor %}
 
{% comment %} 2. One block per bundle {% endcomment %}
{% for bid in bundle_ids %}
  {% assign b_name = "" %}
  {% assign b_sku = "" %}
  {% assign b_count = 0 %}
  {% assign b_total = 0 %}
 
  {% for line_item in line_items %}
    {% if line_item.properties._bundle_id == bid %}
      {% assign b_count = b_count | plus: line_item.quantity %}
      {% assign b_total = b_total | plus: line_item.final_line_price %}
      {% if b_sku == "" %}{% assign b_sku = line_item.properties["Bundle SKU"] %}{% endif %}
      {% if b_name == "" %}
        {% if line_item.properties._oxify_builder != blank %}
          {% assign parts = line_item.properties._oxify_builder | split: '"n":"' %}
          {% if parts.size > 1 %}{% assign b_name = parts[1] | split: '"' | first %}{% endif %}
        {% endif %}
        {% if b_name == "" %}{% assign b_name = line_item.properties._bundle_name %}{% endif %}
      {% endif %}
    {% endif %}
  {% endfor %}
 
  <tr>
    <td>{{ b_count }} x</td>
    <td>
      <strong>{{ b_name | default: "Bundle" }}</strong>
      {% if b_sku != blank %}<br><small>SKU {{ b_sku }}</small>{% endif %}
      <div style="padding-left:12px; border-left:2px solid #e3e3e3; margin-top:6px;">
        {% for line_item in line_items %}
          {% if line_item.properties._bundle_id == bid %}
            <div>{{ line_item.quantity }} &times; {{ line_item.title }}{% if line_item.sku != blank %} — {{ line_item.sku }}{% endif %}</div>
          {% endif %}
        {% endfor %}
      </div>
    </td>
    <td>{{ b_total | money }}</td>
  </tr>
{% endfor %}

The header name comes from _oxify_builder (individual mode) or _bundle_name (older merged lines). A merged box ordered today carries neither, so its header reads Bundle — see Getting the bundle's name to name it yourself, or lead with the SKU.

Adapting it per template

  • Invoice — keep the price column, as above. The bundle's total is the sum of its components, which is what the customer paid.
  • Packing slip / delivery note — delete the price column and the b_total lines; keep the SKU and item count.
  • Pick list — the component sku on each child row is the field pickers actually need. Keep the bundle header for context and make the child rows the prominent part.
  • Return form — treat components as individually returnable, because they are. Show the group header for identification only.

Next

Theme templates — the cart and the customer's order page.

On this page