---
title: "Costing Templates"
space: "Wiki"
url: "https://jmtradelink.com/docs/setup/costing-templates"
updated: "2026-06-22"
---

# 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

![Costing Templates screenshot](/files/costing-templates.png)


| 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_currency` lives 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`):

```python
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`:

```python
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()`:

1. **New row** — the row's `name` is absent from the pre-save state (the row was just added).
2. **Rate changed** — the row existed before and its `rate` value has changed (float comparison).
3. **Blank stamp** — `rate_updated_on` is 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_template` call 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

```python
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

```python
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:

```python
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`: Cotonou
- `destination_currency`: XOF

**Import duty lane params (demo defaults):**
- `bfu_percent`: 0.20
- `transitor_per_container`: 205,000 XOF
- `tax_percent`: 0.025
- `commission_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.
