Wiki

Wiki

Open in ChatGPT
Ask ChatGPT about this page
Open in Claude
Ask Claude about this page

Items & Packaging

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:

  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.
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:

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_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

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.
Last updated 3 months ago
Was this helpful?
Thanks!