From db2076db693a54a8962588ba26a31682a1acc99f Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 6 Feb 2023 16:25:38 +0530 Subject: [PATCH] refactor: Order based alternative items mapping - Alternatives must be followed by a non-alternative item row - On submit, store non-alternative rows in hidden checkbox to avoid recomputation - Check for valid/mappable rows by row name - UI: Select from table rows.Add single row for original/alternative item in dialog - UI: Indicator for alternative items in dialog grid - UI: Indicator legend and description of table - DB: Added check field 'Has Alternative Item' not to be confused with 'Has Alternative' in Mfg --- .../selling/doctype/quotation/quotation.js | 93 ++++++++++++------- .../selling/doctype/quotation/quotation.py | 72 +++++++------- .../quotation_item/quotation_item.json | 18 ++-- 3 files changed, 102 insertions(+), 81 deletions(-) diff --git a/erpnext/selling/doctype/quotation/quotation.js b/erpnext/selling/doctype/quotation/quotation.js index 183619e6f3..a67f9b05cc 100644 --- a/erpnext/selling/doctype/quotation/quotation.js +++ b/erpnext/selling/doctype/quotation/quotation.js @@ -146,7 +146,7 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext. let has_alternative_item = this.frm.doc.items.some((item) => item.is_alternative); if (has_alternative_item) { - this.show_alternative_item_dialog(); + this.show_alternative_items_dialog(); } else { frappe.model.open_mapped_doc({ method: "erpnext.selling.doctype.quotation.quotation.make_sales_order", @@ -231,60 +231,83 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext. }) } - show_alternative_item_dialog() { + show_alternative_items_dialog() { var me = this; - let item_alt_map = {}; - // Create a `{original item: [alternate items]}` map - this.frm.doc.items.filter( - (item) => item.is_alternative - ).forEach((item) => - (item_alt_map[item.alternative_to] ??= []).push(item.item_code) - ) - - const fields = [{ - fieldtype:"Link", - fieldname:"original_item", - options: "Item", - label: __("Original Item"), + const table_fields = [ + { + fieldtype:"Data", + fieldname:"name", + label: __("Name"), read_only: 1, - in_list_view: 1, }, { fieldtype:"Link", - fieldname:"alternative_item", + fieldname:"item_code", options: "Item", - label: __("Alternative Item"), + label: __("Item Code"), + read_only: 1, in_list_view: 1, - get_query: (row, cdt, cdn) => { - return { - filters: { - "item_code": ["in", item_alt_map[row.original_item]] - } - } - }, + columns: 2, + formatter: (value, df, options, doc) => { + return doc.is_alternative ? `${value}` : value; + } + }, + { + fieldtype:"Data", + fieldname:"description", + label: __("Description"), + in_list_view: 1, + read_only: 1, + }, + { + fieldtype:"Currency", + fieldname:"amount", + label: __("Amount"), + options: "currency", + in_list_view: 1, + read_only: 1, + }, + { + fieldtype:"Check", + fieldname:"is_alternative", + label: __("Is Alternative"), + read_only: 1, }]; - this.data = Object.keys(item_alt_map).map((item) => { - return {"original_item": item} + + this.data = this.frm.doc.items.filter( + (item) => item.is_alternative || item.has_alternative_item + ).map((item) => { + return { + "name": item.name, + "item_code": item.item_code, + "description": item.description, + "amount": item.amount, + "is_alternative": item.is_alternative, + } }); const dialog = new frappe.ui.Dialog({ - title: __("Select Alternatives for Sales Order"), + title: __("Select Alternative Items for Sales Order"), fields: [ + { + fieldname: "info", + fieldtype: "HTML", + read_only: 1 + }, { fieldname: "alternative_items", fieldtype: "Table", - label: "Items with Alternatives", cannot_add_rows: true, in_place_edit: true, reqd: 1, data: this.data, - description: __("Select an alternative to be used in the Sales Order or leave it blank to use the original item."), + description: __("Select an item from each set to be used in the Sales Order."), get_data: () => { return this.data; }, - fields: fields + fields: table_fields }, ], primary_action: function() { @@ -292,7 +315,7 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext. method: "erpnext.selling.doctype.quotation.quotation.make_sales_order", frm: me.frm, args: { - mapping: dialog.get_value("alternative_items") + selected_items: dialog.fields_dict.alternative_items.grid.get_selected_children() } }); dialog.hide(); @@ -300,6 +323,12 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext. primary_action_label: __('Continue') }); + dialog.fields_dict.info.$wrapper.html( + `

+ + Alternative Items +

` + ) dialog.show(); } }; diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index d7882c9eb4..a4a5667f8e 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -28,7 +28,6 @@ class Quotation(SellingController): self.validate_valid_till() self.validate_shopping_cart_items() self.set_customer_name() - self.validate_alternative_items() if self.items: self.with_items = 1 @@ -36,6 +35,9 @@ class Quotation(SellingController): make_packing_list(self) + def before_submit(self): + self.set_has_alternative_item() + def validate_valid_till(self): if self.valid_till and getdate(self.valid_till) < getdate(self.transaction_date): frappe.throw(_("Valid till date cannot be before transaction date")) @@ -60,6 +62,16 @@ class Quotation(SellingController): title=_("Unpublished Item"), ) + def set_has_alternative_item(self): + """Mark 'Has Alternative Item' for rows.""" + if not any(row.is_alternative for row in self.get("items")): + return + + items_with_alternatives = self.get_rows_with_alternatives() + for row in self.get("items"): + if not row.is_alternative and row.name in items_with_alternatives: + row.has_alternative_item = 1 + def get_ordered_status(self): status = "Open" ordered_items = frappe._dict( @@ -98,10 +110,8 @@ class Quotation(SellingController): ) return in_sales_order - items_with_alternatives = self.get_items_having_alternatives() - def can_map(row) -> bool: - if row.is_alternative or (row.item_code in items_with_alternatives): + if row.is_alternative or row.has_alternative_item: return is_in_sales_order(row) return True @@ -127,24 +137,6 @@ class Quotation(SellingController): ) self.customer_name = company_name or lead_name - def validate_alternative_items(self): - if not any(row.is_alternative for row in self.get("items")): - return - - non_alternative_items = filter(lambda item: not item.is_alternative, self.get("items")) - non_alternative_items = list(map(lambda item: item.item_code, non_alternative_items)) - - alternative_items = filter(lambda item: item.is_alternative, self.get("items")) - - for row in alternative_items: - if row.alternative_to not in non_alternative_items: - frappe.throw( - _("Row #{0}: {1} is not a valid non-alternative Item from the table").format( - row.idx, frappe.bold(row.alternative_to) - ), - title=_("Invalid Item"), - ) - def update_opportunity(self, status): for opportunity in set(d.prevdoc_docname for d in self.get("items")): if opportunity: @@ -222,10 +214,21 @@ class Quotation(SellingController): def on_recurring(self, reference_doc, auto_repeat_doc): self.valid_till = None - def get_items_having_alternatives(self): - alternative_items = filter(lambda item: item.is_alternative, self.get("items")) - items_with_alternatives = set((map(lambda item: item.alternative_to, alternative_items))) - return items_with_alternatives + def get_rows_with_alternatives(self): + rows_with_alternatives = [] + table_length = len(self.get("items")) + + for idx, row in enumerate(self.get("items")): + if row.is_alternative: + continue + + if idx == (table_length - 1): + break + + if self.get("items")[idx + 1].is_alternative: + rows_with_alternatives.append(row.name) + + return rows_with_alternatives def get_list_context(context=None): @@ -261,10 +264,7 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): ) ) - alternative_map = { - x.get("original_item"): x.get("alternative_item") - for x in frappe.flags.get("args", {}).get("mapping", []) - } + selected_rows = [x.get("name") for x in frappe.flags.get("args", {}).get("selected_items", [])] def set_missing_values(source, target): if customer: @@ -297,19 +297,11 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): 3. Is Alternative Item: Map if alternative was selected against original item and #1 """ has_qty = item.qty > 0 - - has_alternative = item.item_code in alternative_map - is_alternative = item.is_alternative - - if not alternative_map or not (is_alternative or has_alternative): + if not (item.is_alternative or item.has_alternative_item): # No alternative items in doc or current row is a simple item (without alternatives) return has_qty - if is_alternative: - is_selected = alternative_map.get(item.alternative_to) == item.item_code - else: - is_selected = alternative_map.get(item.item_code) is None - return is_selected and has_qty + return (item.name in selected_rows) and has_qty doclist = get_mapped_doc( "Quotation", diff --git a/erpnext/selling/doctype/quotation_item/quotation_item.json b/erpnext/selling/doctype/quotation_item/quotation_item.json index f62a0997dc..f2aabc5240 100644 --- a/erpnext/selling/doctype/quotation_item/quotation_item.json +++ b/erpnext/selling/doctype/quotation_item/quotation_item.json @@ -50,7 +50,7 @@ "stock_uom_rate", "is_free_item", "is_alternative", - "alternative_to", + "has_alternative_item", "section_break_43", "valuation_rate", "column_break_45", @@ -654,19 +654,19 @@ "print_hide": 1 }, { - "depends_on": "is_alternative", - "fieldname": "alternative_to", - "fieldtype": "Link", - "label": "Alternative To", - "mandatory_depends_on": "is_alternative", - "options": "Item", - "print_hide": 1 + "default": "0", + "fieldname": "has_alternative_item", + "fieldtype": "Check", + "hidden": 1, + "label": "Has Alternative Item", + "print_hide": 1, + "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2023-01-26 07:32:02.768197", + "modified": "2023-02-06 11:00:07.042364", "modified_by": "Administrator", "module": "Selling", "name": "Quotation Item",