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
This commit is contained in:
marination 2023-02-06 16:25:38 +05:30
parent 03321f5f13
commit db2076db69
3 changed files with 102 additions and 81 deletions

View File

@ -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 ? `<span class="indicator yellow">${value}</span>` : 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(
`<p class="small text-muted">
<span class="indicator yellow"></span>
Alternative Items
</p>`
)
dialog.show();
}
};

View File

@ -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",

View File

@ -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",