Wiki

Wiki

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

Landed Cost & Currency Comparison

Landed Cost & Currency Comparison

Section: The Costing Workflow

This page covers the destination-side of an Export Costing: how the engine converts a CIF-level cost into a per-bag landed figure in CFA francs, runs the same conversion on a USD path to find the cheaper invoice currency, and computes the margin the seller makes at the current selling price.


Background: where this sits in the validate chain

Every time an Export Costing is saved, ExportCosting.validate() runs a fixed sequence:

set_defaults_from_settings → set_packaging_defaults → calculate_item_totals
→ sync_item_procurement_rows → set_exchange_rate_buffers → validate_destination_charges
→ calculate_charges → calculate_per_mt_costs → set_quoted_price
→ calculate_destination_block          ← this page

calculate_destination_block is the last step. It consumes the fully-settled CIF total from calculate_charges, the per-MT EUR/USD views from calculate_per_mt_costs, and the lane configuration snapshots to produce everything visible in the "Destination" section of the form.


Key fields and where they come from

Landed Cost & Currency Comparison screenshot

Exchange rates and pegs

Field Type Description
cfa_rate_per_eur Float CFA francs per 1 EUR. Snapshotted from Export Settings on first save; never updated by later settings changes so historical costings stay consistent.
cfa_rate_per_usd Float CFA francs per 1 USD. Same snapshot behaviour.
exchange_rates (child table) INR exchange rates per foreign currency, with a configurable FX buffer applied. EUR is always required — the system adds it even when no charge row uses EUR.

The EUR peg is the authoritative chain. INR → EUR is read from the exchange_rates table (base rate, no buffer). EUR → CFA is the fixed peg. The USD path uses an independent peg (cfa_rate_per_usd), not a EUR→USD→CFA chain.

Import-duty lane parameters

Four fields are snapshotted from the Costing Template at populate_from_template time (mirroring the peg snapshot) so that changing the template later does not rewrite existing costings. Two additional duty-rate fields (convertable_duty_rate and customs_duty_rate) are set from the template the same way but are not included in the IMPORT_DUTY_LANE_PARAMS constant.

Field Meaning
bfu_percent BFU rate applied to the customs CFA value
transitor_per_container Fixed CFA transit charge per container
tax_percent Tax rate applied to (convertable CFA value + BFU + transitor)
commission_per_bag Agent commission in CFA, per bag
convertable_duty_rate Rate used to derive the convertable CFA value
customs_duty_rate Rate used to derive the customs CFA value (BFU base)

calculate_destination_block: the EUR path

def calculate_destination_block(self):
    eur_rate = self.get_base_rate("EUR")
    if not (cint(self.total_bags) and eur_rate):
        self.reset_destination_block()
        return

    mt_per_bag = flt(self.total_gross_mt) / cint(self.total_bags)
    cost_per_mt_eur = self.get_cost_per_mt_inr() / eur_rate
    self.cost_per_bag_destination = flt(cost_per_mt_eur * mt_per_bag * flt(self.cfa_rate_per_eur))
    self.pod_expense_per_bag = self.calculate_pod_expense_per_bag()

    self.compute_pod_clearing()

    self.landed_cost_per_bag = flt(
        self.cost_per_bag_destination + self.pod_clearing_per_bag + self.pod_expense_per_bag
    )
    self.compute_usd_landed(mt_per_bag)
    self.set_destination_margins()

Step 1 — weight per bag

mt_per_bag = total_gross_mt / total_bags

This is the average gross tonne per bag across all items in the costing. Because calculate_item_totals already computed total_gross_mt and total_bags from the packaging spec, this division is always consistent with the item lines.

Step 2 — CIF cost per MT in EUR

cost_per_mt_eur = get_cost_per_mt_inr() / eur_rate

get_cost_per_mt_inr() applies finance cost and profit mark-up to the CIF total and divides by gross MT. Dividing by the base (un-buffered) EUR rate converts it to EUR. The base rate is used here — not the buffered rate — because this is an internal economics view, not a hedge-conservative procurement price.

Step 3 — CFA cost per bag at destination

cost_per_bag_destination = cost_per_mt_eur × mt_per_bag × cfa_rate_per_eur

This is the goods-only cost in CFA: the raw material plus all upstream charges (Ex-Works → FOB → CFR → CIF), scaled down to one bag's weight and converted via the EUR→CFA peg.

Why EUR and not USD? The client's Estimate Cost sheet records customs values in a EUR-formatted column. The EUR column is the "golden basis" — using the USD column would overstate the customs value by approximately 17% (see comment in compute_pod_clearing). The EUR path is therefore canonical for duty calculations.

Step 4 — destination-stage charge expenses per bag

calculate_pod_expense_per_bag loops over the charges table and sums only rows whose stage == "Destination":

  • Per Bag rows: the rate is already in CFA per bag — added directly.
  • Lumpsum rows: the total rate is spread evenly across all bags.
