---
title: "Items & Packaging"
space: "Wiki"
url: "https://jmtradelink.com/docs/setup/items-packaging"
updated: "2026-06-22"
---

# Items & Packaging

**Section: Setup**

---

Items and packaging are the two foundational masters that drive every weight calculation in an Export Costing. Before you can produce a meaningful costing, you need at least one ERPNext Item (the commodity) and one Item Packaging record that describes how that commodity is packed and containerised.

---

## The commodity: ERPNext Item

The commodity being exported (e.g. IR64 Parboiled Rice, Cashew) is a standard ERPNext **Item** (`Item` DocType). Export Management does not introduce a separate commodity master — it links directly to the existing Item via the `item_code` field.

This means all standard ERPNext Item attributes (item group, UOM, description, valuation, etc.) are available, but the app itself only ever uses `item_code` as a foreign-key identifier. Pricing and weight logic live entirely in Export Management's own structures.

> **Weight-based limitation.** The entire app is denominated in metric tonnes and kilograms. Gross MT, Net MT, per-MT cost, per-MT quoted price — all assume weight is the billing unit. There is no UOM-flexibility layer, so goods sold per piece, litre, or carton cannot be correctly modelled without a refactor. This is a known, deferred limitation documented in `USAGE.md`.

---

## Item Packaging master

**DocType:** `Item Packaging`
**Path:** Export Management › Setup › Item Packaging

Each record answers one question: *for this item, in this container type, how heavy is one bag and how many bags fit per container?*

### Autoname format

The name is computed in `ItemPackaging.autoname()`:

```python
self.name = f"{self.item_code}-{flt(self.unit_weight_kg):g}kg"
```

