From cef7dfd0b48ba6ebd6dfb9eabb722c78e4493ccb Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 26 Jan 2023 14:36:25 +0530 Subject: [PATCH] feat: Dialog to select alternative item before creating Sales order - Users can leave the row blank in the dialog if original item is to be used - Else users can select an alternative item against an original item - In the document, users must check `Is Alternative Item` if needed and also specify which item it is an altenrative to since there are no documented mappings --- erpnext/controllers/taxes_and_totals.py | 2 +- .../public/js/controllers/taxes_and_totals.js | 2 +- .../selling/doctype/quotation/quotation.js | 79 ++++++++++++++++++- .../quotation_item/quotation_item.json | 18 ++++- 4 files changed, 94 insertions(+), 7 deletions(-) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 5815cfa7ac9..1edd7bf85e1 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -32,7 +32,7 @@ class calculate_taxes_and_totals(object): def filter_rows(self): """Exclude rows, that do not fulfill the filter criteria, from totals computation.""" - items = list(filter(lambda item: not item.get("is_alternative_item"), self.doc.get("items"))) + items = list(filter(lambda item: not item.get("is_alternative"), self.doc.get("items"))) return items def calculate(self): diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 607b928d019..029d6c0c417 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -889,6 +889,6 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { } filtered_items() { - return this.frm.doc.items.filter(item => !item["is_alternative_item"]); + return this.frm.doc.items.filter(item => !item["is_alternative"]); } }; diff --git a/erpnext/selling/doctype/quotation/quotation.js b/erpnext/selling/doctype/quotation/quotation.js index 6b42e4daead..6f75673a8e5 100644 --- a/erpnext/selling/doctype/quotation/quotation.js +++ b/erpnext/selling/doctype/quotation/quotation.js @@ -87,7 +87,7 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext. if (doc.docstatus == 1 && !["Lost", "Ordered"].includes(doc.status)) { this.frm.add_custom_button( __("Sales Order"), - this.frm.cscript["Make Sales Order"], + () => this.make_sales_order(), __("Create") ); @@ -141,6 +141,20 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext. } + make_sales_order() { + var me = this; + + let has_alternative_item = this.frm.doc.items.some((item) => item.is_alternative); + if (has_alternative_item) { + this.show_alternative_item_dialog(); + } else { + frappe.model.open_mapped_doc({ + method: "erpnext.selling.doctype.quotation.quotation.make_sales_order", + frm: me.frm + }); + } + } + set_dynamic_field_label(){ if (this.frm.doc.quotation_to == "Customer") { @@ -216,6 +230,69 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext. } }) } + + show_alternative_item_dialog() { + // Create a `{original item: [alternate items]}` map + const item_alt_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"), + read_only: 1, + in_list_view: 1, + }, + { + fieldtype:"Link", + fieldname:"alternative_item", + options: "Item", + label: __("Alternative Item"), + in_list_view: 1, + get_query: (row, cdt, cdn) => { + return { + filters: { + "item_code": ["in", item_alt_map[row.original_item]] + } + } + }, + }]; + + this.data = Object.keys(item_alt_map).map((item) => { + return {"original_item": item} + }); + + const dialog = new frappe.ui.Dialog({ + title: __("Select Alternatives for Sales Order"), + fields: [ + { + 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."), + get_data: () => { + return this.data; + }, + fields: fields + }, + ], + primary_action: function() { + this.hide(); + }, + primary_action_label: __('Continue') + }); + + dialog.show(); + } }; cur_frm.script_manager.make(erpnext.selling.QuotationController); diff --git a/erpnext/selling/doctype/quotation_item/quotation_item.json b/erpnext/selling/doctype/quotation_item/quotation_item.json index eaa4d1d7477..f62a0997dcf 100644 --- a/erpnext/selling/doctype/quotation_item/quotation_item.json +++ b/erpnext/selling/doctype/quotation_item/quotation_item.json @@ -49,7 +49,8 @@ "pricing_rules", "stock_uom_rate", "is_free_item", - "is_alternative_item", + "is_alternative", + "alternative_to", "section_break_43", "valuation_rate", "column_break_45", @@ -647,16 +648,25 @@ }, { "default": "0", - "fieldname": "is_alternative_item", + "fieldname": "is_alternative", "fieldtype": "Check", - "label": "Is Alternative Item", + "label": "Is Alternative", + "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 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2023-01-24 08:48:06.290335", + "modified": "2023-01-26 07:32:02.768197", "modified_by": "Administrator", "module": "Selling", "name": "Quotation Item",