From 7556517a4fde103476cc39e0d32289595e56271e Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Mon, 24 Aug 2020 14:07:09 +0000 Subject: [PATCH] feat: validate if removed item attributes exist in variants (#22911) * feat: validate if removed item attributes exist in variants * Update erpnext/stock/doctype/item/item.py Co-authored-by: Marica * Update erpnext/stock/doctype/item/item.py Co-authored-by: Marica * refactor: smaller loop Co-authored-by: marination * feat: don't run validation for new entries Co-authored-by: Marica * fix: use tuple as is Co-authored-by: Marica * refactor: error description Co-authored-by: Marica * refactor: remove unused variable Co-authored-by: Marica --- erpnext/stock/doctype/item/item.py | 72 ++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index d209f48353..d22fda85f4 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -111,6 +111,7 @@ class Item(WebsiteGenerator): self.synced_with_hub = 0 self.validate_has_variants() + self.validate_attributes_in_variants() self.validate_stock_exists_for_template_item() self.validate_attributes() self.validate_variant_attributes() @@ -806,6 +807,77 @@ class Item(WebsiteGenerator): if frappe.db.exists("Item", {"variant_of": self.name}): frappe.throw(_("Item has variants.")) + def validate_attributes_in_variants(self): + if not self.has_variants or self.get("__islocal"): + return + + old_doc = self.get_doc_before_save() + old_doc_attributes = set([attr.attribute for attr in old_doc.attributes]) + own_attributes = [attr.attribute for attr in self.attributes] + + # Check if old attributes were removed from the list + # Is old_attrs is a subset of new ones + # that means we need not check any changes + if old_doc_attributes.issubset(set(own_attributes)): + return + + from collections import defaultdict + + # get all item variants + items = [item["name"] for item in frappe.get_all("Item", {"variant_of": self.name})] + + # get all deleted attributes + deleted_attribute = list(old_doc_attributes.difference(set(own_attributes))) + + # fetch all attributes of these items + item_attributes = frappe.get_all( + "Item Variant Attribute", + filters={ + "parent": ["in", items], + "attribute": ["in", deleted_attribute] + }, + fields=["attribute", "parent"] + ) + not_included = defaultdict(list) + + for attr in item_attributes: + if attr["attribute"] not in own_attributes: + not_included[attr["parent"]].append(attr["attribute"]) + + if not len(not_included): + return + + def body(docnames): + docnames.sort() + return "
".join(docnames) + + def table_row(title, body): + return """ + {0} + {1} + """.format(title, body) + + rows = '' + for docname, attr_list in not_included.items(): + link = "{0}".format(frappe.bold(_(docname))) + rows += table_row(link, body(attr_list)) + + error_description = _('The following deleted attributes exist in Variants but not in the Template. You can either delete the Variants or keep the attribute(s) in template.') + + message = """ +
{0}

+ + + + + + {3} +
{1}{2}
+ """.format(error_description, _('Variant Items'), _('Attributes'), rows) + + frappe.throw(message, title=_("Variant Attribute Error"), is_minimizable=True, wide=True) + + def validate_stock_exists_for_template_item(self): if self.stock_ledger_created() and self._doc_before_save: if (cint(self._doc_before_save.has_variants) != cint(self.has_variants)