The `%g` format specifier (via Python's `{:g}`) drops trailing zeros and the decimal point for whole numbers. A 50 kg bag produces `IR64-50kg`, not `IR64-50.0kg`. A 25.5 kg bag would produce `IR64-25.5kg`.

This makes the name human-readable at a glance and unique per item-per-bag-weight combination. You can have two records for the same item if you ever ship in both 50 kg and 25 kg bags.

### Fields

| Field | Fieldname | Type | Required | Description |
|---|---|---|---|---|
| Item | `item_code` | Link → Item | Yes | The commodity this spec applies to |
| Container Type | `container_type` | Select | No | `20' FCL`, `40' FCL`, or `40' HC` — informational, not used in calculations |
| Is Default | `is_default` | Check | No | UI convenience flag; not enforced by code |
| Unit Weight (Kg) | `unit_weight_kg` | Float | Yes | Filled bag weight used for gross tonnage (e.g. `50`) |
| Empty Bag Weight (Kg) | `empty_unit_weight_kg` | Float | No | Weight of the empty bag/sack, in kilograms (e.g. `0.13`). Deducted from gross to arrive at net commodity weight |
| Bags per Container | `units_per_container` | Int | No | How many bags fit in one container of the chosen type (e.g. `400`) |

`container_type` and `is_default` are recorded for human reference but are not read by any calculation code. All weight math uses only `unit_weight_kg`, `empty_unit_weight_kg`, and `units_per_container`.

### Why a separate master instead of fields on the Item?

A single commodity can be packed in different bag sizes for different markets (50 kg in West Africa, 25 kg in some Gulf markets). Keeping packaging as a separate linked record means the same Item can have multiple specs, and a costing line explicitly selects which spec applies to that shipment.

---

## How a costing line inherits packaging defaults

When an Export Costing is saved, `validate()` runs several steps in order: it first copies header-level settings from Export Settings (`set_defaults_from_settings`), then calls `set_packaging_defaults()`. This method:

1. Collects every `item_packaging` value referenced across the costing's `items` child table.
2. Fetches all matching Item Packaging records in a **single `frappe.get_all` call** (no N+1).
3. For each item line, copies the three weight fields from the spec **only if the line's own field is currently zero/blank** — a value that was already entered manually is never overwritten.

```python
def set_packaging_defaults(self):
    packaging_names = [row.item_packaging for row in self.items if row.item_packaging]
    if not packaging_names:
        return

    specs = {
        spec.name: spec
        for spec in frappe.get_all(
            "Item Packaging",
            filters={"name": ("in", packaging_names)},
            fields=["name", "unit_weight_kg", "empty_unit_weight_kg", "units_per_container"],
        )
    }
    for row in self.items:
        spec = specs.get(row.item_packaging)
        if not spec:
            continue
        if not flt(row.unit_weight_kg):
            row.unit_weight_kg = spec.unit_weight_kg
        if not flt(row.empty_unit_weight_kg):
            row.empty_unit_weight_kg = spec.empty_unit_weight_kg
        if not cint(row.units_per_container):
            row.units_per_container = spec.units_per_container
```

> **Gotcha: the defaults are "sticky-on-first-write".** If you change the Item Packaging master (e.g. update `units_per_container` from 400 to 420), existing costing lines that already have a non-zero value will **not** be updated. Only lines where the field is still zero will absorb the new value on next save. This is intentional — a submitted costing reflects the spec at the time it was built.

This means you can also override any of the three fields directly on the costing line without touching the master. That override persists across re-saves.

---

## Weight calculations: `calculate_item_totals`

Called immediately after `set_packaging_defaults()` on every save. It re-derives all computed weight fields from scratch — there is no caching between saves.

### Per-line formulas

Given one item line with `no_of_containers`, `units_per_container`, `unit_weight_kg`, and `empty_unit_weight_kg`:

```python
row.total_bags = cint(row.no_of_containers) * cint(row.units_per_container)

row.gross_weight_mt = flt(row.total_bags * flt(row.unit_weight_kg) / 1000)

row.net_weight_mt = flt(
    row.gross_weight_mt - row.total_bags * flt(row.empty_unit_weight_kg) / 1000
)
```

In plain terms:

| Computed field | Formula | What it represents |
|---|---|---|
| `total_bags` | `no_of_containers × units_per_container` | Total bag count for this line |
| `gross_weight_mt` | `total_bags × unit_weight_kg ÷ 1000` | Weight including the packaging (bags/sacks) |
| `net_weight_mt` | `gross_weight_mt − (total_bags × empty_unit_weight_kg ÷ 1000)` | Weight of commodity only, with tare removed |

**Concrete example** — IR64 parboiled rice, 50 kg bags, 400 bags/FCL, 0.13 kg empty bag weight, 10 containers:

| Field | Value |
|---|---|
| `no_of_containers` | 10 |
| `units_per_container` | 400 |
| `unit_weight_kg` | 50 |
| `empty_unit_weight_kg` | 0.13 |
| `total_bags` | 4,000 |
| `gross_weight_mt` | 4,000 × 50 ÷ 1,000 = **200 MT** |
| `net_weight_mt` | 200 − (4,000 × 0.13 ÷ 1,000) = 200 − 0.52 = **199.48 MT** |

The 0.52 MT difference is the total tare weight of 4,000 polypropylene sacks. On a 200 MT shipment this is small but not negligible — at USD 350/MT it is about USD 182 absorbed silently if you invoice on gross weight instead of net.

### Roll-up to costing header

After all lines are processed, the header-level totals are summed:

```python
self.total_containers = sum(cint(row.no_of_containers) for row in self.items)
self.total_bags       = sum(cint(row.total_bags) for row in self.items)
self.total_gross_mt   = flt(sum(flt(row.gross_weight_mt) for row in self.items))
self.total_net_mt     = flt(sum(flt(row.net_weight_mt) for row in self.items))
```

| Header field | Source |
|---|---|
| `total_containers` | Sum of `no_of_containers` across all item lines |
| `total_bags` | Sum of `total_bags` across all item lines |
| `total_gross_mt` | Sum of `gross_weight_mt` across all item lines |
| `total_net_mt` | Sum of `net_weight_mt` across all item lines |

These four header fields are then consumed by virtually every downstream calculation — freight (per container), insurance (per MT), surveyor fees (per MT), procurement cost (per net MT/Kg), and the final per-MT quoted price.

> **Gotcha: mixed commodities.** If a costing has two item lines (e.g. rice and cashew), `total_bags` and `total_gross_mt` are blended across both. The per-item charge rows use each line's own `gross_weight_mt` / `net_weight_mt` for item-scoped charges (procurement cost), while shipment-level charges (sea freight, insurance) use the header totals. See the `resolve_charge_qty` method in `export_costing.py` for the exact logic.

---

## Export Costing Item child table fields

![Items & Packaging screenshot](/files/items-and-packaging.png)


For reference, these are the fields on each item line in a costing:

| Field | Fieldname | Editable | Notes |
|---|---|---|---|
| Item | `item_code` | Yes | Link to ERPNext Item |
| Packaging | `item_packaging` | Yes | Link to Item Packaging master |
| Containers | `no_of_containers` | Yes | How many FCLs of this item |
| Units / FCL | `units_per_container` | Yes | Pre-filled from packaging spec; overridable |
| Unit Weight (Kg) | `unit_weight_kg` | Yes | Pre-filled from packaging spec; overridable |
| Empty Unit Wt (Kg) | `empty_unit_weight_kg` | Yes | Pre-filled from packaging spec; overridable |
| Total Bags | `total_bags` | Read-only | Computed on save |
| Gross MT | `gross_weight_mt` | Read-only | Computed on save |
| Net MT | `net_weight_mt` | Read-only | Computed on save |

---

## Setup checklist

Before creating an Export Costing:

1. Confirm the commodity exists as an ERPNext **Item** (Item Group, Stock UOM and description are sufficient — no pricing setup needed at this stage).
2. Create an **Item Packaging** record for each item-packaging combination you ship. At minimum, set `unit_weight_kg` and `units_per_container`. Add `empty_unit_weight_kg` if you need accurate net weight (required for charges billed per net MT or per Kg).
3. If you ship the same commodity in two bag sizes, create two Item Packaging records — the autoname format will differentiate them (e.g. `IR64-50kg` and `IR64-25kg`).
4. On the costing's item table, select the correct **Packaging** for each line and enter `no_of_containers`. The weight fields populate automatically on first save.
