---
title: "Export Settings"
space: "Wiki"
url: "https://jmtradelink.com/docs/setup/export-settings"
updated: "2026-06-22"
---

# Export Settings

**Section:** Setup

Export Settings is a [Single DocType](https://frappeframework.com/docs/user/en/basics/doctypes/single-doctype) — there is exactly one record, accessible at **Desk → Export Settings**. It holds three categories of configuration: FX and CFA conversion parameters that flow into every costing, the Exporter Profile that appears on all printed documents and guest web pages, and Live Tracking credentials for vessel/container tracking.

---

## FX & CFA Parameters

These fields govern how foreign-currency costs are converted and how the CFA destination block is calculated.

| Field | Fieldname | Type | Default | What it controls |
|---|---|---|---|---|
| FX Buffer | `fx_buffer` | Float | **1.0** | Added to every base exchange rate to produce the `buffered_rate` |
| CFA Francs per EUR | `cfa_rate_per_eur` | Float (6dp) | **655.957** | XOF/XAF treaty peg used to convert EUR costs into CFA in the destination block |
| CFA Francs per USD | `cfa_rate_per_usd` | Float (6dp) | **576** | Informational USD-path CFA rate shown alongside the EUR path |
| Rate Staleness Threshold | `rate_staleness_days` | Int | **14** | Days after which a template charge rate is considered stale |
| Max Combinations | `max_combinations` | Int | **500** | Hard cap on the number of cost combinations the engine will generate |

### FX Buffer

When a costing is saved, `set_exchange_rate_buffers` runs across every row in the Exchange Rates child table:

```python
row.buffered_rate = flt(row.base_rate) + flt(self.fx_buffer)
```

The buffered rate is used for **cost conversion** (what you pay): every foreign-currency charge in `calculate_charges` calls `get_buffered_rate`, so costs always convert at a rate that is 1.0 INR/unit higher than the live market rate by default. The base (unbuffered) rate is used for **quote prices** (what you charge the buyer): `calculate_per_mt_costs` and `set_quoted_price` both call `get_base_rate`. This asymmetry means the margin calculation is inherently conservative — the costing absorbs FX slippage before it becomes a loss.

> **Gotcha:** The Desk form initialises Float fields to `0` on new Export Costing documents, which is indistinguishable from a deliberate zero on the server. `get_export_settings` is therefore whitelisted so the new-costing form can pre-fill `fx_buffer` via a server call rather than relying on field defaults set client-side.

### CFA Francs per EUR (655.957)

The West and Central African CFA franc (XOF/XAF) is treaty-pegged to the EUR at **655.957** — this is not a market rate and does not float. The destination block converts EUR-denominated import duties and clearing costs into CFA using:

```python
def get_destination_to_inr_rate(self) -> float:
    eur_rate = self.get_base_rate("EUR")
    if not (eur_rate and flt(self.cfa_rate_per_eur)):
        return 0.0
    return eur_rate / flt(self.cfa_rate_per_eur)
```

This gives INR-per-CFA, which then converts the destination per-bag charges (expressed in CFA) into the INR cascade.

> **Gotcha:** Only change `cfa_rate_per_eur` if the client's bank charges a spread on top of the peg, or if the monetary union changes the treaty rate (historically once in 1994). The JSON description explicitly calls this out: *"Change only if the client uses a bank rate or the peg changes."*

### CFA Francs per USD (576)

This is a market-derived rate used only for the **informational USD-path landed cost** displayed next to the primary EUR-path figure. It does not enter the INR cost cascade. Update it when the USD/XOF rate drifts materially.

### Rate Staleness Threshold (14 days)

When `populate_from_template` copies charge rows from a Costing Template onto a new Export Costing, it compares each template row's `rate_updated_on` date against today:

```python
is_stale = (
    not template_row.rate_updated_on
    or date_diff(today(), template_row.rate_updated_on) > settings.rate_staleness_days
)
```

Rows older than the threshold get `rate_stale = 1` on the costing, which the UI can surface as a visual flag so the operator knows to re-fetch those rates before quoting. Rows with no `rate_updated_on` at all are always treated as stale.

> **Gotcha:** This flag is set at template-copy time only. If you open an existing costing weeks later, the staleness is not rechecked — it reflects when the costing was created.

### Max Combinations (500)

The combination engine cross-joins vendor rates across loading ports and container types to produce a matrix of possible cost scenarios. With many vendors and items this can explode. The cap prevents runaway generation:

```python
# a zero cap would block all generation, so 0 falls back to the default too
max_combinations=cint(settings.max_combinations) or DEFAULT_MAX_COMBINATIONS
```

Note the special case: **a value of `0` falls back to 500**, not to zero. A true zero would block all generation and is assumed to be an unset field rather than a deliberate choice. If generation hits the cap, prune the number of vendor rate rows or raise this setting.

---

## The `get_export_settings` Helper

```python
@frappe.whitelist()
def get_export_settings() -> frappe._dict:
```

This is the canonical way to read Export Settings throughout the codebase. It uses `frappe.get_cached_doc` (no extra DB hit per call), then applies fallback logic for every numeric field:

- `fx_buffer`, `cfa_rate_per_eur`, `cfa_rate_per_usd`, `rate_staleness_days` — fall back to the module-level `DEFAULT_*` constants when the field is `None` (i.e., the Single has never been saved with a value).
- `max_combinations` — additionally treats `0` as unset and falls back to `DEFAULT_MAX_COMBINATIONS`.

The helper also returns Exporter Profile fields (`exporter_name`, `exporter_address`, `iec_code`, `gstin`, `exporter_email`, `exporter_phone`, `bank_details`) with no fallback — they pass through as-is and may be `None` if never filled in. `company_logo` is not included; it is only available via `frappe.get_cached_doc("Export Settings")` directly.

Callers: `ExportCosting.validate` (via `set_defaults_from_settings`), `ExportCosting.populate_from_template`, the combination engine, and the desk client for pre-filling new costings.

---

## Snapshot Semantics — Settings Changes Never Rewrite Historical Costings

`set_defaults_from_settings` only writes the CFA rates onto a costing when they are currently zero:

```python
def set_defaults_from_settings(self, settings):
    if self.fx_buffer is None:
        self.fx_buffer = settings.fx_buffer
    if not flt(self.cfa_rate_per_eur):
        # snapshot once; later settings changes must not rewrite old costings
        self.cfa_rate_per_eur = settings.cfa_rate_per_eur
    if not flt(self.cfa_rate_per_usd):
        self.cfa_rate_per_usd = settings.cfa_rate_per_usd
```

The comment in the code is explicit about the intent. Once a costing has been saved with a non-zero `cfa_rate_per_eur`, updating Export Settings will not touch it. This preserves the accuracy of historical costings — a margin calculated at 655.957 stays at 655.957 even if the bank rate is later updated to 660.

Note that `fx_buffer` uses a stricter guard (`is None`) than the CFA rates (`not flt(...)`): a costing where `fx_buffer` was explicitly set to `0.0` will not be overwritten, but one where the field was never touched (database `NULL`) will be seeded from settings.

The same snapshot pattern applies to import-duty lane parameters: `populate_from_template` snapshots them with the guard `if not flt(self.get(fieldname)) and template.get(fieldname)`.

---

## Exporter Profile

These fields are printed on every outbound trade document (Proforma Invoice, Commercial Invoice, Packing List) and injected into guest web pages. They describe your company to buyers and suppliers, and carry the legal identity fields required on Indian export documents.

| Field | Fieldname | Type | Appears on |
|---|---|---|---|
| Company Logo | `company_logo` | Attach Image | Guest `/quote/<token>` page header |
| Exporter Name | `exporter_name` | Data | Document headers, guest page header/footer |
| Exporter Address | `exporter_address` | Small Text | Document headers |
| IEC Code | `iec_code` | Data | Document headers (Indian customs identifier) |
| GSTIN | `gstin` | Data | Document headers (GST registration number) |
| Email | `exporter_email` | Data (Email) | Guest page "Questions?" line, document footer |
| Phone | `exporter_phone` | Data | Guest page "Questions?" line, document footer |
| Bank Details | `bank_details` | Small Text | Commercial Invoice footer (beneficiary, account number, SWIFT, AD code) |

### Company Logo and `update_website_context`

Guest portal pages (`/quote/<token>`) run outside an ERPNext session — there is no logged-in user. The logo and contact information are injected into every web page via a Frappe hook:

```python
# export_management/website_context.py
def update_website_context(context):
    settings = frappe.get_cached_doc("Export Settings")
    context.update({
        "company_logo": settings.get("company_logo"),
        "exporter_name": settings.get("exporter_name"),
        "exporter_phone": settings.get("exporter_phone"),
        "exporter_email": settings.get("exporter_email"),
    })
```

This function is registered as the `update_website_context` hook in `hooks.py`. Frappe calls it before rendering any web page in the app, so every template that extends `export_management/templates/layout.html` automatically receives these four context variables. The layout's header include renders the logo and the contact line without any per-page plumbing.

> **Gotcha:** `company_logo` is not included in `get_export_settings` — it is only available via `frappe.get_cached_doc("Export Settings")` directly. The helper exposes the other profile fields (name, address, IEC, GSTIN, email, phone, bank details) but omits the logo because it is not needed for costing calculations.

Use a WebP or PNG resized to approximately 96 px height for the logo. Oversized images will scale down in the browser but the raw file is still served, so resize before uploading.

---

## Live Tracking

A third section in Export Settings holds vessel/container tracking credentials. These are separate from costing and Exporter Profile concerns.

| Field | Fieldname | Type | Default | Notes |
|---|---|---|---|---|
| Tracking Provider | `tracking_provider` | Select | **Mock** | `Mock` yields deterministic demo data with no credentials; `Maersk` and `Vessel AIS` are dormant scaffolds that need the API keys below |
| Maersk API Key | `maersk_api_key` | Password | — | Only read when provider is `Maersk` |
| AIS API Key | `ais_api_key` | Password | — | Only read when provider is `Vessel AIS` |
| AIS API Base URL | `ais_api_base_url` | Data | — | Base URL for the AIS provider endpoint |

The `Mock` provider is safe for demos and development — it requires no credentials and returns predictable tracking events.

---

## Access Control

Export Settings is restricted to the **System Manager** role (read, write, create, share, email, print). Non-manager users interact with it only indirectly: the costing form pre-fills from it via the whitelisted `get_export_settings` call, and guest pages receive the four website context variables automatically.

![Export Settings screenshot](/files/export-settings.png)
