Wiki

Wiki

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

Creating a Quotation

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:

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.

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.
Last updated 3 months ago
Was this helpful?
Thanks!