Wiki

Wiki

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

Revisions

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 typed rate. The charge structure is part of the deal template, not the vendor negotiation.
  • Rates as typed — the rate and currency columns 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[*].supplier and charges[*].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:

  1. Destroy the audit trail of what was actually quoted and agreed.
  2. Break the link integrity: the Quotation and Sales Order priced off the old math would silently mismatch the new numbers.
  3. 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

  1. On an existing costing in any status → Create > Revision.
  2. The new costing opens in Draft with a fresh costing_date and an incremented version.
  3. Pull Vendor Ratesraw_rates fills with the latest submitted Supplier Quotation rows.
  4. Generate Combinations → picks the cheapest vendor/port combination, applies rates, resets quoted_price_per_mt so it recomputes from the new cost.
  5. Adjust Selling Price / Bag if the new cost requires a price change.
  6. Create > Quotation → sends the revised price to the customer.
  7. 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:

  1. The field is empty (zero or null).
  2. 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_mt to 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 set quoted_price_per_mt directly. 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 in USAGE.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:

  1. Open the Won costing.
  2. Create > Revision → produces, say, EC-0005 (v2, revision_of = EC-0003).
  3. Pull the new freight SQ (the forwarder re-quoted at a different rate).
  4. Generate Combinations — the engine picks the cheapest port/vendor for the new rates.
  5. If the new landed cost is higher, adjust Selling Price / Bag accordingly.
  6. 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
Last updated 3 months ago
Was this helpful?
Thanks!