---
title: "Landed Cost & Currency Comparison"
space: "Wiki"
url: "https://jmtradelink.com/docs/the-costing-workflow/landed-cost-currency-comparison"
updated: "2026-06-22"
---

# 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](/files/landed-cost-and-currency-comparison.png)


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

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

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

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

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

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