Costing Templates
Section: Setup
A Costing Template pre-fills an Export Costing with every charge whose rate is known before a specific shipment is planned: port charges, surveyor fees, freight-document fees, insurance, ECTN, BL charges, and POD/destination clearing parameters. It also captures the destination lane (port and currency) and the import-duty calculation parameters that apply to that lane.
What the template deliberately does not hold: the loading port and the container type. Both are decided at a later stage — either by the Combination Engine (which tests multiple loading ports) or by the shipment plan. Pinning them on the template would make a single template useless across the loading-port variations the system is built to compare.
Header Fields

| Field | Type | Description |
|---|---|---|
template_name |
Data (unique) | Human name; also the document name (autoname by fieldname). e.g. Cotonou – Standard. |
port_of_discharge |
Link → Port | The destination port. Copied to the costing on populate if the costing does not already have one. |
destination_currency |
Link → Currency | Currency of all Destination-stage charges and the per-bag margin block. e.g. XOF for Cotonou, XAF for Cameroon. |
Why
destination_currencylives on the template: every charge in the Destination stage is validated on costing save to be in this currency. Capturing it here means every costing derived from the template starts with the correct currency set and the validator does not reject the copied rows.
Import Duty / POD Clearing Section
These four fields are the lane parameters for the compute_pod_clearing calculation (implemented in pod_clearing.py). They are grouped in the UI under "Import Duty / POD Clearing".
| Field | Type | Default | Description |
|---|---|---|---|
bfu_percent |
Float | 0.20 |
BFU (bond) as a fraction of the customs-rate CFA duty value. |
transitor_per_container |
Currency (destination_currency) |
205000 |
Fixed transitor fee per container, in the destination currency. |
tax_percent |
Float | 0.025 |
Tax as a fraction of (convertable CFA value + BFU + transitor). |
commission_per_bag |
Currency (destination_currency) |
150 |
Clearing-agent commission per bag, added to pod_clearing_per_bag. |
These four fields are collectively referenced in code as IMPORT_DUTY_LANE_PARAMS (defined in pod_clearing.py):
IMPORT_DUTY_LANE_PARAMS = ("bfu_percent", "transitor_per_container", "tax_percent", "commission_per_bag")
Why these live on the template rather than on Export Settings: different destination lanes have different duty regimes. Cotonou and Cameroon use different rates. Per-template capture means each lane can have its own figures without cross-contamination.
Charges Child Table (Costing Template Charge)
Each row in charges represents one fixed-cost head — a charge whose rate is stable enough to be pre-configured and reused across multiple costings.
| Field | Type | Notes |
|---|---|---|
charge_item |
Link → Export Charge Item | Required. The named charge head (CFS, Surveyor, ECTN, BL, Insurance, etc.). |
stage |
Select | Ex-Works / FOB / CFR / CIF / Destination. Fetched from charge_item.default_stage if empty. |
qty_basis |
Select | How the charge quantity is resolved at costing time. Options: Per Net MT, Per Gross MT, Per Kg, Per Bag, Per Container, Per BL, Lumpsum, Percent of Subtotal, Manual. Fetched from charge_item.default_qty_basis if empty. |
rate |
Float (9 dp) | The rate to copy. For Lumpsum charges this is the total amount; for per-unit charges it is the unit rate. |
currency |
Link → Currency | Currency the rate is denominated in. Fetched from charge_item.default_currency if empty. |
rate_updated_on |
Date (read-only) | Set automatically by stamp_rate_update_dates on every save. Never edited manually. |
Only charge heads with is_variable = 0 on their Export Charge Item belong in a template. Variable heads (ocean freight, etc.) are appended as empty rows by append_variable_charge_rows when populate_from_template runs. Goods/procurement heads (is_variable = 1, is_goods = 1) are handled separately by sync_item_procurement_rows, which is called during costing validate — not by populate_from_template.
Rate Staleness Stamping
Every time a Costing Template is saved, CostingTemplate.validate calls stamp_rate_update_dates. The logic, verbatim from costing_template.py:
def stamp_rate_update_dates(self):
before = self.get_doc_before_save()
previous_rates = {row.name: flt(row.rate) for row in before.charges} if before else {}
for row in self.charges:
is_new_row = row.name not in previous_rates
rate_changed = not is_new_row and previous_rates[row.name] != flt(row.rate)
if not row.rate_updated_on or is_new_row or rate_changed:
row.rate_updated_on = today()
Three conditions each stamp rate_updated_on = today():
- New row — the row's
nameis absent from the pre-save state (the row was just added). - Rate changed — the row existed before and its
ratevalue has changed (float comparison). - Blank stamp —
rate_updated_onis empty for any reason (defensive fallback).
The field is read-only in the UI — it is purely system-managed.
Why stamp on the template rather than on the costing: the age of a rate is a property of the template row, not of any individual costing derived from it. Recording the date of last change here lets every future
populate_from_templatecall evaluate staleness without needing to know the history of any previous costing.
populate_from_template
ExportCosting.populate_from_template is a whitelisted method (called from the costing form). It rebuilds the costing's charge table from scratch and snapshots the lane parameters. The full sequence:
Step 1 — Snapshot the destination lane
for fieldname in ("port_of_discharge", "destination_currency"):
if not self.get(fieldname) and template.get(fieldname):
self.set(fieldname, template.get(fieldname))
Only copies if the costing field is currently blank. A user who manually set a different discharge port before clicking populate is not overwritten.
Step 2 — Snapshot the import-duty lane parameters
for fieldname in IMPORT_DUTY_LANE_PARAMS:
if not flt(self.get(fieldname)) and template.get(fieldname):
self.set(fieldname, template.get(fieldname))
Same "only if blank/zero" guard, and for the same reason as cfa_rate_per_eur on the costing: once a costing has a captured value, a later change to the template's defaults must not silently rewrite it. Historical costings must reflect the lane parameters that were in force when they were created.
Step 3 — Rebuild the charges table
The existing charges table is cleared completely, then each template row is appended with a staleness check:
is_stale = (
not template_row.rate_updated_on
or date_diff(today(), template_row.rate_updated_on) > settings.rate_staleness_days
)
self.append("charges", {
"charge_item": template_row.charge_item,
"stage": template_row.stage,
"qty_basis": template_row.qty_basis,
"rate": template_row.rate,
"currency": template_row.currency,
"rate_stale": cint(is_stale),
})
The threshold is Export Settings.rate_staleness_days (default: 14 days). A row whose rate_updated_on is older than 14 days, or which has no stamp at all, arrives on the costing with rate_stale = 1. This flag is informational — it does not block saving — but it signals which rates need verification before the costing is submitted.
Step 4 — Append variable charge rows
After the fixed template rows are in place, append_variable_charge_rows queries all Export Charge Item records where is_variable = 1, is_goods = 0, and disabled = 0, then appends an empty row (rate = 0) for any non-goods variable head not already present. These blank rows are the placeholders that the vendor-rate pull and the Combination Engine fill.
Goods/procurement heads (is_variable = 1, is_goods = 1) are handled by a separate method, sync_item_procurement_rows, which runs during costing validate and adds one item-scoped charge row per goods item per goods head. It is additive — existing rows with pulled rates are not touched.
Step 5 — Ensure FX rows
ensure_exchange_rate_rows scans the charge table for every non-INR currency used outside the Destination stage, then unconditionally adds EUR (the CFA destination block always needs it regardless of whether any charge is denominated in EUR). For each missing currency an exchange rate row is appended with a suggested live rate fetched via ERPNext's get_exchange_rate. This network call runs once here, never inside the combination generation loop.
What the Template Does NOT Do
| Omission | Reason |
|---|---|
| No loading port | The Combination Engine varies the loading port to find the cheapest freight lane. Pinning it on the template produces one combination instead of many. |
| No container type | Also a combination-level variable; a single shipment may compare 20' FCL against 40' HC. |
| No variable charge rates | Non-goods variable heads (ocean freight, etc.) come from submitted Supplier Quotations at costing time. Goods/procurement rates are pulled via a separate mechanism. |
| No exchange rates | FX is live and must reflect conditions at costing time, not template-creation time. |
Staleness Threshold Configuration
Configured globally in Export Settings:
| Setting | Default | Description |
|---|---|---|
rate_staleness_days |
14 | Template charge rates older than this many days are flagged rate_stale = 1 when copied to a costing. |
Concretely: a CFS charge row on the Cotonou template last edited 20 days ago will arrive on any new costing with rate_stale = 1, prompting the operator to verify the current port tariff before trusting the costing.
Example: Cotonou Template
A representative Cotonou template carries these fixed-charge rows (Destination-stage rows are priced in XOF):
| Charge Item | Stage | Qty Basis | Currency |
|---|---|---|---|
| CFS Charges | FOB | Per Container | INR |
| Surveyor Fee | FOB | Lumpsum | INR |
| ECTN | CFR | Lumpsum | USD |
| BL Fee | CFR | Per BL | USD |
| Insurance | CIF | Percent of Subtotal | INR |
| POD Handling | Destination | Per Bag | XOF |
| Documentation Fee | Destination | Lumpsum | XOF |
Header lane:
port_of_discharge: Cotonoudestination_currency: XOF
Import duty lane params (demo defaults):
bfu_percent: 0.20transitor_per_container: 205,000 XOFtax_percent: 0.025commission_per_bag: 150 XOF
When populate_from_template runs on a new costing linked to this template, all seven rows arrive with their rates and a staleness flag where applicable. The four duty params are snapshotted onto the costing. Non-goods variable heads (e.g. ocean freight) arrive as empty rows with rate = 0 via append_variable_charge_rows. Goods/procurement rows are added per-item via sync_item_procurement_rows during validate. An FX row for USD is created automatically; EUR is always created regardless because the CFA duty block depends on it even when no charge is denominated in EUR.