FoxrFoxr

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 → AppsOrder PrinterTemplates → pick a template → Edit.

The differences from packing slips

Packing slipOrder Printer
Loop variableline_items_in_shipmentline_items
Quantity fieldshipping_quantityquantity
line_item.groupsAvailableNot available
PricesNot shownAvailable (line_item.line_price)

Because groups isn't available, grouping here relies entirely on the app's _bundle_id property — which is present in both cart line modes, so nothing is lost.

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.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.line_price %}
      {% if b_sku == "" %}{% assign b_sku = line_item.properties["Bundle SKU"] %}{% endif %}
      {% if b_name == "" %}
        {% if line_item.properties._foxr_builder != blank %}
          {% assign parts = line_item.properties._foxr_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 %}

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