From 8b4824fef5af1378f3061221c95be1ebcebe9025 Mon Sep 17 00:00:00 2001 From: Ayush Marhatta Date: Tue, 3 Jun 2025 17:32:28 +0545 Subject: [PATCH 01/49] fix: typo (cherry picked from commit a243abb5fd1bba6c257a489f079477fcbef6c83f) --- .../report/purchase_order_analysis/purchase_order_analysis.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.js b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.js index 54d1cdc8b93..2651023639e 100644 --- a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.js +++ b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.js @@ -10,7 +10,7 @@ frappe.query_reports["Purchase Order Analysis"] = { width: "80", options: "Company", reqd: 1, - default: frappe.defaults.get_user_default("company"), + default: frappe.defaults.get_user_default("Company"), }, { fieldname: "from_date", From b99f8fd0213d6b4f608da9498dbf5947f28f0a5f Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 3 Jun 2025 18:18:55 +0530 Subject: [PATCH 02/49] fix: zero division error in purchase receipt (cherry picked from commit 32229fb64600077d3013be46f99b980e43e3c02c) --- erpnext/stock/doctype/purchase_receipt/purchase_receipt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index c8c81c2a124..f38375f943f 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -1146,7 +1146,7 @@ def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate adjusted_amt = flt(adjusted_amt * flt(pr_doc.conversion_rate), item.precision("amount")) item.db_set("amount_difference_with_purchase_invoice", adjusted_amt, update_modified=False) - elif item.billed_amt > amount: + elif amount and item.billed_amt > amount: per_over_billed = (flt(item.billed_amt / amount, 2) * 100) - 100 if per_over_billed > over_billing_allowance: frappe.throw( From e5d06f8c863e610735e1fd5a057cface6ac55501 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 4 Jun 2025 12:51:10 +0530 Subject: [PATCH 03/49] fix: stock adjustment entry during reposting (backport #47878) (#47883) fix: stock adjustment entry during reposting (#47878) fix: stock adjustment entry (cherry picked from commit cbcd580daa3d2bc1779313887ed37d2c5aaaae49) Co-authored-by: rohitwaghchaure --- erpnext/stock/stock_ledger.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 91641f1e35e..c8d2238f490 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -908,8 +908,11 @@ class update_entries_after: if not sle.is_adjustment_entry: sle.stock_value_difference = stock_value_difference elif sle.is_adjustment_entry and not self.args.get("sle_id"): - sle.stock_value_difference = get_stock_value_difference( - sle.item_code, sle.warehouse, sle.posting_date, sle.posting_time, sle.voucher_no + sle.stock_value_difference = ( + get_stock_value_difference( + sle.item_code, sle.warehouse, sle.posting_date, sle.posting_time, sle.voucher_no + ) + * -1 ) sle.doctype = "Stock Ledger Entry" From 0314a39fabc58f2f397a396e74ef674355691e2b Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 4 Jun 2025 16:11:07 +0530 Subject: [PATCH 04/49] fix: pos permission error on strict permission (backport #47896) (#47897) fix: pos permission error on strict permission (#47896) (cherry picked from commit bb903a4bef8ff82840e6fef9388670a25848030a) Co-authored-by: Diptanil Saha --- erpnext/selling/page/point_of_sale/pos_controller.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index d86bf92177f..50bdc294987 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -591,6 +591,7 @@ erpnext.PointOfSale.Controller = class { ) { this.frm.doc.pos_profile = this.pos_profile; } + this.frm.doc.set_warehouse = this.settings.warehouse; if (!this.frm.doc.company) return; @@ -603,8 +604,6 @@ erpnext.PointOfSale.Controller = class { async on_cart_update(args) { frappe.dom.freeze(); - if (this.frm.doc.set_warehouse != this.settings.warehouse) - this.frm.doc.set_warehouse = this.settings.warehouse; let item_row = undefined; try { let { field, value, item } = args; @@ -658,6 +657,7 @@ erpnext.PointOfSale.Controller = class { } new_item["use_serial_batch_fields"] = 1; + new_item["warehouse"] = this.settings.warehouse; if (field === "serial_no") new_item["qty"] = value.split(`\n`).length || 0; item_row = this.frm.add_child("items", new_item); From e98ad4ce270cfb7984f18c9e5d6425f7e7f30fd3 Mon Sep 17 00:00:00 2001 From: Syed Mujeer Hashmi Date: Wed, 21 May 2025 18:56:46 +0530 Subject: [PATCH 05/49] fix: Project argument is passed correctly for MR creation (cherry picked from commit 9eab434ae8384485ee3ed51d0c505903fa226f55) --- erpnext/selling/doctype/sales_order/sales_order.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 0c4cd43ec36..516845a5c2c 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -1029,7 +1029,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex items: data, company: me.frm.doc.company, sales_order: me.frm.docname, - project: me.frm.project, + project: me.frm.doc.project, }, freeze: true, callback: function (r) { From 1b1550708d1e8e806d7ee237acf7984c32bee6a8 Mon Sep 17 00:00:00 2001 From: Diptanil Saha Date: Thu, 5 Jun 2025 14:08:37 +0530 Subject: [PATCH 06/49] fix: remove use sales invoice check (#47908) --- .../doctype/accounts_settings/accounts_settings.json | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index 2e59bd8461d..d54acb32d38 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -540,13 +540,6 @@ "fieldname": "column_break_xrnd", "fieldtype": "Column Break" }, - { - "default": "0", - "description": "If enabled, Sales Invoice will be generated instead of POS Invoice in POS Transactions for real-time update of G/L and Stock Ledger.", - "fieldname": "use_sales_invoice_in_pos", - "fieldtype": "Check", - "label": "Use Sales Invoice" - }, { "default": "Buffered Cursor", "fieldname": "receivable_payable_fetch_method", @@ -599,4 +592,4 @@ "sort_order": "ASC", "states": [], "track_changes": 1 -} \ No newline at end of file +} From 76c2477d2332142bff7403e997400673fd3abedc Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 5 Jun 2025 12:42:16 +0530 Subject: [PATCH 07/49] feat: Add hook to update gl dict by apps (cherry picked from commit 10ff369ff23f9c65d7d8d508186d50481ccf19ea) --- erpnext/controllers/accounts_controller.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 237f19274e3..d38fbb0a0db 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1151,6 +1151,8 @@ class AccountsController(TransactionBase): with temporary_flag("company", self.company): update_gl_dict_with_regional_fields(self, gl_dict) + update_gl_dict_with_app_based_fields(self, gl_dict) + accounting_dimensions = get_accounting_dimensions() dimension_dict = frappe._dict() @@ -3937,3 +3939,7 @@ def validate_einvoice_fields(doc): @erpnext.allow_regional def update_gl_dict_with_regional_fields(doc, gl_dict): pass + +def update_gl_dict_with_app_based_fields(doc, gl_dict): + for method in frappe.get_hooks("update_gl_dict_with_app_based_fields", default=[]): + frappe.get_attr(method)(doc, gl_dict) \ No newline at end of file From d3202068d90d49ab64c4e323c37de7a71a46a612 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 5 Jun 2025 12:48:28 +0530 Subject: [PATCH 08/49] style: Linting issues (cherry picked from commit c4aecb15ce450df5de60920ee586243b2389d4ce) --- erpnext/controllers/accounts_controller.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index d38fbb0a0db..c0815c89439 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -3940,6 +3940,7 @@ def validate_einvoice_fields(doc): def update_gl_dict_with_regional_fields(doc, gl_dict): pass + def update_gl_dict_with_app_based_fields(doc, gl_dict): for method in frappe.get_hooks("update_gl_dict_with_app_based_fields", default=[]): - frappe.get_attr(method)(doc, gl_dict) \ No newline at end of file + frappe.get_attr(method)(doc, gl_dict) From 996fb7552ade1b192ebfcec5fe353d67cdad3803 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 5 Jun 2025 17:18:30 +0530 Subject: [PATCH 09/49] fix: key-error for COGS By Item Group report (backport #47914) (#47915) fix: key-error for COGS By Item Group report (#47914) fix: keyerror for COGS By Item Group report (cherry picked from commit 997ce4eaa7f6ea5c09c07f1b0f40491dad0361ca) Co-authored-by: rohitwaghchaure --- .../stock/report/cogs_by_item_group/cogs_by_item_group.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.py b/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.py index 07119411304..000aca9f43e 100644 --- a/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.py +++ b/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.py @@ -159,10 +159,11 @@ def assign_item_groups_to_svd_list(svd_list: SVDList) -> None: def get_item_groups_map(svd_list: SVDList) -> dict[str, str]: item_codes = set(i["item_code"] for i in svd_list) - ig_list = frappe.get_list( - "Item", fields=["item_code", "item_group"], filters=[("item_code", "in", item_codes)] + return frappe._dict( + frappe.get_all( + "Item", fields=["name", "item_group"], filters=[("name", "in", item_codes)], as_list=True + ) ) - return {i["item_code"]: i["item_group"] for i in ig_list} def get_item_groups_dict() -> ItemGroupsDict: From 2e78e14c7e8e02247c307875345264e21236ba70 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 5 Jun 2025 17:35:23 +0530 Subject: [PATCH 10/49] fix: consider expired batches in the stock reco (backport #47909) (#47919) fix: consider expired batches in the stock reco (#47909) (cherry picked from commit 8fa34739458164a51f3927fb1acdcc924b574f92) Co-authored-by: rohitwaghchaure --- .../stock/doctype/stock_reconciliation/stock_reconciliation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 4ea683a904d..d958fecf9d2 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -286,6 +286,7 @@ class StockReconciliation(StockController): "warehouse": item.warehouse, "posting_date": self.posting_date, "posting_time": self.posting_time, + "for_stock_levels": True, "ignore_voucher_nos": [self.name], } ) From 6d2c14c75e638891ac90a7851db6d18366d46140 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 6 Jun 2025 09:51:23 +0530 Subject: [PATCH 11/49] fix: stock reco qty with inventory dimension (backport #47918) (#47922) fix: stock reco qty with inventory dimension (#47918) (cherry picked from commit 342cebc778ac02cf51870f24a13d667d5f1f9db3) Co-authored-by: rohitwaghchaure --- .../doctype/stock_reconciliation/stock_reconciliation.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index d958fecf9d2..a2fa6f3e410 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -888,6 +888,10 @@ class StockReconciliation(StockController): self.update_inventory_dimensions(row, data) + if self.docstatus == 1 and has_dimensions and (not row.batch_no or not row.serial_and_batch_bundle): + data.qty_after_transaction = data.actual_qty + data.actual_qty = 0.0 + return data def make_sle_on_cancel(self): From 5a5449c60c68d58d8901952633920432fa1fe7bb Mon Sep 17 00:00:00 2001 From: Sagar Vora <16315650+sagarvora@users.noreply.github.com> Date: Fri, 6 Jun 2025 11:35:30 +0530 Subject: [PATCH 12/49] =?UTF-8?q?Revert=20"fix:=20calculate=20discount=20p?= =?UTF-8?q?ercentage=20if=20discount=20amount=20is=20specified=20(#?= =?UTF-8?q?=E2=80=A6"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit bb474f4f42aa6c5385a63df31c9d9af14238fad8. (cherry picked from commit 27dc0f5b7076f0dfdc19074f2d86c032e6bae83b) --- erpnext/controllers/taxes_and_totals.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index c47f04b71f1..5543129d323 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -13,7 +13,6 @@ from frappe.utils.deprecations import deprecated import erpnext from erpnext.accounts.doctype.journal_entry.journal_entry import get_exchange_rate from erpnext.accounts.doctype.pricing_rule.utils import get_applied_pricing_rules -from erpnext.accounts.utils import get_currency_precision from erpnext.controllers.accounts_controller import ( validate_conversion_rate, validate_inclusive_tax, @@ -675,16 +674,7 @@ class calculate_taxes_and_totals: tax.item_wise_tax_detail = json.dumps(tax.item_wise_tax_detail, separators=(",", ":")) def set_discount_amount(self): - if self.doc.discount_amount: - self.doc.additional_discount_percentage = flt( - flt( - self.doc.discount_amount / flt(self.doc.get(scrub(self.doc.apply_discount_on))), - get_currency_precision(), - ) - * 100, - self.doc.precision("additional_discount_percentage"), - ) - elif self.doc.additional_discount_percentage: + if self.doc.additional_discount_percentage: self.doc.discount_amount = flt( flt(self.doc.get(scrub(self.doc.apply_discount_on))) * self.doc.additional_discount_percentage From 129cd7ae8a6eac452d348c2d2e767b8e62bb08ff Mon Sep 17 00:00:00 2001 From: thomasantony12 Date: Fri, 6 Jun 2025 12:09:35 +0530 Subject: [PATCH 13/49] fix(sales order): error message on creation of work order from sales order --- erpnext/selling/doctype/sales_order/sales_order.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 516845a5c2c..4edf378a938 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -878,8 +878,8 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex fields: fields, primary_action: function () { var data = { items: d.fields_dict.items.grid.get_selected_children() }; - if (!data) { - frappe.throw(__("Please select items")); + if (!data.items.length) { + frappe.throw(__("Please select atleast one item to continue")); } me.frm.call({ method: "make_work_orders", From ef77791bd6e350d5213e0defb4cf75b47130be14 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 4 Jun 2025 13:08:45 +0530 Subject: [PATCH 14/49] fix: do not remove item which has zero qty and zero valuation (cherry picked from commit 86e4a658a5f4ecf6954d482949bec4febe77d69d) # Conflicts: # erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py --- .../stock_reconciliation/stock_reconciliation.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index a2fa6f3e410..86b24dc6449 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -163,8 +163,16 @@ class StockReconciliation(StockController): def set_current_serial_and_batch_bundle(self, voucher_detail_no=None, save=False) -> None: """Set Serial and Batch Bundle for each item""" for item in self.items: +<<<<<<< HEAD if not frappe.db.exists("Item", item.item_code): frappe.throw(_("Item {0} does not exist").format(item.item_code)) +======= + if voucher_detail_no and voucher_detail_no != item.name: + continue + + if not item.item_code: + continue +>>>>>>> 86e4a658a5 (fix: do not remove item which has zero qty and zero valuation) item_details = frappe.get_cached_value( "Item", item.item_code, ["has_serial_no", "has_batch_no"], as_dict=1 @@ -232,9 +240,6 @@ class StockReconciliation(StockController): if not save and item.use_serial_batch_fields: continue - if voucher_detail_no and voucher_detail_no != item.name: - continue - if not item.current_serial_and_batch_bundle: serial_and_batch_bundle = frappe.get_doc( { From ea393ef008ce9bb56ebfa3a095f0f3bfb968f400 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Fri, 6 Jun 2025 15:38:10 +0530 Subject: [PATCH 15/49] chore: fix conflicts --- .../doctype/stock_reconciliation/stock_reconciliation.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 86b24dc6449..d3b17cd3ad8 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -163,16 +163,11 @@ class StockReconciliation(StockController): def set_current_serial_and_batch_bundle(self, voucher_detail_no=None, save=False) -> None: """Set Serial and Batch Bundle for each item""" for item in self.items: -<<<<<<< HEAD - if not frappe.db.exists("Item", item.item_code): - frappe.throw(_("Item {0} does not exist").format(item.item_code)) -======= if voucher_detail_no and voucher_detail_no != item.name: continue if not item.item_code: continue ->>>>>>> 86e4a658a5 (fix: do not remove item which has zero qty and zero valuation) item_details = frappe.get_cached_value( "Item", item.item_code, ["has_serial_no", "has_batch_no"], as_dict=1 From a5e555352008ca7c1c408d6cb46e7db18e3e83f3 Mon Sep 17 00:00:00 2001 From: RAVIBHARATHI P C <131471282+ravibharathi656@users.noreply.github.com> Date: Wed, 21 May 2025 17:57:34 +0530 Subject: [PATCH 16/49] fix(asset): make purchase date mandatory (cherry picked from commit e6f47be4b04ede683bf07e67cf29f9055c51a1e7) --- erpnext/assets/doctype/asset/asset.json | 4 ++-- erpnext/assets/doctype/asset/asset.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index 25897df22b5..8e75762a92d 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -205,8 +205,8 @@ "fieldname": "purchase_date", "fieldtype": "Date", "label": "Purchase Date", - "mandatory_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset", - "read_only_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset" + "read_only_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset", + "reqd": 1 }, { "fieldname": "disposal_date", diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index f182c526e38..616680d07ba 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -93,7 +93,7 @@ class Asset(AccountsController): opening_number_of_booked_depreciations: DF.Int policy_number: DF.Data | None purchase_amount: DF.Currency - purchase_date: DF.Date | None + purchase_date: DF.Date purchase_invoice: DF.Link | None purchase_invoice_item: DF.Data | None purchase_receipt: DF.Link | None From f490de928556d302fb345447cb146e868507476f Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 6 Jun 2025 21:00:49 +0530 Subject: [PATCH 17/49] perf: Batch GLE/SLE rename commits (backport #47950) (#47951) perf: Batch GLE/SLE rename commits (#47950) (cherry picked from commit bb693c0a4fcb4056528882710b959a61c96137c5) Co-authored-by: Ankush Menat --- erpnext/accounts/doctype/gl_entry/gl_entry.py | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index 2243420f13c..89b184e89d0 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -7,7 +7,7 @@ from frappe import _ from frappe.model.document import Document from frappe.model.meta import get_field_precision from frappe.model.naming import set_name_from_naming_options -from frappe.utils import flt, fmt_money, now +from frappe.utils import create_batch, flt, fmt_money, now import erpnext from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( @@ -451,12 +451,15 @@ def rename_gle_sle_docs(): def rename_temporarily_named_docs(doctype): """Rename temporarily named docs using autoname options""" docs_to_rename = frappe.get_all(doctype, {"to_rename": "1"}, order_by="creation", limit=50000) - for doc in docs_to_rename: - oldname = doc.name - set_name_from_naming_options(frappe.get_meta(doctype).autoname, doc) - newname = doc.name - frappe.db.sql( - f"UPDATE `tab{doctype}` SET name = %s, to_rename = 0, modified = %s where name = %s", - (newname, now(), oldname), - auto_commit=True, - ) + autoname = frappe.get_meta(doctype).autoname + + for batch in create_batch(docs_to_rename, 100): + for doc in batch: + oldname = doc.name + set_name_from_naming_options(autoname, doc) + newname = doc.name + frappe.db.sql( + f"UPDATE `tab{doctype}` SET name = %s, to_rename = 0, modified = %s where name = %s", + (newname, now(), oldname), + ) + frappe.db.commit() From d05b49b0f8ededce1da1f86fcf6c99db3da3b3db Mon Sep 17 00:00:00 2001 From: mahsem <137205921+mahsem@users.noreply.github.com> Date: Fri, 13 Dec 2024 19:46:31 +0100 Subject: [PATCH 18/49] fix: better description of tab name (#44697) (cherry picked from commit 6119d4384a526fccf5affbd03a0c8270762cade1) --- erpnext/setup/doctype/employee/employee.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/setup/doctype/employee/employee.json b/erpnext/setup/doctype/employee/employee.json index 22c1ce7927a..30de87e151a 100644 --- a/erpnext/setup/doctype/employee/employee.json +++ b/erpnext/setup/doctype/employee/employee.json @@ -507,7 +507,7 @@ "collapsible": 1, "fieldname": "personal_details", "fieldtype": "Tab Break", - "label": "Personal" + "label": "Personal Details" }, { "fieldname": "passport_number", From d21bfa219d75779a005fa93d81708b2064bfdd0b Mon Sep 17 00:00:00 2001 From: FATHIH MOHAMMED <99068504+FathihMohammed@users.noreply.github.com> Date: Sat, 17 May 2025 10:54:23 +0000 Subject: [PATCH 19/49] fix(report): include descendants when filtering by parent item group --- .../item_wise_sales_register/item_wise_sales_register.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py index af2c4e7e38b..db93c0ef325 100644 --- a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py +++ b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py @@ -6,6 +6,7 @@ import frappe from frappe import _ from frappe.model.meta import get_field_precision from frappe.utils import cstr, flt +from frappe.utils.nestedset import get_descendants_of from frappe.utils.xlsxutils import handle_html from pypika import Order @@ -375,7 +376,12 @@ def apply_conditions(query, si, sii, filters, additional_conditions=None): query = query.where(sii.item_code == filters.get("item_code")) if filters.get("item_group"): - query = query.where(sii.item_group == filters.get("item_group")) + if frappe.db.get_value("Item Group", filters.get("item_group"), "is_group"): + item_groups = get_descendants_of("Item Group", filters.get("item_group")) + item_groups.append(filters.get("item_group")) + query = query.where(sii.item_group.isin(item_groups)) + else: + query = query.where(sii.item_group == filters.get("item_group")) if filters.get("income_account"): query = query.where( From 617b0658b8ed962e63c902b10fca520784e169c4 Mon Sep 17 00:00:00 2001 From: l0gesh29 Date: Tue, 3 Jun 2025 13:28:01 +0530 Subject: [PATCH 20/49] fix: consider user permission while populating the data (cherry picked from commit 074dc6d7dd216544668eef75ca56fde2c767ed56) --- .../accounts_receivable.py | 62 +++++++++---------- 1 file changed, 29 insertions(+), 33 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 268d246ea8e..a79ca1bfcba 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -377,6 +377,8 @@ class ReceivablePayableReport: self.data.append(self.total_row_map.get("Total", {})) def append_row(self, row): + if row.voucher_no not in self.invoice_details.keys(): + return self.allocate_future_payments(row) self.set_invoice_details(row) self.set_party_details(row) @@ -449,16 +451,14 @@ class ReceivablePayableReport: self.invoice_details = frappe._dict() if self.account_type == "Receivable": # nosemgrep - si_list = frappe.db.sql( - """ - select name, due_date, po_no - from `tabSales Invoice` - where posting_date <= %s - and company = %s - and docstatus = 1 - """, - (self.filters.report_date, self.filters.company), - as_dict=1, + si_list = frappe.get_list( + "Sales Invoice", + filters={ + "posting_date": ("<=", self.filters.report_date), + "company": self.filters.company, + "docstatus": 1, + }, + fields=["name", "due_date", "po_no"], ) for d in si_list: self.invoice_details.setdefault(d.name, d) @@ -481,33 +481,29 @@ class ReceivablePayableReport: if self.account_type == "Payable": # nosemgrep - for pi in frappe.db.sql( - """ - select name, due_date, bill_no, bill_date - from `tabPurchase Invoice` - where - posting_date <= %s - and company = %s - and docstatus = 1 - """, - (self.filters.report_date, self.filters.company), - as_dict=1, - ): + invoices = frappe.get_list( + "Purchase Invoice", + filters={ + "posting_date": ("<=", self.filters.report_date), + "company": self.filters.company, + "docstatus": 1, + }, + fields=["name", "due_date", "bill_no", "bill_date"], + ) + + for pi in invoices: self.invoice_details.setdefault(pi.name, pi) # Invoices booked via Journal Entries # nosemgrep - journal_entries = frappe.db.sql( - """ - select name, due_date, bill_no, bill_date - from `tabJournal Entry` - where - posting_date <= %s - and company = %s - and docstatus = 1 - """, - (self.filters.report_date, self.filters.company), - as_dict=1, + journal_entries = frappe.get_list( + "Journal Entry", + filters={ + "posting_date": ("<=", self.filters.report_date), + "company": self.filters.company, + "docstatus": 1, + }, + fields=["name", "due_date", "bill_no", "bill_date"], ) for je in journal_entries: From a2cdd91a0d265421572556b37ec5fe8564f9a24f Mon Sep 17 00:00:00 2001 From: l0gesh29 Date: Tue, 3 Jun 2025 18:17:11 +0530 Subject: [PATCH 21/49] fix: add user permission while fetching ple (cherry picked from commit 1a4bb3092399b2f674f51762c0fdb0ac04d9f83c) --- .../accounts_receivable.py | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index a79ca1bfcba..8c0a2b708af 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -6,6 +6,7 @@ from collections import OrderedDict import frappe from frappe import _, qb, query_builder, scrub +from frappe.desk.reportview import build_match_conditions from frappe.query_builder import Criterion from frappe.query_builder.functions import Date, Substring, Sum from frappe.utils import cint, cstr, flt, getdate, nowdate @@ -126,7 +127,7 @@ class ReceivablePayableReport: self.build_data() def fetch_ple_in_buffered_cursor(self): - query, param = self.ple_query.walk() + query, param = self.ple_query self.ple_entries = frappe.db.sql(query, param, as_dict=True) for ple in self.ple_entries: @@ -140,7 +141,7 @@ class ReceivablePayableReport: def fetch_ple_in_unbuffered_cursor(self): self.ple_entries = [] - query, param = self.ple_query.walk() + query, param = self.ple_query with frappe.db.unbuffered_cursor(): for ple in frappe.db.sql(query, param, as_dict=True, as_iterator=True): self.init_voucher_balance(ple) # invoiced, paid, credit_note, outstanding @@ -377,8 +378,6 @@ class ReceivablePayableReport: self.data.append(self.total_row_map.get("Total", {})) def append_row(self, row): - if row.voucher_no not in self.invoice_details.keys(): - return self.allocate_future_payments(row) self.set_invoice_details(row) self.set_party_details(row) @@ -852,12 +851,18 @@ class ReceivablePayableReport: else: query = query.select(ple.remarks) - if self.filters.get("group_by_party"): - query = query.orderby(self.ple.party, self.ple.posting_date) - else: - query = query.orderby(self.ple.posting_date, self.ple.party) + query, param = query.walk() - self.ple_query = query + match_conditions = build_match_conditions("Payment Ledger Entry") + if match_conditions: + query += " AND " + match_conditions + + if self.filters.get("group_by_party"): + query += f" ORDER BY `{self.ple.party.name}`, `{self.ple.posting_date.name}`" + else: + query += f" ORDER BY `{self.ple.posting_date.name}`, `{self.ple.party.name}`" + + self.ple_query = (query, param) def get_sales_invoices_or_customers_based_on_sales_person(self): if self.filters.get("sales_person"): From 9a47c507c0187f173cb92c546b1bfa48dc981533 Mon Sep 17 00:00:00 2001 From: ravibharathi656 Date: Mon, 9 Jun 2025 17:22:45 +0530 Subject: [PATCH 22/49] feat: add validation for inter company transactions --- .../accounts_settings/accounts_settings.json | 36 ++++++++ .../accounts_settings/accounts_settings.py | 3 + erpnext/controllers/accounts_controller.py | 87 +++++++++++++++++++ 3 files changed, 126 insertions(+) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index d54acb32d38..292f722aff0 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -38,6 +38,11 @@ "show_taxes_as_table_in_print", "column_break_12", "show_payment_schedule_in_print", + "item_price_settings_section", + "maintain_same_internal_transaction_rate", + "column_break_feyo", + "maintain_same_rate_action", + "role_to_override_stop_action", "currency_exchange_section", "allow_stale", "column_break_yuug", @@ -556,6 +561,37 @@ "fieldname": "legacy_section", "fieldtype": "Section Break", "label": "Legacy Fields" + }, + { + "default": "0", + "fieldname": "maintain_same_internal_transaction_rate", + "fieldtype": "Check", + "label": "Maintain Same Rate Throughout Internal Transaction" + }, + { + "default": "Stop", + "depends_on": "maintain_same_internal_transaction_rate", + "fieldname": "maintain_same_rate_action", + "fieldtype": "Select", + "label": "Action if Same Rate is Not Maintained Throughout Internal Transaction", + "mandatory_depends_on": "maintain_same_internal_transaction_rate", + "options": "Stop\nWarn" + }, + { + "depends_on": "eval: doc.maintain_same_internal_transaction_rate && doc.maintain_same_rate_action == 'Stop'", + "fieldname": "role_to_override_stop_action", + "fieldtype": "Link", + "label": "Role Allowed to Override Stop Action", + "options": "Role" + }, + { + "fieldname": "item_price_settings_section", + "fieldtype": "Section Break", + "label": "Item Price Settings" + }, + { + "fieldname": "column_break_feyo", + "fieldtype": "Column Break" } ], "icon": "icon-cog", diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py index ad39350f1c0..8dd73491072 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py @@ -50,6 +50,8 @@ class AccountsSettings(Document): general_ledger_remarks_length: DF.Int ignore_account_closing_balance: DF.Check ignore_is_opening_check_for_reporting: DF.Check + maintain_same_internal_transaction_rate: DF.Check + maintain_same_rate_action: DF.Literal["Stop", "Warn"] make_payment_via_journal_entry: DF.Check merge_similar_account_heads: DF.Check over_billing_allowance: DF.Currency @@ -58,6 +60,7 @@ class AccountsSettings(Document): receivable_payable_remarks_length: DF.Int reconciliation_queue_size: DF.Int role_allowed_to_over_bill: DF.Link | None + role_to_override_stop_action: DF.Link | None round_row_wise_tax: DF.Check show_balance_in_coa: DF.Check show_inclusive_tax_in_print: DF.Check diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index c0815c89439..e33f8f8b64c 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -230,6 +230,8 @@ class AccountsController(TransactionBase): self.validate_party_accounts() self.validate_inter_company_reference() + # validate inter company transaction rate + self.validate_internal_transaction() self.disable_pricing_rule_on_internal_transfer() self.disable_tax_included_prices_for_internal_transfer() @@ -740,6 +742,91 @@ class AccountsController(TransactionBase): msg = f"At Row {row.idx}: The field {bold(label)} is mandatory for internal transfer" frappe.throw(_(msg), title=_("Internal Transfer Reference Missing")) + def validate_internal_transaction(self): + if not cint( + frappe.db.get_single_value("Accounts Settings", "maintain_same_internal_transaction_rate") + ): + return + + doctypes_list = ["Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"] + + if self.doctype in doctypes_list and ( + self.get("is_internal_customer") or self.get("is_internal_supplier") + ): + self.validate_internal_transaction_based_on_voucher_type() + + def validate_internal_transaction_based_on_voucher_type(self): + order = ["Sales Order", "Purchase Order"] + invoice = ["Sales Invoice", "Purchase Invoice"] + + if self.doctype in order and self.get("inter_company_order_reference"): + # Fetch the linked order + linked_doctype = "Sales Order" if self.doctype == "Purchase Order" else "Purchase Order" + self.validate_line_items( + linked_doctype, + "sales_order" if linked_doctype == "Sales Order" else "purchase_order", + "sales_order_item" if linked_doctype == "Sales Order" else "purchase_order_item", + ) + elif self.doctype in invoice and self.get("inter_company_invoice_reference"): + # Fetch the linked invoice + linked_doctype = "Sales Invoice" if self.doctype == "Purchase Invoice" else "Purchase Invoice" + self.validate_line_items( + linked_doctype, + "sales_invoice" if linked_doctype == "Sales Invoice" else "purchase_invoice", + "sales_invoice_item" if linked_doctype == "Sales Invoice" else "purchase_invoice_item", + ) + + def validate_line_items(self, ref_dt, ref_dn_field, ref_link_field): + action, role_allowed_to_override = frappe.get_cached_value( + "Accounts Settings", "None", ["maintain_same_rate_action", "role_to_override_stop_action"] + ) + + reference_names = [d.get(ref_link_field) for d in self.get("items") if d.get(ref_link_field)] + reference_details = self.get_reference_details(reference_names, ref_dt + " Item") + + stop_actions = [] + + for d in self.get("items"): + if d.get(ref_link_field): + ref_rate = reference_details.get(d.get(ref_link_field)) + if ref_rate is not None and abs(flt(d.rate - ref_rate, d.precision("rate"))) >= 0.01: + if action == "Stop": + user_roles = [ + r["role"] + for r in frappe.get_all( + "Has Role", filters={"parent": frappe.session.user}, fields=["role"] + ) + ] + if role_allowed_to_override not in user_roles: + stop_actions.append( + _("Row #{0}: Rate must be same as {1}: {2} ({3} / {4})").format( + d.idx, + ref_dt, + self.inter_company_invoice_reference + if d.parenttype in ("Sales Invoice", "Purchase Invoice") + else d.get(ref_dn_field), + d.rate, + ref_rate, + ) + ) + else: + frappe.msgprint( + _("Row #{0}: Rate must be same as {1}: {2} ({3} / {4})").format( + d.idx, + ref_dt, + self.inter_company_invoice_reference + if d.parenttype in ("Sales Invoice", "Purchase Invoice") + else d.get(ref_dn_field), + d.rate, + ref_rate, + ), + title=_("Warning"), + indicator="orange", + ) + + if stop_actions: + frappe.throw(stop_actions, as_list=True) + def disable_pricing_rule_on_internal_transfer(self): if not self.get("ignore_pricing_rule") and self.is_internal_transfer(): self.ignore_pricing_rule = 1 From d6796da4648c9efd20de39e5eb013ab05fe1b6ad Mon Sep 17 00:00:00 2001 From: ravibharathi656 Date: Mon, 9 Jun 2025 17:24:10 +0530 Subject: [PATCH 23/49] test: add unit test for inter company transaction rate validation --- .../sales_invoice/test_sales_invoice.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 5e9c799fb86..c5b907685e8 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -64,6 +64,28 @@ class TestSalesInvoice(FrappeTestCase): ) frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None) + @change_settings( + "Accounts Settings", + {"maintain_same_internal_transaction_rate": 1, "maintain_same_rate_action": "Stop"}, + ) + def test_invalid_rate_without_override(self): + from frappe import ValidationError + + from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_company_purchase_invoice + + # frappe.get_cached_doc("Company", "_Test Company") + si = create_sales_invoice( + customer="_Test Internal Customer 3", company="_Test Company", is_internal_customer=1, rate=100 + ) + frappe.get_cached_doc("Customer", "_Test Internal Customer 3") + pi = make_inter_company_purchase_invoice(si) + pi.items[0].rate = 120 + + with self.assertRaises(ValidationError) as e: + pi.insert() + pi.submit() + self.assertIn("Rate must be same", str(e.exception)) + def tearDown(self): frappe.db.rollback() @@ -4441,6 +4463,7 @@ def create_sales_invoice(**args): si.conversion_rate = args.conversion_rate or 1 si.naming_series = args.naming_series or "T-SINV-" si.cost_center = args.parent_cost_center + si.is_internal_customer = args.is_internal_customer or 0 bundle_id = None if si.update_stock and (args.get("batch_no") or args.get("serial_no")): @@ -4643,6 +4666,12 @@ def create_internal_parties(): allowed_to_interact_with="_Test Company with perpetual inventory", ) + create_internal_supplier( + supplier_name="_Test Internal Supplier 3", + represents_company="_Test Company", + allowed_to_interact_with="_Test Company", + ) + def create_internal_supplier(supplier_name, represents_company, allowed_to_interact_with): if not frappe.db.exists("Supplier", supplier_name): From 3aee14176c34048df87bceb6fb1c18d165f0389d Mon Sep 17 00:00:00 2001 From: ravibharathi656 Date: Mon, 9 Jun 2025 19:07:26 +0530 Subject: [PATCH 24/49] test: pass sales invoice name instead of doc --- erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index c5b907685e8..3ad08885b94 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -73,12 +73,10 @@ class TestSalesInvoice(FrappeTestCase): from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_company_purchase_invoice - # frappe.get_cached_doc("Company", "_Test Company") si = create_sales_invoice( customer="_Test Internal Customer 3", company="_Test Company", is_internal_customer=1, rate=100 ) - frappe.get_cached_doc("Customer", "_Test Internal Customer 3") - pi = make_inter_company_purchase_invoice(si) + pi = make_inter_company_purchase_invoice(si.name) pi.items[0].rate = 120 with self.assertRaises(ValidationError) as e: From f88e68230a00e246da7eae99af66eec048b1cc00 Mon Sep 17 00:00:00 2001 From: Diptanil Saha Date: Mon, 9 Jun 2025 19:23:33 +0530 Subject: [PATCH 25/49] fix: fetch correct item tax template on item rate update (#47973) --- erpnext/public/js/controllers/transaction.js | 27 ++++++++++---------- erpnext/stock/get_item_details.py | 22 ++++++++++------ 2 files changed, 27 insertions(+), 22 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 0f0d68b3d8b..74a814b20f0 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -44,23 +44,22 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe if (item.item_code && item.rate) { frappe.call({ - method: "frappe.client.get_value", + method: "erpnext.stock.get_item_details.get_item_tax_template", args: { - doctype: "Item Tax", - parent: "Item", - filters: { - parent: item.item_code, - minimum_net_rate: ["<=", item.rate], - maximum_net_rate: [">=", item.rate] - }, - fieldname: "item_tax_template" + args: { + item_code: item.item_code, + company: frm.doc.company, + base_net_rate: item.base_net_rate, + tax_category: frm.doc.tax_category, + item_tax_template: item.item_tax_template, + posting_date: frm.doc.posting_date, + bill_date: frm.doc.bill_date, + transaction_date: frm.doc.transaction_date, + } }, callback: function(r) { - const tax_rule = r.message; - - let matched_template = tax_rule ? tax_rule.item_tax_template : null; - - frappe.model.set_value(cdt, cdn, 'item_tax_template', matched_template); + const item_tax_template = r.message; + frappe.model.set_value(cdt, cdn, 'item_tax_template', item_tax_template); } }); } diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 46cd2f52c4d..879b4836ac9 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -662,13 +662,17 @@ def get_item_tax_info(company, tax_category, item_codes, item_rates=None, item_t return out -def get_item_tax_template(args, item, out): - """ - args = { - "tax_category": None - "item_tax_template": None - } - """ +@frappe.whitelist() +def get_item_tax_template(args, item=None, out=None): + if isinstance(args, str): + args = json.loads(args) + + if not item: + if not args.get("item_code"): + frappe.throw(_("Item/Item Code required to get Item Tax Template.")) + else: + item = frappe.get_cached_doc("Item", args.get("item_code")) + item_tax_template = None if item.taxes: item_tax_template = _get_item_tax_template(args, item.taxes, out) @@ -680,9 +684,11 @@ def get_item_tax_template(args, item, out): item_tax_template = _get_item_tax_template(args, item_group_doc.taxes, out) item_group = item_group_doc.parent_item_group - if args.get("child_doctype") and item_tax_template: + if out and args.get("child_doctype") and item_tax_template: out.update(get_fetch_values(args.get("child_doctype"), "item_tax_template", item_tax_template)) + return item_tax_template + def _get_item_tax_template(args, taxes, out=None, for_validate=False): if out is None: From 9167d2ef648d0bc04e82ae2bb4009c24a3466b82 Mon Sep 17 00:00:00 2001 From: Aayush Dalal Date: Mon, 9 Jun 2025 22:58:46 +0530 Subject: [PATCH 26/49] fix: throw permission error (#47976) Co-authored-by: Sagar Vora <16315650+sagarvora@users.noreply.github.com> (cherry picked from commit 8b6a8d0c4fbb52884f07dcdf86855eb2067a921d) # Conflicts: # erpnext/stock/utils.py --- .../doctype/bank_statement_import/bank_statement_import.py | 2 +- erpnext/stock/utils.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py index fd18819187e..aefaf8c12fe 100644 --- a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py +++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py @@ -277,7 +277,7 @@ def get_import_status(docname): @frappe.whitelist() def get_import_logs(docname: str): - frappe.has_permission("Bank Statement Import") + frappe.has_permission("Bank Statement Import", throw=True) return frappe.get_all( "Data Import Log", diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index f1055b114cf..9710321d075 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -109,6 +109,11 @@ def get_stock_balance( from erpnext.stock.stock_ledger import get_previous_sle +<<<<<<< HEAD +======= + frappe.has_permission("Item", "read", throw=True) + +>>>>>>> 8b6a8d0c4f (fix: throw permission error (#47976)) if posting_date is None: posting_date = nowdate() if posting_time is None: From 43d4e26ac54fa5ebc73008b6a8770916b9dbdb67 Mon Sep 17 00:00:00 2001 From: Khushi Rawat <142375893+khushi8112@users.noreply.github.com> Date: Mon, 9 Jun 2025 23:58:40 +0530 Subject: [PATCH 27/49] fix: AttributeError due to incorrect object (cherry picked from commit 351796bce645b4c15c799fcdefef31902ece1d7d) --- erpnext/assets/doctype/asset/depreciation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index 9b4ec6f67d8..d888e549cea 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -798,7 +798,7 @@ def get_value_after_depreciation_on_disposal_date(asset, disposal_date, finance_ idx = 1 if finance_book: - for d in asset.finance_books: + for d in asset_doc.finance_books: if d.finance_book == finance_book: idx = d.idx break From 66f41d44c4249efa4f259356fbef301d7d536faf Mon Sep 17 00:00:00 2001 From: l0gesh29 Date: Tue, 10 Jun 2025 04:45:31 +0530 Subject: [PATCH 28/49] fix: add .length in list validation (#47974) Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> (cherry picked from commit c8cec8cedffcee0cf161f6236ca9144da4d999e5) --- .../report/accounts_receivable/accounts_receivable.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.html b/erpnext/accounts/report/accounts_receivable/accounts_receivable.html index 9cae94ff8b4..680ccf69ac6 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.html +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.html @@ -185,7 +185,7 @@ {% if(!filters.show_future_payments) { %} - {% if(!(filters.party)) { %} + {% if(!filters.party?.length) { %} {%= data[i]["party"] %} {% if(data[i]["customer_name"] && data[i]["customer_name"] != data[i]["party"]) { %}
{%= data[i]["customer_name"] %} @@ -258,7 +258,7 @@ {% if(data[i]["party"]|| " ") { %} {% if(!data[i]["is_total_row"]) { %} - {% if(!(filters.party)) { %} + {% if(!filters.party?.length) { %} {%= data[i]["party"] %} {% if(data[i]["customer_name"] && data[i]["customer_name"] != data[i]["party"]) { %}
{%= data[i]["customer_name"] %} From 039c47e3f294d45083bca3f1ac4e759d15c72390 Mon Sep 17 00:00:00 2001 From: priyanshshah2442 Date: Fri, 6 Jun 2025 18:40:06 +0530 Subject: [PATCH 29/49] fix: patch to set discount percentange in case of mismatch (cherry picked from commit f7eda8a156df74053a0a773c50032db8de2eb67b) --- .../set_additional_discount_percentage.py | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 erpnext/patches/v15_0/set_additional_discount_percentage.py diff --git a/erpnext/patches/v15_0/set_additional_discount_percentage.py b/erpnext/patches/v15_0/set_additional_discount_percentage.py new file mode 100644 index 00000000000..2015bfd6c32 --- /dev/null +++ b/erpnext/patches/v15_0/set_additional_discount_percentage.py @@ -0,0 +1,56 @@ +import frappe +from frappe import scrub +from frappe.model.meta import get_field_precision +from frappe.utils import flt + +from erpnext.accounts.report.calculated_discount_mismatch.calculated_discount_mismatch import ( + DISCOUNT_DOCTYPES, + LAST_MODIFIED_DATE_THRESHOLD, +) + + +def execute(): + for doctype in DISCOUNT_DOCTYPES: + documents = frappe.get_all( + doctype, + { + "docstatus": 0, + "modified": [">", LAST_MODIFIED_DATE_THRESHOLD], + "discount_amount": ["is", "set"], + }, + [ + "name", + "additional_discount_percentage", + "discount_amount", + "apply_discount_on", + "grand_total", + "net_total", + ], + ) + + if not documents: + continue + + precision = get_field_precision(frappe.get_meta(doctype).get_field("additional_discount_percentage")) + mismatched_documents = [] + + for doc in documents: + discount_applied_on = scrub(doc.apply_discount_on) + + calculated_discount_amount = flt( + doc.additional_discount_percentage * doc.get(discount_applied_on) / 100, + precision, + ) + + if calculated_discount_amount != doc.discount_amount: + mismatched_documents.append(doc.name) + + if mismatched_documents: + frappe.db.set_value( + doctype, + { + "name": ["in", mismatched_documents], + }, + "additional_discount_percentage", + 0, + ) From b3eb49d39d3b3051f30b1969307d1924a08ff78f Mon Sep 17 00:00:00 2001 From: priyanshshah2442 Date: Mon, 9 Jun 2025 11:13:59 +0530 Subject: [PATCH 30/49] feat: report to verify discount amount mismatch (cherry picked from commit 62dd6df24f4ed8e48b5cd8493c8b6082bbe12501) --- .../calculated_discount_mismatch/__init__.py | 0 .../calculated_discount_mismatch.js | 13 +++ .../calculated_discount_mismatch.json | 38 ++++++++ .../calculated_discount_mismatch.py | 86 +++++++++++++++++++ erpnext/patches.txt | 1 + 5 files changed, 138 insertions(+) create mode 100644 erpnext/accounts/report/calculated_discount_mismatch/__init__.py create mode 100644 erpnext/accounts/report/calculated_discount_mismatch/calculated_discount_mismatch.js create mode 100644 erpnext/accounts/report/calculated_discount_mismatch/calculated_discount_mismatch.json create mode 100644 erpnext/accounts/report/calculated_discount_mismatch/calculated_discount_mismatch.py diff --git a/erpnext/accounts/report/calculated_discount_mismatch/__init__.py b/erpnext/accounts/report/calculated_discount_mismatch/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/accounts/report/calculated_discount_mismatch/calculated_discount_mismatch.js b/erpnext/accounts/report/calculated_discount_mismatch/calculated_discount_mismatch.js new file mode 100644 index 00000000000..b17e1e335c3 --- /dev/null +++ b/erpnext/accounts/report/calculated_discount_mismatch/calculated_discount_mismatch.js @@ -0,0 +1,13 @@ +// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.query_reports["Calculated Discount Mismatch"] = { + filters: [ + // { + // "fieldname": "my_filter", + // "label": __("My Filter"), + // "fieldtype": "Data", + // "reqd": 1, + // }, + ], +}; diff --git a/erpnext/accounts/report/calculated_discount_mismatch/calculated_discount_mismatch.json b/erpnext/accounts/report/calculated_discount_mismatch/calculated_discount_mismatch.json new file mode 100644 index 00000000000..84f081303e0 --- /dev/null +++ b/erpnext/accounts/report/calculated_discount_mismatch/calculated_discount_mismatch.json @@ -0,0 +1,38 @@ +{ + "add_total_row": 0, + "add_translate_data": 0, + "columns": [], + "creation": "2025-06-06 17:09:50.681090", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "letter_head": "", + "letterhead": null, + "modified": "2025-06-06 18:09:18.221911", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Calculated Discount Mismatch", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Version", + "report_name": "Calculated Discount Mismatch", + "report_type": "Script Report", + "roles": [ + { + "role": "System Manager" + }, + { + "role": "Administrator" + }, + { + "role": "Accounts Manager" + }, + { + "role": "Accounts User" + } + ], + "timeout": 0 +} diff --git a/erpnext/accounts/report/calculated_discount_mismatch/calculated_discount_mismatch.py b/erpnext/accounts/report/calculated_discount_mismatch/calculated_discount_mismatch.py new file mode 100644 index 00000000000..a51a3c57071 --- /dev/null +++ b/erpnext/accounts/report/calculated_discount_mismatch/calculated_discount_mismatch.py @@ -0,0 +1,86 @@ +# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ + +DISCOUNT_DOCTYPES = frozenset( + ( + "POS Invoice", + "Purchase Invoice", + "Sales Invoice", + "Purchase Order", + "Supplier Quotation", + "Quotation", + "Sales Order", + "Delivery Note", + "Purchase Receipt", + ) +) +LAST_MODIFIED_DATE_THRESHOLD = "2025-05-30" + + +def execute(filters: dict | None = None): + """Return columns and data for the report. + + This is the main entry point for the report. It accepts the filters as a + dictionary and should return columns and data. It is called by the framework + every time the report is refreshed or a filter is updated. + """ + columns = get_columns() + data = get_data() + + return columns, data + + +def get_columns() -> list[dict]: + """Return columns for the report. + + One field definition per column, just like a DocType field definition. + """ + return [ + { + "label": _("Doctype"), + "fieldname": "doctype", + "fieldtype": "Data", + "width": 150, + }, + { + "label": _("Document Name"), + "fieldname": "document_name", + "fieldtype": "Dynamic Link", + "options": "doctype", + "width": 200, + }, + ] + + +def get_data() -> list[list]: + """Return data for the report. + + The report data is a list of rows, with each row being a list of cell values. + """ + data = [] + VERSION = frappe.qb.DocType("Version") + + result = ( + frappe.qb.from_(VERSION) + .select(VERSION.ref_doctype, VERSION.docname, VERSION.data, VERSION.name) + .where(VERSION.modified > LAST_MODIFIED_DATE_THRESHOLD) + .where(VERSION.ref_doctype.isin(list(DISCOUNT_DOCTYPES))) + .run(as_dict=True) + ) + + for row in result: + changed_data = {entry[0]: entry for entry in frappe.parse_json(row.data).get("changed", [])} + + docstatus = changed_data.get("docstatus") + if not docstatus or docstatus[2] != 1: + continue + + if "discount_amount" not in changed_data: + continue + + data.append({"doctype": row.ref_doctype, "document_name": row.docname}) + + return data diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 57e5465f8d0..edf91c225d7 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -408,3 +408,4 @@ erpnext.patches.v15_0.set_cancelled_status_to_cancelled_pos_invoice erpnext.patches.v15_0.rename_group_by_to_categorize_by_in_custom_reports erpnext.patches.v14_0.update_full_name_in_contract erpnext.patches.v15_0.drop_sle_indexes +erpnext.patches.v15_0.set_additional_discount_percentage \ No newline at end of file From 5237ff8d945644ae32f52e21da076a7d01b5621a Mon Sep 17 00:00:00 2001 From: Sagar Vora <16315650+sagarvora@users.noreply.github.com> Date: Mon, 9 Jun 2025 13:52:19 +0530 Subject: [PATCH 31/49] fix: changes to report and patch (cherry picked from commit daad6137f832c628de54e28223d47f5dd66d4456) --- .../calculated_discount_mismatch.js | 20 +-- .../calculated_discount_mismatch.py | 168 ++++++++++++++---- erpnext/patches.txt | 2 +- .../set_additional_discount_percentage.py | 56 ------ ...ncorrect_additional_discount_percentage.py | 87 +++++++++ 5 files changed, 229 insertions(+), 104 deletions(-) delete mode 100644 erpnext/patches/v15_0/set_additional_discount_percentage.py create mode 100644 erpnext/patches/v15_0/unset_incorrect_additional_discount_percentage.py diff --git a/erpnext/accounts/report/calculated_discount_mismatch/calculated_discount_mismatch.js b/erpnext/accounts/report/calculated_discount_mismatch/calculated_discount_mismatch.js index b17e1e335c3..21d88d2e546 100644 --- a/erpnext/accounts/report/calculated_discount_mismatch/calculated_discount_mismatch.js +++ b/erpnext/accounts/report/calculated_discount_mismatch/calculated_discount_mismatch.js @@ -1,13 +1,13 @@ // Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.query_reports["Calculated Discount Mismatch"] = { - filters: [ - // { - // "fieldname": "my_filter", - // "label": __("My Filter"), - // "fieldtype": "Data", - // "reqd": 1, - // }, - ], -}; +// frappe.query_reports["Calculated Discount Mismatch"] = { +// filters: [ +// { +// "fieldname": "my_filter", +// "label": __("My Filter"), +// "fieldtype": "Data", +// "reqd": 1, +// }, +// ], +// }; diff --git a/erpnext/accounts/report/calculated_discount_mismatch/calculated_discount_mismatch.py b/erpnext/accounts/report/calculated_discount_mismatch/calculated_discount_mismatch.py index a51a3c57071..30d13c87afc 100644 --- a/erpnext/accounts/report/calculated_discount_mismatch/calculated_discount_mismatch.py +++ b/erpnext/accounts/report/calculated_discount_mismatch/calculated_discount_mismatch.py @@ -1,10 +1,14 @@ # Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt +import json + import frappe from frappe import _ +from frappe.query_builder import Order, Tuple +from frappe.utils import flt -DISCOUNT_DOCTYPES = frozenset( +AFFECTED_DOCTYPES = frozenset( ( "POS Invoice", "Purchase Invoice", @@ -20,67 +24,157 @@ DISCOUNT_DOCTYPES = frozenset( LAST_MODIFIED_DATE_THRESHOLD = "2025-05-30" -def execute(filters: dict | None = None): - """Return columns and data for the report. - - This is the main entry point for the report. It accepts the filters as a - dictionary and should return columns and data. It is called by the framework - every time the report is refreshed or a filter is updated. - """ +def execute(): columns = get_columns() data = get_data() return columns, data -def get_columns() -> list[dict]: - """Return columns for the report. - - One field definition per column, just like a DocType field definition. - """ +def get_columns(): return [ { - "label": _("Doctype"), "fieldname": "doctype", - "fieldtype": "Data", + "label": _("Transaction Type"), + "fieldtype": "Link", + "options": "DocType", + "width": 120, + }, + { + "fieldname": "docname", + "label": _("Transaction Name"), + "fieldtype": "Dynamic Link", + "options": "doctype", "width": 150, }, { - "label": _("Document Name"), - "fieldname": "document_name", - "fieldtype": "Dynamic Link", - "options": "doctype", - "width": 200, + "fieldname": "currency", + "label": _("Currency"), + "fieldtype": "Link", + "options": "Currency", + }, + { + "fieldname": "actual_discount_percentage", + "label": _("Discount Percentage in Transaction"), + "fieldtype": "Percent", + "width": 180, + }, + { + "fieldname": "actual_discount_amount", + "label": _("Discount Amount in Transaction"), + "fieldtype": "Currency", + "options": "currency", + "width": 180, + }, + { + "fieldname": "suspected_discount_amount", + "label": _("Suspected Discount Amount"), + "fieldtype": "Currency", + "options": "currency", + "width": 180, + }, + { + "fieldname": "difference", + "label": _("Difference"), + "fieldtype": "Currency", + "options": "currency", + "width": 180, }, ] -def get_data() -> list[list]: - """Return data for the report. +def get_data(): + transactions_with_discount_percentage = {} + + for doctype in AFFECTED_DOCTYPES: + transactions = get_transactions_with_discount_percentage(doctype) + + for transaction in transactions: + transactions_with_discount_percentage[(doctype, transaction.name)] = transaction + + if not transactions_with_discount_percentage: + return [] - The report data is a list of rows, with each row being a list of cell values. - """ - data = [] VERSION = frappe.qb.DocType("Version") - result = ( + versions = ( frappe.qb.from_(VERSION) - .select(VERSION.ref_doctype, VERSION.docname, VERSION.data, VERSION.name) - .where(VERSION.modified > LAST_MODIFIED_DATE_THRESHOLD) - .where(VERSION.ref_doctype.isin(list(DISCOUNT_DOCTYPES))) + .select(VERSION.ref_doctype, VERSION.docname, VERSION.data) + .where(VERSION.creation > LAST_MODIFIED_DATE_THRESHOLD) + .where(Tuple(VERSION.ref_doctype, VERSION.docname).isin(list(transactions_with_discount_percentage))) + .where( + VERSION.data.like('%"discount\\_amount"%') + | VERSION.data.like('%"additional\\_discount\\_percentage"%') + ) + .orderby(VERSION.creation, order=Order.desc) .run(as_dict=True) ) - for row in result: - changed_data = {entry[0]: entry for entry in frappe.parse_json(row.data).get("changed", [])} + if not versions: + return [] - docstatus = changed_data.get("docstatus") - if not docstatus or docstatus[2] != 1: - continue + version_map = {} + for version in versions: + key = (version.ref_doctype, version.docname) + if key not in version_map: + version_map[key] = [] - if "discount_amount" not in changed_data: - continue + version_map[key].append(version.data) - data.append({"doctype": row.ref_doctype, "document_name": row.docname}) + data = [] + for doc, versions in version_map.items(): + for version_data in versions: + if '"additional_discount_percentage"' in version_data: + # don't consider doc if additional_discount_percentage is changed in newest version + break + + version_data = json.loads(version_data) + changed_values = version_data.get("changed") + if not changed_values: + continue + + discount_values = next((row for row in changed_values if row[0] == "discount_amount"), None) + if not discount_values: + continue + + old = discount_values[1] + new = discount_values[2] + doc_values = transactions_with_discount_percentage.get(doc) + if new != doc_values.discount_amount: + # if the discount amount in the version is not equal to the current value, skip + break + + data.append( + { + "doctype": doc[0], + "docname": doc[1], + "currency": doc_values.currency, + "actual_discount_percentage": doc_values.additional_discount_percentage, + "actual_discount_amount": new, + "suspected_discount_amount": old, + "difference": flt(old - new, 9), + } + ) + break return data + + +def get_transactions_with_discount_percentage(doctype): + transactions = frappe.get_all( + doctype, + fields=[ + "name", + "currency", + "additional_discount_percentage", + "discount_amount", + ], + filters={ + "docstatus": 1, + "additional_discount_percentage": [">", 0], + "discount_amount": ["!=", 0], + "modified": [">", LAST_MODIFIED_DATE_THRESHOLD], + }, + ) + + return transactions diff --git a/erpnext/patches.txt b/erpnext/patches.txt index edf91c225d7..ca3e61f5d26 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -262,6 +262,7 @@ erpnext.patches.v14_0.clear_reconciliation_values_from_singles execute:frappe.rename_doc("Report", "TDS Payable Monthly", "Tax Withholding Details", force=True) erpnext.patches.v14_0.update_proprietorship_to_individual erpnext.patches.v15_0.rename_subcontracting_fields +erpnext.patches.v15_0.unset_incorrect_additional_discount_percentage [post_model_sync] erpnext.patches.v15_0.create_asset_depreciation_schedules_from_assets @@ -408,4 +409,3 @@ erpnext.patches.v15_0.set_cancelled_status_to_cancelled_pos_invoice erpnext.patches.v15_0.rename_group_by_to_categorize_by_in_custom_reports erpnext.patches.v14_0.update_full_name_in_contract erpnext.patches.v15_0.drop_sle_indexes -erpnext.patches.v15_0.set_additional_discount_percentage \ No newline at end of file diff --git a/erpnext/patches/v15_0/set_additional_discount_percentage.py b/erpnext/patches/v15_0/set_additional_discount_percentage.py deleted file mode 100644 index 2015bfd6c32..00000000000 --- a/erpnext/patches/v15_0/set_additional_discount_percentage.py +++ /dev/null @@ -1,56 +0,0 @@ -import frappe -from frappe import scrub -from frappe.model.meta import get_field_precision -from frappe.utils import flt - -from erpnext.accounts.report.calculated_discount_mismatch.calculated_discount_mismatch import ( - DISCOUNT_DOCTYPES, - LAST_MODIFIED_DATE_THRESHOLD, -) - - -def execute(): - for doctype in DISCOUNT_DOCTYPES: - documents = frappe.get_all( - doctype, - { - "docstatus": 0, - "modified": [">", LAST_MODIFIED_DATE_THRESHOLD], - "discount_amount": ["is", "set"], - }, - [ - "name", - "additional_discount_percentage", - "discount_amount", - "apply_discount_on", - "grand_total", - "net_total", - ], - ) - - if not documents: - continue - - precision = get_field_precision(frappe.get_meta(doctype).get_field("additional_discount_percentage")) - mismatched_documents = [] - - for doc in documents: - discount_applied_on = scrub(doc.apply_discount_on) - - calculated_discount_amount = flt( - doc.additional_discount_percentage * doc.get(discount_applied_on) / 100, - precision, - ) - - if calculated_discount_amount != doc.discount_amount: - mismatched_documents.append(doc.name) - - if mismatched_documents: - frappe.db.set_value( - doctype, - { - "name": ["in", mismatched_documents], - }, - "additional_discount_percentage", - 0, - ) diff --git a/erpnext/patches/v15_0/unset_incorrect_additional_discount_percentage.py b/erpnext/patches/v15_0/unset_incorrect_additional_discount_percentage.py new file mode 100644 index 00000000000..40be90f1396 --- /dev/null +++ b/erpnext/patches/v15_0/unset_incorrect_additional_discount_percentage.py @@ -0,0 +1,87 @@ +import frappe +from frappe import scrub +from frappe.model.meta import get_field_precision +from frappe.utils import flt +from semantic_version import Version + +from erpnext.accounts.report.calculated_discount_mismatch.calculated_discount_mismatch import ( + AFFECTED_DOCTYPES, + LAST_MODIFIED_DATE_THRESHOLD, +) + + +def execute(): + # run this patch only if erpnext version before update is v15.64.0 or higher + version, git_branch = frappe.db.get_value( + "Installed Application", + {"app_name": "erpnext"}, + ["app_version", "git_branch"], + ) + + semantic_version = get_semantic_version(version) + if semantic_version and ( + semantic_version.major < 15 or (git_branch == "version-15" and semantic_version.minor < 64) + ): + return + + for doctype in AFFECTED_DOCTYPES: + meta = frappe.get_meta(doctype) + filters = { + "modified": [">", LAST_MODIFIED_DATE_THRESHOLD], + "additional_discount_percentage": [">", 0], + "discount_amount": ["!=", 0], + } + + # can't reverse calculate grand_total if shipping rule is set + if meta.has_field("shipping_rule"): + filters["shipping_rule"] = ["is", "not set"] + + documents = frappe.get_all( + doctype, + fields=[ + "name", + "additional_discount_percentage", + "discount_amount", + "apply_discount_on", + "grand_total", + "net_total", + ], + filters=filters, + ) + + if not documents: + continue + + precision = get_field_precision(frappe.get_meta(doctype).get_field("additional_discount_percentage")) + mismatched_documents = [] + + for doc in documents: + # we need grand_total before applying discount + doc.grand_total += doc.discount_amount + discount_applied_on = scrub(doc.apply_discount_on) + calculated_discount_amount = flt( + doc.additional_discount_percentage * doc.get(discount_applied_on) / 100, + precision, + ) + + # if difference is more than 0.02 (based on precision), unset the additional discount percentage + if abs(calculated_discount_amount - doc.discount_amount) > 2 / (10**precision): + mismatched_documents.append(doc.name) + + if mismatched_documents: + # changing the discount percentage has no accounting effect + # so we can safely set it to 0 in the database + frappe.db.set_value( + doctype, + {"name": ["in", mismatched_documents]}, + "additional_discount_percentage", + 0, + update_modified=False, + ) + + +def get_semantic_version(version): + try: + return Version(version) + except Exception: + pass From d24c2c4ccafec82c185af05eced2daac00da9a2f Mon Sep 17 00:00:00 2001 From: priyanshshah2442 Date: Mon, 9 Jun 2025 17:49:40 +0530 Subject: [PATCH 32/49] fix: ensure proper float conversion for discount values (cherry picked from commit 3dcb801a3791a67cf8f38f84700472457a5835e3) --- .../calculated_discount_mismatch.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/report/calculated_discount_mismatch/calculated_discount_mismatch.py b/erpnext/accounts/report/calculated_discount_mismatch/calculated_discount_mismatch.py index 30d13c87afc..e3cba25e8f4 100644 --- a/erpnext/accounts/report/calculated_discount_mismatch/calculated_discount_mismatch.py +++ b/erpnext/accounts/report/calculated_discount_mismatch/calculated_discount_mismatch.py @@ -24,7 +24,7 @@ AFFECTED_DOCTYPES = frozenset( LAST_MODIFIED_DATE_THRESHOLD = "2025-05-30" -def execute(): +def execute(filters=None): columns = get_columns() data = get_data() @@ -137,8 +137,8 @@ def get_data(): if not discount_values: continue - old = discount_values[1] - new = discount_values[2] + old = flt(discount_values[1][2:]) + new = flt(discount_values[2][2:]) doc_values = transactions_with_discount_percentage.get(doc) if new != doc_values.discount_amount: # if the discount amount in the version is not equal to the current value, skip From f27e591d88c19c0fe7e7a922b5d4e19d43986ece Mon Sep 17 00:00:00 2001 From: priyanshshah2442 Date: Mon, 9 Jun 2025 18:06:16 +0530 Subject: [PATCH 33/49] fix: add change log for bug fix in Additional Discount functionality (cherry picked from commit 9120927a650aa93eaa43d2d753363a67725d0a27) --- erpnext/change_log/v15/v15_64_0.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 erpnext/change_log/v15/v15_64_0.md diff --git a/erpnext/change_log/v15/v15_64_0.md b/erpnext/change_log/v15/v15_64_0.md new file mode 100644 index 00000000000..67f50707203 --- /dev/null +++ b/erpnext/change_log/v15/v15_64_0.md @@ -0,0 +1,11 @@ +There was a bug in the **Additional Discount** functionality of ERPNext in **v15.64.0**. This has since been fixed. + +**If you've updated from a version older than v15.64.0, no action is needed on your side.** + +If you're updating from v15.64.0, the **Additional Discount Amount** in some transactions may differ from the value you entered. This only affects cases where **Additional Discount Amount** is manually entered. If it is computed from **Additional Discount Percentage** entered by you, there shouldn't be any issue. + +This report can help identify such transactions: [Calculated Discount Mismatch](/app/query-report/Calculated%20Discount%20Mismatch) + +Please review and amend these as necessary. + +We apologize for the inconvenience caused. \ No newline at end of file From 06ea957ae5e398a58237c77e63b5108e97acef9c Mon Sep 17 00:00:00 2001 From: priyanshshah2442 Date: Mon, 9 Jun 2025 18:43:58 +0530 Subject: [PATCH 34/49] fix: test case to verify correct setting of discount amount and percentage (cherry picked from commit 3f0c5be5d9d5efde5fd9086791c503431dc559a9) --- .../doctype/purchase_invoice/test_purchase_invoice.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 055f5ca9676..99eb1de2cf8 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -2806,6 +2806,17 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): # Test 4 - Since this PI is overbilled by 130% and only 120% is allowed, it will fail self.assertRaises(frappe.ValidationError, pi.submit) + def test_discount_percentage_not_set_when_amount_is_manually_set(self): + pi = make_purchase_invoice(do_not_save=True) + discount_amount = 7 + pi.discount_amount = discount_amount + pi.save() + self.assertEqual(pi.additional_discount_percentage, None) + pi.set_posting_time = 1 + pi.posting_date = add_days(today(), -1) + pi.save() + self.assertEqual(pi.discount_amount, discount_amount) + def set_advance_flag(company, flag, default_account): frappe.db.set_value( From 78c63869e0043a5b76338ef810d4e3cd659d1b40 Mon Sep 17 00:00:00 2001 From: Sagar Vora <16315650+sagarvora@users.noreply.github.com> Date: Mon, 9 Jun 2025 19:13:03 +0530 Subject: [PATCH 35/49] fix: changes in report (cherry picked from commit 33e793354cf8220b8462a0bce8efee43941882c1) --- .../calculated_discount_mismatch.py | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/erpnext/accounts/report/calculated_discount_mismatch/calculated_discount_mismatch.py b/erpnext/accounts/report/calculated_discount_mismatch/calculated_discount_mismatch.py index e3cba25e8f4..48d78eb9d6f 100644 --- a/erpnext/accounts/report/calculated_discount_mismatch/calculated_discount_mismatch.py +++ b/erpnext/accounts/report/calculated_discount_mismatch/calculated_discount_mismatch.py @@ -7,6 +7,7 @@ import frappe from frappe import _ from frappe.query_builder import Order, Tuple from frappe.utils import flt +from frappe.utils.formatters import format_value AFFECTED_DOCTYPES = frozenset( ( @@ -62,22 +63,13 @@ def get_columns(): { "fieldname": "actual_discount_amount", "label": _("Discount Amount in Transaction"), - "fieldtype": "Currency", - "options": "currency", + "fieldtype": "Data", "width": 180, }, { "fieldname": "suspected_discount_amount", "label": _("Suspected Discount Amount"), - "fieldtype": "Currency", - "options": "currency", - "width": 180, - }, - { - "fieldname": "difference", - "label": _("Difference"), - "fieldtype": "Currency", - "options": "currency", + "fieldtype": "Data", "width": 180, }, ] @@ -122,6 +114,9 @@ def get_data(): version_map[key].append(version.data) data = [] + discount_amount_field_map = { + doctype: frappe.get_meta(doctype).get_field("discount_amount") for doctype in AFFECTED_DOCTYPES + } for doc, versions in version_map.items(): for version_data in versions: if '"additional_discount_percentage"' in version_data: @@ -137,22 +132,28 @@ def get_data(): if not discount_values: continue - old = flt(discount_values[1][2:]) - new = flt(discount_values[2][2:]) + old = discount_values[1] + new = discount_values[2] + doctype = doc[0] doc_values = transactions_with_discount_percentage.get(doc) - if new != doc_values.discount_amount: + formatted_discount_amount = format_value( + doc_values.discount_amount, + df=discount_amount_field_map[doctype], + currency=doc_values.currency, + ) + + if new != formatted_discount_amount: # if the discount amount in the version is not equal to the current value, skip break data.append( { - "doctype": doc[0], - "docname": doc[1], + "doctype": doctype, + "docname": doc_values.name, "currency": doc_values.currency, "actual_discount_percentage": doc_values.additional_discount_percentage, "actual_discount_amount": new, "suspected_discount_amount": old, - "difference": flt(old - new, 9), } ) break From 35035c2a31a9d81295d90900907b8ea28e275644 Mon Sep 17 00:00:00 2001 From: Sagar Vora <16315650+sagarvora@users.noreply.github.com> Date: Mon, 9 Jun 2025 19:13:53 +0530 Subject: [PATCH 36/49] fix: remove currency col (cherry picked from commit 9bf9b34ac4ff1bbf0e9fb395e9616c99ac9805d1) --- .../calculated_discount_mismatch.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/erpnext/accounts/report/calculated_discount_mismatch/calculated_discount_mismatch.py b/erpnext/accounts/report/calculated_discount_mismatch/calculated_discount_mismatch.py index 48d78eb9d6f..d8005761e92 100644 --- a/erpnext/accounts/report/calculated_discount_mismatch/calculated_discount_mismatch.py +++ b/erpnext/accounts/report/calculated_discount_mismatch/calculated_discount_mismatch.py @@ -48,12 +48,6 @@ def get_columns(): "options": "doctype", "width": 150, }, - { - "fieldname": "currency", - "label": _("Currency"), - "fieldtype": "Link", - "options": "Currency", - }, { "fieldname": "actual_discount_percentage", "label": _("Discount Percentage in Transaction"), @@ -150,7 +144,6 @@ def get_data(): { "doctype": doctype, "docname": doc_values.name, - "currency": doc_values.currency, "actual_discount_percentage": doc_values.additional_discount_percentage, "actual_discount_amount": new, "suspected_discount_amount": old, From 59dd5fee2626223bd1b75cd30a47b7e20160bcc4 Mon Sep 17 00:00:00 2001 From: priyanshshah2442 Date: Mon, 9 Jun 2025 19:20:40 +0530 Subject: [PATCH 37/49] fix: fieldtype to Currency for discount amounts (cherry picked from commit f781a39dbef72667eda9d248344ec7e9f11acdb0) --- .../calculated_discount_mismatch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/calculated_discount_mismatch/calculated_discount_mismatch.py b/erpnext/accounts/report/calculated_discount_mismatch/calculated_discount_mismatch.py index d8005761e92..4017fca2b38 100644 --- a/erpnext/accounts/report/calculated_discount_mismatch/calculated_discount_mismatch.py +++ b/erpnext/accounts/report/calculated_discount_mismatch/calculated_discount_mismatch.py @@ -57,13 +57,13 @@ def get_columns(): { "fieldname": "actual_discount_amount", "label": _("Discount Amount in Transaction"), - "fieldtype": "Data", + "fieldtype": "Currency", "width": 180, }, { "fieldname": "suspected_discount_amount", "label": _("Suspected Discount Amount"), - "fieldtype": "Data", + "fieldtype": "Currency", "width": 180, }, ] From 84b2f871bae87f22cee2c52df39b02ac4e4a4687 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 6 Jun 2025 16:14:43 +0530 Subject: [PATCH 38/49] fix: available qty in BOM Stock Report (cherry picked from commit ea689bbe3f6dd4ae50d0015cc119b816e8581a4e) --- .../manufacturing/report/bom_stock_report/bom_stock_report.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py index d233643c244..96a6822cd11 100644 --- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py +++ b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py @@ -79,7 +79,7 @@ def get_bom_stock(filters): BOM_ITEM.stock_qty, BOM_ITEM.stock_uom, BOM_ITEM.stock_qty * qty_to_produce / BOM.quantity, - Sum(BIN.actual_qty).as_("actual_qty"), + BIN.actual_qty.as_("actual_qty"), Sum(Floor(BIN.actual_qty / (BOM_ITEM.stock_qty * qty_to_produce / BOM.quantity))), ) .where((BOM_ITEM.parent == filters.get("bom")) & (BOM_ITEM.parenttype == "BOM")) From 23b5d2db2c2a8dee7a5fc92c9391a36a49c240cf Mon Sep 17 00:00:00 2001 From: priyanshshah2442 Date: Tue, 10 Jun 2025 12:38:07 +0530 Subject: [PATCH 39/49] fix: add draft transactions also in calculated mismatch report (cherry picked from commit 4e1abc181459817ec481e999b3991a986352866a) --- .../calculated_discount_mismatch.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/accounts/report/calculated_discount_mismatch/calculated_discount_mismatch.py b/erpnext/accounts/report/calculated_discount_mismatch/calculated_discount_mismatch.py index 4017fca2b38..6c1e32b62e7 100644 --- a/erpnext/accounts/report/calculated_discount_mismatch/calculated_discount_mismatch.py +++ b/erpnext/accounts/report/calculated_discount_mismatch/calculated_discount_mismatch.py @@ -6,7 +6,6 @@ import json import frappe from frappe import _ from frappe.query_builder import Order, Tuple -from frappe.utils import flt from frappe.utils.formatters import format_value AFFECTED_DOCTYPES = frozenset( @@ -164,7 +163,7 @@ def get_transactions_with_discount_percentage(doctype): "discount_amount", ], filters={ - "docstatus": 1, + "docstatus": ["<", 2], "additional_discount_percentage": [">", 0], "discount_amount": ["!=", 0], "modified": [">", LAST_MODIFIED_DATE_THRESHOLD], From aa29c5dde2a92a25f8c584924ee65a19f9dc7e9c Mon Sep 17 00:00:00 2001 From: Sagar Vora <16315650+sagarvora@users.noreply.github.com> Date: Tue, 10 Jun 2025 13:05:37 +0530 Subject: [PATCH 40/49] fix: conflicts --- erpnext/stock/utils.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 9710321d075..637d56f093a 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -109,11 +109,8 @@ def get_stock_balance( from erpnext.stock.stock_ledger import get_previous_sle -<<<<<<< HEAD -======= frappe.has_permission("Item", "read", throw=True) ->>>>>>> 8b6a8d0c4f (fix: throw permission error (#47976)) if posting_date is None: posting_date = nowdate() if posting_time is None: From eaeb18c6517ebea6ce9d8cf68938980709eb370d Mon Sep 17 00:00:00 2001 From: DHINESH00 <18csa09@karpagamtech.ac.in> Date: Thu, 5 Jun 2025 20:04:20 +0530 Subject: [PATCH 41/49] fix: update currency based on transaction (cherry picked from commit fc4f38eed11eb0f0e1d6b9382acb8b9208ba7c1a) --- erpnext/controllers/trends.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/erpnext/controllers/trends.py b/erpnext/controllers/trends.py index 24d11e6050a..7e6062d34b2 100644 --- a/erpnext/controllers/trends.py +++ b/erpnext/controllers/trends.py @@ -20,14 +20,14 @@ def get_columns(filters, trans): columns = ( based_on_details["based_on_cols"] + period_cols - + [_("Total(Qty)") + ":Float:120", _("Total(Amt)") + ":Currency:120"] + + [_("Total(Qty)") + ":Float:120", _("Total(Amt)") + ":Currency/currency:120"] ) if group_by_cols: columns = ( based_on_details["based_on_cols"] + group_by_cols + period_cols - + [_("Total(Qty)") + ":Float:120", _("Total(Amt)") + ":Currency:120"] + + [_("Total(Qty)") + ":Float:120", _("Total(Amt)") + ":Currency/currency:120"] ) conditions = { @@ -157,7 +157,7 @@ def get_data(filters, conditions): # get data for group_by filter row1 = frappe.db.sql( - """ select {} , {} from `tab{}` t1, `tab{} Item` t2 {} + """ select t1.currency , {} , {} from `tab{}` t1, `tab{} Item` t2 {} where t2.parent = t1.name and t1.company = {} and {} between {} and {} and t1.docstatus = 1 and {} = {} and {} = {} {} {} """.format( @@ -182,6 +182,7 @@ def get_data(filters, conditions): ) des[ind] = row[i][0] + des[ind - 1] = row1[0][0] for j in range(1, len(conditions["columns"]) - inc): des[j + inc] = row1[0][j] @@ -236,7 +237,7 @@ def period_wise_columns_query(filters, trans): else: pwc = [ _(filters.get("fiscal_year")) + " (" + _("Qty") + "):Float:120", - _(filters.get("fiscal_year")) + " (" + _("Amt") + "):Currency:120", + _(filters.get("fiscal_year")) + " (" + _("Amt") + "):Currency/currency:120", ] query_details = " SUM(t2.stock_qty), SUM(t2.base_net_amount)," @@ -248,12 +249,17 @@ def get_period_wise_columns(bet_dates, period, pwc): if period == "Monthly": pwc += [ _(get_mon(bet_dates[0])) + " (" + _("Qty") + "):Float:120", - _(get_mon(bet_dates[0])) + " (" + _("Amt") + "):Currency:120", + _(get_mon(bet_dates[0])) + " (" + _("Amt") + "):Currency/currency:120", ] else: pwc += [ _(get_mon(bet_dates[0])) + "-" + _(get_mon(bet_dates[1])) + " (" + _("Qty") + "):Float:120", - _(get_mon(bet_dates[0])) + "-" + _(get_mon(bet_dates[1])) + " (" + _("Amt") + "):Currency:120", + _(get_mon(bet_dates[0])) + + "-" + + _(get_mon(bet_dates[1])) + + " (" + + _("Amt") + + "):Currency/currency:120", ] @@ -375,6 +381,9 @@ def based_wise_columns_query(based_on, trans): else: frappe.throw(_("Project-wise data is not available for Quotation")) + based_on_details["based_on_select"] += "t1.currency," + based_on_details["based_on_cols"].append("Currency:Link/Currency:120") + return based_on_details From 795108c1ddaa607d8b47bde8cdc403a823c5e30c Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 9 Jun 2025 17:44:39 +0530 Subject: [PATCH 42/49] fix: do not create repeat work orders (cherry picked from commit 384f4e120a0df9eb0e88f580a077a4ed1ccf55df) # Conflicts: # erpnext/manufacturing/doctype/production_plan/production_plan.js # erpnext/manufacturing/doctype/production_plan/test_production_plan.py # erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json --- .../production_plan/production_plan.js | 91 +++- .../production_plan/production_plan.py | 9 + .../production_plan/test_production_plan.py | 471 ++++++++++++++++++ .../production_plan_sub_assembly_item.json | 35 ++ .../production_plan_sub_assembly_item.py | 1 + .../doctype/work_order/work_order.py | 28 +- 6 files changed, 626 insertions(+), 9 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index f381c9d39e2..5072b130b2c 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -116,7 +116,9 @@ frappe.ui.form.on("Production Plan", { ); } - if (frm.doc.po_items && frm.doc.status !== "Closed") { + let items = frm.events.get_items_for_work_order(frm); + + if (items?.length && frm.doc.status !== "Closed") { frm.add_custom_button( __("Work Order / Subcontract PO"), () => { @@ -193,6 +195,93 @@ frappe.ui.form.on("Production Plan", { set_field_options("projected_qty_formula", projected_qty_formula); }, +<<<<<<< HEAD +======= + get_items_for_work_order(frm) { + let items = frm.doc.po_items; + if (frm.doc.sub_assembly_items?.length) { + items = [...items, ...frm.doc.sub_assembly_items]; + } + + let has_items = + items.filter((item) => { + if (item.pending_qty) { + return item.pending_qty > item.ordered_qty; + } else { + return item.qty > (item.received_qty || item.ordered_qty); + } + }) || []; + + return has_items; + }, + + has_unreserved_stock(frm, table, qty_field = "required_qty") { + let has_unreserved_stock = frm.doc[table].some( + (item) => flt(item[qty_field]) > flt(item.stock_reserved_qty) + ); + + return has_unreserved_stock; + }, + + has_reserved_stock(frm, table) { + let has_reserved_stock = frm.doc[table].some((item) => flt(item.stock_reserved_qty) > 0); + + return has_reserved_stock; + }, + + setup_stock_reservation_for_sub_assembly(frm) { + if (frm.doc.docstatus === 1 && frm.doc.reserve_stock) { + if (frm.events.has_unreserved_stock(frm, "sub_assembly_items")) { + frm.add_custom_button( + __("Reserve for Sub-assembly"), + () => erpnext.stock_reservation.make_entries(frm, "sub_assembly_items"), + __("Stock Reservation") + ); + } + + if (frm.events.has_reserved_stock(frm, "sub_assembly_items")) { + frm.add_custom_button( + __("Unreserve for Sub-assembly"), + () => erpnext.stock_reservation.unreserve_stock(frm), + __("Stock Reservation") + ); + + frm.add_custom_button( + __("Reserved Stock for Sub-assembly"), + () => erpnext.stock_reservation.show_reserved_stock(frm, "sub_assembly_items"), + __("Stock Reservation") + ); + } + } + }, + + setup_stock_reservation_for_raw_materials(frm) { + if (frm.doc.docstatus === 1 && frm.doc.reserve_stock) { + if (frm.events.has_unreserved_stock(frm, "mr_items", "required_bom_qty")) { + frm.add_custom_button( + __("Reserve for Raw Materials"), + () => erpnext.stock_reservation.make_entries(frm, "mr_items"), + __("Stock Reservation") + ); + } + + if (frm.events.has_reserved_stock(frm, "mr_items")) { + frm.add_custom_button( + __("Unreserve for Raw Materials"), + () => erpnext.stock_reservation.unreserve_stock(frm), + __("Stock Reservation") + ); + + frm.add_custom_button( + __("Reserved Stock for Raw Materials"), + () => erpnext.stock_reservation.show_reserved_stock(frm, "mr_items"), + __("Stock Reservation") + ); + } + } + }, + +>>>>>>> 384f4e120a (fix: do not create repeat work orders) close_open_production_plan(frm, close = false) { frappe.call({ method: "set_status", diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index dbcf07bbccf..c6b6b7e1d9d 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -751,7 +751,14 @@ class ProductionPlan(Document): "company": self.get("company"), } + if flt(row.qty) <= flt(row.ordered_qty): + continue + self.prepare_data_for_sub_assembly_items(row, work_order_data) + + if work_order_data.get("qty") <= 0: + continue + work_order = self.create_work_order(work_order_data) if work_order: wo_list.append(work_order) @@ -771,6 +778,8 @@ class ProductionPlan(Document): if row.get(field): wo_data[field] = row.get(field) + wo_data["qty"] = flt(row.get("qty")) - flt(row.get("ordered_qty")) + wo_data.update( { "use_multi_level_bom": 0, diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index e5b60c7a4e6..0bac38dd47d 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -1693,6 +1693,477 @@ class TestProductionPlan(FrappeTestCase): self.assertEqual(mr_items[0].get("quantity"), 80) self.assertEqual(mr_items[1].get("quantity"), 70) +<<<<<<< HEAD +======= + def test_stock_reservation_against_production_plan(self): + from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt + from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom + from erpnext.stock.doctype.material_request.material_request import make_purchase_order + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 1) + + bom_tree = { + "Finished Good For SR": { + "Sub Assembly For SR 1": {"Raw Material For SR 1": {}}, + "Sub Assembly For SR 2": {"Raw Material For SR 2": {}}, + "Sub Assembly For SR 3": {"Raw Material For SR 3": {}}, + } + } + parent_bom = create_nested_bom(bom_tree, prefix="") + + warehouse = "_Test Warehouse - _TC" + + for item_code in [ + "Sub Assembly For SR 1", + "Sub Assembly For SR 2", + "Sub Assembly For SR 3", + "Raw Material For SR 1", + "Raw Material For SR 2", + "Raw Material For SR 3", + ]: + make_stock_entry(item_code=item_code, target=warehouse, qty=5, basic_rate=100) + + plan = create_production_plan( + item_code=parent_bom.item, + planned_qty=15, + skip_available_sub_assembly_item=1, + ignore_existing_ordered_qty=1, + do_not_submit=1, + warehouse=warehouse, + sub_assembly_warehouse=warehouse, + for_warehouse=warehouse, + reserve_stock=1, + ) + + plan.get_sub_assembly_items() + plan.set("mr_items", []) + mr_items = get_items_for_material_requests(plan.as_dict()) + for d in mr_items: + plan.append("mr_items", d) + + plan.save() + + self.assertTrue(len(plan.sub_assembly_items) == 3) + for row in plan.sub_assembly_items: + self.assertEqual(row.required_qty, 15.0) + self.assertEqual(row.qty, 10.0) + + self.assertTrue(len(plan.mr_items) == 3) + for row in plan.mr_items: + self.assertEqual(row.required_bom_qty, 10.0) + self.assertEqual(row.quantity, 5.0) + + plan.submit() + + sre = StockReservation(plan) + reserved_entries = sre.get_reserved_entries("Production Plan", plan.name) + self.assertTrue(len(reserved_entries) == 6) + + for row in reserved_entries: + self.assertEqual(row.reserved_qty, 5.0) + + plan.submit_material_request = 1 + plan.make_material_request() + plan.make_work_order() + + material_requests = frappe.get_all( + "Material Request", filters={"production_plan": plan.name}, pluck="name" + ) + + self.assertTrue(len(material_requests) > 0) + for mr_name in list(set(material_requests)): + po = make_purchase_order(mr_name) + po.supplier = "_Test Supplier" + po.submit() + + pr = make_purchase_receipt(po.name) + pr.submit() + + sre = StockReservation(plan) + reserved_entries = sre.get_reserved_entries("Production Plan", plan.name) + self.assertTrue(len(reserved_entries) == 9) + + work_orders = frappe.get_all("Work Order", filters={"production_plan": plan.name}, pluck="name") + for wo_name in list(set(work_orders)): + wo_doc = frappe.get_doc("Work Order", wo_name) + self.assertEqual(wo_doc.reserve_stock, 1) + + wo_doc.source_warehouse = warehouse + wo_doc.wip_warehouse = warehouse + wo_doc.fg_warehouse = warehouse + wo_doc.submit() + + sre = StockReservation(wo_doc) + reserved_entries = sre.get_reserved_entries("Work Order", wo_doc.name) + if wo_doc.production_item == "Finished Good For SR": + self.assertEqual(len(reserved_entries), 3) + else: + # For raw materials 2 stock reservation entries + # 5 qty was present already in stock and 5 added from new PO + self.assertEqual(len(reserved_entries), 2) + + sre = StockReservation(plan) + reserved_entries = sre.get_reserved_entries("Production Plan", plan.name) + self.assertTrue(len(reserved_entries) == 0) + frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 0) + + def test_stock_reservation_of_serial_nos_against_production_plan(self): + from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt + from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom + from erpnext.stock.doctype.material_request.material_request import make_purchase_order + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 1) + + bom_tree = { + "Finished Good For SR": { + "SN Sub Assembly For SR 1": {"SN Raw Material For SR 1": {}}, + "SN Sub Assembly For SR 2": {"SN Raw Material For SR 2": {}}, + "SN Sub Assembly For SR 3": {"SN Raw Material For SR 3": {}}, + } + } + parent_bom = create_nested_bom(bom_tree, prefix="") + + warehouse = "_Test Warehouse - _TC" + + for item_code in [ + "SN Sub Assembly For SR 1", + "SN Sub Assembly For SR 2", + "SN Sub Assembly For SR 3", + "SN Raw Material For SR 1", + "SN Raw Material For SR 2", + "SN Raw Material For SR 3", + ]: + doc = frappe.get_doc("Item", item_code) + doc.has_serial_no = 1 + doc.serial_no_series = f"SNN-{item_code}.-.#####" + doc.save() + + make_stock_entry(item_code=item_code, target=warehouse, qty=5, basic_rate=100) + + plan = create_production_plan( + item_code=parent_bom.item, + planned_qty=15, + skip_available_sub_assembly_item=1, + ignore_existing_ordered_qty=1, + do_not_submit=1, + warehouse=warehouse, + sub_assembly_warehouse=warehouse, + for_warehouse=warehouse, + reserve_stock=1, + ) + + plan.get_sub_assembly_items() + plan.set("mr_items", []) + mr_items = get_items_for_material_requests(plan.as_dict()) + for d in mr_items: + plan.append("mr_items", d) + + plan.save() + + self.assertTrue(len(plan.sub_assembly_items) == 3) + for row in plan.sub_assembly_items: + self.assertEqual(row.required_qty, 15.0) + self.assertEqual(row.qty, 10.0) + + self.assertTrue(len(plan.mr_items) == 3) + for row in plan.mr_items: + self.assertEqual(row.required_bom_qty, 10.0) + self.assertEqual(row.quantity, 5.0) + + plan.submit() + + sre = StockReservation(plan) + reserved_entries = sre.get_reserved_entries("Production Plan", plan.name) + self.assertTrue(len(reserved_entries) == 6) + + for row in reserved_entries: + self.assertEqual(row.reserved_qty, 5.0) + + plan.submit_material_request = 1 + plan.make_material_request() + plan.make_work_order() + + material_requests = frappe.get_all( + "Material Request", filters={"production_plan": plan.name}, pluck="name" + ) + + additional_serial_nos = [] + + for item_code in [ + "SN Sub Assembly For SR 1", + "SN Sub Assembly For SR 2", + "SN Sub Assembly For SR 3", + "SN Raw Material For SR 1", + "SN Raw Material For SR 2", + "SN Raw Material For SR 3", + ]: + se = make_stock_entry(item_code=item_code, target=warehouse, qty=5, basic_rate=100) + additional_serial_nos.extend(get_serial_nos_from_bundle(se.items[0].serial_and_batch_bundle)) + + self.assertTrue(additional_serial_nos) + + self.assertTrue(len(material_requests) > 0) + for mr_name in list(set(material_requests)): + po = make_purchase_order(mr_name) + po.supplier = "_Test Supplier" + po.submit() + + pr = make_purchase_receipt(po.name) + pr.submit() + + sre = StockReservation(plan) + reserved_entries = sre.get_reserved_entries("Production Plan", plan.name) + self.assertTrue(len(reserved_entries) == 9) + serial_nos_res_for_pp = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": ("in", [x.name for x in reserved_entries]), "docstatus": 1}, + pluck="serial_no", + ) + + work_orders = frappe.get_all("Work Order", filters={"production_plan": plan.name}, pluck="name") + for wo_name in list(set(work_orders)): + wo_doc = frappe.get_doc("Work Order", wo_name) + self.assertEqual(wo_doc.reserve_stock, 1) + + wo_doc.source_warehouse = warehouse + wo_doc.wip_warehouse = warehouse + wo_doc.fg_warehouse = warehouse + wo_doc.submit() + + sre = StockReservation(wo_doc) + reserved_entries = sre.get_reserved_entries("Work Order", wo_doc.name) + serial_nos_res_for_wo = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": ("in", [x.name for x in reserved_entries]), "docstatus": 1}, + pluck="serial_no", + ) + + for serial_no in serial_nos_res_for_wo: + self.assertTrue(serial_no in serial_nos_res_for_pp) + self.assertFalse(serial_no in additional_serial_nos) + + if wo_doc.production_item == "Finished Good For SR": + self.assertEqual(len(reserved_entries), 3) + else: + # For raw materials 2 stock reservation entries + # 5 qty was present already in stock and 5 added from new PO + self.assertEqual(len(reserved_entries), 2) + + sre = StockReservation(plan) + reserved_entries = sre.get_reserved_entries("Production Plan", plan.name) + self.assertTrue(len(reserved_entries) == 0) + frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 0) + + def test_stock_reservation_of_batch_nos_against_production_plan(self): + from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt + from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom + from erpnext.stock.doctype.material_request.material_request import make_purchase_order + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 1) + + bom_tree = { + "Finished Good For SR": { + "Batch Sub Assembly For SR 1": {"Batch Raw Material For SR 1": {}}, + "Batch Sub Assembly For SR 2": {"Batch Raw Material For SR 2": {}}, + "Batch Sub Assembly For SR 3": {"Batch Raw Material For SR 3": {}}, + } + } + parent_bom = create_nested_bom(bom_tree, prefix="") + + warehouse = "_Test Warehouse - _TC" + + for item_code in [ + "Batch Sub Assembly For SR 1", + "Batch Sub Assembly For SR 2", + "Batch Sub Assembly For SR 3", + "Batch Raw Material For SR 1", + "Batch Raw Material For SR 2", + "Batch Raw Material For SR 3", + ]: + doc = frappe.get_doc("Item", item_code) + doc.has_batch_no = 1 + doc.create_new_batch = 1 + doc.batch_number_series = f"BCH-{item_code}.-.#####" + doc.save() + + make_stock_entry(item_code=item_code, target=warehouse, qty=5, basic_rate=100) + + plan = create_production_plan( + item_code=parent_bom.item, + planned_qty=15, + skip_available_sub_assembly_item=1, + ignore_existing_ordered_qty=1, + do_not_submit=1, + warehouse=warehouse, + sub_assembly_warehouse=warehouse, + for_warehouse=warehouse, + reserve_stock=1, + ) + + plan.get_sub_assembly_items() + plan.set("mr_items", []) + mr_items = get_items_for_material_requests(plan.as_dict()) + for d in mr_items: + plan.append("mr_items", d) + + plan.save() + + self.assertTrue(len(plan.sub_assembly_items) == 3) + for row in plan.sub_assembly_items: + self.assertEqual(row.required_qty, 15.0) + self.assertEqual(row.qty, 10.0) + + self.assertTrue(len(plan.mr_items) == 3) + for row in plan.mr_items: + self.assertEqual(row.required_bom_qty, 10.0) + self.assertEqual(row.quantity, 5.0) + + plan.submit() + + sre = StockReservation(plan) + reserved_entries = sre.get_reserved_entries("Production Plan", plan.name) + self.assertTrue(len(reserved_entries) == 6) + + for row in reserved_entries: + self.assertEqual(row.reserved_qty, 5.0) + + plan.submit_material_request = 1 + plan.make_material_request() + plan.make_work_order() + + material_requests = frappe.get_all( + "Material Request", filters={"production_plan": plan.name}, pluck="name" + ) + + additional_batches = [] + + for item_code in [ + "Batch Sub Assembly For SR 1", + "Batch Sub Assembly For SR 2", + "Batch Sub Assembly For SR 3", + "Batch Raw Material For SR 1", + "Batch Raw Material For SR 2", + "Batch Raw Material For SR 3", + ]: + se = make_stock_entry(item_code=item_code, target=warehouse, qty=5, basic_rate=100) + batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle) + additional_batches.append(batch_no) + + self.assertTrue(additional_batches) + + self.assertTrue(len(material_requests) > 0) + for mr_name in list(set(material_requests)): + po = make_purchase_order(mr_name) + po.supplier = "_Test Supplier" + po.submit() + + pr = make_purchase_receipt(po.name) + pr.submit() + + sre = StockReservation(plan) + reserved_entries = sre.get_reserved_entries("Production Plan", plan.name) + self.assertTrue(len(reserved_entries) == 9) + batches_reserved_for_pp = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": ("in", [x.name for x in reserved_entries]), "docstatus": 1}, + pluck="batch_no", + ) + + work_orders = frappe.get_all("Work Order", filters={"production_plan": plan.name}, pluck="name") + for wo_name in list(set(work_orders)): + wo_doc = frappe.get_doc("Work Order", wo_name) + self.assertEqual(wo_doc.reserve_stock, 1) + + wo_doc.source_warehouse = warehouse + wo_doc.wip_warehouse = warehouse + wo_doc.fg_warehouse = warehouse + wo_doc.submit() + + sre = StockReservation(wo_doc) + reserved_entries = sre.get_reserved_entries("Work Order", wo_doc.name) + batches_reserved_for_wo = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": ("in", [x.name for x in reserved_entries]), "docstatus": 1}, + pluck="batch_no", + ) + + for batch_no in batches_reserved_for_wo: + self.assertTrue(batch_no in batches_reserved_for_pp) + self.assertFalse(batch_no in additional_batches) + + if wo_doc.production_item == "Finished Good For SR": + self.assertEqual(len(reserved_entries), 3) + else: + # For raw materials 2 stock reservation entries + # 5 qty was present already in stock and 5 added from new PO + self.assertEqual(len(reserved_entries), 2) + + sre = StockReservation(plan) + reserved_entries = sre.get_reserved_entries("Production Plan", plan.name) + self.assertTrue(len(reserved_entries) == 0) + frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 0) + + def test_production_plan_for_partial_sub_assembly_items(self): + from erpnext.controllers.status_updater import OverAllowanceError + from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom + from erpnext.subcontracting.doctype.subcontracting_bom.test_subcontracting_bom import ( + create_subcontracting_bom, + ) + + frappe.flags.test_print = False + + fg_wo_item = "Test Motherboard 11" + bom_tree_1 = {"Test Laptop 11": {fg_wo_item: {"Test Motherboard Wires 11": {}}}} + create_nested_bom(bom_tree_1, prefix="") + + plan = create_production_plan( + item_code="Test Laptop 11", + planned_qty=10, + use_multi_level_bom=1, + do_not_submit=True, + company="_Test Company", + skip_getting_mr_items=True, + ) + plan.get_sub_assembly_items() + plan.submit() + plan.make_work_order() + + work_order = frappe.db.get_value("Work Order", {"production_plan": plan.name, "docstatus": 0}, "name") + wo_doc = frappe.get_doc("Work Order", work_order) + + wo_doc.qty = 5.0 + wo_doc.skip_transfer = 1 + wo_doc.from_wip_warehouse = 1 + wo_doc.wip_warehouse = "_Test Warehouse - _TC" + wo_doc.fg_warehouse = "_Test Warehouse - _TC" + wo_doc.submit() + + plan.reload() + + for row in plan.sub_assembly_items: + self.assertEqual(row.ordered_qty, 5.0) + + plan.make_work_order() + + work_order = frappe.db.get_value("Work Order", {"production_plan": plan.name, "docstatus": 0}, "name") + wo_doc = frappe.get_doc("Work Order", work_order) + self.assertEqual(wo_doc.qty, 5.0) + + wo_doc.skip_transfer = 1 + wo_doc.from_wip_warehouse = 1 + wo_doc.wip_warehouse = "_Test Warehouse - _TC" + wo_doc.fg_warehouse = "_Test Warehouse - _TC" + wo_doc.submit() + + plan.reload() + + for row in plan.sub_assembly_items: + self.assertEqual(row.ordered_qty, 10.0) + +>>>>>>> 384f4e120a (fix: do not create repeat work orders) def create_production_plan(**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 7965965d2b6..76d52f12bcd 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 @@ -21,6 +21,7 @@ "purchase_order", "production_plan_item", "column_break_7", + "ordered_qty", "received_qty", "indent", "section_break_19", @@ -204,12 +205,46 @@ "fieldtype": "Float", "label": "Produced Qty", "read_only": 1 +<<<<<<< HEAD +======= + }, + { + "columns": 2, + "fieldname": "required_qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Required Qty" + }, + { + "fieldname": "subcontracting_section", + "fieldtype": "Section Break", + "label": "Subcontracting" + }, + { + "fieldname": "stock_reserved_qty", + "fieldtype": "Float", + "label": "Stock Reserved Qty", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "ordered_qty", + "fieldtype": "Float", + "label": "Ordered Qty", + "no_copy": 1, + "read_only": 1 +>>>>>>> 384f4e120a (fix: do not create repeat work orders) } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], +<<<<<<< HEAD "modified": "2024-02-27 13:45:17.422435", +======= + "modified": "2025-06-10 13:36:24.759101", +>>>>>>> 384f4e120a (fix: do not create repeat work orders) "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Plan Sub Assembly Item", diff --git a/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.py b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.py index ad1d655de8b..7e29675136c 100644 --- a/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.py +++ b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.py @@ -22,6 +22,7 @@ class ProductionPlanSubAssemblyItem(Document): fg_warehouse: DF.Link | None indent: DF.Int item_name: DF.Data | None + ordered_qty: DF.Float parent: DF.Data parent_item_code: DF.Link | None parentfield: DF.Data diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 270c23e913d..709d64cf99f 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -762,22 +762,34 @@ class WorkOrder(Document): ) def update_ordered_qty(self): - if self.production_plan and self.production_plan_item and not self.production_plan_sub_assembly_item: + if self.production_plan and (self.production_plan_item or self.production_plan_sub_assembly_item): table = frappe.qb.DocType("Work Order") query = ( frappe.qb.from_(table) .select(Sum(table.qty)) - .where( - (table.production_plan == self.production_plan) - & (table.production_plan_item == self.production_plan_item) - & (table.docstatus == 1) - ) - ).run() + .where((table.production_plan == self.production_plan) & (table.docstatus == 1)) + ) + if self.production_plan_item: + query = query.where(table.production_plan_item == self.production_plan_item) + elif self.production_plan_sub_assembly_item: + query = query.where( + table.production_plan_sub_assembly_item == self.production_plan_sub_assembly_item + ) + + query = query.run() qty = flt(query[0][0]) if query else 0 - frappe.db.set_value("Production Plan Item", self.production_plan_item, "ordered_qty", qty) + if self.production_plan_item: + frappe.db.set_value("Production Plan Item", self.production_plan_item, "ordered_qty", qty) + elif self.production_plan_sub_assembly_item: + frappe.db.set_value( + "Production Plan Sub Assembly Item", + self.production_plan_sub_assembly_item, + "ordered_qty", + qty, + ) doc = frappe.get_doc("Production Plan", self.production_plan) doc.set_status() From efd3b1c9660e0e550809f817f0b5deb5920cac23 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 9 Jun 2025 22:15:41 +0200 Subject: [PATCH 43/49] refactor(Work Order): query_sales_order - Use `get_list` instead of `db.sql_list` The method is used for setting link options in the frontend and the Link field doesn't ignore permissions, so get_list should be fine here. - Added type hints to enable argument validation (cherry picked from commit 2dbdacf905c5898da843bf319420371a47c7acc9) --- .../doctype/work_order/work_order.py | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 270c23e913d..8733df4182e 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -1503,20 +1503,20 @@ def stop_unstop(work_order, status): @frappe.whitelist() -def query_sales_order(production_item): - out = frappe.db.sql_list( - """ - select distinct so.name from `tabSales Order` so, `tabSales Order Item` so_item - where so_item.parent=so.name and so_item.item_code=%s and so.docstatus=1 - union - select distinct so.name from `tabSales Order` so, `tabPacked Item` pi_item - where pi_item.parent=so.name and pi_item.item_code=%s and so.docstatus=1 - """, - (production_item, production_item), +def query_sales_order(production_item: str) -> list[str]: + return frappe.get_list( + "Sales Order", + filters=[ + ["Sales Order", "docstatus", "=", 1], + ], + or_filters=[ + ["Sales Order Item", "item_code", "=", production_item], + ["Packed Item", "item_code", "=", production_item], + ], + pluck="name", + distinct=True, ) - return out - @frappe.whitelist() def make_job_card(work_order, operations): From 781b66e252db554a1888c880228e41ecb84f499c Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 10 Jun 2025 14:35:23 +0530 Subject: [PATCH 44/49] chore: fix conflicts --- .../production_plan/production_plan.js | 69 ------------------- 1 file changed, 69 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index 5072b130b2c..268bf847a90 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -195,8 +195,6 @@ frappe.ui.form.on("Production Plan", { set_field_options("projected_qty_formula", projected_qty_formula); }, -<<<<<<< HEAD -======= get_items_for_work_order(frm) { let items = frm.doc.po_items; if (frm.doc.sub_assembly_items?.length) { @@ -215,73 +213,6 @@ frappe.ui.form.on("Production Plan", { return has_items; }, - has_unreserved_stock(frm, table, qty_field = "required_qty") { - let has_unreserved_stock = frm.doc[table].some( - (item) => flt(item[qty_field]) > flt(item.stock_reserved_qty) - ); - - return has_unreserved_stock; - }, - - has_reserved_stock(frm, table) { - let has_reserved_stock = frm.doc[table].some((item) => flt(item.stock_reserved_qty) > 0); - - return has_reserved_stock; - }, - - setup_stock_reservation_for_sub_assembly(frm) { - if (frm.doc.docstatus === 1 && frm.doc.reserve_stock) { - if (frm.events.has_unreserved_stock(frm, "sub_assembly_items")) { - frm.add_custom_button( - __("Reserve for Sub-assembly"), - () => erpnext.stock_reservation.make_entries(frm, "sub_assembly_items"), - __("Stock Reservation") - ); - } - - if (frm.events.has_reserved_stock(frm, "sub_assembly_items")) { - frm.add_custom_button( - __("Unreserve for Sub-assembly"), - () => erpnext.stock_reservation.unreserve_stock(frm), - __("Stock Reservation") - ); - - frm.add_custom_button( - __("Reserved Stock for Sub-assembly"), - () => erpnext.stock_reservation.show_reserved_stock(frm, "sub_assembly_items"), - __("Stock Reservation") - ); - } - } - }, - - setup_stock_reservation_for_raw_materials(frm) { - if (frm.doc.docstatus === 1 && frm.doc.reserve_stock) { - if (frm.events.has_unreserved_stock(frm, "mr_items", "required_bom_qty")) { - frm.add_custom_button( - __("Reserve for Raw Materials"), - () => erpnext.stock_reservation.make_entries(frm, "mr_items"), - __("Stock Reservation") - ); - } - - if (frm.events.has_reserved_stock(frm, "mr_items")) { - frm.add_custom_button( - __("Unreserve for Raw Materials"), - () => erpnext.stock_reservation.unreserve_stock(frm), - __("Stock Reservation") - ); - - frm.add_custom_button( - __("Reserved Stock for Raw Materials"), - () => erpnext.stock_reservation.show_reserved_stock(frm, "mr_items"), - __("Stock Reservation") - ); - } - } - }, - ->>>>>>> 384f4e120a (fix: do not create repeat work orders) close_open_production_plan(frm, close = false) { frappe.call({ method: "set_status", From ccb0f7ac4239d081a7b1ce5712da8189a0cdadf6 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 10 Jun 2025 14:36:31 +0530 Subject: [PATCH 45/49] chore: fix conflicts --- .../production_plan/test_production_plan.py | 414 ------------------ 1 file changed, 414 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 0bac38dd47d..3c9e29b2811 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -1693,419 +1693,6 @@ class TestProductionPlan(FrappeTestCase): self.assertEqual(mr_items[0].get("quantity"), 80) self.assertEqual(mr_items[1].get("quantity"), 70) -<<<<<<< HEAD -======= - def test_stock_reservation_against_production_plan(self): - from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt - from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom - from erpnext.stock.doctype.material_request.material_request import make_purchase_order - from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse - - frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 1) - - bom_tree = { - "Finished Good For SR": { - "Sub Assembly For SR 1": {"Raw Material For SR 1": {}}, - "Sub Assembly For SR 2": {"Raw Material For SR 2": {}}, - "Sub Assembly For SR 3": {"Raw Material For SR 3": {}}, - } - } - parent_bom = create_nested_bom(bom_tree, prefix="") - - warehouse = "_Test Warehouse - _TC" - - for item_code in [ - "Sub Assembly For SR 1", - "Sub Assembly For SR 2", - "Sub Assembly For SR 3", - "Raw Material For SR 1", - "Raw Material For SR 2", - "Raw Material For SR 3", - ]: - make_stock_entry(item_code=item_code, target=warehouse, qty=5, basic_rate=100) - - plan = create_production_plan( - item_code=parent_bom.item, - planned_qty=15, - skip_available_sub_assembly_item=1, - ignore_existing_ordered_qty=1, - do_not_submit=1, - warehouse=warehouse, - sub_assembly_warehouse=warehouse, - for_warehouse=warehouse, - reserve_stock=1, - ) - - plan.get_sub_assembly_items() - plan.set("mr_items", []) - mr_items = get_items_for_material_requests(plan.as_dict()) - for d in mr_items: - plan.append("mr_items", d) - - plan.save() - - self.assertTrue(len(plan.sub_assembly_items) == 3) - for row in plan.sub_assembly_items: - self.assertEqual(row.required_qty, 15.0) - self.assertEqual(row.qty, 10.0) - - self.assertTrue(len(plan.mr_items) == 3) - for row in plan.mr_items: - self.assertEqual(row.required_bom_qty, 10.0) - self.assertEqual(row.quantity, 5.0) - - plan.submit() - - sre = StockReservation(plan) - reserved_entries = sre.get_reserved_entries("Production Plan", plan.name) - self.assertTrue(len(reserved_entries) == 6) - - for row in reserved_entries: - self.assertEqual(row.reserved_qty, 5.0) - - plan.submit_material_request = 1 - plan.make_material_request() - plan.make_work_order() - - material_requests = frappe.get_all( - "Material Request", filters={"production_plan": plan.name}, pluck="name" - ) - - self.assertTrue(len(material_requests) > 0) - for mr_name in list(set(material_requests)): - po = make_purchase_order(mr_name) - po.supplier = "_Test Supplier" - po.submit() - - pr = make_purchase_receipt(po.name) - pr.submit() - - sre = StockReservation(plan) - reserved_entries = sre.get_reserved_entries("Production Plan", plan.name) - self.assertTrue(len(reserved_entries) == 9) - - work_orders = frappe.get_all("Work Order", filters={"production_plan": plan.name}, pluck="name") - for wo_name in list(set(work_orders)): - wo_doc = frappe.get_doc("Work Order", wo_name) - self.assertEqual(wo_doc.reserve_stock, 1) - - wo_doc.source_warehouse = warehouse - wo_doc.wip_warehouse = warehouse - wo_doc.fg_warehouse = warehouse - wo_doc.submit() - - sre = StockReservation(wo_doc) - reserved_entries = sre.get_reserved_entries("Work Order", wo_doc.name) - if wo_doc.production_item == "Finished Good For SR": - self.assertEqual(len(reserved_entries), 3) - else: - # For raw materials 2 stock reservation entries - # 5 qty was present already in stock and 5 added from new PO - self.assertEqual(len(reserved_entries), 2) - - sre = StockReservation(plan) - reserved_entries = sre.get_reserved_entries("Production Plan", plan.name) - self.assertTrue(len(reserved_entries) == 0) - frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 0) - - def test_stock_reservation_of_serial_nos_against_production_plan(self): - from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt - from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom - from erpnext.stock.doctype.material_request.material_request import make_purchase_order - from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse - - frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 1) - - bom_tree = { - "Finished Good For SR": { - "SN Sub Assembly For SR 1": {"SN Raw Material For SR 1": {}}, - "SN Sub Assembly For SR 2": {"SN Raw Material For SR 2": {}}, - "SN Sub Assembly For SR 3": {"SN Raw Material For SR 3": {}}, - } - } - parent_bom = create_nested_bom(bom_tree, prefix="") - - warehouse = "_Test Warehouse - _TC" - - for item_code in [ - "SN Sub Assembly For SR 1", - "SN Sub Assembly For SR 2", - "SN Sub Assembly For SR 3", - "SN Raw Material For SR 1", - "SN Raw Material For SR 2", - "SN Raw Material For SR 3", - ]: - doc = frappe.get_doc("Item", item_code) - doc.has_serial_no = 1 - doc.serial_no_series = f"SNN-{item_code}.-.#####" - doc.save() - - make_stock_entry(item_code=item_code, target=warehouse, qty=5, basic_rate=100) - - plan = create_production_plan( - item_code=parent_bom.item, - planned_qty=15, - skip_available_sub_assembly_item=1, - ignore_existing_ordered_qty=1, - do_not_submit=1, - warehouse=warehouse, - sub_assembly_warehouse=warehouse, - for_warehouse=warehouse, - reserve_stock=1, - ) - - plan.get_sub_assembly_items() - plan.set("mr_items", []) - mr_items = get_items_for_material_requests(plan.as_dict()) - for d in mr_items: - plan.append("mr_items", d) - - plan.save() - - self.assertTrue(len(plan.sub_assembly_items) == 3) - for row in plan.sub_assembly_items: - self.assertEqual(row.required_qty, 15.0) - self.assertEqual(row.qty, 10.0) - - self.assertTrue(len(plan.mr_items) == 3) - for row in plan.mr_items: - self.assertEqual(row.required_bom_qty, 10.0) - self.assertEqual(row.quantity, 5.0) - - plan.submit() - - sre = StockReservation(plan) - reserved_entries = sre.get_reserved_entries("Production Plan", plan.name) - self.assertTrue(len(reserved_entries) == 6) - - for row in reserved_entries: - self.assertEqual(row.reserved_qty, 5.0) - - plan.submit_material_request = 1 - plan.make_material_request() - plan.make_work_order() - - material_requests = frappe.get_all( - "Material Request", filters={"production_plan": plan.name}, pluck="name" - ) - - additional_serial_nos = [] - - for item_code in [ - "SN Sub Assembly For SR 1", - "SN Sub Assembly For SR 2", - "SN Sub Assembly For SR 3", - "SN Raw Material For SR 1", - "SN Raw Material For SR 2", - "SN Raw Material For SR 3", - ]: - se = make_stock_entry(item_code=item_code, target=warehouse, qty=5, basic_rate=100) - additional_serial_nos.extend(get_serial_nos_from_bundle(se.items[0].serial_and_batch_bundle)) - - self.assertTrue(additional_serial_nos) - - self.assertTrue(len(material_requests) > 0) - for mr_name in list(set(material_requests)): - po = make_purchase_order(mr_name) - po.supplier = "_Test Supplier" - po.submit() - - pr = make_purchase_receipt(po.name) - pr.submit() - - sre = StockReservation(plan) - reserved_entries = sre.get_reserved_entries("Production Plan", plan.name) - self.assertTrue(len(reserved_entries) == 9) - serial_nos_res_for_pp = frappe.get_all( - "Serial and Batch Entry", - filters={"parent": ("in", [x.name for x in reserved_entries]), "docstatus": 1}, - pluck="serial_no", - ) - - work_orders = frappe.get_all("Work Order", filters={"production_plan": plan.name}, pluck="name") - for wo_name in list(set(work_orders)): - wo_doc = frappe.get_doc("Work Order", wo_name) - self.assertEqual(wo_doc.reserve_stock, 1) - - wo_doc.source_warehouse = warehouse - wo_doc.wip_warehouse = warehouse - wo_doc.fg_warehouse = warehouse - wo_doc.submit() - - sre = StockReservation(wo_doc) - reserved_entries = sre.get_reserved_entries("Work Order", wo_doc.name) - serial_nos_res_for_wo = frappe.get_all( - "Serial and Batch Entry", - filters={"parent": ("in", [x.name for x in reserved_entries]), "docstatus": 1}, - pluck="serial_no", - ) - - for serial_no in serial_nos_res_for_wo: - self.assertTrue(serial_no in serial_nos_res_for_pp) - self.assertFalse(serial_no in additional_serial_nos) - - if wo_doc.production_item == "Finished Good For SR": - self.assertEqual(len(reserved_entries), 3) - else: - # For raw materials 2 stock reservation entries - # 5 qty was present already in stock and 5 added from new PO - self.assertEqual(len(reserved_entries), 2) - - sre = StockReservation(plan) - reserved_entries = sre.get_reserved_entries("Production Plan", plan.name) - self.assertTrue(len(reserved_entries) == 0) - frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 0) - - def test_stock_reservation_of_batch_nos_against_production_plan(self): - from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt - from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom - from erpnext.stock.doctype.material_request.material_request import make_purchase_order - from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse - - frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 1) - - bom_tree = { - "Finished Good For SR": { - "Batch Sub Assembly For SR 1": {"Batch Raw Material For SR 1": {}}, - "Batch Sub Assembly For SR 2": {"Batch Raw Material For SR 2": {}}, - "Batch Sub Assembly For SR 3": {"Batch Raw Material For SR 3": {}}, - } - } - parent_bom = create_nested_bom(bom_tree, prefix="") - - warehouse = "_Test Warehouse - _TC" - - for item_code in [ - "Batch Sub Assembly For SR 1", - "Batch Sub Assembly For SR 2", - "Batch Sub Assembly For SR 3", - "Batch Raw Material For SR 1", - "Batch Raw Material For SR 2", - "Batch Raw Material For SR 3", - ]: - doc = frappe.get_doc("Item", item_code) - doc.has_batch_no = 1 - doc.create_new_batch = 1 - doc.batch_number_series = f"BCH-{item_code}.-.#####" - doc.save() - - make_stock_entry(item_code=item_code, target=warehouse, qty=5, basic_rate=100) - - plan = create_production_plan( - item_code=parent_bom.item, - planned_qty=15, - skip_available_sub_assembly_item=1, - ignore_existing_ordered_qty=1, - do_not_submit=1, - warehouse=warehouse, - sub_assembly_warehouse=warehouse, - for_warehouse=warehouse, - reserve_stock=1, - ) - - plan.get_sub_assembly_items() - plan.set("mr_items", []) - mr_items = get_items_for_material_requests(plan.as_dict()) - for d in mr_items: - plan.append("mr_items", d) - - plan.save() - - self.assertTrue(len(plan.sub_assembly_items) == 3) - for row in plan.sub_assembly_items: - self.assertEqual(row.required_qty, 15.0) - self.assertEqual(row.qty, 10.0) - - self.assertTrue(len(plan.mr_items) == 3) - for row in plan.mr_items: - self.assertEqual(row.required_bom_qty, 10.0) - self.assertEqual(row.quantity, 5.0) - - plan.submit() - - sre = StockReservation(plan) - reserved_entries = sre.get_reserved_entries("Production Plan", plan.name) - self.assertTrue(len(reserved_entries) == 6) - - for row in reserved_entries: - self.assertEqual(row.reserved_qty, 5.0) - - plan.submit_material_request = 1 - plan.make_material_request() - plan.make_work_order() - - material_requests = frappe.get_all( - "Material Request", filters={"production_plan": plan.name}, pluck="name" - ) - - additional_batches = [] - - for item_code in [ - "Batch Sub Assembly For SR 1", - "Batch Sub Assembly For SR 2", - "Batch Sub Assembly For SR 3", - "Batch Raw Material For SR 1", - "Batch Raw Material For SR 2", - "Batch Raw Material For SR 3", - ]: - se = make_stock_entry(item_code=item_code, target=warehouse, qty=5, basic_rate=100) - batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle) - additional_batches.append(batch_no) - - self.assertTrue(additional_batches) - - self.assertTrue(len(material_requests) > 0) - for mr_name in list(set(material_requests)): - po = make_purchase_order(mr_name) - po.supplier = "_Test Supplier" - po.submit() - - pr = make_purchase_receipt(po.name) - pr.submit() - - sre = StockReservation(plan) - reserved_entries = sre.get_reserved_entries("Production Plan", plan.name) - self.assertTrue(len(reserved_entries) == 9) - batches_reserved_for_pp = frappe.get_all( - "Serial and Batch Entry", - filters={"parent": ("in", [x.name for x in reserved_entries]), "docstatus": 1}, - pluck="batch_no", - ) - - work_orders = frappe.get_all("Work Order", filters={"production_plan": plan.name}, pluck="name") - for wo_name in list(set(work_orders)): - wo_doc = frappe.get_doc("Work Order", wo_name) - self.assertEqual(wo_doc.reserve_stock, 1) - - wo_doc.source_warehouse = warehouse - wo_doc.wip_warehouse = warehouse - wo_doc.fg_warehouse = warehouse - wo_doc.submit() - - sre = StockReservation(wo_doc) - reserved_entries = sre.get_reserved_entries("Work Order", wo_doc.name) - batches_reserved_for_wo = frappe.get_all( - "Serial and Batch Entry", - filters={"parent": ("in", [x.name for x in reserved_entries]), "docstatus": 1}, - pluck="batch_no", - ) - - for batch_no in batches_reserved_for_wo: - self.assertTrue(batch_no in batches_reserved_for_pp) - self.assertFalse(batch_no in additional_batches) - - if wo_doc.production_item == "Finished Good For SR": - self.assertEqual(len(reserved_entries), 3) - else: - # For raw materials 2 stock reservation entries - # 5 qty was present already in stock and 5 added from new PO - self.assertEqual(len(reserved_entries), 2) - - sre = StockReservation(plan) - reserved_entries = sre.get_reserved_entries("Production Plan", plan.name) - self.assertTrue(len(reserved_entries) == 0) - frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 0) - def test_production_plan_for_partial_sub_assembly_items(self): from erpnext.controllers.status_updater import OverAllowanceError from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom @@ -2163,7 +1750,6 @@ class TestProductionPlan(FrappeTestCase): for row in plan.sub_assembly_items: self.assertEqual(row.ordered_qty, 10.0) ->>>>>>> 384f4e120a (fix: do not create repeat work orders) def create_production_plan(**args): """ From c2d7e8c471d17fd9a333f9a84d68b8d780ef0beb Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 10 Jun 2025 14:38:18 +0530 Subject: [PATCH 46/49] chore: fix conflicts --- .../production_plan_sub_assembly_item.json | 29 +------------------ 1 file changed, 1 insertion(+), 28 deletions(-) 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 76d52f12bcd..b5f6a3ab065 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 @@ -205,28 +205,6 @@ "fieldtype": "Float", "label": "Produced Qty", "read_only": 1 -<<<<<<< HEAD -======= - }, - { - "columns": 2, - "fieldname": "required_qty", - "fieldtype": "Float", - "in_list_view": 1, - "label": "Required Qty" - }, - { - "fieldname": "subcontracting_section", - "fieldtype": "Section Break", - "label": "Subcontracting" - }, - { - "fieldname": "stock_reserved_qty", - "fieldtype": "Float", - "label": "Stock Reserved Qty", - "no_copy": 1, - "print_hide": 1, - "read_only": 1 }, { "fieldname": "ordered_qty", @@ -234,17 +212,12 @@ "label": "Ordered Qty", "no_copy": 1, "read_only": 1 ->>>>>>> 384f4e120a (fix: do not create repeat work orders) } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], -<<<<<<< HEAD - "modified": "2024-02-27 13:45:17.422435", -======= "modified": "2025-06-10 13:36:24.759101", ->>>>>>> 384f4e120a (fix: do not create repeat work orders) "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Plan Sub Assembly Item", @@ -255,4 +228,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} From 66b0426155b4080507bdb9dab4453ffeaf84664d Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 10 Jun 2025 18:07:28 +0530 Subject: [PATCH 47/49] feat: populate Timer dialog project field from Timesheet parent_project (backport #47971) (#48001) feat: populate Timer dialog project field from Timesheet parent_project (#47971) * feat: default parent project in timer dialog > project * chore: fix formatting * fix: remove unnecessary or condition --------- (cherry picked from commit bc876092646689c4c14c28a6b40b417d2a9cde1c) Co-authored-by: Rahul Agrawal <12agrawalrahul@gmail.com> Co-authored-by: Rahul Agrawal --- erpnext/public/js/projects/timer.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/public/js/projects/timer.js b/erpnext/public/js/projects/timer.js index 8370cc6ffff..3000320644e 100644 --- a/erpnext/public/js/projects/timer.js +++ b/erpnext/public/js/projects/timer.js @@ -18,7 +18,6 @@ erpnext.timesheet.timer = function (frm, row, timestamp = 0) { { fieldtype: "HTML", fieldname: "timer_html" }, ], }); - if (row) { dialog.set_values({ activity_type: row.activity_type, @@ -26,6 +25,10 @@ erpnext.timesheet.timer = function (frm, row, timestamp = 0) { task: row.task, expected_hours: row.expected_hours, }); + } else { + dialog.set_values({ + project: frm.doc.parent_project, + }); } dialog.get_field("timer_html").$wrapper.append(get_timer_html()); function get_timer_html() { From 8156d89903f7f30accca6bc10e1305a0e2f60be0 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 10 Jun 2025 17:36:47 +0530 Subject: [PATCH 48/49] fix: incorrect warehouse in MR (cherry picked from commit 2b9ca79291400205cedb338208e1f7348d051b43) # Conflicts: # erpnext/manufacturing/doctype/production_plan/production_plan.py --- .../production_plan/production_plan.py | 27 +++++++++++++++++++ .../doctype/sales_order/sales_order.py | 7 ++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index c6b6b7e1d9d..a74fb51d33f 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -1251,6 +1251,7 @@ def get_subitems( item_default.default_warehouse, item.purchase_uom, item_uom.conversion_factor, + bom.item.as_("main_bom_item"), ) .where( (bom.name == bom_no) @@ -1342,6 +1343,7 @@ def get_material_request_items( get_conversion_factor(row.item_code, item_details.purchase_uom).get("conversion_factor") or 1.0 ) +<<<<<<< HEAD if required_qty > 0: return { "item_code": row.item_code, @@ -1365,6 +1367,31 @@ def get_material_request_items( "description": row.get("description"), "uom": row.get("purchase_uom") or row.get("stock_uom"), } +======= + return { + "item_code": row.item_code, + "item_name": row.item_name, + "quantity": required_qty / conversion_factor, + "conversion_factor": conversion_factor, + "required_bom_qty": total_qty, + "stock_uom": row.get("stock_uom"), + "warehouse": warehouse + or row.get("source_warehouse") + or row.get("default_warehouse") + or item_group_defaults.get("default_warehouse"), + "safety_stock": row.safety_stock, + "actual_qty": bin_dict.get("actual_qty", 0), + "projected_qty": bin_dict.get("projected_qty", 0), + "ordered_qty": bin_dict.get("ordered_qty", 0), + "reserved_qty_for_production": bin_dict.get("reserved_qty_for_production", 0), + "min_order_qty": row["min_order_qty"], + "material_request_type": row.get("default_material_request_type"), + "sales_order": sales_order, + "description": row.get("description"), + "uom": row.get("purchase_uom") or row.get("stock_uom"), + "main_bom_item": row.get("main_bom_item"), + } +>>>>>>> 2b9ca79291 (fix: incorrect warehouse in MR) def get_sales_orders(self): diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index c7597c80fc6..66ffbba9e63 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -1630,6 +1630,11 @@ def make_raw_material_request(items, company, sales_order, project=None): items.update({"company": company, "sales_order": sales_order}) + item_wh = {} + for item in items.get("items"): + if item.get("warehouse"): + item_wh[item.get("item_code")] = item.get("warehouse") + raw_materials = get_items_for_material_requests(items) if not raw_materials: frappe.msgprint(_("Material Request not created, as quantity for Raw Materials already available.")) @@ -1654,7 +1659,7 @@ def make_raw_material_request(items, company, sales_order, project=None): "item_code": item.get("item_code"), "qty": item.get("quantity"), "schedule_date": schedule_date, - "warehouse": item.get("warehouse"), + "warehouse": item_wh.get(item.get("main_bom_item")) or item.get("warehouse"), "sales_order": sales_order, "project": project, }, From 60de0474a116ab4276a625f44f60a5539ea11036 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 10 Jun 2025 18:19:27 +0530 Subject: [PATCH 49/49] chore: fix conflicts --- .../production_plan/production_plan.py | 27 +------------------ 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index a74fb51d33f..e4ed92e7d3d 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -1343,7 +1343,6 @@ def get_material_request_items( get_conversion_factor(row.item_code, item_details.purchase_uom).get("conversion_factor") or 1.0 ) -<<<<<<< HEAD if required_qty > 0: return { "item_code": row.item_code, @@ -1366,32 +1365,8 @@ def get_material_request_items( "sales_order": sales_order, "description": row.get("description"), "uom": row.get("purchase_uom") or row.get("stock_uom"), + "main_bom_item": row.get("main_bom_item"), } -======= - return { - "item_code": row.item_code, - "item_name": row.item_name, - "quantity": required_qty / conversion_factor, - "conversion_factor": conversion_factor, - "required_bom_qty": total_qty, - "stock_uom": row.get("stock_uom"), - "warehouse": warehouse - or row.get("source_warehouse") - or row.get("default_warehouse") - or item_group_defaults.get("default_warehouse"), - "safety_stock": row.safety_stock, - "actual_qty": bin_dict.get("actual_qty", 0), - "projected_qty": bin_dict.get("projected_qty", 0), - "ordered_qty": bin_dict.get("ordered_qty", 0), - "reserved_qty_for_production": bin_dict.get("reserved_qty_for_production", 0), - "min_order_qty": row["min_order_qty"], - "material_request_type": row.get("default_material_request_type"), - "sales_order": sales_order, - "description": row.get("description"), - "uom": row.get("purchase_uom") or row.get("stock_uom"), - "main_bom_item": row.get("main_bom_item"), - } ->>>>>>> 2b9ca79291 (fix: incorrect warehouse in MR) def get_sales_orders(self):