Compare commits

...

1 Commits

Author SHA1 Message Date
Mihir Kandoi
4a2c60e098 feat(selling): show the latest quotation revision as latest (#59485) 2026-09-26 13:41:48 +00:00
4 changed files with 72 additions and 10 deletions

View File

@@ -25,6 +25,7 @@
"has_unit_price_items",
"amended_from",
"revision_of",
"is_latest_revision",
"currency_and_price_list",
"currency",
"conversion_rate",
@@ -227,6 +228,16 @@
"read_only": 1,
"search_index": 1
},
{
"default": "0",
"fieldname": "is_latest_revision",
"fieldtype": "Check",
"hidden": 1,
"label": "Is Latest Revision",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "company",
"fieldtype": "Link",
@@ -1169,7 +1180,7 @@
"idx": 82,
"is_submittable": 1,
"links": [],
"modified": "2026-09-24 14:30:00.000000",
"modified": "2026-09-26 12:00:00.000000",
"modified_by": "Administrator",
"module": "Selling",
"name": "Quotation",

View File

@@ -79,6 +79,7 @@ class Quotation(SellingController):
in_words: DF.Data | None
incoterm: DF.Link | None
is_active: DF.Check
is_latest_revision: DF.Check
item_wise_tax_details: DF.Table[ItemWiseTaxDetail]
items: DF.Table[QuotationItem]
language: DF.Link | None
@@ -384,6 +385,7 @@ class Quotation(SellingController):
self.update_opportunity("Quotation")
self.update_lead()
self.deactivate_other_versions()
self.update_latest_revision()
def deactivate_other_versions(self):
if not (self.revision_of and self.is_active):
@@ -403,9 +405,31 @@ class Quotation(SellingController):
return bool(self.get_other_versions(VERSIONS_TO_SET_AS_LOST))
def update_other_versions(self, filters: dict, values: dict):
names = [version.name for version in self.get_other_versions(filters)]
frappe.db.bulk_update("Quotation", {name: values for name in names})
for name in names:
self.update_versions({version.name: values for version in self.get_other_versions(filters)})
def update_latest_revision(self):
versions = self.get_other_versions({})
if not (versions or self.is_latest_revision):
return
if self.docstatus == 1:
versions.append(self)
latest = max(versions, key=get_version_order).name if len(versions) > 1 else None
self.update_versions(
{
version.name: {"is_latest_revision": int(version.name == latest)}
for version in versions
if version.name != self.name
},
update_modified=False,
)
self.db_set("is_latest_revision", int(self.name == latest), update_modified=False)
@staticmethod
def update_versions(updates: dict[str, dict], update_modified: bool = True):
frappe.db.bulk_update("Quotation", updates, update_modified=update_modified)
for name in updates:
frappe.clear_document_cache("Quotation", name)
@property
@@ -413,12 +437,8 @@ class Quotation(SellingController):
return not self.get_newer_versions()
def get_newer_versions(self) -> list[frappe._dict]:
own_order = (getdate(self.transaction_date), get_datetime(self.creation))
return [
version
for version in self.get_other_versions({})
if (version.transaction_date, version.creation) > own_order
]
own_order = get_version_order(self)
return [version for version in self.get_other_versions({}) if get_version_order(version) > own_order]
def validate_can_be_revised(self):
if self.status in ("Lost", "Ordered"):
@@ -443,6 +463,7 @@ class Quotation(SellingController):
self.set_status(update=True)
self.update_opportunity("Open")
self.update_lead()
self.update_latest_revision()
def carry_forward_communication(self):
from erpnext.crm.utils import copy_comments, link_communications
@@ -485,6 +506,10 @@ class Quotation(SellingController):
return rows_with_alternatives
def get_version_order(version) -> tuple:
return (getdate(version.transaction_date), get_datetime(version.creation))
def get_list_context(context=None):
from erpnext.controllers.website_list_for_contact import get_list_context

View File

@@ -7,6 +7,7 @@ frappe.listview_settings["Quotation"] = {
"currency",
"valid_till",
"is_active",
"is_latest_revision",
],
onload: function (listview) {
@@ -38,6 +39,8 @@ frappe.listview_settings["Quotation"] = {
return [__("Lost"), "gray", "status,=,Lost"];
} else if (doc.docstatus === 1 && !doc.is_active) {
return [__("Inactive"), "red", "is_active,=,0"];
} else if (doc.status === "Open" && doc.is_latest_revision) {
return [__("Latest"), "orange", "is_latest_revision,=,1"];
} else if (doc.status === "Open") {
return [__("Open"), "orange", "status,=,Open"];
} else if (doc.status === "Partially Ordered") {

View File

@@ -539,6 +539,29 @@ class TestQuotation(ERPNextTestSuite):
self.assertEqual(revision.items[0].rate, 250)
self.assertEqual(revision.items[0].prevdoc_docname, opportunity.name)
def test_latest_revision_is_flagged(self):
quotation = make_quotation()
self.assertEqual(quotation.is_latest_revision, 0)
first_revision = make_revision(quotation.name)
first_revision.insert()
first_revision.submit()
second_revision = make_revision(first_revision.name)
second_revision.insert()
second_revision.submit()
self.assertEqual(self.get_latest_revision_flags(quotation), [0, 0, 1])
second_revision.cancel()
self.assertEqual(self.get_latest_revision_flags(quotation), [0, 1, 0])
def get_latest_revision_flags(self, quotation):
return [
frappe.db.get_value("Quotation", name, "is_latest_revision")
for name in (quotation.name, f"{quotation.name}-R1", f"{quotation.name}-R2")
]
def test_submitting_a_revision_deactivates_other_versions(self):
quotation = make_quotation()
first_revision = make_revision(quotation.name)