---
title: "Creating a Quotation"
space: "Wiki"
url: "https://jmtradelink.com/docs/quoting-and-winning/creating-a-quotation"
updated: "2026-06-22"
---

# Creating a Quotation

**Section:** Quoting and Winning

---

A Quotation in this system is not a generic ERPNext price document — it is a customer-facing offer priced **the way the buyer buys**: per bag, in the destination currency (typically XOF/CFA). The costing lives in INR internally; the Quotation bridges the two worlds by converting through the CFA-per-EUR peg. This page covers how that conversion works, what must be true before you can quote, what the line items look like, and how regeneration is guarded against overwriting a live offer.

---

## Prerequisites — the four guards

`make_quotation` enforces four conditions before touching ERPNext. All four throw a hard `frappe.throw` error if not met; the first one that fails stops execution.

| Guard | Field / source | Why it is required |
|---|---|---|
| Customer is set | `export_costing.customer` | A Quotation in ERPNext requires a `party_name`. Without a linked customer the document cannot be saved. |
| Destination currency is set | `export_costing.destination_currency` | This becomes the Quotation's `currency`. It must match the buyer's invoice currency — for West African buyers this is typically `XOF`. |
| Selling price per bag is non-zero | `export_costing.present_selling_price_per_bag` | The rate on every line item is this single figure. A zero rate produces a zero-value quotation, which is never a valid offer. |
| EUR rate + CFA peg exist | `export_costing.exchange_rates` (EUR row) + `export_costing.cfa_rate_per_eur` | Required to compute the conversion rate (see below). Without both, the Quotation's INR base amounts cannot be derived. |

> **Set your selling price before quoting.** The margin panel on the costing shows landed cost / bag and the resulting margin once a combination is applied. Adjust `present_selling_price_per_bag` and save until the margin is acceptable, then create the Quotation.

---

## The CFA-to-INR conversion rate

XOF (CFA franc) is not freely floating. It is **pegged to the EUR** at a fixed rate (historically 655.957 XOF = 1 EUR, though the peg value is configurable in Export Settings and defaults onto each costing at save time via `cfa_rate_per_eur`). EUR does have a live INR rate, pulled from the market and stored in the costing's `exchange_rates` child table.

The method `get_destination_to_inr_rate` on the Export Costing combines both to produce one conversion rate:

```
INR per XOF = (INR per EUR) / (XOF per EUR)
            = eur_base_rate / cfa_rate_per_eur
```

`eur_base_rate` is read from the `base_rate` field of the EUR row in the `exchange_rates` child table (the unbuffered market rate — the FX buffer is not applied here).

For example: if EUR/INR = 92.00 and the peg is 655.957 XOF/EUR, then 1 XOF = 92.00 / 655.957 ≈ 0.1402 INR.

This rate becomes the Quotation's `conversion_rate` field. ERPNext uses it to compute base amounts in INR from the XOF line totals, keeping the company books in INR while the customer sees prices in XOF.

The method returns `0.0` if either the EUR row is missing from `exchange_rates` or `cfa_rate_per_eur` is zero, which triggers the fourth guard above.

---

## What gets written to the Quotation

```
Quotation header
  quotation_to        = "Customer"
  party_name          = costing.customer
  company             = resolved from Global Defaults
  transaction_date    = today
  order_type          = "Sales"
  currency            = costing.destination_currency   (e.g. XOF)
  conversion_rate     = get_destination_to_inr_rate()  (≈ 0.1402 for CFA)
  selling_price_list  = "Standard Selling"
  price_list_currency = "INR"
  plc_conversion_rate = 1
```

One line item per commodity line in `costing.items`:

| Quotation Item field | Source |
|---|---|
| `item_code` | `costing.items[n].item_code` |
| `qty` | `costing.items[n].total_bags` (integer) |
| `uom` | `"Bag"` (always) |
| `conversion_factor` | `costing.items[n].unit_weight_kg`, falling back to `1` if zero |
| `rate` | `costing.present_selling_price_per_bag` (in destination currency) |

**Example:** a 5,400-bag parcel of rice at 15,500 XOF/bag becomes one line: `qty=5400, rate=15500, uom=Bag, currency=XOF`. ERPNext multiplies by the conversion rate to derive the INR base amount for the ledger.

---

## The Bag UOM conversion factor

Before each line is appended, `ensure_bag_uom_conversion(item_code, unit_weight_kg)` runs for each item. This function:

1. Ensures the `Bag` UOM record exists in ERPNext (creates it if missing).
2. Fetches the Item document and scans its `uoms` child table for an existing `Bag` row.
3. If found and the conversion factor differs from `unit_weight_kg`, it updates it and saves the Item, then returns.
4. If not found, it appends a new row and saves.

