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():
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:
- Collects every
item_packagingvalue referenced across the costing'sitemschild table. - Fetches all matching Item Packaging records in a single
frappe.get_allcall (no N+1). - 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.
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_containerfrom 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:
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:
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_bagsandtotal_gross_mtare blended across both. The per-item charge rows use each line's owngross_weight_mt/net_weight_mtfor item-scoped charges (procurement cost), while shipment-level charges (sea freight, insurance) use the header totals. See theresolve_charge_qtymethod inexport_costing.pyfor the exact logic.
Export Costing Item child table fields

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:
- Confirm the commodity exists as an ERPNext Item (Item Group, Stock UOM and description are sufficient — no pricing setup needed at this stage).
- Create an Item Packaging record for each item-packaging combination you ship. At minimum, set
unit_weight_kgandunits_per_container. Addempty_unit_weight_kgif you need accurate net weight (required for charges billed per net MT or per Kg). - If you ship the same commodity in two bag sizes, create two Item Packaging records — the autoname format will differentiate them (e.g.
IR64-50kgandIR64-25kg). - 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.