def calculate_pod_expense_per_bag(self) -> float:
    pod_expense = 0.0
    for row in self.charges:
        if row.stage != DESTINATION_STAGE:
            continue
        if row.qty_basis == "Per Bag":
            pod_expense += flt(row.rate)
        else:  # Lumpsum
            pod_expense += flt(row.rate) / cint(self.total_bags)
    return flt(pod_expense)

Validation rule: Destination-stage charge rows must use Per Bag or Lumpsum as their qty basis (those are the only two DESTINATION_ALLOWED_BASES). Any other basis throws at save time. All destination rows must be in the destination_currency — mixing currencies in this block is blocked.

Step 5 — structured POD clearing (compute_pod_clearing)

This reproduces the multi-line duty build-up from the client's "Estimate Cost" sheet (shipment JMC-252623). It is inert — held at zero — until convertable_duty_rate, total_bags, and cif_per_mt_eur are all non-zero, so old or selling-only costings are unaffected.

duty_base_grand_total_eur = MROUND(cif_per_mt_eur, 5) × total_gross_mt
cfa_customs_value         = duty_base_grand_total_eur × customs_duty_rate
cfa_convertable_value     = duty_base_grand_total_eur × convertable_duty_rate

bfu_amount       = cfa_customs_value × bfu_percent
transitor_amount = transitor_per_container × total_containers
tax_amount       = (cfa_convertable_value + bfu_amount + transitor_amount) × tax_percent

pod_clearing_in_total = bfu_amount + transitor_amount + tax_amount
pod_clearing_per_bag  = pod_clearing_in_total / total_bags + commission_per_bag

MROUND: The duty base rounds cif_per_mt_eur to the nearest 5 — matching Excel's MROUND function. This is intentional; the sheet shows a EUR-formatted customs cell with this rounding, and the CFA values downstream depend on it.

Cross-column BFU/tax bases: BFU uses the customs CFA value as its base, but the tax calculation uses the convertable CFA value (not the customs value) as its base — plus BFU and transitor on top. This cross-column design matches the client's sheet exactly. The comment in the code flags it for client confirmation and explicitly says do not "fix" it to a single column.

The pod_clearing_per_bag result is then added in calculate_destination_block alongside pod_expense_per_bag to form the full landed cost.

Step 6 — landed cost per bag (EUR path)

landed_cost_per_bag = cost_per_bag_destination + pod_clearing_per_bag + pod_expense_per_bag
Component What it covers
cost_per_bag_destination CIF goods cost in CFA (via EUR peg)
pod_clearing_per_bag BFU + transitor + tax + commission in CFA
pod_expense_per_bag Destination-stage Per Bag and Lumpsum charge rows in CFA

compute_usd_landed: the parallel USD path

def compute_usd_landed(self, mt_per_bag: float) -> None:
    usd_rate = self.get_base_rate("USD")
    cost_per_mt_usd = self.get_cost_per_mt_inr() / usd_rate if usd_rate else 0.0
    self.cost_per_bag_destination_usd = flt(cost_per_mt_usd * mt_per_bag * flt(self.cfa_rate_per_usd))
    self.landed_cost_per_bag_usd = flt(
        self.cost_per_bag_destination_usd + self.pod_clearing_per_bag + self.pod_expense_per_bag
    )
    self.cheaper_currency = "USD" if self.landed_cost_per_bag_usd < self.landed_cost_per_bag else "EUR"

The USD path reuses the same mt_per_bag and the same get_cost_per_mt_inr() result. The only difference is the conversion chain:

cost_per_bag_destination_usd = (cost_per_mt_inr / usd_rate) × mt_per_bag × cfa_rate_per_usd

Crucially, pod_clearing_per_bag and pod_expense_per_bag are reused unchanged. The duty and POD expenses are assessed in CFA at the destination regardless of which currency the invoice is in, so they are computed once on the EUR basis (step 5) and shared between the two paths.

The only thing that differs between EUR and USD landed cost is the goods CIF component translated via a different peg.

cheaper_currency flag

self.cheaper_currency = "USD" if self.landed_cost_per_bag_usd < self.landed_cost_per_bag else "EUR"

EUR wins ties. This is deliberate: the combination engine ranks combinations on the EUR basis, so EUR is the default pick. A strict < comparison means USD only wins when it is genuinely cheaper, and EUR remains the default in all parity cases.

This flag is informational. It indicates which invoice currency produces a lower CFA landed cost for the buyer, but it does not automatically flip the quote_currency or the quotation. The trader uses it to choose how to structure the offer.


set_destination_margins

