---
title: "Export Charge Items"
space: "Wiki"
url: "https://jmtradelink.com/docs/setup/export-charge-items"
updated: "2026-06-22"
---

# 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

![Export Charge Items screenshot](/files/export-charge-items.png)


| 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](#goods-vs-service-heads). |
| `is_variable` | Check | — | Marks this head as vendor-sourced. See [Variable vs Fixed heads](#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:

```python
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_quotation` processes a goods head, it reads `charge_row.item_code` (the actual commodity like "Sona Masoori Raw Rice") and puts that on the RFQ item table.
- `service_item` is always set to `None` — `ensure_service_item` exits immediately if `is_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_item` is called on every `validate`. It creates a synthetic non-stock ERPNext Item whose `item_code` equals `charge_name`, in the item group `Export Charges`.
- The `service_item` field on the charge item record is set to that Item's name (which is identical to `charge_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:

```python
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 Subtotal` is stage-aware. It computes against `stage_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 = 1`
- `default_stage = CFR`, `default_qty_basis = Per Container`, `default_currency = USD`
- On save: a synthetic Item `"Ocean Freight"` is created in the `Export Charges` group, `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 = 1`
- `default_stage = Ex-Works`, `default_qty_basis = Per Net MT`, `default_currency = INR`
- On save: `service_item` is set to `None`. 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.
