Wiki

Wiki

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

Costing Templates

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

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

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

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

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

Last updated 3 months ago
Was this helpful?
Thanks!