def set_destination_margins(self) -> None:
    if not flt(self.present_selling_price_per_bag):
        self.margin_per_bag = 0
        self.margin_percent = 0
        self.margin_percent_before_pod = 0
        return

    self.margin_per_bag = flt(self.present_selling_price_per_bag - self.landed_cost_per_bag)
    self.margin_percent = (
        self.margin_per_bag / self.landed_cost_per_bag * 100 if self.landed_cost_per_bag else 0
    )
    self.margin_percent_before_pod = (
        self.margin_per_bag / self.cost_per_bag_destination * 100
        if self.cost_per_bag_destination
        else 0
    )

Fields produced

Field Formula Meaning
margin_per_bag present_selling_price_per_bag − landed_cost_per_bag Absolute margin in CFA per bag
margin_percent margin_per_bag / landed_cost_per_bag × 100 Margin on full landed cost (includes POD/duty). This is the primary margin — labelled E57 in the client's sheet.
margin_percent_before_pod margin_per_bag / cost_per_bag_destination × 100 Margin on the goods cost only (excludes pod_clearing_per_bag and pod_expense_per_bag). Labelled E55 in the sheet.

The margin_percent_before_pod figure is shown alongside margin_percent because the client has not yet locked which basis to use for ranking. Both are live until a decision is made.

If present_selling_price_per_bag is blank or zero, all three margin fields zero out. No margin is computed against an empty selling price.

Updating the margin

The margin updates on every save. There is no separate "recalculate margin" button — just change present_selling_price_per_bag and save the document. validate reruns the full chain and set_destination_margins recomputes from the new price.


The zero-rates gotcha

Variable heads start at ₹0 until a combination is applied. A fresh costing has empty rows for Sea Freight, Procurement Cost, Bags, etc. Those rows contribute ₹0 to the CIF total. The resulting landed_cost_per_bag is therefore artificially low — far below a real landed cost — and margin_percent will look enormous.

The margin figure is meaningless until Generate Combinations has been run and the cheapest combination auto-applied. Only then do the variable heads carry real vendor rates. This behaviour is documented in USAGE.md under "Key behaviours to know."


Numeric walkthrough (demo costing)

The demo seed (JM Tradelink, shipment JMC-252623) ships rice in a 40' HC container. Using approximate demo values:

Item Value Source
Total gross MT ~28 MT packaging spec
Total bags ~500 bags 500 × 50 kg bags
mt_per_bag 0.056 MT/bag 28 / 500
CIF total (after vendor rates applied) ~₹X calculate_charges
cif_per_mt_eur ~€270 CIF/MT at base EUR rate
EUR→CFA peg 655.957 BCEAO fixed rate
cost_per_bag_destination ~9,900 XOF 270 × 0.056 × 655.957
bfu_amount + transitor_amount + tax_amount ~varies duty lane params
pod_expense_per_bag sum of Destination rows charge table
landed_cost_per_bag ~11,000–13,000 XOF all three components
present_selling_price_per_bag e.g. 15,500 XOF entered by trader
margin_per_bag ~2,500–4,500 XOF selling price − landed

Exact numbers depend on vendor rates pulled; run the demo seed and open the Won costing to see live figures.


Reset behaviour

If total_bags is zero or the EUR base rate is missing, reset_destination_block zeros every field in this section (and internally calls compute_pod_clearing, which also self-resets via reset_pod_clearing because the guard conditions are not met). This prevents stale values from persisting when the costing is in an incomplete state.

Similarly, compute_pod_clearing self-resets (via reset_pod_clearing) when any of convertable_duty_rate, total_bags, or cif_per_mt_eur is zero — the duty block stays at zero rather than producing a half-computed result.


Summary of the computation graph

get_cost_per_mt_inr()
    │
    ├─── ÷ eur_rate ──▶ cost_per_mt_eur ──▶ × mt_per_bag × cfa_rate_per_eur
    │                                              │
    │                                     cost_per_bag_destination (EUR path)
    │
    ├─── ÷ usd_rate ──▶ cost_per_mt_usd ──▶ × mt_per_bag × cfa_rate_per_usd
    │                                              │
    │                                     cost_per_bag_destination_usd (USD path)
    │
    └─── ÷ eur_rate ──▶ cif_per_mt_eur ──▶ MROUND(5) ──▶ duty build-up
                                                                │
                                                      pod_clearing_per_bag

landed_cost_per_bag     = cost_per_bag_destination     + pod_clearing_per_bag + pod_expense_per_bag
landed_cost_per_bag_usd = cost_per_bag_destination_usd + pod_clearing_per_bag + pod_expense_per_bag

cheaper_currency = "USD" if landed_cost_per_bag_usd < landed_cost_per_bag else "EUR"

margin_per_bag             = present_selling_price_per_bag − landed_cost_per_bag
margin_percent             = margin_per_bag / landed_cost_per_bag × 100
margin_percent_before_pod  = margin_per_bag / cost_per_bag_destination × 100
Last updated 3 months ago
Was this helpful?
Thanks!