From e8bc4f12a734ff286a008e6b31dffa9c6c946365 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 24 Jan 2022 15:59:38 +0530 Subject: [PATCH 01/21] fix:Add shipping charges to taxes only if applicable --- erpnext/accounts/doctype/shipping_rule/shipping_rule.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/shipping_rule/shipping_rule.py b/erpnext/accounts/doctype/shipping_rule/shipping_rule.py index 7e5129911e46..792e7d21a78a 100644 --- a/erpnext/accounts/doctype/shipping_rule/shipping_rule.py +++ b/erpnext/accounts/doctype/shipping_rule/shipping_rule.py @@ -71,7 +71,8 @@ def apply(self, doc): if doc.currency != doc.company_currency: shipping_amount = flt(shipping_amount / doc.conversion_rate, 2) - self.add_shipping_rule_to_tax_table(doc, shipping_amount) + if shipping_amount: + self.add_shipping_rule_to_tax_table(doc, shipping_amount) def get_shipping_amount_from_rules(self, value): for condition in self.get("conditions"): From c24724ac7a58602d3ec28450395edb3791db5a45 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 14 Dec 2021 14:58:20 +0530 Subject: [PATCH 02/21] fix: qty filter not working if apply_multiple_pricing_rules is enabled (cherry picked from commit 1e17d6a6079d1f65980c3735f180f35fb7d265ab) --- erpnext/accounts/doctype/pricing_rule/utils.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index 02bfc9defd72..1bd18bca86d8 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -67,13 +67,12 @@ def sorted_by_priority(pricing_rules, args, doc=None): if not pricing_rule.get('priority'): pricing_rule['priority'] = 1 - if pricing_rule.get('apply_multiple_pricing_rules'): - pricing_rule_dict.setdefault(cint(pricing_rule.get("priority")), []).append(pricing_rule) + pricing_rule_dict.setdefault(cint(pricing_rule.get("priority")), []).append(pricing_rule) for key in sorted(pricing_rule_dict): pricing_rules_list.extend(pricing_rule_dict.get(key)) - return pricing_rules_list or pricing_rules + return pricing_rules_list def filter_pricing_rule_based_on_condition(pricing_rules, doc=None): filtered_pricing_rules = [] From e7e0359e3ed6f8a7ed616538708c969cd49c7ec8 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 14 Dec 2021 14:58:31 +0530 Subject: [PATCH 03/21] test: add test_multiple_pricing_rules (cherry picked from commit c0d1f4869f52b4def9d83017577b307e34494e09) --- .../doctype/pricing_rule/test_pricing_rule.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py index e9a018ec46cd..f14999a3583b 100644 --- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py @@ -630,6 +630,26 @@ def test_pricing_rule_for_transaction(self): for doc in [si, si1]: doc.delete() + def test_multiple_pricing_rules(self): + make_pricing_rule(discount_percentage=20, selling=1, priority=1, min_qty=4, + apply_multiple_pricing_rules=1, title="_Test Pricing Rule with Min Qty - 1") + make_pricing_rule(discount_percentage=10, selling=1, priority=2, min_qty=4, + apply_multiple_pricing_rules=1, title="_Test Pricing Rule with Min Qty - 2") + + si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 1", qty=1, currency="USD") + item = si.items[0] + item.stock_qty = 1 + si.save() + self.assertFalse(item.discount_percentage) + item.qty = 5 + item.stock_qty = 5 + si.save() + self.assertEqual(item.discount_percentage, 30) + si.delete() + + frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule with Min Qty - 1") + frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule with Min Qty - 2") + test_dependencies = ["Campaign"] def make_pricing_rule(**args): From eeb2402764098830fbc478a79695c5d08ad0ce8e Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 15 Dec 2021 10:32:23 +0530 Subject: [PATCH 04/21] chore: rename redefinition of multiple pricing rule test (cherry picked from commit 5a7a9a598b39553901bbd37e8f003943a0446a58) --- erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py index f14999a3583b..6571e1674c2e 100644 --- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py @@ -630,7 +630,7 @@ def test_pricing_rule_for_transaction(self): for doc in [si, si1]: doc.delete() - def test_multiple_pricing_rules(self): + def test_multiple_pricing_rules_with_min_qty(self): make_pricing_rule(discount_percentage=20, selling=1, priority=1, min_qty=4, apply_multiple_pricing_rules=1, title="_Test Pricing Rule with Min Qty - 1") make_pricing_rule(discount_percentage=10, selling=1, priority=2, min_qty=4, From 8535ad5294c47ff6020f7aecc136dce117e33d39 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 20 Jan 2022 12:09:04 +0530 Subject: [PATCH 05/21] chore: undo unnecessary changes (cherry picked from commit 3da2cac772b0557e15ddf4ee9673381b0d98bca1) --- erpnext/accounts/doctype/pricing_rule/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index 1bd18bca86d8..7792590c9c76 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -67,7 +67,8 @@ def sorted_by_priority(pricing_rules, args, doc=None): if not pricing_rule.get('priority'): pricing_rule['priority'] = 1 - pricing_rule_dict.setdefault(cint(pricing_rule.get("priority")), []).append(pricing_rule) + if pricing_rule.get('apply_multiple_pricing_rules'): + pricing_rule_dict.setdefault(cint(pricing_rule.get("priority")), []).append(pricing_rule) for key in sorted(pricing_rule_dict): pricing_rules_list.extend(pricing_rule_dict.get(key)) From 997ddbea78dc230fcc44a9cdca932132b023db3c Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 28 Jan 2022 14:12:57 +0530 Subject: [PATCH 06/21] fix: ignore alternate item while checking pending qty (cherry picked from commit 14e3e163aeec88a61748beca415e59bc0aa62870) --- erpnext/public/js/utils/serial_no_batch_selector.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js index b5d3981ba7f0..16e3fa0abd1e 100644 --- a/erpnext/public/js/utils/serial_no_batch_selector.js +++ b/erpnext/public/js/utils/serial_no_batch_selector.js @@ -590,6 +590,6 @@ function check_can_calculate_pending_qty(me) { && doc.fg_completed_qty && erpnext.stock.bom && erpnext.stock.bom.name === doc.bom_no; - const itemChecks = !!item; + const itemChecks = !!item && !item.allow_alternative_item; return docChecks && itemChecks; } From cc08125f39bbc0b0c2392eafab77da39ffe3939d Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 24 Jan 2022 19:19:58 +0530 Subject: [PATCH 07/21] refactor: reusable clean_serial_nos (cherry picked from commit b20df3745eafdcc18d3b7cb9a93065562aef5c6f) --- erpnext/controllers/stock_controller.py | 13 ++++--------- erpnext/stock/doctype/serial_no/serial_no.py | 7 +++++++ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index b97432e74856..d7a68f780089 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -77,17 +77,12 @@ def validate_serialized_batch(self): .format(d.idx, get_link_to_form("Batch", d.get("batch_no")))) def clean_serial_nos(self): + from erpnext.stock.doctype.serial_no.serial_no import clean_serial_no_string + for row in self.get("items"): if hasattr(row, "serial_no") and row.serial_no: - # replace commas by linefeed - row.serial_no = row.serial_no.replace(",", "\n") - - # strip preceeding and succeeding spaces for each SN - # (SN could have valid spaces in between e.g. SN - 123 - 2021) - serial_no_list = row.serial_no.split("\n") - serial_no_list = [sn.strip() for sn in serial_no_list] - - row.serial_no = "\n".join(serial_no_list) + # remove extra whitespace and store one serial no on each line + row.serial_no = clean_serial_no_string(row.serial_no) def get_gl_entries(self, warehouse_account=None, default_expense_account=None, default_cost_center=None): diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index 3b325b802954..e300d46db839 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -484,6 +484,13 @@ def get_serial_nos(serial_no): return [s.strip() for s in cstr(serial_no).strip().upper().replace(',', '\n').split('\n') if s.strip()] +def clean_serial_no_string(serial_no: str) -> str: + if not serial_no: + return "" + + serial_no_list = get_serial_nos(serial_no) + return "\n".join(serial_no_list) + def update_args_for_serial_no(serial_no_doc, serial_no, args, is_new=False): for field in ["item_code", "work_order", "company", "batch_no", "supplier", "location"]: if args.get(field): From db3eef9a8b64f8453837b178b7ac95bfdce6f4fd Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 24 Jan 2022 19:28:26 +0530 Subject: [PATCH 08/21] fix: extend sr_no cleanup to packed items too (cherry picked from commit e177c5277f223cd0f513d7189053e444554bd745) --- erpnext/controllers/stock_controller.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index d7a68f780089..2912d3eb0bda 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -84,6 +84,11 @@ def clean_serial_nos(self): # remove extra whitespace and store one serial no on each line row.serial_no = clean_serial_no_string(row.serial_no) + for row in self.get('packed_items') or []: + if hasattr(row, "serial_no") and row.serial_no: + # remove extra whitespace and store one serial no on each line + row.serial_no = clean_serial_no_string(row.serial_no) + def get_gl_entries(self, warehouse_account=None, default_expense_account=None, default_cost_center=None): From 70dc77305b1e054bb859fea269dc1a818065dc82 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 24 Jan 2022 19:33:47 +0530 Subject: [PATCH 09/21] fix: work order serial no allows storing whitespace (cherry picked from commit 43bd88e741b69fcfd4a2547fd59e5e6286229522) --- erpnext/manufacturing/doctype/work_order/work_order.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 23fb9697cdb6..b12e157390f6 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -31,6 +31,7 @@ from erpnext.stock.doctype.item.item import get_item_defaults, validate_end_of_life from erpnext.stock.doctype.serial_no.serial_no import ( auto_make_serial_nos, + clean_serial_no_string, get_auto_serial_nos, get_serial_nos, ) @@ -358,6 +359,7 @@ def delete_auto_created_batch_and_serial_no(self): frappe.delete_doc("Batch", row.name) def make_serial_nos(self, args): + self.serial_no = clean_serial_no_string(self.serial_no) serial_no_series = frappe.get_cached_value("Item", self.production_item, "serial_no_series") if serial_no_series: self.serial_no = get_auto_serial_nos(serial_no_series, self.qty) From dc82becbd2eb1b197b30807b64e3086750b7a45b Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 28 Jan 2022 16:10:30 +0530 Subject: [PATCH 10/21] fix(India) Tax calculation for overseas suppliers (cherry picked from commit e45c38337c3afc78347f0d26ecd8365148b26d83) --- erpnext/regional/india/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 2287714a008d..0126b090fcad 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -221,6 +221,7 @@ def get_regional_address_details(party_details, doctype, company): if not party_details.place_of_supply: return party_details if not party_details.company_gstin: return party_details + if not party_details.supplier_gstin: return party_details if ((doctype in ("Sales Invoice", "Delivery Note", "Sales Order") and party_details.company_gstin and party_details.company_gstin[:2] != party_details.place_of_supply[:2]) or (doctype in ("Purchase Invoice", From 0fb4e81a30fbcd72b2abc6ec23d2a034ef118763 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 30 Jan 2022 20:49:41 +0530 Subject: [PATCH 11/21] fix: cost of poor quality report time filters not working (#28958) (#29521) * fix: cost of poor quality report time filters not working * chore:update cost of poor quality report to use query builder * fix: linter warnings * chore: updated report query * chore: added test filters * fix : cleared linter warnings * chore: formatting * refactor: query generation - optionally apply date filters - join instead of expensive sub-query - return as dictionary * test: simplify test Co-authored-by: Ankush Menat (cherry picked from commit 0f7c2a19de34c9bd14d457c9b6e7f6cf279c6ea2) Co-authored-by: aaronmenezes --- .../cost_of_poor_quality_report.js | 2 - .../cost_of_poor_quality_report.py | 63 ++++++++++++------- erpnext/manufacturing/report/test_reports.py | 2 +- 3 files changed, 42 insertions(+), 25 deletions(-) diff --git a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js index 97e7e0a7d202..72eed5e0d7cc 100644 --- a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js +++ b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js @@ -17,14 +17,12 @@ frappe.query_reports["Cost of Poor Quality Report"] = { fieldname:"from_date", fieldtype: "Datetime", default: frappe.datetime.convert_to_system_tz(frappe.datetime.add_months(frappe.datetime.now_datetime(), -1)), - reqd: 1 }, { label: __("To Date"), fieldname:"to_date", fieldtype: "Datetime", default: frappe.datetime.now_datetime(), - reqd: 1, }, { label: __("Job Card"), diff --git a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py index 77418235b075..88b21170e8be 100644 --- a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py +++ b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py @@ -3,46 +3,65 @@ import frappe from frappe import _ -from frappe.utils import flt def execute(filters=None): - columns, data = [], [] + return get_columns(filters), get_data(filters) - columns = get_columns(filters) - data = get_data(filters) - - return columns, data def get_data(report_filters): data = [] operations = frappe.get_all("Operation", filters = {"is_corrective_operation": 1}) if operations: - operations = [d.name for d in operations] - fields = ["production_item as item_code", "item_name", "work_order", "operation", - "workstation", "total_time_in_mins", "name", "hour_rate", "serial_no", "batch_no"] + if report_filters.get('operation'): + operations = [report_filters.get('operation')] + else: + operations = [d.name for d in operations] - filters = get_filters(report_filters, operations) + job_card = frappe.qb.DocType("Job Card") - job_cards = frappe.get_all("Job Card", fields = fields, - filters = filters) + operating_cost = ((job_card.hour_rate) * (job_card.total_time_in_mins) / 60.0).as_('operating_cost') + item_code = (job_card.production_item).as_('item_code') - for row in job_cards: - row.operating_cost = flt(row.hour_rate) * (flt(row.total_time_in_mins) / 60.0) - data.append(row) + query = (frappe.qb + .from_(job_card) + .select(job_card.name, job_card.work_order, item_code, job_card.item_name, + job_card.operation, job_card.serial_no, job_card.batch_no, + job_card.workstation, job_card.total_time_in_mins, job_card.hour_rate, + operating_cost) + .where( + (job_card.docstatus == 1) + & (job_card.is_corrective_job_card == 1)) + .groupby(job_card.name) + ) + query = append_filters(query, report_filters, operations, job_card) + data = query.run(as_dict=True) return data -def get_filters(report_filters, operations): - filters = {"docstatus": 1, "operation": ("in", operations), "is_corrective_job_card": 1} - for field in ["name", "work_order", "operation", "workstation", "company", "serial_no", "batch_no", "production_item"]: +def append_filters(query, report_filters, operations, job_card): + """Append optional filters to query builder. """ + + for field in ("name", "work_order", "operation", "workstation", + "company", "serial_no", "batch_no", "production_item"): if report_filters.get(field): - if field != 'serial_no': - filters[field] = report_filters.get(field) + if field == 'serial_no': + query = query.where(job_card[field].like('%{}%'.format(report_filters.get(field)))) + elif field == 'operation': + query = query.where(job_card[field].isin(operations)) else: - filters[field] = ('like', '% {} %'.format(report_filters.get(field))) + query = query.where(job_card[field] == report_filters.get(field)) + + if report_filters.get('from_date') or report_filters.get('to_date'): + job_card_time_log = frappe.qb.DocType("Job Card Time Log") + + query = query.join(job_card_time_log).on(job_card.name == job_card_time_log.parent) + if report_filters.get('from_date'): + query = query.where(job_card_time_log.from_time >= report_filters.get('from_date')) + if report_filters.get('to_date'): + query = query.where(job_card_time_log.to_time <= report_filters.get('to_date')) - return filters + return query def get_columns(filters): return [ diff --git a/erpnext/manufacturing/report/test_reports.py b/erpnext/manufacturing/report/test_reports.py index 1de472659eb7..9f51ded6c776 100644 --- a/erpnext/manufacturing/report/test_reports.py +++ b/erpnext/manufacturing/report/test_reports.py @@ -18,7 +18,7 @@ ("BOM Operations Time", {}), ("BOM Stock Calculated", {"bom": frappe.get_last_doc("BOM").name, "qty_to_make": 2}), ("BOM Stock Report", {"bom": frappe.get_last_doc("BOM").name, "qty_to_produce": 2}), - ("Cost of Poor Quality Report", {}), + ("Cost of Poor Quality Report", {"item": "_Test Item", "serial_no": "00"}), ("Downtime Analysis", {}), ( "Exponential Smoothing Forecasting", From 9da63f2e7953559fff9f7bd82b60a2ab336fc80e Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 25 Jan 2022 17:12:41 +0530 Subject: [PATCH 12/21] fix(sales order): incorrect no. of items fetched while creating dn (cherry picked from commit a73ad6759d7e9ffadda036121a6f1498117cf085) # Conflicts: # erpnext/selling/doctype/sales_order/sales_order.js --- .../doctype/sales_order/sales_order.js | 26 ++++++++----------- .../doctype/sales_order/sales_order.py | 8 +++++- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 886ed071716c..13def783a35c 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -457,12 +457,8 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( make_delivery_note_based_on_delivery_date: function() { var me = this; - var delivery_dates = []; - $.each(this.frm.doc.items || [], function(i, d) { - if(!delivery_dates.includes(d.delivery_date)) { - delivery_dates.push(d.delivery_date); - } - }); + var delivery_dates = this.frm.doc.items.map(i => i.delivery_date); + delivery_dates = [ ...new Set(delivery_dates) ]; var item_grid = this.frm.fields_dict["items"].grid; if(!item_grid.get_selected().length && delivery_dates.length > 1) { @@ -500,14 +496,7 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( if(!dates) return; - $.each(dates, function(i, d) { - $.each(item_grid.grid_rows || [], function(j, row) { - if(row.doc.delivery_date == d) { - row.doc.__checked = 1; - } - }); - }) - me.make_delivery_note(); + me.make_delivery_note(dates); dialog.hide(); }); dialog.show(); @@ -516,10 +505,17 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( } }, +<<<<<<< HEAD make_delivery_note: function() { +======= + make_delivery_note(delivery_dates) { +>>>>>>> a73ad6759d (fix(sales order): incorrect no. of items fetched while creating dn) frappe.model.open_mapped_doc({ method: "erpnext.selling.doctype.sales_order.sales_order.make_delivery_note", - frm: this.frm + frm: this.frm, + args: { + delivery_dates + } }) }, diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 658691548f1c..db414e97a711 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -611,6 +611,12 @@ def update_item(source, target, source_parent): } if not skip_item_mapping: + def condition(doc): + if frappe.flags.args and frappe.flags.args.delivery_dates: + if cstr(doc.delivery_date) not in frappe.flags.args.delivery_dates: + return False + return abs(doc.delivered_qty) < abs(doc.qty) and doc.delivered_by_supplier!=1 + mapper["Sales Order Item"] = { "doctype": "Delivery Note Item", "field_map": { @@ -619,7 +625,7 @@ def update_item(source, target, source_parent): "parent": "against_sales_order", }, "postprocess": update_item, - "condition": lambda doc: abs(doc.delivered_qty) < abs(doc.qty) and doc.delivered_by_supplier!=1 + "condition": condition } target_doc = get_mapped_doc("Sales Order", source_name, mapper, target_doc, set_missing_values) From e6f2343fb4cd56b88544b3d3528e66a29a24fbe2 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 25 Jan 2022 17:28:28 +0530 Subject: [PATCH 13/21] chore: add comment (cherry picked from commit 0b9a850a176fce76964709c6e32ef0592955efbe) --- erpnext/selling/doctype/sales_order/sales_order.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index db414e97a711..8336a1436176 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -612,6 +612,7 @@ def update_item(source, target, source_parent): if not skip_item_mapping: def condition(doc): + # make_mapped_doc sets js `args` into `frappe.flags.args` if frappe.flags.args and frappe.flags.args.delivery_dates: if cstr(doc.delivery_date) not in frappe.flags.args.delivery_dates: return False From bed7201efe15808df66cf21c4e53eee577b105bf Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 31 Jan 2022 11:40:59 +0530 Subject: [PATCH 14/21] fix: merge conflicts --- erpnext/selling/doctype/sales_order/sales_order.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 13def783a35c..2d5bb2013f2e 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -505,11 +505,7 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( } }, -<<<<<<< HEAD - make_delivery_note: function() { -======= - make_delivery_note(delivery_dates) { ->>>>>>> a73ad6759d (fix(sales order): incorrect no. of items fetched while creating dn) + make_delivery_note: function(delivery_dates) { frappe.model.open_mapped_doc({ method: "erpnext.selling.doctype.sales_order.sales_order.make_delivery_note", frm: this.frm, From 8aa642309372593ea80cd4faf5df36ec8cccf0d1 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 30 Jan 2022 16:25:42 +0530 Subject: [PATCH 15/21] fix: add unique constraint on bin at db level (cherry picked from commit 08810dbcce3c96855c2c5bfe266ba9d7ea61fc42) --- erpnext/stock/doctype/bin/bin.json | 8 ++++-- erpnext/stock/doctype/bin/bin.py | 2 +- erpnext/stock/doctype/bin/test_bin.py | 35 ++++++++++++++++++++++++--- erpnext/stock/utils.py | 30 ++++++++++++----------- 4 files changed, 54 insertions(+), 21 deletions(-) diff --git a/erpnext/stock/doctype/bin/bin.json b/erpnext/stock/doctype/bin/bin.json index 8e79f0e55520..56dc71c57e17 100644 --- a/erpnext/stock/doctype/bin/bin.json +++ b/erpnext/stock/doctype/bin/bin.json @@ -33,6 +33,7 @@ "oldfieldtype": "Link", "options": "Warehouse", "read_only": 1, + "reqd": 1, "search_index": 1 }, { @@ -46,6 +47,7 @@ "oldfieldtype": "Link", "options": "Item", "read_only": 1, + "reqd": 1, "search_index": 1 }, { @@ -169,10 +171,11 @@ "idx": 1, "in_create": 1, "links": [], - "modified": "2021-03-30 23:09:39.572776", + "modified": "2022-01-30 17:04:54.715288", "modified_by": "Administrator", "module": "Stock", "name": "Bin", + "naming_rule": "Expression (old style)", "owner": "Administrator", "permissions": [ { @@ -200,5 +203,6 @@ "quick_entry": 1, "search_fields": "item_code,warehouse", "sort_field": "modified", - "sort_order": "ASC" + "sort_order": "ASC", + "states": [] } \ No newline at end of file diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py index 11ff359b4836..1d874cd06fbf 100644 --- a/erpnext/stock/doctype/bin/bin.py +++ b/erpnext/stock/doctype/bin/bin.py @@ -96,7 +96,7 @@ def update_reserved_qty_for_sub_contracting(self): self.db_set('projected_qty', self.projected_qty) def on_doctype_update(): - frappe.db.add_index("Bin", ["item_code", "warehouse"]) + frappe.db.add_unique("Bin", ["item_code", "warehouse"], constraint_name="unique_item_warehouse") def update_stock(bin_name, args, allow_negative_stock=False, via_landed_cost_voucher=False): diff --git a/erpnext/stock/doctype/bin/test_bin.py b/erpnext/stock/doctype/bin/test_bin.py index 9c390d94b4e1..250126c6b98c 100644 --- a/erpnext/stock/doctype/bin/test_bin.py +++ b/erpnext/stock/doctype/bin/test_bin.py @@ -1,9 +1,36 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -import unittest +import frappe -# test_records = frappe.get_test_records('Bin') +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.utils import _create_bin +from erpnext.tests.utils import ERPNextTestCase -class TestBin(unittest.TestCase): - pass + +class TestBin(ERPNextTestCase): + + + def test_concurrent_inserts(self): + """ Ensure no duplicates are possible in case of concurrent inserts""" + item_code = "_TestConcurrentBin" + make_item(item_code) + warehouse = "_Test Warehouse - _TC" + + bin1 = frappe.get_doc(doctype="Bin", item_code=item_code, warehouse=warehouse) + bin1.insert() + + bin2 = frappe.get_doc(doctype="Bin", item_code=item_code, warehouse=warehouse) + with self.assertRaises(frappe.UniqueValidationError): + bin2.insert() + + # util method should handle it + bin = _create_bin(item_code, warehouse) + self.assertEqual(bin.item_code, item_code) + + frappe.db.rollback() + + def test_index_exists(self): + indexes = frappe.db.sql("show index from tabBin where Non_unique = 0", as_dict=1) + if not any(index.get("Key_name") == "unique_item_warehouse" for index in indexes): + self.fail(f"Expected unique index on item-warehouse") diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 4d77367a0511..b8bdf39301e1 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -177,13 +177,7 @@ def get_latest_stock_balance(): def get_bin(item_code, warehouse): bin = frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}) if not bin: - bin_obj = frappe.get_doc({ - "doctype": "Bin", - "item_code": item_code, - "warehouse": warehouse, - }) - bin_obj.flags.ignore_permissions = 1 - bin_obj.insert() + bin_obj = _create_bin(item_code, warehouse) else: bin_obj = frappe.get_doc('Bin', bin, for_update=True) bin_obj.flags.ignore_permissions = True @@ -193,16 +187,24 @@ def get_or_make_bin(item_code: str , warehouse: str) -> str: bin_record = frappe.db.get_value('Bin', {'item_code': item_code, 'warehouse': warehouse}) if not bin_record: - bin_obj = frappe.get_doc({ - "doctype": "Bin", - "item_code": item_code, - "warehouse": warehouse, - }) + bin_obj = _create_bin(item_code, warehouse) + bin_record = bin_obj.name + return bin_record + +def _create_bin(item_code, warehouse): + """Create a bin and take care of concurrent inserts.""" + + bin_creation_savepoint = "create_bin" + try: + frappe.db.savepoint(bin_creation_savepoint) + bin_obj = frappe.get_doc(doctype="Bin", item_code=item_code, warehouse=warehouse) bin_obj.flags.ignore_permissions = 1 bin_obj.insert() - bin_record = bin_obj.name + except frappe.UniqueValidationError: + frappe.db.rollback(save_point=bin_creation_savepoint) # preserve transaction in postgres + bin_obj = frappe.get_last_doc("Bin", {"item_code": item_code, "warehouse": warehouse}) - return bin_record + return bin_obj def update_bin(args, allow_negative_stock=False, via_landed_cost_voucher=False): """WARNING: This function is deprecated. Inline this function instead of using it.""" From b3f36efa8f9c2fabcacfe1c0dbbcad9d6e8ea89b Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 30 Jan 2022 18:06:23 +0530 Subject: [PATCH 16/21] fix: ignore None item/wh whie updating reservation (cherry picked from commit a1e7771cdd7713c64ab525ba7955a139712c727e) --- erpnext/stock/doctype/stock_entry/stock_entry.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index ab106e036f1c..a5bf2397411c 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1674,6 +1674,8 @@ def update_purchase_order_supplied_items(self): for d in self.get("items"): item_code = d.get('original_item') or d.get('item_code') reserve_warehouse = item_wh.get(item_code) + if not (reserve_warehouse and item_code): + continue stock_bin = get_bin(item_code, reserve_warehouse) stock_bin.update_reserved_qty_for_sub_contracting() From a7b30d9197cb4bd307beab2a320e424e979be95f Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Sun, 30 Jan 2022 18:10:43 +0530 Subject: [PATCH 17/21] fix(patch): patch duplicate bins (cherry picked from commit 7ff3ca25e5b8916d9ebb375f7574dc1a5ebdd4ff) --- erpnext/patches.txt | 1 + .../v13_0/add_bin_unique_constraint.py | 45 +++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 erpnext/patches/v13_0/add_bin_unique_constraint.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 704b6696db15..b5ee1a4ce8c6 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -1,4 +1,5 @@ erpnext.patches.v12_0.update_is_cancelled_field +erpnext.patches.v13_0.add_bin_unique_constraint erpnext.patches.v11_0.rename_production_order_to_work_order erpnext.patches.v11_0.refactor_naming_series erpnext.patches.v11_0.refactor_autoname_naming diff --git a/erpnext/patches/v13_0/add_bin_unique_constraint.py b/erpnext/patches/v13_0/add_bin_unique_constraint.py new file mode 100644 index 000000000000..29ae631be85c --- /dev/null +++ b/erpnext/patches/v13_0/add_bin_unique_constraint.py @@ -0,0 +1,45 @@ +import frappe + +from erpnext.stock.stock_balance import ( + get_balance_qty_from_sle, + get_indented_qty, + get_ordered_qty, + get_planned_qty, + get_reserved_qty, + update_bin_qty, +) + + +def execute(): + + duplicate_rows = frappe.db.sql(""" + SELECT + item_code, warehouse + FROM + tabBin + GROUP BY + item_code, warehouse + HAVING + COUNT(*) > 1 + """, as_dict=1) + + for row in duplicate_rows: + bins = frappe.get_list("Bin", + filters={"item_code": row.item_code, + "warehouse": row.warehouse}, + fields=["name"], + order_by="creation", + ) + + for x in range(len(bins) - 1): + frappe.delete_doc("Bin", bins[x].name) + + qty_dict = { + "reserved_qty": get_reserved_qty(row.item_code, row.warehouse), + "indented_qty": get_indented_qty(row.item_code, row.warehouse), + "ordered_qty": get_ordered_qty(row.item_code, row.warehouse), + "planned_qty": get_planned_qty(row.item_code, row.warehouse), + "actual_qty": get_balance_qty_from_sle(row.item_code, row.warehouse) + } + + update_bin_qty(row.item_code, row.warehouse, qty_dict) From f99f537b573a51f2598cf2487450ed995e5a418c Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 30 Jan 2022 18:16:14 +0530 Subject: [PATCH 18/21] refactor: patch for fixing broken bins fix(patch): delete fully broken bins if bin doesn't have item_code or warehouse then it's not recoverable. (cherry picked from commit c2ecc7a2d1da839423fd768821b1f77ddcf7f53d) --- .../v13_0/add_bin_unique_constraint.py | 48 ++++++++++++------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/erpnext/patches/v13_0/add_bin_unique_constraint.py b/erpnext/patches/v13_0/add_bin_unique_constraint.py index 29ae631be85c..979fcf5a4f83 100644 --- a/erpnext/patches/v13_0/add_bin_unique_constraint.py +++ b/erpnext/patches/v13_0/add_bin_unique_constraint.py @@ -11,35 +11,47 @@ def execute(): + delete_broken_bins() + delete_and_patch_duplicate_bins() - duplicate_rows = frappe.db.sql(""" +def delete_broken_bins(): + # delete useless bins + frappe.db.sql("delete from `tabBin` where item_code is null or warehouse is null") + +def delete_and_patch_duplicate_bins(): + + duplicate_bins = frappe.db.sql(""" SELECT - item_code, warehouse + item_code, warehouse, count(*) as bin_count FROM tabBin GROUP BY item_code, warehouse HAVING - COUNT(*) > 1 + bin_count > 1 """, as_dict=1) - for row in duplicate_rows: - bins = frappe.get_list("Bin", - filters={"item_code": row.item_code, - "warehouse": row.warehouse}, - fields=["name"], - order_by="creation", - ) + for duplicate_bin in duplicate_bins: + existing_bins = frappe.get_list("Bin", + filters={ + "item_code": duplicate_bin.item_code, + "warehouse": duplicate_bin.warehouse + }, + fields=["name"], + order_by="creation",) + + # keep last one + existing_bins.pop() - for x in range(len(bins) - 1): - frappe.delete_doc("Bin", bins[x].name) + for broken_bin in existing_bins: + frappe.delete_doc("Bin", broken_bin.name) qty_dict = { - "reserved_qty": get_reserved_qty(row.item_code, row.warehouse), - "indented_qty": get_indented_qty(row.item_code, row.warehouse), - "ordered_qty": get_ordered_qty(row.item_code, row.warehouse), - "planned_qty": get_planned_qty(row.item_code, row.warehouse), - "actual_qty": get_balance_qty_from_sle(row.item_code, row.warehouse) + "reserved_qty": get_reserved_qty(duplicate_bin.item_code, duplicate_bin.warehouse), + "indented_qty": get_indented_qty(duplicate_bin.item_code, duplicate_bin.warehouse), + "ordered_qty": get_ordered_qty(duplicate_bin.item_code, duplicate_bin.warehouse), + "planned_qty": get_planned_qty(duplicate_bin.item_code, duplicate_bin.warehouse), + "actual_qty": get_balance_qty_from_sle(duplicate_bin.item_code, duplicate_bin.warehouse) } - update_bin_qty(row.item_code, row.warehouse, qty_dict) + update_bin_qty(duplicate_bin.item_code, duplicate_bin.warehouse, qty_dict) From 2b868e7f565c14653d0fd8a113704145a30b264d Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 31 Jan 2022 14:10:42 +0530 Subject: [PATCH 19/21] fix: update reserved qty for production/ s/c (cherry picked from commit 0a1533446495f27445afbcb9d58c30d4d31e7b20) --- .../v13_0/add_bin_unique_constraint.py | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/erpnext/patches/v13_0/add_bin_unique_constraint.py b/erpnext/patches/v13_0/add_bin_unique_constraint.py index 979fcf5a4f83..57fbaae9d8dd 100644 --- a/erpnext/patches/v13_0/add_bin_unique_constraint.py +++ b/erpnext/patches/v13_0/add_bin_unique_constraint.py @@ -6,8 +6,8 @@ get_ordered_qty, get_planned_qty, get_reserved_qty, - update_bin_qty, ) +from erpnext.stock.utils import get_bin def execute(): @@ -32,10 +32,12 @@ def delete_and_patch_duplicate_bins(): """, as_dict=1) for duplicate_bin in duplicate_bins: + item_code = duplicate_bin.item_code + warehouse = duplicate_bin.warehouse existing_bins = frappe.get_list("Bin", filters={ - "item_code": duplicate_bin.item_code, - "warehouse": duplicate_bin.warehouse + "item_code": item_code, + "warehouse": warehouse }, fields=["name"], order_by="creation",) @@ -47,11 +49,15 @@ def delete_and_patch_duplicate_bins(): frappe.delete_doc("Bin", broken_bin.name) qty_dict = { - "reserved_qty": get_reserved_qty(duplicate_bin.item_code, duplicate_bin.warehouse), - "indented_qty": get_indented_qty(duplicate_bin.item_code, duplicate_bin.warehouse), - "ordered_qty": get_ordered_qty(duplicate_bin.item_code, duplicate_bin.warehouse), - "planned_qty": get_planned_qty(duplicate_bin.item_code, duplicate_bin.warehouse), - "actual_qty": get_balance_qty_from_sle(duplicate_bin.item_code, duplicate_bin.warehouse) + "reserved_qty": get_reserved_qty(item_code, warehouse), + "indented_qty": get_indented_qty(item_code, warehouse), + "ordered_qty": get_ordered_qty(item_code, warehouse), + "planned_qty": get_planned_qty(item_code, warehouse), + "actual_qty": get_balance_qty_from_sle(item_code, warehouse) } - update_bin_qty(duplicate_bin.item_code, duplicate_bin.warehouse, qty_dict) + bin = get_bin(item_code, warehouse) + bin.update(qty_dict) + bin.update_reserved_qty_for_production() + bin.update_reserved_qty_for_sub_contracting() + bin.db_update() From 9425ca07f58da491fb718b1f45b042a2db2bacee Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 31 Jan 2022 15:43:08 +0530 Subject: [PATCH 20/21] chore: revert manual handling of stock level section (#29537) (#29538) (cherry picked from commit a2542016965c0bbd8ff97facee2789b187ab9030) Co-authored-by: Ankush Menat --- erpnext/stock/doctype/item/item.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index e346ea87214a..da371d968c94 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -380,8 +380,7 @@ $.extend(erpnext.item, { // Show Stock Levels only if is_stock_item if (frm.doc.is_stock_item) { frappe.require('assets/js/item-dashboard.min.js', function() { - frm.dashboard.parent.find('.stock-levels').remove(); - const section = frm.dashboard.add_section('', __("Stock Levels"), 'stock-levels'); + const section = frm.dashboard.add_section('', __("Stock Levels")); erpnext.item.item_dashboard = new erpnext.stock.ItemDashboard({ parent: section, item_code: frm.doc.name, From 5ac11a4ba516860906a2c81a75a2755a817e32a5 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 31 Jan 2022 17:11:43 +0530 Subject: [PATCH 21/21] refactor!: dynamically compute bom_level (backport #29522) (#29540) * refactor!: dynamically compute bom_level (cherry picked from commit 157b4b33fe633ed96c7e36fdcc81c9f2245241ec) # Conflicts: # erpnext/manufacturing/doctype/bom/bom.py # erpnext/patches.txt * fix(ux): sort multi-production item plans correctly (cherry picked from commit d38fd8635cdf3ad5aa8d9c441e4d7cbcf9aa4190) * fix: conflicts * fix: removed set_bom_level * fix: conflict Co-authored-by: Ankush Menat Co-authored-by: rohitwaghchaure --- erpnext/manufacturing/doctype/bom/bom.json | 11 +----- erpnext/manufacturing/doctype/bom/bom.py | 17 -------- .../production_plan/production_plan.py | 11 +++--- .../production_plan/test_production_plan.py | 39 +++++++++++++++++++ .../production_plan_sub_assembly_item.json | 4 +- .../report/bom_explorer/bom_explorer.py | 5 +-- .../production_plan_summary.py | 2 +- erpnext/patches.txt | 1 - erpnext/patches/v13_0/update_level_in_bom.py | 31 --------------- 9 files changed, 51 insertions(+), 70 deletions(-) delete mode 100644 erpnext/patches/v13_0/update_level_in_bom.py diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json index 218ac64d8da5..0b4419694002 100644 --- a/erpnext/manufacturing/doctype/bom/bom.json +++ b/erpnext/manufacturing/doctype/bom/bom.json @@ -37,7 +37,6 @@ "inspection_required", "quality_inspection_template", "column_break_31", - "bom_level", "section_break_33", "items", "scrap_section", @@ -522,13 +521,6 @@ "fieldname": "column_break_31", "fieldtype": "Column Break" }, - { - "default": "0", - "fieldname": "bom_level", - "fieldtype": "Int", - "label": "BOM Level", - "read_only": 1 - }, { "fieldname": "section_break_33", "fieldtype": "Section Break", @@ -540,7 +532,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2021-11-18 13:04:16.271975", + "modified": "2022-01-30 21:27:54.727298", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM", @@ -577,5 +569,6 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 0b5207ef8598..b97dcab632f8 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -156,8 +156,6 @@ def validate(self): self.update_stock_qty() self.validate_scrap_items() self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate = False, save=False) - self.set_bom_level() - def get_context(self, context): context.parents = [{'name': 'boms', 'title': _('All BOMs') }] @@ -704,7 +702,6 @@ def validate_operations(self): if not d.batch_size or d.batch_size <= 0: d.batch_size = 1 - def validate_scrap_items(self): for item in self.scrap_items: msg = "" @@ -735,20 +732,6 @@ def get_tree_representation(self) -> BOMTree: """Get a complete tree representation preserving order of child items.""" return BOMTree(self.name) - def set_bom_level(self, update=False): - levels = [] - - self.bom_level = 0 - for row in self.items: - if row.bom_no: - levels.append(frappe.get_cached_value("BOM", row.bom_no, "bom_level") or 0) - - if levels: - self.bom_level = max(levels) + 1 - - if update: - self.db_set("bom_level", self.bom_level) - 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) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 106777b82efe..f06624fe92c7 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -560,9 +560,11 @@ def get_sub_assembly_items(self, manufacturing_type=None): get_sub_assembly_items(row.bom_no, bom_data, row.planned_qty) self.set_sub_assembly_items_based_on_level(row, bom_data, manufacturing_type) - def set_sub_assembly_items_based_on_level(self, row, bom_data, manufacturing_type=None): - bom_data = sorted(bom_data, key = lambda i: i.bom_level) + self.sub_assembly_items.sort(key= lambda d: d.bom_level, reverse=True) + for idx, row in enumerate(self.sub_assembly_items, start=1): + row.idx = idx + def set_sub_assembly_items_based_on_level(self, row, bom_data, manufacturing_type=None): for data in bom_data: data.qty = data.stock_qty data.production_plan_item = row.name @@ -1005,9 +1007,6 @@ def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, indent=0): for d in data: if d.expandable: parent_item_code = frappe.get_cached_value("BOM", bom_no, "item") - bom_level = (frappe.get_cached_value("BOM", d.value, "bom_level") - if d.value else 0) - stock_qty = (d.stock_qty / d.parent_bom_qty) * flt(to_produce_qty) bom_data.append(frappe._dict({ 'parent_item_code': parent_item_code, @@ -1018,7 +1017,7 @@ def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, indent=0): 'uom': d.stock_uom, 'bom_no': d.value, 'is_sub_contracted_item': d.is_sub_contracted_item, - 'bom_level': bom_level, + 'bom_level': indent, 'indent': indent, 'stock_qty': stock_qty })) diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 2febc1e23c0a..21a126b2a79e 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -347,6 +347,45 @@ def test_get_sales_order_with_variant(self): frappe.db.rollback() + def test_subassmebly_sorting(self): + """ Test subassembly sorting in case of multiple items with nested BOMs""" + from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom + + prefix = "_TestLevel_" + boms = { + "Assembly": { + "SubAssembly1": {"ChildPart1": {}, "ChildPart2": {},}, + "SubAssembly2": {"ChildPart3": {}}, + "SubAssembly3": {"SubSubAssy1": {"ChildPart4": {}}}, + "ChildPart5": {}, + "ChildPart6": {}, + "SubAssembly4": {"SubSubAssy2": {"ChildPart7": {}}}, + }, + "MegaDeepAssy": { + "SecretSubassy": {"SecretPart": {"VerySecret" : { "SuperSecret": {"Classified": {}}}},}, + # ^ assert that this is + # first item in subassy table + } + } + create_nested_bom(boms, prefix=prefix) + + items = [prefix + item_code for item_code in boms.keys()] + plan = create_production_plan(item_code=items[0], do_not_save=True) + plan.append("po_items", { + 'use_multi_level_bom': 1, + 'item_code': items[1], + 'bom_no': frappe.db.get_value('Item', items[1], 'default_bom'), + 'planned_qty': 1, + 'planned_start_date': now_datetime() + }) + plan.get_sub_assembly_items() + + bom_level_order = [d.bom_level for d in plan.sub_assembly_items] + self.assertEqual(bom_level_order, sorted(bom_level_order, reverse=True)) + # lowest most level of subassembly should be first + self.assertIn("SuperSecret", plan.sub_assembly_items[0].production_item) + + def create_production_plan(**args): args = frappe._dict(args) diff --git a/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json index 657ee35a852d..45ea26c3a8a4 100644 --- a/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json +++ b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json @@ -102,7 +102,6 @@ }, { "columns": 1, - "fetch_from": "bom_no.bom_level", "fieldname": "bom_level", "fieldtype": "Int", "in_list_view": 1, @@ -189,7 +188,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-06-28 20:10:56.296410", + "modified": "2022-01-30 21:31:10.527559", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Plan Sub Assembly Item", @@ -198,5 +197,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/manufacturing/report/bom_explorer/bom_explorer.py b/erpnext/manufacturing/report/bom_explorer/bom_explorer.py index 25de2e03797b..19a80ab4076e 100644 --- a/erpnext/manufacturing/report/bom_explorer/bom_explorer.py +++ b/erpnext/manufacturing/report/bom_explorer/bom_explorer.py @@ -26,8 +26,7 @@ def get_exploded_items(bom, data, indent=0, qty=1): 'item_code': item.item_code, 'item_name': item.item_name, 'indent': indent, - 'bom_level': (frappe.get_cached_value("BOM", item.bom_no, "bom_level") - if item.bom_no else ""), + 'bom_level': indent, 'bom': item.bom_no, 'qty': item.qty * qty, 'uom': item.uom, @@ -73,7 +72,7 @@ def get_columns(): }, { "label": "BOM Level", - "fieldtype": "Data", + "fieldtype": "Int", "fieldname": "bom_level", "width": 100 }, diff --git a/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py index 55b1a3f2f9ae..aaa231466fda 100644 --- a/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py +++ b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py @@ -48,7 +48,7 @@ def get_production_plan_item_details(filters, data, order_details): "qty": row.planned_qty, "document_type": "Work Order", "document_name": work_order or "", - "bom_level": frappe.get_cached_value("BOM", row.bom_no, "bom_level"), + "bom_level": 0, "produced_qty": order_details.get((work_order, row.item_code), {}).get("produced_qty", 0), "pending_qty": flt(row.planned_qty) - flt(order_details.get((work_order, row.item_code), {}).get("produced_qty", 0)) }) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index b5ee1a4ce8c6..bcc0c019bf1f 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -292,7 +292,6 @@ erpnext.patches.v13_0.set_training_event_attendance erpnext.patches.v13_0.rename_issue_status_hold_to_on_hold erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice erpnext.patches.v13_0.update_job_card_details -erpnext.patches.v13_0.update_level_in_bom #1234sswef erpnext.patches.v13_0.create_gst_payment_entry_fields #27-11-2021 erpnext.patches.v13_0.add_missing_fg_item_for_stock_entry erpnext.patches.v13_0.update_subscription_status_in_memberships diff --git a/erpnext/patches/v13_0/update_level_in_bom.py b/erpnext/patches/v13_0/update_level_in_bom.py deleted file mode 100644 index 499412ee270d..000000000000 --- a/erpnext/patches/v13_0/update_level_in_bom.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright (c) 2020, Frappe and Contributors -# License: GNU General Public License v3. See license.txt - - -import frappe - - -def execute(): - for document in ["bom", "bom_item", "bom_explosion_item"]: - frappe.reload_doc('manufacturing', 'doctype', document) - - frappe.db.sql(" update `tabBOM` set bom_level = 0 where docstatus = 1") - - bom_list = frappe.db.sql_list("""select name from `tabBOM` bom - where docstatus=1 and is_active=1 and not exists(select bom_no from `tabBOM Item` - where parent=bom.name and ifnull(bom_no, '')!='')""") - - count = 0 - while(count < len(bom_list)): - for parent_bom in get_parent_boms(bom_list[count]): - bom_doc = frappe.get_cached_doc("BOM", parent_bom) - bom_doc.set_bom_level(update=True) - bom_list.append(parent_bom) - count += 1 - -def get_parent_boms(bom_no): - return frappe.db.sql_list(""" - select distinct bom_item.parent from `tabBOM Item` bom_item - where bom_item.bom_no = %s and bom_item.docstatus=1 and bom_item.parenttype='BOM' - and exists(select bom.name from `tabBOM` bom where bom.name=bom_item.parent and bom.is_active=1) - """, bom_no)