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:
parent
03321f5f13
commit
db2076db69
@ -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();
|
||||
}
|
||||
};
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
Loading…
x
Reference in New Issue
Block a user