The conversion factor stored on the Item is `unit_weight_kg or 1` — when the item line has no weight recorded, `1` is used as a safe fallback so ERPNext does not store a zero conversion factor. The same `or 1` fallback applies to the `conversion_factor` field on the Quotation Item row itself.

The conversion factor is **kg per bag** — it tells ERPNext how to convert bag quantities back to the Item's stock UOM (typically Kg or MT). This matters for stock and valuation; without it, ERPNext cannot post inventory movements in the correct base unit.

> **Changing bag weight after quoting:** if the packaging spec changes (e.g. from 50 kg bags to 25 kg bags), the Item's Bag UOM factor will be updated on the next `make_quotation` call. Any previously saved Quotation lines are not retroactively corrected — regenerate the Quotation (see below) to pick up the new factor.

---

## Regeneration semantics

It is normal to create a Quotation, show it to the customer, then revise the selling price and need a fresh document. `make_quotation` handles this without creating duplicate orphan records.

The costing stores a link to its Quotation in `costing.quotation`. On each call, the function checks:

```
if costing.quotation and frappe.db.exists("Quotation", costing.quotation):
    existing_docstatus = frappe.db.get_value("Quotation", costing.quotation, "docstatus")
```

| Existing Quotation state | `docstatus` value | Behaviour |
|---|---|---|
| Draft | `0` | Treated as a working copy. The stale Quotation is deleted, `costing.quotation` is cleared, and a fresh one is created with current numbers. |
| Submitted | `1` | The offer was sent to the customer. The function throws: *"Quotation {name} is already submitted — cancel it before re-quoting."* Nothing is changed. |
| None / does not exist | — | No check needed; proceeds to create. |

> **Why refuse to overwrite a submitted Quotation?** A submitted Quotation in ERPNext has legal and audit weight — it is the document the customer received and may have signed. Silently replacing it with new numbers would create a discrepancy between the customer's copy and the system record. The correct workflow is: cancel the submitted Quotation (which reverses any GL entries), then call Create > Quotation again.

**View Quotation** on the costing form is a shortcut that opens `costing.quotation` directly — useful when you need to cancel it before regenerating.

---

## Status transition

After the Quotation is inserted and saved, the costing's own status is updated:

```python
if costing.status not in ("Won", "Lost"):
    costing.db_set("status", "Quoted")
```

The guard on `Won` and `Lost` is deliberate: if the customer has already confirmed the order (`Won`) or walked away (`Lost`), creating a Quotation for audit or reference purposes must not silently revert the costing to an earlier stage.

The full status sequence is: `Draft → Rates Received → Costing Selected → Quoted → Won` (or `Lost`). Auto-transitions never move the status backward.

---

## Current limitation: one price for all items

`present_selling_price_per_bag` is a single scalar on the costing. Every item line in the Quotation receives the same rate. This is correct for the common single-commodity shipment (e.g. one grade of rice in multiple containers). For a mixed-commodity costing (rice + cashew), both items will be quoted at the same per-bag price, which is almost certainly wrong.

> **Limitation (from USAGE.md):** "Per-item quoted price — one Selling Price / Bag applies to all items, so a rice+cashew costing quotes both at the same per-bag price. Correct for single-commodity (the normal case)."

The fix would be to move `present_selling_price_per_bag` to the item child table and read each line's own price during the loop. Until then, avoid creating mixed-commodity costings where items carry materially different per-bag values.

---

## Step-by-step: creating a Quotation

1. Open the Export Costing. Confirm `Customer` and `Destination Currency` are set.
2. Ensure a combination is applied (variable cost heads must be non-zero for margin to be meaningful).
3. Set **Selling Price / Bag** (in the destination currency) and save. Verify the margin panel shows the expected figure.
4. Confirm the `exchange_rates` child table has an EUR row with a non-zero `base_rate`, and that `CFA Rate per EUR` is non-zero (it defaults from Export Settings at save time).
5. Click **Create ▸ Quotation**. The system validates the four guards, builds the document, and links it.
6. Click **View Quotation** to open, review, and submit it to send to the customer.
7. To revise: if the Quotation is still Draft, click **Create ▸ Quotation** again — the draft is replaced. If it is Submitted, cancel it first, then regenerate.

---

## Related actions

| Action | When to use |
|---|---|
| **Create ▸ Revision** | Customer wants a fundamentally different cost structure — create a new costing version rather than regenerating the same Quotation. |
| **Create ▸ Sales Order** | Customer accepts the Quotation. Submit the Quotation first, then create the Sales Order from it. The costing status flips to Won. |
| **Create ▸ Purchase Orders** | After Won — drafts one PO per winning Supplier Quotation. |
