From b6dc0efa27bd3196f01e2bb1a5ade2422073ac00 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Tue, 22 Jun 2021 16:53:35 +0530 Subject: [PATCH 01/19] feat: add provision for process loss in manufac --- erpnext/manufacturing/doctype/bom/bom.py | 7 +- .../bom_scrap_item/bom_scrap_item.json | 429 ++++-------------- .../stock/doctype/stock_entry/stock_entry.py | 23 +- .../stock_entry_detail.json | 9 +- 4 files changed, 133 insertions(+), 335 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 0ba85078ea..6bd2a985e2 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -808,8 +808,11 @@ def get_bom_items_as_dict(bom, company, qty=1, fetch_exploded=1, fetch_scrap_ite items = frappe.db.sql(query, { "parent": bom, "qty": qty, "bom": bom, "company": company }, as_dict=True) elif fetch_scrap_items: - query = query.format(table="BOM Scrap Item", where_conditions="", - select_columns=", bom_item.idx, item.description", is_stock_item=is_stock_item, qty_field="stock_qty") + query = query.format( + table="BOM Scrap Item", where_conditions="", + select_columns=", bom_item.idx, item.description, is_process_loss", + is_stock_item=is_stock_item, qty_field="stock_qty" + ) items = frappe.db.sql(query, { "qty": qty, "bom": bom, "company": company }, as_dict=True) else: diff --git a/erpnext/manufacturing/doctype/bom_scrap_item/bom_scrap_item.json b/erpnext/manufacturing/doctype/bom_scrap_item/bom_scrap_item.json index 9f7091dd8d..7018082e40 100644 --- a/erpnext/manufacturing/doctype/bom_scrap_item/bom_scrap_item.json +++ b/erpnext/manufacturing/doctype/bom_scrap_item/bom_scrap_item.json @@ -1,345 +1,112 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2016-09-26 02:19:21.642081", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, + "actions": [], + "creation": "2016-09-26 02:19:21.642081", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "item_code", + "column_break_2", + "item_name", + "is_process_loss", + "quantity_and_rate", + "stock_qty", + "rate", + "amount", + "column_break_6", + "stock_uom", + "base_rate", + "base_amount" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "item_code", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Item Code", - "length": 0, - "no_copy": 0, - "options": "Item", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "item_code", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Item Code", + "options": "Item", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "item_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Item Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "item_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Item Name" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "quantity_and_rate", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Quantity and Rate", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "quantity_and_rate", + "fieldtype": "Section Break", + "label": "Quantity and Rate" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "stock_qty", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Qty", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "stock_qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Qty", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "rate", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Rate", - "length": 0, - "no_copy": 0, - "options": "currency", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "rate", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Rate", + "options": "currency" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Amount", - "length": 0, - "no_copy": 0, - "options": "currency", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "amount", + "fieldtype": "Currency", + "label": "Amount", + "options": "currency", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_6", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "column_break_6", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "stock_uom", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Stock UOM", - "length": 0, - "no_copy": 0, - "options": "UOM", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "stock_uom", + "fieldtype": "Link", + "label": "Stock UOM", + "options": "UOM", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "base_rate", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Basic Rate (Company Currency)", - "length": 0, - "no_copy": 0, - "options": "Company:company:default_currency", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "base_rate", + "fieldtype": "Currency", + "label": "Basic Rate (Company Currency)", + "options": "Company:company:default_currency", + "print_hide": 1, + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "base_amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Basic Amount (Company Currency)", - "length": 0, - "no_copy": 0, - "options": "Company:company:default_currency", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "fieldname": "base_amount", + "fieldtype": "Currency", + "label": "Basic Amount (Company Currency)", + "options": "Company:company:default_currency", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "is_process_loss", + "fieldtype": "Check", + "label": "Is Process Loss" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2017-07-04 16:04:32.442287", - "modified_by": "Administrator", - "module": "Manufacturing", - "name": "BOM Scrap Item", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0 + ], + "istable": 1, + "links": [], + "modified": "2021-06-22 16:46:12.153311", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "BOM Scrap Item", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 7b31d2fdf2..478d5b2e0f 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -463,7 +463,7 @@ class StockEntry(StockController): """ # Set rate for outgoing items outgoing_items_cost = self.set_rate_for_outgoing_items(reset_outgoing_rate, raise_error_if_no_rate) - finished_item_qty = sum(d.transfer_qty for d in self.items if d.is_finished_item) + finished_item_qty = sum(d.transfer_qty for d in self.items if d.is_finished_item or d.is_process_loss) # Set basic rate for incoming items for d in self.get('items'): @@ -484,6 +484,8 @@ class StockEntry(StockController): raise_error_if_no_rate=raise_error_if_no_rate) d.basic_rate = flt(d.basic_rate, d.precision("basic_rate")) + if d.is_process_loss: + d.basic_rate = flt(0.) d.basic_amount = flt(flt(d.transfer_qty) * flt(d.basic_rate), d.precision("basic_amount")) def set_rate_for_outgoing_items(self, reset_outgoing_rate=True, raise_error_if_no_rate=True): @@ -1041,6 +1043,7 @@ class StockEntry(StockController): self.set_scrap_items() self.set_actual_qty() + self.adjust_qty_for_process_loss() self.validate_customer_provided_item() self.calculate_rate_and_amount() @@ -1398,6 +1401,7 @@ class StockEntry(StockController): get_default_cost_center(item_dict[d], company = self.company)) se_child.is_finished_item = item_dict[d].get("is_finished_item", 0) se_child.is_scrap_item = item_dict[d].get("is_scrap_item", 0) + se_child.is_process_loss = item_dict[d].get("is_process_loss", 0) for field in ["idx", "po_detail", "original_item", "expense_account", "description", "item_name", "serial_no", "batch_no"]: @@ -1576,6 +1580,23 @@ class StockEntry(StockController): if material_request and material_request not in material_requests: material_requests.append(material_request) frappe.db.set_value('Material Request', material_request, 'transfer_status', status) + + def adjust_qty_for_process_loss(self): + process_loss_dict = {} + for d in self.get("items"): + if not d.is_process_loss: + continue + if d.item_code not in process_loss_dict: + process_loss_dict[d.item_code] = [flt(0), flt(0)] + process_loss_dict[d.item_code][0] += flt(d.transfer_qty) + process_loss_dict[d.item_code][1] += flt(d.qty) + + for d in self.get("items"): + if not d.is_finished_item or d.item_code not in process_loss_dict: + continue + # Assumption: 1 FG has 1 row. + d.transfer_qty -= process_loss_dict[d.item_code][0] + d.qty -= process_loss_dict[d.item_code][1] def set_serial_no_batch_for_finished_good(self): args = {} diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json index 22f412a298..df65706c39 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json @@ -19,6 +19,7 @@ "is_finished_item", "is_scrap_item", "quality_inspection", + "is_process_loss", "subcontracted_item", "section_break_8", "description", @@ -543,13 +544,19 @@ "no_copy": 1, "print_hide": 1, "read_only": 1 + }, + { + "default": "0", + "fieldname": "is_process_loss", + "fieldtype": "Check", + "label": "Is Process Loss" } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-06-21 16:03:18.834880", + "modified": "2021-06-22 16:47:11.268975", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry Detail", From 984c97ed4ee80c72edf8f12776ab3a8d99605424 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Wed, 23 Jun 2021 15:06:00 +0530 Subject: [PATCH 02/19] feat: add is process loss autoset and validation --- erpnext/manufacturing/doctype/bom/bom.js | 15 +++++++++++++++ erpnext/manufacturing/doctype/bom/bom.py | 10 ++++++++++ 2 files changed, 25 insertions(+) diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index 3f50b41be1..a5ce8c6195 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -379,6 +379,9 @@ erpnext.bom.BomController = class BomController extends erpnext.TransactionContr child.bom_no = ''; } + if (scrap_items) { + set_is_process_loss(doc, cdt, cdn) + } get_bom_material_detail(doc, cdt, cdn, scrap_items); } @@ -446,6 +449,10 @@ var get_bom_material_detail = function(doc, cdt, cdn, scrap_items) { }, callback: function(r) { d = locals[cdt][cdn]; + if (d.is_process_loss) { + r.message.rate = 0 + r.message.base_rate = 0 + } $.extend(d, r.message); refresh_field("items"); refresh_field("scrap_items"); @@ -655,3 +662,11 @@ frappe.ui.form.on("BOM", "with_operations", function(frm) { frm.set_value("operations", []); } }); + +function set_is_process_loss(doc, cdt, cdn) { + const row = locals[cdt][cdn] + if (row.item_code === doc.item) { + row.is_process_loss = 1 + frappe.msgprint(__("Item:") + ` ${row.item_code} ` + __("set as process loss.")) + } +} diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 6bd2a985e2..de0c521cf5 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -155,6 +155,7 @@ class BOM(WebsiteGenerator): self.update_stock_qty() self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate = False, save=False) self.set_bom_level() + self.validate_scrap_items() def get_context(self, context): context.parents = [{'name': 'boms', 'title': _('All BOMs') }] @@ -691,6 +692,15 @@ class BOM(WebsiteGenerator): if update: self.db_set("bom_level", self.bom_level) + def validate_scrap_items(self): + for item in self.scrap_items: + if item.item_code == self.item and not item.is_process_loss: + frappe.throw(_('Item:') + f' {item.item_code} ' +\ + _('in Scrap/Loss Items table should have Is Process Loss checked.')) + elif item.item_code != self.item and item.is_process_loss: + frappe.throw(_('Item:') + f' {item.item_code} ' +\ + _('in Scrap/Loss Items table should not have Is Process Loss checked.')) + def get_bom_item_rate(args, bom_doc): if bom_doc.rm_cost_as_per == 'Valuation Rate': rate = get_valuation_rate(args) * (args.get("conversion_factor") or 1) From 3df8d0cdf0f00e78c64ac3044f13e6e50f0010d5 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Wed, 23 Jun 2021 15:30:48 +0530 Subject: [PATCH 03/19] fix: add warehouse and unset is scrap for process loss items --- erpnext/stock/doctype/stock_entry/stock_entry.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 478d5b2e0f..4f724ec637 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1043,7 +1043,7 @@ class StockEntry(StockController): self.set_scrap_items() self.set_actual_qty() - self.adjust_qty_for_process_loss() + self.update_items_for_process_loss() self.validate_customer_provided_item() self.calculate_rate_and_amount() @@ -1581,11 +1581,17 @@ class StockEntry(StockController): material_requests.append(material_request) frappe.db.set_value('Material Request', material_request, 'transfer_status', status) - def adjust_qty_for_process_loss(self): + def update_items_for_process_loss(self): process_loss_dict = {} for d in self.get("items"): if not d.is_process_loss: continue + + scrap_warehouse = frappe.db.get_single_value("Manufacturing Settings", "default_scrap_warehouse") + if scrap_warehouse is not None: + d.t_warehouse = scrap_warehouse + d.is_scrap_item = 0 + if d.item_code not in process_loss_dict: process_loss_dict[d.item_code] = [flt(0), flt(0)] process_loss_dict[d.item_code][0] += flt(d.transfer_qty) @@ -1594,7 +1600,7 @@ class StockEntry(StockController): for d in self.get("items"): if not d.is_finished_item or d.item_code not in process_loss_dict: continue - # Assumption: 1 FG has 1 row. + # Assumption: 1 finished item has 1 row. d.transfer_qty -= process_loss_dict[d.item_code][0] d.qty -= process_loss_dict[d.item_code][1] From 7433b971060a671971af0c23e64efd4dab951799 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Thu, 24 Jun 2021 15:05:52 +0530 Subject: [PATCH 04/19] refactor: shift auto entry of is process loss check, update validations --- erpnext/manufacturing/doctype/bom/bom.js | 45 ++++++++++++++++++------ erpnext/manufacturing/doctype/bom/bom.py | 22 +++++++++--- 2 files changed, 52 insertions(+), 15 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index a5ce8c6195..dd437dd555 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -379,9 +379,6 @@ erpnext.bom.BomController = class BomController extends erpnext.TransactionContr child.bom_no = ''; } - if (scrap_items) { - set_is_process_loss(doc, cdt, cdn) - } get_bom_material_detail(doc, cdt, cdn, scrap_items); } @@ -450,9 +447,10 @@ var get_bom_material_detail = function(doc, cdt, cdn, scrap_items) { callback: function(r) { d = locals[cdt][cdn]; if (d.is_process_loss) { - r.message.rate = 0 - r.message.base_rate = 0 + r.message.rate = 0; + r.message.base_rate = 0; } + $.extend(d, r.message); refresh_field("items"); refresh_field("scrap_items"); @@ -661,12 +659,37 @@ frappe.ui.form.on("BOM", "with_operations", function(frm) { if(!cint(frm.doc.with_operations)) { frm.set_value("operations", []); } + toggle_operations(frm); }); -function set_is_process_loss(doc, cdt, cdn) { - const row = locals[cdt][cdn] - if (row.item_code === doc.item) { - row.is_process_loss = 1 - frappe.msgprint(__("Item:") + ` ${row.item_code} ` + __("set as process loss.")) - } +frappe.ui.form.on("BOM Scrap Item", { + item_code(frm, cdt, cdn) { + const { item_code } = locals[cdt][cdn]; + if (item_code === frm.doc.item) { + locals[cdt][cdn].is_process_loss = 1; + trigger_process_loss_qty_prompt(frm, cdt, cdn, item_code) + } + }, +}); + +function trigger_process_loss_qty_prompt(frm, cdt, cdn, item_code) { + frappe.prompt( + { + fieldname: "percent", + fieldtype: "Percent", + label: __("% Finished Item Quantity"), + description: + __("Set quantity of process loss item:") + + ` ${item_code} ` + + __("as a percentage of finished item quantity"), + }, + (data) => { + const row = locals[cdt][cdn]; + row.stock_qty = (frm.doc.quantity * data.percent) / 100; + row.qty = row.stock_qty / (row.conversion_factor ?? 1); + refresh_field("scrap_items"); + }, + __("Set Process Loss Item Quantity"), + __("Set Quantity") + ); } diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index de0c521cf5..b90d54dea5 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -695,11 +695,25 @@ class BOM(WebsiteGenerator): def validate_scrap_items(self): for item in self.scrap_items: if item.item_code == self.item and not item.is_process_loss: - frappe.throw(_('Item:') + f' {item.item_code} ' +\ - _('in Scrap/Loss Items table should have Is Process Loss checked.')) + frappe.throw(_('Scrap/Loss Item:') + f' {frappe.bold(item.item_code)} ' +\ + _('should have') + ' ' + frappe.bold(_('Is Process Loss')) + ' ' + ('checked.')) elif item.item_code != self.item and item.is_process_loss: - frappe.throw(_('Item:') + f' {item.item_code} ' +\ - _('in Scrap/Loss Items table should not have Is Process Loss checked.')) + frappe.throw(_('Scrap/Loss Item:') + f' {frappe.bold(item.item_code)} ' +\ + _('should not have') + ' ' + frappe.bold(_('Is Process Loss')) + ' ' + ('checked.')) + + stock_uom = item.stock_uom + must_be_whole_number = frappe.get_value("UOM", stock_uom, "must_be_whole_number") + if item.is_process_loss and must_be_whole_number: + frappe.throw(_('Item:') + f' {frappe.bold(item.item_code)} ' +\ + _('with Stock UOM:') + f' {frappe.bold(stock_uom)} '+\ + _('cannot be a Scrap/Loss Item.')) + + if item.is_process_loss and (item.stock_qty >= self.quantity): + frappe.throw(_('Scrap/Loss Item:') + f' {item.item_code} ' +\ + _('should have') +' '+frappe.bold(_('Qty')) +\ + ' ' + _('less than finished goods') + ' ' +\ + frappe.bold(_('Quantity.'))) + def get_bom_item_rate(args, bom_doc): if bom_doc.rm_cost_as_per == 'Valuation Rate': From 8ecb14623175e8a789bd82a9bbeb1738787046f2 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Fri, 25 Jun 2021 14:20:22 +0530 Subject: [PATCH 05/19] test: add bom tests for process loss val, add se test for qty calc --- erpnext/manufacturing/doctype/bom/test_bom.py | 73 +++++++++++++++++++ .../tests/test_stock_entry_for_manufacture.js | 27 +++++++ 2 files changed, 100 insertions(+) create mode 100644 erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_manufacture.js diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index c89f7d66fd..e61bb52592 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -280,6 +280,38 @@ class TestBOM(unittest.TestCase): self.assertEqual(reqd_item.qty, created_item.qty) self.assertEqual(reqd_item.exploded_qty, created_item.exploded_qty) + def test_bom_with_process_loss_item(self): + fg_item_non_whole, fg_item_whole, bom_item = create_process_loss_bom_items() + + if frappe.db.exists("BOM", f"BOM-{fg_item_non_whole.item_code}-001") is None: + bom_doc = create_bom_with_process_loss_item( + fg_item_non_whole, bom_item, 0.25, 0, 1 + ) + bom_doc.submit() + + bom_doc = create_bom_with_process_loss_item( + fg_item_non_whole, bom_item, 2, 0 + ) + # PL Item qty can't be >= FG Item qty + self.assertRaises(frappe.ValidationError, bom_doc.submit) + + bom_doc = create_bom_with_process_loss_item( + fg_item_non_whole, bom_item, 1, 100 + ) + # PL Item rate has to be 0 + self.assertRaises(frappe.ValidationError, bom_doc.submit) + + bom_doc = create_bom_with_process_loss_item( + fg_item_whole, bom_item, 0.25, 0 + ) + # Items with whole UOMs can't be PL Items + self.assertRaises(frappe.ValidationError, bom_doc.submit) + + bom_doc = create_bom_with_process_loss_item( + fg_item_non_whole, bom_item, 0.25, 0, is_process_loss=0 + ) + # FG Items in Scrap/Loss Table should have Is Process Loss set + self.assertRaises(frappe.ValidationError, bom_doc.submit) def get_default_bom(item_code="_Test FG Item 2"): return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1}) @@ -353,3 +385,44 @@ def reset_item_valuation_rate(item_code, warehouse_list=None, qty=None, rate=Non for warehouse in warehouse_list: create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=qty, rate=rate) + +def create_bom_with_process_loss_item( + fg_item, bom_item, scrap_qty, scrap_rate, fg_qty=2, is_process_loss=1): + bom_doc = frappe.new_doc("BOM") + bom_doc.item = fg_item.item_code + bom_doc.quantity = fg_qty + bom_doc.append("items", { + "item_code": bom_item.item_code, + "qty": 1, + "uom": bom_item.stock_uom, + "stock_uom": bom_item.stock_uom, + "rate": 100.0 + }) + bom_doc.append("scrap_items", { + "item_code": fg_item.item_code, + "qty": scrap_qty, + "stock_qty": scrap_qty, + "uom": fg_item.stock_uom, + "stock_uom": fg_item.stock_uom, + "rate": scrap_rate, + "is_process_loss": is_process_loss + }) + return bom_doc + +def create_process_loss_bom_items(): + item_list = [ + ("_Test Item - Non Whole UOM", "Kg"), + ("_Test Item - Whole UOM", "Unit"), + ( "_Test PL BOM Item", "Unit") + ] + return [create_process_loss_bom_item(it) for it in item_list] + +def create_process_loss_bom_item(item_tuple): + item_code, stock_uom = item_tuple + if frappe.db.exists("Item", item_code) is None: + return make_item( + item_code, + {'stock_uom':stock_uom, 'valuation_rate':100} + ) + else: + return frappe.get_doc("Item", item_code) diff --git a/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_manufacture.js b/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_manufacture.js new file mode 100644 index 0000000000..d74f31672d --- /dev/null +++ b/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_manufacture.js @@ -0,0 +1,27 @@ +QUnit.module('Stock'); + +QUnit.test("test manufacture from bom", function(assert) { + assert.expect(2); + let done = assert.async(); + frappe.run_serially([ + () => { + return frappe.tests.make("Stock Entry", [ + {purpose:"Manufacture"}, + {from_bom:1}, + {bom_no:"BOM-_Test Item - Non Whole UOM-001"}, + {fg_completed_qty:2} + ]); + }, + () => cur_frm.save(), + () => frappe.click_button("Update Rate and Availability"), + () => { + assert.ok(cur_frm.doc.items[1] === 0.75, " Finished Item Qty correct"); + assert.ok(cur_frm.doc.items[2] === 0.25, " Process Loss Item Qty correct"); + }, + () => frappe.tests.click_button('Submit'), + () => frappe.tests.click_button('Yes'), + () => frappe.timeout(0.3), + () => done() + ]); +}); + From cdf253aeb4b248846c2979ed41f2f239ab75c804 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Fri, 25 Jun 2021 14:21:12 +0530 Subject: [PATCH 06/19] fix: add more validations, remove source wh req for pl item --- erpnext/manufacturing/doctype/bom/bom.py | 29 ++++++++++--------- .../stock/doctype/stock_entry/stock_entry.py | 2 +- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index b90d54dea5..8f01edd0e2 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -695,25 +695,28 @@ class BOM(WebsiteGenerator): def validate_scrap_items(self): for item in self.scrap_items: if item.item_code == self.item and not item.is_process_loss: - frappe.throw(_('Scrap/Loss Item:') + f' {frappe.bold(item.item_code)} ' +\ - _('should have') + ' ' + frappe.bold(_('Is Process Loss')) + ' ' + ('checked.')) + frappe.throw(_('Scrap/Loss Item:') + f' {frappe.bold(item.item_code)} ' + + _('should have') + ' ' + frappe.bold(_('Is Process Loss')) + ' ' + ('checked')) elif item.item_code != self.item and item.is_process_loss: - frappe.throw(_('Scrap/Loss Item:') + f' {frappe.bold(item.item_code)} ' +\ - _('should not have') + ' ' + frappe.bold(_('Is Process Loss')) + ' ' + ('checked.')) + frappe.throw(_('Scrap/Loss Item:') + f' {frappe.bold(item.item_code)} ' + + _('should not have') + ' ' + frappe.bold(_('Is Process Loss')) + ' ' + ('checked')) - stock_uom = item.stock_uom - must_be_whole_number = frappe.get_value("UOM", stock_uom, "must_be_whole_number") + must_be_whole_number = frappe.get_value("UOM", item.stock_uom, "must_be_whole_number") if item.is_process_loss and must_be_whole_number: - frappe.throw(_('Item:') + f' {frappe.bold(item.item_code)} ' +\ - _('with Stock UOM:') + f' {frappe.bold(stock_uom)} '+\ - _('cannot be a Scrap/Loss Item.')) + frappe.throw(_('Item:') + f' {frappe.bold(item.item_code)} ' + + _('with Stock UOM:') + f' {frappe.bold(item.stock_uom)} ' + + _('cannot be a Scrap/Loss Item')) if item.is_process_loss and (item.stock_qty >= self.quantity): - frappe.throw(_('Scrap/Loss Item:') + f' {item.item_code} ' +\ - _('should have') +' '+frappe.bold(_('Qty')) +\ - ' ' + _('less than finished goods') + ' ' +\ - frappe.bold(_('Quantity.'))) + frappe.throw(_('Scrap/Loss Item:') + f' {item.item_code} ' + + _('should have') +' '+frappe.bold(_('Qty')) + ' ' + + _('less than finished goods') + ' ' + frappe.bold(_('Quantity'))) + if item.is_process_loss and (item.rate > 0): + frappe.throw(_('Scrap/Loss Item:') + f' {item.item_code} ' + + _('should have') + ' ' + frappe.bold(_('Rate')) + + ' ' + _('set to 0 because') + ' ' + + frappe.bold(_('Is Process Loss')) + ' ' + _('is checked')) def get_bom_item_rate(args, bom_doc): if bom_doc.rm_cost_as_per == 'Valuation Rate': diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 4f724ec637..21c0e75393 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -334,7 +334,7 @@ class StockEntry(StockController): if self.purpose == "Manufacture": if validate_for_manufacture: - if d.is_finished_item or d.is_scrap_item: + if d.is_finished_item or d.is_scrap_item or d.is_process_loss: d.s_warehouse = None if not d.t_warehouse: frappe.throw(_("Target warehouse is mandatory for row {0}").format(d.idx)) From 55acb2e8434747658ea6cfd319840a55e11c0380 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Fri, 25 Jun 2021 14:46:08 +0530 Subject: [PATCH 07/19] fix: sider --- erpnext/manufacturing/doctype/bom/test_bom.py | 2 +- .../stock_entry/tests/test_stock_entry_for_manufacture.js | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index e61bb52592..fe7a8f151b 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -413,7 +413,7 @@ def create_process_loss_bom_items(): item_list = [ ("_Test Item - Non Whole UOM", "Kg"), ("_Test Item - Whole UOM", "Unit"), - ( "_Test PL BOM Item", "Unit") + ("_Test PL BOM Item", "Unit") ] return [create_process_loss_bom_item(it) for it in item_list] diff --git a/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_manufacture.js b/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_manufacture.js index d74f31672d..285ae4f59e 100644 --- a/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_manufacture.js +++ b/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_manufacture.js @@ -6,10 +6,10 @@ QUnit.test("test manufacture from bom", function(assert) { frappe.run_serially([ () => { return frappe.tests.make("Stock Entry", [ - {purpose:"Manufacture"}, - {from_bom:1}, - {bom_no:"BOM-_Test Item - Non Whole UOM-001"}, - {fg_completed_qty:2} + { purpose: "Manufacture" }, + { from_bom: 1 }, + { bom_no: "BOM-_Test Item - Non Whole UOM-001" }, + { fg_completed_qty: 2 } ]); }, () => cur_frm.save(), From 47a4a3d88898fc13800d90ce9e9b7f62f0b7e272 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Fri, 25 Jun 2021 15:14:55 +0530 Subject: [PATCH 08/19] refactor: polyfill ?? --- erpnext/manufacturing/doctype/bom/bom.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index dd437dd555..7e755d424c 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -686,7 +686,7 @@ function trigger_process_loss_qty_prompt(frm, cdt, cdn, item_code) { (data) => { const row = locals[cdt][cdn]; row.stock_qty = (frm.doc.quantity * data.percent) / 100; - row.qty = row.stock_qty / (row.conversion_factor ?? 1); + row.qty = row.stock_qty / (row.conversion_factor || 1); refresh_field("scrap_items"); }, __("Set Process Loss Item Quantity"), From 23ef51a9819eda14c89b963b1e64a4555b2f95a7 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Fri, 25 Jun 2021 15:48:28 +0530 Subject: [PATCH 09/19] fix: sider --- erpnext/manufacturing/doctype/bom/bom.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index 7e755d424c..7de7e17abc 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -667,7 +667,7 @@ frappe.ui.form.on("BOM Scrap Item", { const { item_code } = locals[cdt][cdn]; if (item_code === frm.doc.item) { locals[cdt][cdn].is_process_loss = 1; - trigger_process_loss_qty_prompt(frm, cdt, cdn, item_code) + trigger_process_loss_qty_prompt(frm, cdt, cdn, item_code); } }, }); From ad73d3fbfb7894c74a259a489da5cba72d95f15f Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Mon, 9 Aug 2021 17:37:17 +0530 Subject: [PATCH 10/19] refactor: validation error message formatting --- erpnext/manufacturing/doctype/bom/bom.py | 28 ++++++++++++------------ 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 8f01edd0e2..8d9b10ddc1 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -694,29 +694,29 @@ class BOM(WebsiteGenerator): def validate_scrap_items(self): for item in self.scrap_items: + msg = "" if item.item_code == self.item and not item.is_process_loss: - frappe.throw(_('Scrap/Loss Item:') + f' {frappe.bold(item.item_code)} ' + - _('should have') + ' ' + frappe.bold(_('Is Process Loss')) + ' ' + ('checked')) + msg = _('Scrap/Loss Item: {0} should have Is Process Loss checked') \ + .format(frappe.bold(item.item_code)) elif item.item_code != self.item and item.is_process_loss: - frappe.throw(_('Scrap/Loss Item:') + f' {frappe.bold(item.item_code)} ' + - _('should not have') + ' ' + frappe.bold(_('Is Process Loss')) + ' ' + ('checked')) + msg = _('Scrap/Loss Item: {0} should not have Is Process Loss checked') \ + .format(frappe.bold(item.item_code)) must_be_whole_number = frappe.get_value("UOM", item.stock_uom, "must_be_whole_number") if item.is_process_loss and must_be_whole_number: - frappe.throw(_('Item:') + f' {frappe.bold(item.item_code)} ' + - _('with Stock UOM:') + f' {frappe.bold(item.stock_uom)} ' + - _('cannot be a Scrap/Loss Item')) + msg = _("Item: {0} with Stock UOM: {1} cannot be a Scrap/Loss Item") \ + .format(frappe.bold(item.item_code), frappe.bold(item.stock_uom)) if item.is_process_loss and (item.stock_qty >= self.quantity): - frappe.throw(_('Scrap/Loss Item:') + f' {item.item_code} ' + - _('should have') +' '+frappe.bold(_('Qty')) + ' ' + - _('less than finished goods') + ' ' + frappe.bold(_('Quantity'))) + msg = _("Scrap/Loss Item: {0} should have Qty less than finished goods Quantity") \ + .format(frappe.bold(item.item_code)) if item.is_process_loss and (item.rate > 0): - frappe.throw(_('Scrap/Loss Item:') + f' {item.item_code} ' + - _('should have') + ' ' + frappe.bold(_('Rate')) + - ' ' + _('set to 0 because') + ' ' + - frappe.bold(_('Is Process Loss')) + ' ' + _('is checked')) + msg = _("Scrap/Loss Item: {0} should have Rate set to 0 because Is Process Loss is checked") \ + .format(frappe.bold(item.item_code)) + + if msg: + frappe.throw(msg, title=_("Note")) def get_bom_item_rate(args, bom_doc): if bom_doc.rm_cost_as_per == 'Valuation Rate': From 2670adc0c00a1ea48e19285156f65a0c23b4a42c Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Tue, 10 Aug 2021 12:23:19 +0530 Subject: [PATCH 11/19] test: check manufacture completion qty in se and wo --- erpnext/manufacturing/doctype/bom/test_bom.py | 2 +- .../doctype/work_order/test_work_order.py | 58 +++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index fe7a8f151b..6e17f2a831 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -283,7 +283,7 @@ class TestBOM(unittest.TestCase): def test_bom_with_process_loss_item(self): fg_item_non_whole, fg_item_whole, bom_item = create_process_loss_bom_items() - if frappe.db.exists("BOM", f"BOM-{fg_item_non_whole.item_code}-001") is None: + if not frappe.db.exists("BOM", f"BOM-{fg_item_non_whole.item_code}-001"): bom_doc = create_bom_with_process_loss_item( fg_item_non_whole, bom_item, 0.25, 0, 1 ) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index bf1ccb7159..7f943d9cbb 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -690,6 +690,64 @@ class TestWorkOrder(unittest.TestCase): self.assertRaises(frappe.ValidationError, make_stock_entry, wo.name, 'Material Transfer for Manufacture') + def test_wo_completion_with_pl_bom(self): + from erpnext.manufacturing.doctype.bom.test_bom import create_process_loss_bom_items + from erpnext.manufacturing.doctype.bom.test_bom import create_bom_with_process_loss_item + + qty = fg_qty = 4 + scrap_qty = 0.25 # bom item qty = 1, consider as 25% of FG + source_warehouse = "Stores - _TC" + wip_warehouse = "_Test Warehouse - _TC" + fg_item_non_whole, _, bom_item = create_process_loss_bom_items() + + test_stock_entry.make_stock_entry(item_code=bom_item.item_code, + target=source_warehouse, qty=4, basic_rate=100) + + bom_no = f"BOM-{fg_item_non_whole.item_code}-001" + if not frappe.db.exists("BOM", bom_no): + bom_doc = create_bom_with_process_loss_item( + fg_item_non_whole, bom_item, scrap_qty=scrap_qty, + scrap_rate=0, fg_qty=fg_qty, is_process_loss=1 + ) + bom_doc.submit() + + wo = make_wo_order_test_record( + production_item=fg_item_non_whole.item_code, + bom_no=bom_no, + wip_warehouse=wip_warehouse, + qty=qty, + skip_transfer=1, + stock_uom=fg_item_non_whole.stock_uom, + ) + + se = frappe.get_doc( + make_stock_entry(wo.name, "Material Transfer for Manufacture", 4) + ) + se.get("items")[0].s_warehouse = "Stores - _TC" + se.insert() + se.submit() + + se = frappe.get_doc( + make_stock_entry(wo.name, "Manufacture", 4) + ) + se.insert() + se.submit() + + # Testing stock entry values + items = se.get("items") + self.assertEqual(len(items), 3, "There should be 3 items including process loss.") + + source_item, fg_item, pl_item = items + + total_pl_qty = scrap_qty * fg_qty + actual_fg_qty = fg_qty - total_pl_qty + + self.assertEqual(pl_item.qty, total_pl_qty) + self.assertEqual(fg_item.qty, actual_fg_qty) + + # Testing Work Order values + self.assertEqual( frappe.db.get_value("Work Order", wo.name, "produced_qty"), actual_fg_qty) + def get_scrap_item_details(bom_no): scrap_items = {} for item in frappe.db.sql("""select item_code, stock_qty from `tabBOM Scrap Item` From cc177f34158d3ffabdbfbb2ace500fa17a7b7c9d Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Tue, 10 Aug 2021 14:42:39 +0530 Subject: [PATCH 12/19] fix: wo tests, sider, account for pl in se validation --- .../doctype/work_order/test_work_order.py | 16 ++++++++-------- erpnext/stock/doctype/stock_entry/stock_entry.py | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 7f943d9cbb..d6a20df0c8 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -694,7 +694,7 @@ class TestWorkOrder(unittest.TestCase): from erpnext.manufacturing.doctype.bom.test_bom import create_process_loss_bom_items from erpnext.manufacturing.doctype.bom.test_bom import create_bom_with_process_loss_item - qty = fg_qty = 4 + qty = 4 scrap_qty = 0.25 # bom item qty = 1, consider as 25% of FG source_warehouse = "Stores - _TC" wip_warehouse = "_Test Warehouse - _TC" @@ -707,7 +707,7 @@ class TestWorkOrder(unittest.TestCase): if not frappe.db.exists("BOM", bom_no): bom_doc = create_bom_with_process_loss_item( fg_item_non_whole, bom_item, scrap_qty=scrap_qty, - scrap_rate=0, fg_qty=fg_qty, is_process_loss=1 + scrap_rate=0, fg_qty=1, is_process_loss=1 ) bom_doc.submit() @@ -721,32 +721,32 @@ class TestWorkOrder(unittest.TestCase): ) se = frappe.get_doc( - make_stock_entry(wo.name, "Material Transfer for Manufacture", 4) + make_stock_entry(wo.name, "Material Transfer for Manufacture", qty) ) se.get("items")[0].s_warehouse = "Stores - _TC" se.insert() se.submit() se = frappe.get_doc( - make_stock_entry(wo.name, "Manufacture", 4) + make_stock_entry(wo.name, "Manufacture", qty) ) se.insert() se.submit() # Testing stock entry values items = se.get("items") - self.assertEqual(len(items), 3, "There should be 3 items including process loss.") + self.assertEqual(len(items), 4, "There should be 3 items including process loss.") source_item, fg_item, pl_item = items - total_pl_qty = scrap_qty * fg_qty - actual_fg_qty = fg_qty - total_pl_qty + total_pl_qty = qty * scrap_qty + actual_fg_qty = qty - total_pl_qty self.assertEqual(pl_item.qty, total_pl_qty) self.assertEqual(fg_item.qty, actual_fg_qty) # Testing Work Order values - self.assertEqual( frappe.db.get_value("Work Order", wo.name, "produced_qty"), actual_fg_qty) + self.assertEqual(frappe.db.get_value("Work Order", wo.name, "produced_qty"), actual_fg_qty) def get_scrap_item_details(bom_no): scrap_items = {} diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 21c0e75393..8ea1275283 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -271,7 +271,7 @@ class StockEntry(StockController): item_wise_qty = {} if self.purpose == "Manufacture" and self.work_order: for d in self.items: - if d.is_finished_item: + if d.is_finished_item or d.is_process_loss: item_wise_qty.setdefault(d.item_code, []).append(d.qty) for item_code, qty_list in iteritems(item_wise_qty): From 7fb08173b54b67a67aac03102a6327cc3c9b475b Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Tue, 17 Aug 2021 16:03:04 +0530 Subject: [PATCH 13/19] fix: reword error messages, fix test values --- erpnext/manufacturing/doctype/bom/bom.py | 10 +++++----- .../doctype/work_order/test_work_order.py | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 8d9b10ddc1..8d105789d0 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -696,23 +696,23 @@ class BOM(WebsiteGenerator): for item in self.scrap_items: msg = "" if item.item_code == self.item and not item.is_process_loss: - msg = _('Scrap/Loss Item: {0} should have Is Process Loss checked') \ + msg = _('Scrap/Loss Item: {0} should have Is Process Loss checked as it is the same as the item to be manufactured or repacked.') \ .format(frappe.bold(item.item_code)) elif item.item_code != self.item and item.is_process_loss: - msg = _('Scrap/Loss Item: {0} should not have Is Process Loss checked') \ + msg = _('Scrap/Loss Item: {0} should not have Is Process Loss checked as it is different from the item to be manufactured or repacked') \ .format(frappe.bold(item.item_code)) must_be_whole_number = frappe.get_value("UOM", item.stock_uom, "must_be_whole_number") if item.is_process_loss and must_be_whole_number: - msg = _("Item: {0} with Stock UOM: {1} cannot be a Scrap/Loss Item") \ + msg = _("Item: {0} with Stock UOM: {1} cannot be a Scrap/Loss Item as {1} is a whole UOM.") \ .format(frappe.bold(item.item_code), frappe.bold(item.stock_uom)) if item.is_process_loss and (item.stock_qty >= self.quantity): - msg = _("Scrap/Loss Item: {0} should have Qty less than finished goods Quantity") \ + msg = _("Scrap/Loss Item: {0} should have Qty less than finished goods Quantity.") \ .format(frappe.bold(item.item_code)) if item.is_process_loss and (item.rate > 0): - msg = _("Scrap/Loss Item: {0} should have Rate set to 0 because Is Process Loss is checked") \ + msg = _("Scrap/Loss Item: {0} should have Rate set to 0 because Is Process Loss is checked.") \ .format(frappe.bold(item.item_code)) if msg: diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index d6a20df0c8..0569092f88 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -735,7 +735,7 @@ class TestWorkOrder(unittest.TestCase): # Testing stock entry values items = se.get("items") - self.assertEqual(len(items), 4, "There should be 3 items including process loss.") + self.assertEqual(len(items), 3, "There should be 3 items including process loss.") source_item, fg_item, pl_item = items @@ -746,7 +746,7 @@ class TestWorkOrder(unittest.TestCase): self.assertEqual(fg_item.qty, actual_fg_qty) # Testing Work Order values - self.assertEqual(frappe.db.get_value("Work Order", wo.name, "produced_qty"), actual_fg_qty) + self.assertEqual(frappe.db.get_value("Work Order", wo.name, "produced_qty"), qty) def get_scrap_item_details(bom_no): scrap_items = {} From b58853e89d9a7a149972cac7de32e5577ac0c16b Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Tue, 24 Aug 2021 16:11:29 +0530 Subject: [PATCH 14/19] feat: add procss_loss_qty field in work order --- .../doctype/work_order/test_work_order.py | 9 +++++- .../doctype/work_order/work_order.json | 30 +++++++++++-------- .../doctype/work_order/work_order.py | 16 ++++++++++ 3 files changed, 42 insertions(+), 13 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 0569092f88..a00520f6a1 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -746,7 +746,14 @@ class TestWorkOrder(unittest.TestCase): self.assertEqual(fg_item.qty, actual_fg_qty) # Testing Work Order values - self.assertEqual(frappe.db.get_value("Work Order", wo.name, "produced_qty"), qty) + self.assertEqual( + frappe.db.get_value("Work Order", wo.name, "produced_qty"), + qty + ) + self.assertEqual( + frappe.db.get_value("Work Order", wo.name, "process_loss_qty"), + actual_fg_qty + ) def get_scrap_item_details(bom_no): scrap_items = {} diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json index 3b56854aaf..913fc85af6 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.json +++ b/erpnext/manufacturing/doctype/work_order/work_order.json @@ -19,6 +19,7 @@ "qty", "material_transferred_for_manufacturing", "produced_qty", + "process_loss_qty", "sales_order", "project", "serial_no_and_batch_for_finished_good_section", @@ -64,16 +65,12 @@ "description", "stock_uom", "column_break2", - "references_section", "material_request", "material_request_item", "sales_order_item", - "column_break_61", "production_plan", "production_plan_item", "production_plan_sub_assembly_item", - "parent_work_order", - "bom_level", "product_bundle_item", "amended_from" ], @@ -553,20 +550,29 @@ "read_only": 1 }, { - "fieldname": "production_plan_sub_assembly_item", - "fieldtype": "Data", - "label": "Production Plan Sub-assembly Item", - "no_copy": 1, - "print_hide": 1, - "read_only": 1 - } + "fieldname": "production_plan_sub_assembly_item", + "fieldtype": "Data", + "label": "Production Plan Sub-assembly Item", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "depends_on": "eval: doc.process_loss_qty", + "fieldname": "process_loss_qty", + "fieldtype": "Float", + "label": "Process Loss Qty", + "no_copy": 1, + "non_negative": 1, + "read_only": 1 + } ], "icon": "fa fa-cogs", "idx": 1, "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2021-06-28 16:19:14.902699", + "modified": "2021-08-24 15:14:03.844937", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order", diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 282b5d0afe..c37a1c9e5a 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -214,6 +214,7 @@ class WorkOrder(Document): self.meta.get_label(fieldname), qty, completed_qty, self.name), StockOverProductionError) self.db_set(fieldname, qty) + self.set_process_loss_qty() from erpnext.selling.doctype.sales_order.sales_order import update_produced_qty_in_so_item @@ -223,6 +224,21 @@ class WorkOrder(Document): if self.production_plan: self.update_production_plan_status() + def set_process_loss_qty(self): + process_loss_qty = flt(frappe.db.sql(""" + SELECT sum(qty) FROM `tabStock Entry Detail` + WHERE + is_process_loss=1 + AND parent IN ( + SELECT name FROM `tabStock Entry` + WHERE + work_order=%s + AND docstatus=1 + ) + """, (self.name, ))[0][0]) + if process_loss_qty is not None: + self.db_set('process_loss_qty', process_loss_qty) + def update_production_plan_status(self): production_plan = frappe.get_doc('Production Plan', self.production_plan) produced_qty = 0 From 8f73d587f92473419301fa6b88b2c1562e5ff31a Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Tue, 24 Aug 2021 18:23:49 +0530 Subject: [PATCH 15/19] feat: process loss report, fix set pl query condition --- .../doctype/work_order/work_order.py | 1 + .../report/process_loss_report/__init__.py | 0 .../process_loss_report.js | 37 +++++ .../process_loss_report.json | 29 ++++ .../process_loss_report.py | 132 ++++++++++++++++++ 5 files changed, 199 insertions(+) create mode 100644 erpnext/stock/report/process_loss_report/__init__.py create mode 100644 erpnext/stock/report/process_loss_report/process_loss_report.js create mode 100644 erpnext/stock/report/process_loss_report/process_loss_report.json create mode 100644 erpnext/stock/report/process_loss_report/process_loss_report.py diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index c37a1c9e5a..1cdbc5f0e1 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -233,6 +233,7 @@ class WorkOrder(Document): SELECT name FROM `tabStock Entry` WHERE work_order=%s + AND purpose='Manufacture' AND docstatus=1 ) """, (self.name, ))[0][0]) diff --git a/erpnext/stock/report/process_loss_report/__init__.py b/erpnext/stock/report/process_loss_report/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/stock/report/process_loss_report/process_loss_report.js b/erpnext/stock/report/process_loss_report/process_loss_report.js new file mode 100644 index 0000000000..078b9e11ce --- /dev/null +++ b/erpnext/stock/report/process_loss_report/process_loss_report.js @@ -0,0 +1,37 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Process Loss Report"] = { + filters: [ + { + label: __("Company"), + fieldname: "company", + fieldtype: "Link", + options: "Company", + mandatory: true, + default: frappe.defaults.get_user_default("Company"), + }, + { + label: __("Item"), + fieldname: "item", + fieldtype: "Link", + options: "Item", + mandatory: false, + }, + { + label: __("From Date"), + fieldname: "from_date", + fieldtype: "Date", + mandatory: true, + default: frappe.datetime.year_start(), + }, + { + label: __("To Date"), + fieldname: "to_date", + fieldtype: "Date", + mandatory: true, + default: frappe.datetime.get_today(), + }, + ] +}; diff --git a/erpnext/stock/report/process_loss_report/process_loss_report.json b/erpnext/stock/report/process_loss_report/process_loss_report.json new file mode 100644 index 0000000000..afe4aff7f1 --- /dev/null +++ b/erpnext/stock/report/process_loss_report/process_loss_report.json @@ -0,0 +1,29 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2021-08-24 16:38:15.233395", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "modified": "2021-08-24 16:38:15.233395", + "modified_by": "Administrator", + "module": "Stock", + "name": "Process Loss Report", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Work Order", + "report_name": "Process Loss Report", + "report_type": "Script Report", + "roles": [ + { + "role": "Manufacturing User" + }, + { + "role": "Stock User" + } + ] +} \ No newline at end of file diff --git a/erpnext/stock/report/process_loss_report/process_loss_report.py b/erpnext/stock/report/process_loss_report/process_loss_report.py new file mode 100644 index 0000000000..be0f0151d4 --- /dev/null +++ b/erpnext/stock/report/process_loss_report/process_loss_report.py @@ -0,0 +1,132 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from typing import Dict, List, Tuple + +Filters = frappe._dict +Row = frappe._dict +Data = List[Row] +Columns = List[Dict[str, str]] +QueryArgs = Dict[str, str] + +def execute(filters: Filters) -> Tuple[Columns, Data]: + columns = get_columns() + data = get_data(filters) + return columns, data + +def get_data(filters: Filters) -> Data: + query_args = get_query_args(filters) + data = run_query(query_args) + update_data_with_total_pl_value(data) + return data + +def get_columns() -> Columns: + return [ + { + 'label': 'Work Order', + 'fieldname': 'name', + 'fieldtype': 'Link', + 'options': 'Work Order', + 'width': '200' + }, + { + 'label': 'Item', + 'fieldname': 'production_item', + 'fieldtype': 'Link', + 'options': 'Item', + 'width': '100' + }, + { + 'label': 'Status', + 'fieldname': 'status', + 'fieldtype': 'Data', + 'width': '100' + }, + { + 'label': 'Qty To Manufacture', + 'fieldname': 'qty', + 'fieldtype': 'Float', + 'width': '150' + }, + { + 'label': 'Manufactured Qty', + 'fieldname': 'produced_qty', + 'fieldtype': 'Float', + 'width': '150' + }, + { + 'label': 'Process Loss Qty', + 'fieldname': 'process_loss_qty', + 'fieldtype': 'Float', + 'width': '150' + }, + { + 'label': 'Actual Manufactured Qty', + 'fieldname': 'actual_produced_qty', + 'fieldtype': 'Float', + 'width': '150' + }, + { + 'label': 'Total FG Value', + 'fieldname': 'total_fg_value', + 'fieldtype': 'Float', + 'width': '150' + }, + { + 'label': 'Total Raw Material Value', + 'fieldname': 'total_rm_value', + 'fieldtype': 'Float', + 'width': '150' + }, + { + 'label': 'Total Process Loss Value', + 'fieldname': 'total_pl_value', + 'fieldtype': 'Float', + 'width': '150' + }, + ] + +def get_query_args(filters: Filters) -> QueryArgs: + query_args = {} + query_args.update(filters) + query_args.update( + get_filter_conditions(filters) + ) + return query_args + +def run_query(query_args: QueryArgs) -> Data: + return frappe.db.sql(""" + SELECT + wo.name, wo.status, wo.production_item, wo.qty, + wo.produced_qty, wo.process_loss_qty, + (wo.produced_qty - wo.process_loss_qty) as actual_produced_qty, + sum(se.total_incoming_value) as total_fg_value, + sum(se.total_outgoing_value) as total_rm_value + FROM + `tabWork Order` wo INNER JOIN `tabStock Entry` se + ON wo.name=se.work_order + WHERE + process_loss_qty > 0 + AND wo.company = %(company)s + AND se.docstatus = 1 + AND se.posting_date BETWEEN %(from_date)s AND %(to_date)s + %(item_filter)s + GROUP BY + se.work_order + """, query_args, as_dict=1) + +def update_data_with_total_pl_value(data: Data) -> None: + for row in data: + value_per_unit_fg = row['total_fg_value'] / row['actual_produced_qty'] + row['total_pl_value'] = row['process_loss_qty'] * value_per_unit_fg + +def get_filter_conditions(filters: Filters) -> QueryArgs: + filter_conditions = dict(item_filter="") + if "item" in filters: + production_item = filters.get("item") + filter_conditions.update( + {"item_filter": f"wo.production_item='{production_item}'"} + ) + return filter_conditions + From c3ce3f918dbe3d871244750de05aa0a59afcfce3 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Tue, 24 Aug 2021 20:15:19 +0530 Subject: [PATCH 16/19] fix: remove spurious function 'toggle_operations' --- erpnext/manufacturing/doctype/bom/bom.js | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index 7de7e17abc..5afda7028a 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -659,7 +659,6 @@ frappe.ui.form.on("BOM", "with_operations", function(frm) { if(!cint(frm.doc.with_operations)) { frm.set_value("operations", []); } - toggle_operations(frm); }); frappe.ui.form.on("BOM Scrap Item", { From 95a2565d86f76858f1e436a871f50862e9874a61 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Tue, 24 Aug 2021 20:18:53 +0530 Subject: [PATCH 17/19] fix: correct value in test --- erpnext/manufacturing/doctype/work_order/test_work_order.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index a00520f6a1..3a334a530c 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -752,7 +752,7 @@ class TestWorkOrder(unittest.TestCase): ) self.assertEqual( frappe.db.get_value("Work Order", wo.name, "process_loss_qty"), - actual_fg_qty + total_pl_qty ) def get_scrap_item_details(bom_no): From c7e11c89ff6636ec9c8aa4a3c3ed1ba5f71e3206 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Wed, 25 Aug 2021 18:58:56 +0530 Subject: [PATCH 18/19] fix: get filters to work - reorder and rename columns - add work order filter --- .../process_loss_report.js | 7 ++++ .../process_loss_report.py | 40 +++++++++---------- 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/erpnext/stock/report/process_loss_report/process_loss_report.js b/erpnext/stock/report/process_loss_report/process_loss_report.js index 078b9e11ce..b0c2b94a25 100644 --- a/erpnext/stock/report/process_loss_report/process_loss_report.js +++ b/erpnext/stock/report/process_loss_report/process_loss_report.js @@ -17,6 +17,13 @@ frappe.query_reports["Process Loss Report"] = { fieldname: "item", fieldtype: "Link", options: "Item", + mandatory: false, + }, + { + label: __("Work Order"), + fieldname: "work_order", + fieldtype: "Link", + options: "Work Order", mandatory: false, }, { diff --git a/erpnext/stock/report/process_loss_report/process_loss_report.py b/erpnext/stock/report/process_loss_report/process_loss_report.py index be0f0151d4..7494328ab4 100644 --- a/erpnext/stock/report/process_loss_report/process_loss_report.py +++ b/erpnext/stock/report/process_loss_report/process_loss_report.py @@ -43,12 +43,6 @@ def get_columns() -> Columns: 'fieldtype': 'Data', 'width': '100' }, - { - 'label': 'Qty To Manufacture', - 'fieldname': 'qty', - 'fieldtype': 'Float', - 'width': '150' - }, { 'label': 'Manufactured Qty', 'fieldname': 'produced_qty', @@ -56,7 +50,7 @@ def get_columns() -> Columns: 'width': '150' }, { - 'label': 'Process Loss Qty', + 'label': 'Loss Qty', 'fieldname': 'process_loss_qty', 'fieldtype': 'Float', 'width': '150' @@ -68,23 +62,23 @@ def get_columns() -> Columns: 'width': '150' }, { - 'label': 'Total FG Value', + 'label': 'Loss Value', + 'fieldname': 'total_pl_value', + 'fieldtype': 'Float', + 'width': '150' + }, + { + 'label': 'FG Value', 'fieldname': 'total_fg_value', 'fieldtype': 'Float', 'width': '150' }, { - 'label': 'Total Raw Material Value', + 'label': 'Raw Material Value', 'fieldname': 'total_rm_value', 'fieldtype': 'Float', 'width': '150' - }, - { - 'label': 'Total Process Loss Value', - 'fieldname': 'total_pl_value', - 'fieldtype': 'Float', - 'width': '150' - }, + } ] def get_query_args(filters: Filters) -> QueryArgs: @@ -111,10 +105,11 @@ def run_query(query_args: QueryArgs) -> Data: AND wo.company = %(company)s AND se.docstatus = 1 AND se.posting_date BETWEEN %(from_date)s AND %(to_date)s - %(item_filter)s + {item_filter} + {work_order_filter} GROUP BY se.work_order - """, query_args, as_dict=1) + """.format(**query_args), query_args, as_dict=1, debug=1) def update_data_with_total_pl_value(data: Data) -> None: for row in data: @@ -122,11 +117,16 @@ def update_data_with_total_pl_value(data: Data) -> None: row['total_pl_value'] = row['process_loss_qty'] * value_per_unit_fg def get_filter_conditions(filters: Filters) -> QueryArgs: - filter_conditions = dict(item_filter="") + filter_conditions = dict(item_filter="", work_order_filter="") if "item" in filters: production_item = filters.get("item") filter_conditions.update( - {"item_filter": f"wo.production_item='{production_item}'"} + {"item_filter": f"AND wo.production_item='{production_item}'"} + ) + if "work_order" in filters: + work_order_name = filters.get("work_order") + filter_conditions.update( + {"work_order_filter": f"AND wo.name='{work_order_name}'"} ) return filter_conditions From b389b8c3ad945f0192c58151e525d9c077ceb1d0 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Thu, 26 Aug 2021 13:15:57 +0530 Subject: [PATCH 19/19] fix: prevent over riding scrap table values, name kwargs, set currency --- erpnext/manufacturing/doctype/bom/bom.py | 2 +- erpnext/manufacturing/doctype/bom/test_bom.py | 15 +++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index ed1e259c3c..24f84e63b3 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -231,7 +231,7 @@ class BOM(WebsiteGenerator): } ret = self.get_bom_material_detail(args) for key, value in ret.items(): - if not item.get(key): + if item.get(key) is None: item.set(key, value) @frappe.whitelist() diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index 6e17f2a831..b8f0db0de2 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -285,30 +285,30 @@ class TestBOM(unittest.TestCase): if not frappe.db.exists("BOM", f"BOM-{fg_item_non_whole.item_code}-001"): bom_doc = create_bom_with_process_loss_item( - fg_item_non_whole, bom_item, 0.25, 0, 1 + fg_item_non_whole, bom_item, scrap_qty=0.25, scrap_rate=0, fg_qty=1 ) bom_doc.submit() bom_doc = create_bom_with_process_loss_item( - fg_item_non_whole, bom_item, 2, 0 + fg_item_non_whole, bom_item, scrap_qty=2, scrap_rate=0 ) # PL Item qty can't be >= FG Item qty self.assertRaises(frappe.ValidationError, bom_doc.submit) bom_doc = create_bom_with_process_loss_item( - fg_item_non_whole, bom_item, 1, 100 + fg_item_non_whole, bom_item, scrap_qty=1, scrap_rate=100 ) # PL Item rate has to be 0 self.assertRaises(frappe.ValidationError, bom_doc.submit) bom_doc = create_bom_with_process_loss_item( - fg_item_whole, bom_item, 0.25, 0 + fg_item_whole, bom_item, scrap_qty=0.25, scrap_rate=0 ) # Items with whole UOMs can't be PL Items self.assertRaises(frappe.ValidationError, bom_doc.submit) bom_doc = create_bom_with_process_loss_item( - fg_item_non_whole, bom_item, 0.25, 0, is_process_loss=0 + fg_item_non_whole, bom_item, scrap_qty=0.25, scrap_rate=0, is_process_loss=0 ) # FG Items in Scrap/Loss Table should have Is Process Loss set self.assertRaises(frappe.ValidationError, bom_doc.submit) @@ -316,9 +316,6 @@ class TestBOM(unittest.TestCase): def get_default_bom(item_code="_Test FG Item 2"): return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1}) - - - def level_order_traversal(node): traversal = [] q = deque() @@ -364,6 +361,7 @@ def create_nested_bom(tree, prefix="_Test bom "): bom = frappe.get_doc(doctype="BOM", item=bom_item_code) for child_item in child_items.keys(): bom.append("items", {"item_code": prefix + child_item}) + bom.currency = "INR" bom.insert() bom.submit() @@ -407,6 +405,7 @@ def create_bom_with_process_loss_item( "rate": scrap_rate, "is_process_loss": is_process_loss }) + bom_doc.currency = "INR" return bom_doc def create_process_loss_bom_items():