Export Charge Items
Export Charge Items
Section: Setup
What is an Export Charge Item?
An Export Charge Item is the master record for a single cost head in the export costing model — think "Ocean Freight", "Customs Duty", "Bagging Charges", or "Rice" itself. Every line that can ever appear in an Export Costing's charges table is backed by one of these records.
The catalogue is not just a label list. Each record carries classification flags that control how rows are created, how they are priced, and which ERPNext procurement documents they can drive.
Fields

| Field | Type | Required | Notes |
|---|---|---|---|
charge_name |
Data | Yes | The human-readable name; also the document's name (autonaming rule: field:charge_name). Must be unique. |
disabled |
Check | — | Excludes this head from all automatic row-appending logic without deleting the record. |
default_stage |
Select | — | Incoterm stage seeded onto new charge rows: Ex-Works, FOB, CFR, CIF, Destination. |
default_qty_basis |
Select | — | Quantity basis seeded onto new rows. Full option set: Per Net MT, Per Gross MT, Per Kg, Per Bag, Per Container, Per BL, Lumpsum, Percent of Subtotal, Manual. |
default_currency |
Link → Currency | — | Currency seeded onto new rows. Falls back to the company currency (INR) when blank. |
is_goods |
Check | — | Marks this head as the physical commodity rather than a service cost. See Goods vs Service heads. |
is_variable |
Check | — | Marks this head as vendor-sourced. See Variable vs Fixed heads. |
service_item |
Link → Item (read-only) | — | Auto-populated by ensure_service_item. The synthetic non-stock Item that stands in for this charge on RFQ/PO rows. Always None when is_goods = 1. |
description |
Small Text | — | Used as the ERPNext Item description when the synthetic service Item is created. Falls back to charge_name if blank. |
Variable vs Fixed heads
is_variable = 1 — vendor-sourced heads
A variable head is one whose rate must come from a supplier quote. You do not know the price at template-design time, so fixed rates are not stored on the Costing Template for these heads.
What happens at costing time:
When a template is applied (populate_from_template), the costing first writes all rows from the template, then calls append_variable_charge_rows which queries:
frappe.get_all(
"Export Charge Item",
filters={"is_variable": 1, "is_goods": 0, "disabled": 0},
fields=["name", "default_stage", "default_qty_basis", "default_currency"],
)
For every variable service head not already present in the charges table, an empty row is appended (rate = 0, currency = default_currency or COMPANY_CURRENCY). These placeholder rows signal that a rate is still needed — they will be populated later via RFQ → Supplier Quotation → Pull.
Similarly, before_insert on a new costing (when no charges rows exist yet) also calls append_variable_charge_rows so a blank costing always starts with at least the variable skeleton.
For variable goods heads (e.g. "Rice"), the logic is different: sync_item_procurement_rows (called on every validate) creates one item-scoped row per product line in the costing's items table (one row per (charge_head, item_code) pair). This is additive — existing rows with already-pulled rates are never overwritten.
is_variable = 0 — fixed/template-loaded heads
A fixed head has rates that are stable enough to store on a Costing Template (e.g. documentation fees, bank charges, BFU, transit insurance). When the template is applied, these rows are copied from the template with their rate and currency. The template itself records a rate_updated_on date; if that date is more than settings.rate_staleness_days old, the row is flagged rate_stale = 1 on the costing — a visual reminder to re-check the rate.
Gotcha: Disabling a charge item stops it being appended to new costings but does not remove it from existing costings that already carry that row.
Goods vs Service heads
is_goods = 1
Reserved for the actual commodity being shipped — e.g. "Rice". There is only one reason for this flag to exist: the procurement flow for goods is fundamentally different from all other charge heads.
- RFQ rows use the real product Item, not a synthetic one. When
make_requests_for_quotationprocesses a goods head, it readscharge_row.item_code(the actual commodity like "Sona Masoori Raw Rice") and puts that on the RFQ item table. service_itemis always set toNone—ensure_service_itemexits immediately ifis_goods = 1.- On purchase: the eventual PO also buys the real product Item, which is what triggers stock and inventory ledger entries in ERPNext.
is_goods = 0 — service heads
All cost heads that are not the commodity (freight, bagging, customs duty, documentation, etc.) are service heads.
ensure_service_itemis called on everyvalidate. It creates a synthetic non-stock ERPNext Item whoseitem_codeequalscharge_name, in the item groupExport Charges.- The
service_itemfield on the charge item record is set to that Item's name (which is identical tocharge_name). - This synthetic Item is then referenced on RFQ, Supplier Quotation, and Purchase Order item rows.
Why does the synthetic Item need to exist?
ERPNext's RFQ, SQ, and PO doctypes all have a mandatory item_code field. There is no way to raise a procurement document for an abstract cost head without an Item master. Rather than polluting the real product catalogue with "Ocean Freight" and "Bagging Charges", the system auto-creates non-stock service Items in a dedicated Export Charges item group. These Items carry is_stock_item = 0 so they never affect inventory.
ensure_service_item — mechanics
Source: export_charge_item.py, ExportChargeItem.validate.
validate()
└── ensure_service_item()
├── if is_goods → set service_item = None, return
├── if service_item field is set AND that Item exists in DB → return (idempotent)
├── ensure_service_item_group() # creates "Export Charges" item group if absent
├── ensure_uom("Nos") # from export_management.setup
├── if Item named charge_name does not exist → frappe.get_doc(Item, ...).insert(ignore_permissions=True)
└── self.service_item = self.charge_name
The operation is fully idempotent. Saving the same record twice does not create a duplicate Item — the check at line 22 (if self.service_item and frappe.db.exists("Item", self.service_item)) short-circuits the whole block when the field is already populated and the Item exists. A second guard (if not frappe.db.exists("Item", self.charge_name)) wraps the actual insert call, so even if service_item was cleared manually but the Item still exists, no duplicate is created.
ensure_service_item_group is also idempotent: it checks whether Export Charges exists first, and creates it as a leaf under the tree root item group only if it does not.
The helper charge_item_for_service_item(item_code) is the reverse lookup — given a synthetic Item's code, it returns the charge item's name. This is used downstream when portal-created Supplier Quotations arrive without a charge_item value on their rows (the portal only knows the Item code, not the internal charge head name).
Qty Bases
The default_qty_basis field (and the corresponding qty_basis on each charge row in the costing) controls how the system resolves the quantity for that row's amount calculation.
Quotable bases — can go on an RFQ
These are the bases for which resolve_charge_qty can compute a real physical quantity from the shipment:
| Qty Basis | Resolved quantity |
|---|---|
Per Net MT |
total_net_mt (or item's net_weight_mt if item-scoped) |
Per Gross MT |
total_gross_mt (or item's gross_weight_mt) |
Per Kg |
total_net_mt × 1000 (net weight, not gross) |
Per Bag |
total_bags (or item's total_bags) |
Per Container |
total_containers (or item's no_of_containers) |
Per BL |
no_of_bl on the costing header |
Lumpsum |
Always 1 (the rate IS the total amount) |
make_requests_for_quotation filters to exactly these bases:
RFQ_QTY_BASES = ("Per Net MT", "Per Gross MT", "Per Kg", "Per Bag", "Per Container", "Per BL", "Lumpsum")
quotable_rows = [row for row in costing.charges if row.qty_basis in RFQ_QTY_BASES]
A vendor receiving an RFQ is being asked to quote a unit rate; the quantity on the RFQ line is the computed physical quantity so the vendor understands the volume they are pricing.
Derived bases — never go on an RFQ
| Qty Basis | How it works |
|---|---|
Percent of Subtotal |
rate / 100 × running_total_at_stage_start. Qty is forced to 1, exchange rate to 1. The "rate" field holds the percentage (e.g. 2.5 for 2.5%). |
Manual |
The user types the quantity directly into the qty field on the charge row. resolve_charge_qty returns flt(row.qty) unchanged. |
Neither of these can meaningfully appear on an RFQ — a vendor cannot quote "2.5% of something not yet known", and a manual quantity is a user judgement, not a supplier rate. Attempting to RFQ a costing with only derived-basis rows raises a frappe.throw.
Gotcha:
Percent of Subtotalis stage-aware. It computes againststage_start, which is the running total at the beginning of the current stage, not the grand total. A 1% bank charge in the FOB stage applies only to Ex-Works costs, not to freight or insurance added later.
Default fields and how they seed charge rows
When append_variable_charge_rows or sync_item_procurement_rows creates a new row on a costing, it reads three fields from the Export Charge Item master:
| Field on master | Copied to charge row as | Fallback |
|---|---|---|
default_stage |
stage |
— (blank) |
default_qty_basis |
qty_basis |
— (blank) |
default_currency |
currency |
COMPANY_CURRENCY (INR) |
These defaults exist so that a newly appended row is immediately valid enough for the costing to save without the user having to fill in every field manually. The user can override any of these values on the charge row after the fact.
The rate is always seeded as 0 for variable heads — there is no default rate because the whole point of is_variable is that the rate comes from a vendor quote.
Permissions
Only the System Manager role has full create/write/delete access out of the box. The charge item catalogue is configuration data, not transactional data, so it is expected to be maintained by a system administrator rather than day-to-day users.
Example: "Ocean Freight" vs "Rice"
Ocean Freight (a typical service head):
is_goods = 0,is_variable = 1default_stage = CFR,default_qty_basis = Per Container,default_currency = USD- On save: a synthetic Item
"Ocean Freight"is created in theExport Chargesgroup,is_stock_item = 0. - On costing: an empty row appears under CFR, awaiting a freight forwarder's quote.
- On RFQ: the RFQ item row carries
item_code = "Ocean Freight"(the synthetic Item),qty = total_containers.
Rice (a goods head):
is_goods = 1,is_variable = 1default_stage = Ex-Works,default_qty_basis = Per Net MT,default_currency = INR- On save:
service_itemis set toNone. No synthetic Item is created. - On costing: one charge row per product line, each scoped to its actual
item_code(e.g."Sona Masoori Raw Rice"). - On RFQ: the RFQ item row carries
item_code = charge_row.item_code(the real commodity Item),qty = item_net_weight_mt. The resulting PO buys actual stock.