Revisions
Section: Quoting and Winning
What a Revision Is
A revision is a new Export Costing that is a child of an earlier one. When you call Create > Revision on any costing, the system copies it in full, advances the version counter, pins both documents to the same root ancestor, and returns the new costing in Draft status — ready to receive fresh vendor rates, a new combination, and a new customer quote.
The original is never touched. It remains exactly as it was — status, math, linked documents and all — so the full pricing history of a deal is auditable without any "track changes" infrastructure.
The Code: make_revision
File: export_management/export_management/doctype/export_costing/export_costing_actions.py
@frappe.whitelist()
def make_revision(export_costing: str) -> str:
# spec §18 — new version for re-quoting; never overwrite old math
source = frappe.get_doc("Export Costing", export_costing)
source.check_permission("write")
revision = frappe.copy_doc(source)
revision.version = cint(source.version or 1) + 1
# chain every revision to the root costing so the whole family lists together
revision.revision_of = source.revision_of or source.name
revision.status = "Draft"
revision.costing_date = today()
revision.shipment = None
revision.quotation = None
revision.sales_order = None
revision.set("raw_rates", [])
for charge_row in revision.charges:
charge_row.supplier = None
charge_row.supplier_quotation = None
revision.insert()
return revision.name
frappe.copy_doc does a deep copy of the document and all its child tables. Everything that is not explicitly reset afterwards carries over unchanged.
What Is Copied vs. What Is Reset
Fields Reset on Every Revision
| Field | Type | Reset to | Why |
|---|---|---|---|
version |
Int | cint(source.version or 1) + 1 |
Distinguishes siblings; v1 → v2 → v3 as the deal evolves |
revision_of |
Link → Export Costing | root ancestor's name | Chains every revision to the same root so the whole family lists together |
status |
Select | "Draft" |
The new costing hasn't been worked yet — no inherited status |
costing_date |
Date | today() |
Date reflects when this version was prepared, not when the original was |
shipment |
Link → Export Shipment | None |
The old shipment belongs to the old version |
quotation |
Link → Quotation | None |
The old quote was priced on the old math |
sales_order |
Link → Sales Order | None |
The order was placed against the original; the revision re-quotes |
raw_rates |
Child table | Emptied ([]) |
These are vendor-rate snapshots for a specific point in time — stale on a new version |
charges[*].supplier |
Link → Supplier | None |
Per-row — the old vendor pick is cleared so Pull can re-assign |
charges[*].supplier_quotation |
Link → Supplier Quotation | None |
Per-row — the old SQ reference is cleared for the same reason |
Fields Carried Over Unchanged
Everything not in the table above is an exact copy:
- Item lines — commodity, packaging spec, container count, weights. The deal is for the same goods.
- Charge structure — every charge head row (FOB, sea freight, CFS, insurance, ECTN, BL, etc.) with its
qty_basis,qty, and typedrate. The charge structure is part of the deal template, not the vendor negotiation. - Rates as typed — the
rateandcurrencycolumns on each charge row carry over. If a fixed charge such as the BL fee or surveyor cost is already known, the revision inherits it and you only need to re-source the variable heads. - Customer, opportunity, port of loading/discharge, container type, destination currency, selling price/bag, costing template, FX rates.
Why carry the rates over? Fixed charges (ECTN, BL, insurance) rarely change between rounds. Keeping them avoids re-entering a dozen known costs by hand. The vendor-sourced heads (
charges[*].supplierandcharges[*].supplier_quotation) are cleared precisely because those are the unknowns that triggered the revision.
The Version Chain
revision_of always points at the root costing, never at an intermediate version. The logic is:
revision.revision_of = source.revision_of or source.name
If you are revising v1 (original, no revision_of), the new v2 gets revision_of = v1.name.
If you then revise v2 (which already has revision_of = v1.name), the new v3 also gets revision_of = v1.name.
This means every member of the family shares the same revision_of value. A list view filtered on revision_of = "EC-0001" returns all revisions of that deal in one place, regardless of depth.
The version field (hidden, read-only on the form) provides the sequence: 1, 2, 3. It defaults to 1 on a fresh costing and increments by exactly 1 on each make_revision call — using cint(source.version or 1) + 1 so an unset version is treated as 1 rather than 0.
Why Not Just Edit the Original?
The original costing may already be in status Quoted or Won, with a linked Supplier Quotation, Quotation, and possibly a Sales Order. Overwriting its numbers would:
- Destroy the audit trail of what was actually quoted and agreed.
- Break the link integrity: the Quotation and Sales Order priced off the old math would silently mismatch the new numbers.
- Make it impossible to compare rounds — "why did we drop $8/MT between v1 and v3?" is a question that comes up after a deal closes.
A revision keeps the old version frozen. You re-pull rates, regenerate combinations, set a new selling price, and create a new customer Quotation — all on the new document, against the original's audit baseline.
Typical Revision Workflow
- On an existing costing in any status → Create > Revision.
- The new costing opens in Draft with a fresh
costing_dateand an incrementedversion. - Pull Vendor Rates →
raw_ratesfills with the latest submitted Supplier Quotation rows. - Generate Combinations → picks the cheapest vendor/port combination, applies rates, resets
quoted_price_per_mtso it recomputes from the new cost. - Adjust Selling Price / Bag if the new cost requires a price change.
- Create > Quotation → sends the revised price to the customer.
- If the customer accepts, submit the Quotation → Sales Order → costing flips to Won.
The quoted_price_per_mt Staleness Limitation
quoted_price_per_mt is a cached internal reference figure (cost per gross MT in the quote currency). It drives quoted_value on the form. The field is hidden on the form (not exposed for direct editing) but is not marked read-only in the DocType definition — it can be set programmatically or via the API.
set_quoted_price in export_costing.py recomputes it only in two situations:
- The field is empty (zero or null).
- The quote currency changed since the last save.
Generate Combinations triggers recomputation indirectly by calling reset_quoted_price() first (sets it to 0), which causes condition 1 to fire on the next save.
Gotcha: If you apply a combination and then set
quoted_price_per_mtto a non-zero value outside the normal flow, that value will persist even if the underlying charge rates change on a subsequent save — because the field is non-zero and the currency has not changed, so the recompute branch is skipped. The safe path is: never setquoted_price_per_mtdirectly. Let Generate Combinations (which resets it) drive the value, or use Regenerate Quotation which also triggers a full recalculation. A future "manual override flag" would close this fully — currently noted as a deferred limitation inUSAGE.md.
On a freshly created revision, raw_rates is empty and no combination has been applied, so quoted_price_per_mt carries the value from the source costing. It will recompute correctly as soon as you run Generate Combinations (which resets it to 0 first), or when you change the quote currency.
Status Flow After a Revision
The status lifecycle on a revision is the same as on any costing:
Draft → Rates Received → Costing Selected → Quoted → Won / Lost
The old costing retains whatever status it had (e.g. Quoted or Lost). The two siblings have independent statuses. Auto-transitions that fire on Sales Order submission only update the specific costing that is linked to that Sales Order, so a Won v1 and a Draft v3 can coexist without conflict.
Example: Demo Scenario
The demo seed (export_management.export_management.demo.seed.run) creates a Won costing for JM Tradelink with a linked Export Shipment. If the shipment is delayed and the customer requests a re-quote at new freight rates, the correct move is:
- Open the Won costing.
- Create > Revision → produces, say,
EC-0005(v2, revision_of =EC-0003). - Pull the new freight SQ (the forwarder re-quoted at a different rate).
- Generate Combinations — the engine picks the cheapest port/vendor for the new rates.
- If the new landed cost is higher, adjust Selling Price / Bag accordingly.
- Create > Quotation → send to customer.
EC-0003 (Won, with its original math and shipment) is untouched throughout.
Summary Table
| Aspect | Behavior |
|---|---|
| Entry point | Create > Revision button on any Export Costing |
| Backend function | make_revision in export_costing_actions.py |
| Copy mechanism | frappe.copy_doc (deep copy of doc + all child tables) |
| Version numbering | cint(source.version or 1) + 1; defaults to 1 on originals |
| Family anchor | revision_of always points at the root (v1), never an intermediate |
| Status on creation | Always Draft |
| Costing date | Set to today() at revision time |
| Linked documents cleared | shipment, quotation, sales_order |
| Vendor data cleared | raw_rates table emptied; supplier + supplier_quotation on every charge row nulled |
| What is kept | Item lines, full charge structure, typed rates, customer, ports, currencies, selling price/bag |
| Old costing | Frozen — never modified by make_revision |