Export Settings
Section: Setup
Export Settings is a 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:
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
0on new Export Costing documents, which is indistinguishable from a deliberate zero on the server.get_export_settingsis therefore whitelisted so the new-costing form can pre-fillfx_buffervia 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:
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_eurif 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:
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:
# 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
@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-levelDEFAULT_*constants when the field isNone(i.e., the Single has never been saved with a value).max_combinations— additionally treats0as unset and falls back toDEFAULT_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:
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) |
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:
# 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_logois not included inget_export_settings— it is only available viafrappe.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.
