From 673635e2c350440d7060a90d8bee050221552b09 Mon Sep 17 00:00:00 2001 From: Nikhil Kothari Date: Mon, 12 Jan 2026 00:42:52 +0530 Subject: [PATCH 01/24] fix(accounts): add missing accounting dimensions in advance taxes and charges (cherry picked from commit 22e9cb4cf4a252f7cfcdb1b534de87d5ae908a0f) # Conflicts: # erpnext/patches.txt --- erpnext/hooks.py | 1 + erpnext/patches.txt | 1 + ...e_accounting_dimensions_in_advance_taxes_and_charges.py | 7 +++++++ 3 files changed, 9 insertions(+) create mode 100644 erpnext/patches/v15_0/create_accounting_dimensions_in_advance_taxes_and_charges.py diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 3b0f338cf84..ca7efec8e58 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -569,6 +569,7 @@ accounting_dimension_doctypes = [ "Payment Request", "Asset Movement Item", "Asset Depreciation Schedule", + "Advance Taxes and Charges", ] get_matching_queries = ( diff --git a/erpnext/patches.txt b/erpnext/patches.txt index c82e257dc6c..6978d0a634e 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -458,3 +458,4 @@ erpnext.patches.v16_0.update_corrected_cancelled_status erpnext.patches.v16_0.fix_barcode_typo erpnext.patches.v16_0.set_post_change_gl_entries_on_pos_settings execute:frappe.delete_doc_if_exists("Workspace Sidebar", "Opening & Closing") +erpnext.patches.v15_0.create_accounting_dimensions_in_advance_taxes_and_charges diff --git a/erpnext/patches/v15_0/create_accounting_dimensions_in_advance_taxes_and_charges.py b/erpnext/patches/v15_0/create_accounting_dimensions_in_advance_taxes_and_charges.py new file mode 100644 index 00000000000..201b16b1e00 --- /dev/null +++ b/erpnext/patches/v15_0/create_accounting_dimensions_in_advance_taxes_and_charges.py @@ -0,0 +1,7 @@ +from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( + create_accounting_dimensions_for_doctype, +) + + +def execute(): + create_accounting_dimensions_for_doctype(doctype="Advance Taxes and Charges") From 5d5d208a49e3150fcda67d59136f21505f8b1602 Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Tue, 20 Jan 2026 00:44:59 +0530 Subject: [PATCH 02/24] fix(bank_account): validation for is_company_account (cherry picked from commit 7532ab01d6fc671939beafa38a973b4fac7d5079) --- .../doctype/bank_account/bank_account.js | 4 --- .../doctype/bank_account/bank_account.json | 4 ++- .../doctype/bank_account/bank_account.py | 36 ++++++++++--------- 3 files changed, 23 insertions(+), 21 deletions(-) diff --git a/erpnext/accounts/doctype/bank_account/bank_account.js b/erpnext/accounts/doctype/bank_account/bank_account.js index 202f750fb50..5173e0539f4 100644 --- a/erpnext/accounts/doctype/bank_account/bank_account.js +++ b/erpnext/accounts/doctype/bank_account/bank_account.js @@ -42,8 +42,4 @@ frappe.ui.form.on("Bank Account", { }); } }, - - is_company_account: function (frm) { - frm.set_df_property("account", "reqd", frm.doc.is_company_account); - }, }); diff --git a/erpnext/accounts/doctype/bank_account/bank_account.json b/erpnext/accounts/doctype/bank_account/bank_account.json index 9ecd9c53503..b44ccb56835 100644 --- a/erpnext/accounts/doctype/bank_account/bank_account.json +++ b/erpnext/accounts/doctype/bank_account/bank_account.json @@ -52,6 +52,7 @@ "fieldtype": "Link", "in_list_view": 1, "label": "Company Account", + "mandatory_depends_on": "is_company_account", "options": "Account" }, { @@ -98,6 +99,7 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Company", + "mandatory_depends_on": "is_company_account", "options": "Company" }, { @@ -252,7 +254,7 @@ "link_fieldname": "default_bank_account" } ], - "modified": "2025-08-29 12:32:01.081687", + "modified": "2026-01-20 00:46:16.633364", "modified_by": "Administrator", "module": "Accounts", "name": "Bank Account", diff --git a/erpnext/accounts/doctype/bank_account/bank_account.py b/erpnext/accounts/doctype/bank_account/bank_account.py index d8dc1191bf7..c0dc6467f8f 100644 --- a/erpnext/accounts/doctype/bank_account/bank_account.py +++ b/erpnext/accounts/doctype/bank_account/bank_account.py @@ -51,25 +51,29 @@ class BankAccount(Document): delete_contact_and_address("Bank Account", self.name) def validate(self): - self.validate_company() - self.validate_account() + self.validate_is_company_account() self.update_default_bank_account() - def validate_account(self): - if self.account: - if accounts := frappe.db.get_all( - "Bank Account", filters={"account": self.account, "name": ["!=", self.name]}, as_list=1 - ): - frappe.throw( - _("'{0}' account is already used by {1}. Use another account.").format( - frappe.bold(self.account), - frappe.bold(comma_and([get_link_to_form(self.doctype, x[0]) for x in accounts])), - ) - ) + def validate_is_company_account(self): + if self.is_company_account: + if not self.company: + frappe.throw(_("Company is mandatory for company account")) - def validate_company(self): - if self.is_company_account and not self.company: - frappe.throw(_("Company is mandatory for company account")) + if not self.account: + frappe.throw(_("Company Account is mandatory")) + + self.validate_account() + + def validate_account(self): + if accounts := frappe.db.get_all( + "Bank Account", filters={"account": self.account, "name": ["!=", self.name]}, as_list=1 + ): + frappe.throw( + _("'{0}' account is already used by {1}. Use another account.").format( + frappe.bold(self.account), + frappe.bold(comma_and([get_link_to_form(self.doctype, x[0]) for x in accounts])), + ) + ) def update_default_bank_account(self): if self.is_default and not self.disabled: From 3ac431bd5008b64c27e0ae1517b9c2f23ff0beac Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 20 Jan 2026 17:12:31 +0530 Subject: [PATCH 03/24] perf: prevent duplicate reposting for the same item (cherry picked from commit 753593157183c0dcb7945255809c6d2c8be3f007) --- erpnext/stock/stock_ledger.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 7a9a2ad273a..05a9c1adae3 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -758,7 +758,8 @@ class update_entries_after: self.distinct_item_warehouses[key] = val self.new_items_found = True elif ( - dependant_sle.actual_qty > 0 + self.via_landed_cost_voucher + and dependant_sle.actual_qty > 0 and dependant_sle.voucher_type == "Stock Entry" and is_transfer_stock_entry(dependant_sle.voucher_no) ): From fef6df709d631fb72251207187d8690870f157da Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 20 Jan 2026 15:08:37 +0530 Subject: [PATCH 04/24] fix: allow creation of DN in SI for items not having DN reference (cherry picked from commit b691de0147cd6b5f19fa3fd9582fb18d97b74371) --- .../accounts/doctype/sales_invoice/sales_invoice.js | 10 ++-------- .../accounts/doctype/sales_invoice/sales_invoice.py | 4 +++- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index c50eaa69a9f..caafdcedc23 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -115,15 +115,9 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends ( } if (cint(doc.update_stock) != 1) { - // show Make Delivery Note button only if Sales Invoice is not created from Delivery Note - var from_delivery_note = false; - from_delivery_note = this.frm.doc.items.some(function (item) { - return item.delivery_note ? true : false; - }); - - if (!from_delivery_note && !is_delivered_by_supplier) { + if (!is_delivered_by_supplier) { this.frm.add_custom_button( - __("Delivery"), + __("Delivery Note"), this.frm.cscript["Make Delivery Note"], __("Create") ); diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index e987a5ad099..befa1ddea24 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -2422,7 +2422,9 @@ def make_delivery_note(source_name, target_doc=None): "cost_center": "cost_center", }, "postprocess": update_item, - "condition": lambda doc: doc.delivered_by_supplier != 1 and not doc.scio_detail, + "condition": lambda doc: doc.delivered_by_supplier != 1 + and not doc.scio_detail + and not doc.dn_detail, }, "Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "reset_value": True}, "Sales Team": { From c9d7c6cd424ccf828d419a362b20f4a9d63be2a1 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 20 Jan 2026 12:56:27 +0000 Subject: [PATCH 05/24] fix: continuous raw material consumption with bom validation (backport #51914) (#51919) Co-authored-by: Mihir Kandoi --- .../doctype/work_order/work_order.js | 2 +- .../stock/doctype/stock_entry/stock_entry.py | 31 +++++++++++++++++-- .../doctype/stock_entry/test_stock_entry.py | 27 ++++++++++++++++ 3 files changed, 57 insertions(+), 3 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index 0d99a923a00..e816c4690df 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -829,7 +829,7 @@ erpnext.work_order = { } } if (counter > 0) { - var consumption_btn = frm.add_custom_button( + frm.add_custom_button( __("Material Consumption"), function () { const backflush_raw_materials_based_on = diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 5d973cdc3a0..ed3c0d7658f 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -938,7 +938,9 @@ class StockEntry(StockController, SubcontractingInwardController): if matched_item := self.get_matched_items(item_code): if flt(details.get("qty"), precision) != flt(matched_item.qty, precision): frappe.throw( - _("For the item {0}, the quantity should be {1} according to the BOM {2}.").format( + _( + "For the item {0}, the consumed quantity should be {1} according to the BOM {2}." + ).format( frappe.bold(item_code), flt(details.get("qty")), get_link_to_form("BOM", self.bom_no), @@ -1003,12 +1005,37 @@ class StockEntry(StockController, SubcontractingInwardController): ) def get_matched_items(self, item_code): - for row in self.items: + items = [item for item in self.items if item.s_warehouse] + for row in items or self.get_consumed_items(): if row.item_code == item_code or row.original_item == item_code: return row return {} + def get_consumed_items(self): + """Get all raw materials consumed through consumption entries""" + parent = frappe.qb.DocType("Stock Entry") + child = frappe.qb.DocType("Stock Entry Detail") + + query = ( + frappe.qb.from_(parent) + .join(child) + .on(parent.name == child.parent) + .select( + child.item_code, + Sum(child.qty).as_("qty"), + child.original_item, + ) + .where( + (parent.docstatus == 1) + & (parent.purpose == "Material Consumption for Manufacture") + & (parent.work_order == self.work_order) + ) + .groupby(child.item_code, child.original_item) + ) + + return query.run(as_dict=True) + @frappe.whitelist() def get_stock_and_rate(self): """ diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 1bcfc567fda..ced7d946f6f 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -2362,6 +2362,33 @@ class TestStockEntry(IntegrationTestCase): self.assertEqual(target_sabb.entries[0].batch_no, batch) self.assertEqual([entry.serial_no for entry in target_sabb.entries], serial_nos[:2]) + @IntegrationTestCase.change_settings( + "Manufacturing Settings", + { + "material_consumption": 1, + "backflush_raw_materials_based_on": "BOM", + "validate_components_quantities_per_bom": 1, + }, + ) + def test_validation_as_per_bom_with_continuous_raw_material_consumption(self): + from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom + from erpnext.manufacturing.doctype.work_order.work_order import make_stock_entry as _make_stock_entry + from erpnext.manufacturing.doctype.work_order.work_order import make_work_order + + fg_item = make_item("_Mobiles", properties={"is_stock_item": 1}).name + rm_item1 = make_item("_Battery", properties={"is_stock_item": 1}).name + warehouse = "Stores - WP" + bom_no = make_bom(item=fg_item, raw_materials=[rm_item1]).name + make_stock_entry(item_code=rm_item1, target=warehouse, qty=5, rate=10, purpose="Material Receipt") + + work_order = make_work_order(bom_no, fg_item, 5) + work_order.skip_transfer = 1 + work_order.fg_warehouse = warehouse + work_order.submit() + + frappe.get_doc(_make_stock_entry(work_order.name, "Material Consumption for Manufacture", 5)).submit() + frappe.get_doc(_make_stock_entry(work_order.name, "Manufacture", 5)).submit() + def make_serialized_item(self, **args): args = frappe._dict(args) From 72cdddbeda50c1faf3fe66e9cb9724eb8288e9b6 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 20 Jan 2026 19:30:42 +0530 Subject: [PATCH 06/24] Revert "perf: prevent duplicate reposting for the same item" (cherry picked from commit 6e4b90055f6058ec6ca65d0a858a498cf869cf16) --- erpnext/stock/stock_ledger.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 05a9c1adae3..7a9a2ad273a 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -758,8 +758,7 @@ class update_entries_after: self.distinct_item_warehouses[key] = val self.new_items_found = True elif ( - self.via_landed_cost_voucher - and dependant_sle.actual_qty > 0 + dependant_sle.actual_qty > 0 and dependant_sle.voucher_type == "Stock Entry" and is_transfer_stock_entry(dependant_sle.voucher_no) ): From 176096bc5b028da80c6a318698b9981711d8b631 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 20 Jan 2026 20:18:59 +0530 Subject: [PATCH 07/24] fix: validation message in stock reco row idx (cherry picked from commit 3960c01798a963d0b99a650f626bfc15bacc082f) --- .../stock_reconciliation.py | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 59683e87e3d..64ac2a94928 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -75,6 +75,7 @@ class StockReconciliation(StockController): self.validate_duplicate_serial_and_batch_bundle("items") self.remove_items_with_no_change() self.validate_data() + self.change_row_indexes() self.validate_expense_account() self.validate_customer_provided_item() self.set_zero_value_for_customer_provided_items() @@ -556,8 +557,7 @@ class StockReconciliation(StockController): elif len(items) != len(self.items): self.items = items - for i, item in enumerate(self.items): - item.idx = i + 1 + self.change_idx = True frappe.msgprint(_("Removed items with no change in quantity or value.")) def calculate_difference_amount(self, item, item_dict): @@ -574,14 +574,14 @@ class StockReconciliation(StockController): def validate_data(self): def _get_msg(row_num, msg): - return _("Row # {0}:").format(row_num + 1) + " " + msg + return _("Row #{0}:").format(row_num) + " " + msg self.validation_messages = [] item_warehouse_combinations = [] default_currency = frappe.db.get_default("currency") - for row_num, row in enumerate(self.items): + for row in self.items: # find duplicates key = [row.item_code, row.warehouse] for field in ["serial_no", "batch_no"]: @@ -594,7 +594,7 @@ class StockReconciliation(StockController): if key in item_warehouse_combinations: self.validation_messages.append( - _get_msg(row_num, _("Same item and warehouse combination already entered.")) + _get_msg(row.idx, _("Same item and warehouse combination already entered.")) ) else: item_warehouse_combinations.append(key) @@ -604,7 +604,7 @@ class StockReconciliation(StockController): if row.serial_no and not row.qty: self.validation_messages.append( _get_msg( - row_num, + row.idx, f"Quantity should not be zero for the {bold(row.item_code)} since serial nos are specified", ) ) @@ -612,17 +612,17 @@ class StockReconciliation(StockController): # if both not specified if row.qty in ["", None] and row.valuation_rate in ["", None]: self.validation_messages.append( - _get_msg(row_num, _("Please specify either Quantity or Valuation Rate or both")) + _get_msg(row.idx, _("Please specify either Quantity or Valuation Rate or both")) ) # do not allow negative quantity if flt(row.qty) < 0: - self.validation_messages.append(_get_msg(row_num, _("Negative Quantity is not allowed"))) + self.validation_messages.append(_get_msg(row.idx, _("Negative Quantity is not allowed"))) # do not allow negative valuation if flt(row.valuation_rate) < 0: self.validation_messages.append( - _get_msg(row_num, _("Negative Valuation Rate is not allowed")) + _get_msg(row.idx, _("Negative Valuation Rate is not allowed")) ) if row.qty and row.valuation_rate in ["", None]: @@ -654,6 +654,11 @@ class StockReconciliation(StockController): raise frappe.ValidationError(self.validation_messages) + def change_row_indexes(self): + if getattr(self, "change_idx", False): + for i, item in enumerate(self.items): + item.idx = i + 1 + def validate_item(self, item_code, row): from erpnext.stock.doctype.item.item import ( validate_cancelled_item, From 504c84e28abff9c5381f9fc3986fdfa83c02acea Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 21 Jan 2026 12:50:50 +0530 Subject: [PATCH 08/24] fix: warehouse permissions in MR incorrectly ignored (cherry picked from commit 5bacb67d36b69e51b820760bd7c6fd487a0f6291) --- erpnext/stock/doctype/material_request/material_request.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/material_request/material_request.json b/erpnext/stock/doctype/material_request/material_request.json index b34bc7ded7d..8c9aff89c73 100644 --- a/erpnext/stock/doctype/material_request/material_request.json +++ b/erpnext/stock/doctype/material_request/material_request.json @@ -282,7 +282,6 @@ { "fieldname": "set_warehouse", "fieldtype": "Link", - "ignore_user_permissions": 1, "in_list_view": 1, "label": "Set Target Warehouse", "options": "Warehouse" @@ -378,7 +377,7 @@ "idx": 70, "is_submittable": 1, "links": [], - "modified": "2026-01-10 15:34:59.000603", + "modified": "2026-01-21 12:48:40.792323", "modified_by": "Administrator", "module": "Stock", "name": "Material Request", From 7c2bbe0d824073fed2668935eba29d70e71481ac Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 21 Jan 2026 11:46:00 +0530 Subject: [PATCH 09/24] fix: job cards should not be deleted on close of WO (cherry picked from commit c919b1de3836bde91a11b61adaedd93322c758c8) --- erpnext/manufacturing/doctype/work_order/work_order.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index faa6b08398d..57bda11fbc4 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -770,6 +770,7 @@ class WorkOrder(Document): self.db_set("status", "Cancelled") self.on_close_or_cancel() + self.delete_job_card() def on_close_or_cancel(self): if self.production_plan and frappe.db.exists( @@ -779,7 +780,6 @@ class WorkOrder(Document): else: self.update_work_order_qty_in_so() - self.delete_job_card() self.update_completed_qty_in_material_request() self.update_planned_qty() self.update_ordered_qty() From 91199ea9c93d680510f31ab7f7aa78a71f9e656c Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 21 Jan 2026 15:30:55 +0530 Subject: [PATCH 10/24] fix: force user to enter batch or serial for serial/batch items (cherry picked from commit 7170a1bd78232c39dd67eea1291c430d4ba8b0d2) --- .../stock_reconciliation/stock_reconciliation.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 64ac2a94928..f0e31b9a6bf 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -666,6 +666,16 @@ class StockReconciliation(StockController): validate_is_stock_item, ) + def validate_serial_batch_items(): + has_batch_no, has_serial_no = frappe.get_value( + "Item", item_code, ["has_batch_no", "has_serial_no"] + ) + if row.use_serial_batch_fields: + if has_batch_no and not row.batch_no: + raise frappe.ValidationError(_("Please enter Batch No")) + if has_serial_no and not row.serial_no: + raise frappe.ValidationError(_("Please enter Serial No")) + # using try except to catch all validation msgs and display together try: @@ -674,12 +684,13 @@ class StockReconciliation(StockController): # end of life and stock item validate_end_of_life(item_code, item.end_of_life, item.disabled) validate_is_stock_item(item_code, item.is_stock_item) + validate_serial_batch_items() # docstatus should be < 2 validate_cancelled_item(item_code, item.docstatus) except Exception as e: - self.validation_messages.append(_("Row #") + " " + ("%d: " % (row.idx)) + cstr(e)) + self.validation_messages.append(_("Row #") + ("%d: " % (row.idx)) + cstr(e)) def validate_reserved_stock(self) -> None: """Raises an exception if there is any reserved stock for the items in the Stock Reconciliation.""" From 6fa60d2f1a69252b1dcefd03a344e5723f8d31ac Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 21 Jan 2026 15:58:46 +0530 Subject: [PATCH 11/24] fix: tests (cherry picked from commit 035b3cb61e89ae6075597b451401f619b3084dd2) --- .../stock/doctype/stock_reconciliation/stock_reconciliation.py | 2 +- .../doctype/stock_reconciliation/test_stock_reconciliation.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index f0e31b9a6bf..59acb04e8ea 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -670,7 +670,7 @@ class StockReconciliation(StockController): has_batch_no, has_serial_no = frappe.get_value( "Item", item_code, ["has_batch_no", "has_serial_no"] ) - if row.use_serial_batch_fields: + if row.use_serial_batch_fields and self.purpose == "Stock Reconciliation": if has_batch_no and not row.batch_no: raise frappe.ValidationError(_("Please enter Batch No")) if has_serial_no and not row.serial_no: diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 2500b521017..40f372f2bc7 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -1450,6 +1450,7 @@ class TestStockReconciliation(IntegrationTestCase, StockTestMixin): qty=10, rate=100, use_serial_batch_fields=1, + purpose="Opening Stock", ) sr.reload() @@ -1592,6 +1593,7 @@ class TestStockReconciliation(IntegrationTestCase, StockTestMixin): qty=10, rate=80, use_serial_batch_fields=1, + purpose="Opening Stock", ) batch_no = get_batch_from_bundle(reco.items[0].serial_and_batch_bundle) @@ -1676,6 +1678,7 @@ class TestStockReconciliation(IntegrationTestCase, StockTestMixin): qty=10, rate=100, use_serial_batch_fields=1, + purpose="Opening Stock", ) sr.reload() From 30e6b5daac923dc0977ab897ab5641e3bf499d0d Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 21 Jan 2026 17:13:35 +0530 Subject: [PATCH 12/24] fix: create DN btn should not be shown if it cannot be created (cherry picked from commit 70ec977cb25a8217b4ec361fac0917a4874c7fea) --- .../doctype/sales_invoice/sales_invoice.js | 17 +++++++++++++---- .../doctype/sales_invoice/sales_invoice.py | 3 ++- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index caafdcedc23..0ba6feef6da 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -116,11 +116,20 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends ( if (cint(doc.update_stock) != 1) { if (!is_delivered_by_supplier) { - this.frm.add_custom_button( - __("Delivery Note"), - this.frm.cscript["Make Delivery Note"], - __("Create") + const should_create_delivery_note = doc.items.some( + (item) => + item.qty - item.delivered_qty > 0 && + !item.scio_detail && + !item.dn_detail && + !item.delivered_by_supplier ); + if (should_create_delivery_note) { + this.frm.add_custom_button( + __("Delivery Note"), + this.frm.cscript["Make Delivery Note"], + __("Create") + ); + } } } diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index befa1ddea24..a87ad06564e 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -2424,7 +2424,8 @@ def make_delivery_note(source_name, target_doc=None): "postprocess": update_item, "condition": lambda doc: doc.delivered_by_supplier != 1 and not doc.scio_detail - and not doc.dn_detail, + and not doc.dn_detail + and doc.qty - doc.delivered_qty > 0, }, "Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "reset_value": True}, "Sales Team": { From c7c7a55a58b8721023b4ef716776edaf90cba4e7 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 21 Jan 2026 22:33:24 +0530 Subject: [PATCH 13/24] fix: rejected qty in PR doesn't consider conversion factor (cherry picked from commit 343ee9695b6992c613c89ab61279238b458ccaa6) --- erpnext/controllers/stock_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 916d9865662..aa09f0ca956 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -552,7 +552,7 @@ class StockController(AccountsController): if is_rejected: serial_nos = row.get("rejected_serial_no") type_of_transaction = "Inward" if not self.is_return else "Outward" - qty = row.get("rejected_qty") + qty = row.get("rejected_qty") * row.get("conversion_factor", 1.0) warehouse = row.get("rejected_warehouse") if ( From 37a237dbb753bab8db734dce5da55c485895f85e Mon Sep 17 00:00:00 2001 From: ravibharathi656 Date: Wed, 21 Jan 2026 19:53:41 +0530 Subject: [PATCH 14/24] fix(project): add missing counter to project update naming series (cherry picked from commit 49e64f4e1c324a6e520e95151c9f8ecf240ec4fb) --- erpnext/projects/doctype/project/project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index c7023dd9423..08a1c0433ad 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -603,7 +603,7 @@ def send_project_update_email_to_users(project): "sent": 0, "date": today(), "time": nowtime(), - "naming_series": "UPDATE-.project.-.YY.MM.DD.-", + "naming_series": "UPDATE-.project.-.YY.MM.DD.-.####", } ).insert() From b1716bfeeffe6d704a59d2c20e2959b4dd05624e Mon Sep 17 00:00:00 2001 From: SowmyaArunachalam Date: Wed, 21 Jan 2026 17:37:37 +0530 Subject: [PATCH 15/24] fix(customer): add customer group filters (cherry picked from commit 1e3db9f91689d26c0d227b57b246b1a74ea9d8a4) --- erpnext/selling/doctype/customer/customer.json | 3 ++- erpnext/selling/doctype/selling_settings/selling_settings.json | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json index 72798f32329..babd09a5591 100644 --- a/erpnext/selling/doctype/customer/customer.json +++ b/erpnext/selling/doctype/customer/customer.json @@ -183,6 +183,7 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Customer Group", + "link_filters": "[[\"Customer Group\", \"is_group\", \"=\", 0]]", "oldfieldname": "customer_group", "oldfieldtype": "Link", "options": "Customer Group", @@ -625,7 +626,7 @@ "link_fieldname": "party" } ], - "modified": "2026-01-16 15:56:05.967663", + "modified": "2026-01-21 17:23:42.151114", "modified_by": "Administrator", "module": "Selling", "name": "Customer", diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json index 6cb3b4fbb3d..1b88bf79ac4 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.json +++ b/erpnext/selling/doctype/selling_settings/selling_settings.json @@ -61,6 +61,7 @@ "fieldtype": "Link", "in_list_view": 1, "label": "Default Customer Group", + "link_filters": "[[\"Customer Group\", \"is_group\", \"=\", 0]]", "options": "Customer Group" }, { @@ -297,7 +298,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2026-01-02 18:17:05.734945", + "modified": "2026-01-21 17:28:37.027837", "modified_by": "Administrator", "module": "Selling", "name": "Selling Settings", From 7120fbd14bb593e4d4c012b65138cbdba7f7fd38 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Tue, 20 Jan 2026 18:48:59 +0530 Subject: [PATCH 16/24] fix: calculate weighted average rate for customer provided items in subcontracting inward order (cherry picked from commit 37ee560eaea0db11e5890cf26ecf1b630204ded3) --- .../subcontracting_inward_controller.py | 14 ++++-- .../test_subcontracting_inward_order.py | 43 +++++++++++++++++++ 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/erpnext/controllers/subcontracting_inward_controller.py b/erpnext/controllers/subcontracting_inward_controller.py index 056bfcdec9d..1a3ff66b825 100644 --- a/erpnext/controllers/subcontracting_inward_controller.py +++ b/erpnext/controllers/subcontracting_inward_controller.py @@ -720,6 +720,7 @@ class SubcontractingInwardController: item.db_set("scio_detail", scio_rm.name) if data: + precision = self.precision("customer_provided_item_cost", "items") result = frappe.get_all( "Subcontracting Inward Order Received Item", filters={ @@ -734,10 +735,17 @@ class SubcontractingInwardController: table = frappe.qb.DocType("Subcontracting Inward Order Received Item") case_expr_qty, case_expr_rate = Case(), Case() for d in result: - d.received_qty += ( - data[d.name].transfer_qty if self._action == "submit" else -data[d.name].transfer_qty + current_qty = flt(data[d.name].transfer_qty) * (1 if self._action == "submit" else -1) + current_rate = flt(data[d.name].rate) + + # Calculate weighted average rate + old_total = d.rate * d.received_qty + current_total = current_rate * current_qty + + d.received_qty = d.received_qty + current_qty + d.rate = ( + flt((old_total + current_total) / d.received_qty, precision) if d.received_qty else 0.0 ) - d.rate += data[d.name].rate if self._action == "submit" else -data[d.name].rate if not d.required_qty and not d.received_qty: deleted_docs.append(d.name) diff --git a/erpnext/subcontracting/doctype/subcontracting_inward_order/test_subcontracting_inward_order.py b/erpnext/subcontracting/doctype/subcontracting_inward_order/test_subcontracting_inward_order.py index 0e227ac4fa5..1d57660d6ef 100644 --- a/erpnext/subcontracting/doctype/subcontracting_inward_order/test_subcontracting_inward_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_inward_order/test_subcontracting_inward_order.py @@ -51,6 +51,49 @@ class IntegrationTestSubcontractingInwardOrder(IntegrationTestCase): for item in rm_in.get("items"): self.assertEqual(item.customer_provided_item_cost, 15) + def test_customer_provided_item_cost_with_multiple_receipts(self): + """ + Validate that rate is calculated correctly (Weighted Average) when multiple receipts + occur for the same SCIO Received Item. + """ + so, scio = create_so_scio() + rm_item = "Basic RM" + + # Receipt 1: 5 Qty @ Unit Cost 10 + rm_in_1 = frappe.new_doc("Stock Entry").update(scio.make_rm_stock_entry_inward()) + rm_in_1.items = [item for item in rm_in_1.items if item.item_code == rm_item] + rm_in_1.items[0].qty = 5 + rm_in_1.items[0].basic_rate = 10 + rm_in_1.items[0].transfer_qty = 5 + rm_in_1.submit() + + scio.reload() + received_item = next(item for item in scio.received_items if item.rm_item_code == rm_item) + self.assertEqual(received_item.rate, 10) + + # Receipt 2: 5 Qty @ Unit Cost 20 + rm_in_2 = frappe.new_doc("Stock Entry").update(scio.make_rm_stock_entry_inward()) + rm_in_2.items = [item for item in rm_in_2.items if item.item_code == rm_item] + rm_in_2.items[0].qty = 5 + rm_in_2.items[0].basic_rate = 20 + rm_in_2.items[0].transfer_qty = 5 + rm_in_2.save() + rm_in_2.submit() + + # Check 2: Rate should be Weighted Average + # (5 * 10 + 5 * 20) / 10 = 150 / 10 = 15 + scio.reload() + received_item = next(item for item in scio.received_items if item.rm_item_code == rm_item) + self.assertEqual(received_item.rate, 15) + + # Cancel Receipt 2: Rate should revert to original + # (15 * 10 - 20 * 5) / 5 = 50 / 5 = 10 + rm_in_2.cancel() + scio.reload() + received_item = next(item for item in scio.received_items if item.rm_item_code == rm_item) + self.assertEqual(received_item.received_qty, 5) + self.assertEqual(received_item.rate, 10) + def test_add_extra_customer_provided_item(self): so, scio = create_so_scio() From 264855e5e1b3e812c1495bbb3bb40c0d5e78d418 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Tue, 20 Jan 2026 17:33:01 +0530 Subject: [PATCH 17/24] fix: throw if item order field is not set in subcontracting controller (cherry picked from commit d256365f4a49f195ff9e98587442b49f3d2a6f27) --- .../controllers/subcontracting_controller.py | 59 ++++++++++++------- 1 file changed, 37 insertions(+), 22 deletions(-) diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index 33c1edbcb72..c825159df71 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -166,29 +166,46 @@ class SubcontractingController(StockController): _("Row {0}: Item {1} must be a subcontracted item.").format(item.idx, item.item_name) ) - if self.doctype != "Subcontracting Receipt" and item.qty > flt( - get_pending_subcontracted_quantity( - self.doctype, - self.purchase_order if self.doctype == "Subcontracting Order" else self.sales_order, - ).get( - item.purchase_order_item - if self.doctype == "Subcontracting Order" - else item.sales_order_item - ) - / item.subcontracting_conversion_factor, - frappe.get_precision( + if self.doctype != "Subcontracting Receipt": + order_item_doctype = ( "Purchase Order Item" if self.doctype == "Subcontracting Order" - else "Sales Order Item", - "qty", - ), - ): - frappe.throw( - _( - "Row {0}: Item {1}'s quantity cannot be higher than the available quantity." - ).format(item.idx, item.item_name) + else "Sales Order Item" ) + order_name = ( + self.purchase_order if self.doctype == "Subcontracting Order" else self.sales_order + ) + order_item_field = frappe.scrub(order_item_doctype) + + if not item.get(order_item_field): + frappe.throw( + _("Row {0}: Item {1} must be linked to a {2}.").format( + item.idx, item.item_name, order_item_doctype + ) + ) + + pending_qty = flt( + flt( + get_pending_subcontracted_quantity( + order_item_doctype, + order_name, + ).get(item.get(order_item_field)) + ) + / item.subcontracting_conversion_factor, + frappe.get_precision( + order_item_doctype, + "qty", + ), + ) + + if item.qty > pending_qty: + frappe.throw( + _( + "Row {0}: Item {1}'s quantity cannot be higher than the available quantity." + ).format(item.idx, item.item_name) + ) + if self.doctype != "Subcontracting Inward Order": item.amount = item.qty * item.rate @@ -1333,9 +1350,7 @@ def get_item_details(items): def get_pending_subcontracted_quantity(doctype, name): - table = frappe.qb.DocType( - "Purchase Order Item" if doctype == "Subcontracting Order" else "Sales Order Item" - ) + table = frappe.qb.DocType(doctype) query = ( frappe.qb.from_(table) .select(table.name, table.stock_qty, table.subcontracted_qty) From 881562fc37fe9603e74cb246ba9c3f4127eb32f3 Mon Sep 17 00:00:00 2001 From: Sudharsanan11 Date: Thu, 22 Jan 2026 12:54:51 +0530 Subject: [PATCH 18/24] fix: autofill warehouse for packed items (cherry picked from commit 3f8a0a483355509360d524cae0debc1b2f8a8b27) --- erpnext/public/js/controllers/transaction.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 54474305643..4ae844c6116 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -3131,10 +3131,16 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe set_warehouse() { this.autofill_warehouse(this.frm.doc.items, "warehouse", this.frm.doc.set_warehouse); + this.autofill_warehouse(this.frm.doc.packed_items, "warehouse", this.frm.doc.set_warehouse); } set_target_warehouse() { this.autofill_warehouse(this.frm.doc.items, "target_warehouse", this.frm.doc.set_target_warehouse); + this.autofill_warehouse( + this.frm.doc.packed_items, + "target_warehouse", + this.frm.doc.set_target_warehouse + ); } set_from_warehouse() { From fb3fb8ca5e21950a1e0566a8f577d73fc4eeb7a0 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 22 Jan 2026 23:56:06 +0530 Subject: [PATCH 19/24] fix: negative stock for purchae return (cherry picked from commit d68a04ad169cd08f34219bbdc31fee765a3e3f76) --- erpnext/stock/deprecated_serial_batch.py | 3 - .../purchase_receipt/test_purchase_receipt.py | 39 ++++++ .../serial_and_batch_bundle.py | 120 ++++++++++++++++-- erpnext/stock/serial_batch_bundle.py | 51 -------- 4 files changed, 148 insertions(+), 65 deletions(-) diff --git a/erpnext/stock/deprecated_serial_batch.py b/erpnext/stock/deprecated_serial_batch.py index 0fb3f048983..f383562e4e9 100644 --- a/erpnext/stock/deprecated_serial_batch.py +++ b/erpnext/stock/deprecated_serial_batch.py @@ -97,7 +97,6 @@ class DeprecatedBatchNoValuation: for ledger in entries: self.stock_value_differece[ledger.batch_no] += flt(ledger.batch_value) self.available_qty[ledger.batch_no] += flt(ledger.batch_qty) - self.total_qty[ledger.batch_no] += flt(ledger.batch_qty) @deprecated( "erpnext.stock.serial_batch_bundle.BatchNoValuation.get_sle_for_batches", @@ -271,7 +270,6 @@ class DeprecatedBatchNoValuation: batch_data = query.run(as_dict=True) for d in batch_data: self.available_qty[d.batch_no] += flt(d.batch_qty) - self.total_qty[d.batch_no] += flt(d.batch_qty) for d in batch_data: if self.available_qty.get(d.batch_no): @@ -383,7 +381,6 @@ class DeprecatedBatchNoValuation: batch_data = query.run(as_dict=True) for d in batch_data: self.available_qty[d.batch_no] += flt(d.batch_qty) - self.total_qty[d.batch_no] += flt(d.batch_qty) if not self.last_sle: return diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index b26bab3d1c4..b426b333c02 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -4997,6 +4997,45 @@ class TestPurchaseReceipt(IntegrationTestCase): self.assertEqual(frappe.parse_json(stock_queue), [[20, 0.0]]) + def test_negative_stock_error_for_purchase_return(self): + from erpnext.controllers.sales_and_purchase_return import make_return_doc + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + + item_code = make_item( + "Test Negative Stock for Purchase Return Item", + {"has_batch_no": 1, "create_new_batch": 1, "batch_number_series": "TNSFPRI.#####"}, + ).name + + pr = make_purchase_receipt( + item_code=item_code, + posting_date=add_days(today(), -3), + qty=10, + rate=100, + warehouse="_Test Warehouse - _TC", + ) + + batch_no = get_batch_from_bundle(pr.items[0].serial_and_batch_bundle) + + make_purchase_receipt( + item_code=item_code, + posting_date=add_days(today(), -4), + qty=10, + rate=100, + warehouse="_Test Warehouse - _TC", + ) + + make_stock_entry( + item_code=item_code, + qty=10, + source="_Test Warehouse - _TC", + target="_Test Warehouse 1 - _TC", + batch_no=batch_no, + use_serial_batch_fields=1, + ) + + return_pr = make_return_doc("Purchase Receipt", pr.name) + self.assertRaises(frappe.ValidationError, return_pr.submit) + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index fce52748283..464a56e68b4 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -576,14 +576,12 @@ class SerialandBatchBundle(Document): d.incoming_rate = abs(flt(sn_obj.batch_avg_rate.get(d.batch_no))) precision = d.precision("qty") - for field in ["available_qty", "total_qty"]: - value = getattr(sn_obj, field) - available_qty = flt(value.get(d.batch_no), precision) - if self.docstatus == 1: - available_qty += flt(d.qty, precision) + available_qty = flt(sn_obj.available_qty.get(d.batch_no), precision) + if self.docstatus == 1: + available_qty += flt(d.qty, precision) - if not allow_negative_stock: - self.validate_negative_batch(d.batch_no, available_qty, field) + if not allow_negative_stock: + self.validate_negative_batch(d.batch_no, available_qty) d.stock_value_difference = flt(d.qty) * flt(d.incoming_rate) @@ -596,8 +594,8 @@ class SerialandBatchBundle(Document): } ) - def validate_negative_batch(self, batch_no, available_qty, field=None): - if available_qty < 0 and not self.is_stock_reco_for_valuation_adjustment(available_qty, field=field): + def validate_negative_batch(self, batch_no, available_qty): + if available_qty < 0 and not self.is_stock_reco_for_valuation_adjustment(available_qty): msg = f"""Batch No {bold(batch_no)} of an Item {bold(self.item_code)} has negative stock of quantity {bold(available_qty)} in the @@ -605,7 +603,7 @@ class SerialandBatchBundle(Document): frappe.throw(_(msg), BatchNegativeStockError) - def is_stock_reco_for_valuation_adjustment(self, available_qty, field=None): + def is_stock_reco_for_valuation_adjustment(self, available_qty): if ( self.voucher_type == "Stock Reconciliation" and self.type_of_transaction == "Outward" @@ -613,7 +611,6 @@ class SerialandBatchBundle(Document): and ( abs(frappe.db.get_value("Stock Reconciliation Item", self.voucher_detail_no, "qty")) == abs(available_qty) - or field == "total_qty" ) ): return True @@ -1344,6 +1341,7 @@ class SerialandBatchBundle(Document): def on_submit(self): self.validate_docstatus() self.validate_serial_nos_inventory() + self.validate_batch_quantity() def validate_docstatus(self): for row in self.entries: @@ -1437,6 +1435,106 @@ class SerialandBatchBundle(Document): def on_cancel(self): self.validate_voucher_no_docstatus() + self.validate_batch_quantity() + + def validate_batch_quantity(self): + if not self.has_batch_no: + return + + if self.type_of_transaction != "Outward" or ( + self.voucher_type == "Stock Reconciliation" and self.type_of_transaction == "Outward" + ): + return + + batch_wise_available_qty = self.get_batchwise_available_qty() + precision = frappe.get_precision("Serial and Batch Entry", "qty") + + for d in self.entries: + available_qty = batch_wise_available_qty.get(d.batch_no, 0) + if flt(available_qty, precision) < 0: + frappe.throw( + _( + """ + The Batch {0} of an item {1} has negative stock in the warehouse {2}. Please add a stock quantity of {3} to proceed with this entry.""" + ).format( + bold(d.batch_no), + bold(self.item_code), + bold(self.warehouse), + bold(abs(flt(available_qty, precision))), + ), + title=_("Negative Stock Error"), + ) + + def get_batchwise_available_qty(self): + available_qty = self.get_available_qty_from_sabb() + available_qty_from_ledger = self.get_available_qty_from_stock_ledger() + + if not available_qty_from_ledger: + return available_qty + + for batch_no, qty in available_qty_from_ledger.items(): + if batch_no in available_qty: + available_qty[batch_no] += qty + else: + available_qty[batch_no] = qty + + return available_qty + + def get_available_qty_from_stock_ledger(self): + batches = [d.batch_no for d in self.entries if d.batch_no] + + sle = frappe.qb.DocType("Stock Ledger Entry") + + query = ( + frappe.qb.from_(sle) + .select( + sle.batch_no, + Sum(sle.actual_qty).as_("available_qty"), + ) + .where( + (sle.item_code == self.item_code) + & (sle.warehouse == self.warehouse) + & (sle.is_cancelled == 0) + & (sle.batch_no.isin(batches)) + & (sle.docstatus == 1) + & (sle.serial_and_batch_bundle.isnull()) + & (sle.batch_no.isnotnull()) + ) + .for_update() + .groupby(sle.batch_no) + ) + + res = query.run(as_list=True) + + return frappe._dict(res) if res else frappe._dict() + + def get_available_qty_from_sabb(self): + batches = [d.batch_no for d in self.entries if d.batch_no] + + child = frappe.qb.DocType("Serial and Batch Entry") + + query = ( + frappe.qb.from_(child) + .select( + child.batch_no, + Sum(child.qty).as_("available_qty"), + ) + .where( + (child.item_code == self.item_code) + & (child.warehouse == self.warehouse) + & (child.is_cancelled == 0) + & (child.batch_no.isin(batches)) + & (child.docstatus == 1) + & (child.type_of_transaction.isin(["Inward", "Outward"])) + ) + .for_update() + .groupby(child.batch_no) + ) + query = query.where(child.voucher_type != "Pick List") + + res = query.run(as_list=True) + + return frappe._dict(res) if res else frappe._dict() def validate_voucher_no_docstatus(self): if self.voucher_type == "POS Invoice": diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index 50603eb609d..199672871dc 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -807,62 +807,11 @@ class BatchNoValuation(DeprecatedBatchNoValuation): for ledger in entries: self.stock_value_differece[ledger.batch_no] += flt(ledger.incoming_rate) self.available_qty[ledger.batch_no] += flt(ledger.qty) - self.total_qty[ledger.batch_no] += flt(ledger.qty) - - entries = self.get_batch_stock_after_date() - for row in entries: - self.total_qty[row.batch_no] += flt(row.total_qty) self.calculate_avg_rate_from_deprecarated_ledgers() self.calculate_avg_rate_for_non_batchwise_valuation() self.set_stock_value_difference() - def get_batch_stock_after_date(self) -> list[dict]: - # Get total qty of each batch no from Serial and Batch Bundle without checking time condition - if not self.batchwise_valuation_batches: - return [] - - child = frappe.qb.DocType("Serial and Batch Entry") - - timestamp_condition = "" - if self.sle.posting_datetime: - timestamp_condition = child.posting_datetime > self.sle.posting_datetime - - if self.sle.creation: - timestamp_condition |= (child.posting_datetime == self.sle.posting_datetime) & ( - child.creation > self.sle.creation - ) - - query = ( - frappe.qb.from_(child) - .select( - child.batch_no, - Sum(child.qty).as_("total_qty"), - ) - .where( - (child.item_code == self.sle.item_code) - & (child.warehouse == self.sle.warehouse) - & (child.batch_no.isin(self.batchwise_valuation_batches)) - & (child.docstatus == 1) - & (child.type_of_transaction.isin(["Inward", "Outward"])) - ) - .for_update() - .groupby(child.batch_no) - ) - - # Important to exclude the current voucher detail no / voucher no to calculate the correct stock value difference - if self.sle.voucher_detail_no: - query = query.where(child.voucher_detail_no != self.sle.voucher_detail_no) - elif self.sle.voucher_no: - query = query.where(child.voucher_no != self.sle.voucher_no) - - query = query.where(child.voucher_type != "Pick List") - - if timestamp_condition: - query = query.where(timestamp_condition) - - return query.run(as_dict=True) - def get_batch_stock_before_date(self) -> list[dict]: # Get batch wise stock value difference from Serial and Batch Bundle considering time condition if not self.batchwise_valuation_batches: From f97b85007781b8cd0c79e67c9910c13465582a1f Mon Sep 17 00:00:00 2001 From: Bharathidhasan06 Date: Thu, 22 Jan 2026 18:07:36 +0530 Subject: [PATCH 20/24] fix(stock): use purchase UOM in Supplier Quotation items (cherry picked from commit 2606ca6fa9c1d2917f096975f33a31da43ee39f4) --- erpnext/stock/get_item_details.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index a1f431c883e..f2ac54898a7 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -439,8 +439,10 @@ def get_basic_details(ctx: ItemDetailsCtx, item, overwrite_warehouse=True) -> It if not ctx.uom: if ctx.doctype in sales_doctypes: ctx.uom = item.sales_uom if item.sales_uom else item.stock_uom - elif (ctx.doctype in ["Purchase Order", "Purchase Receipt", "Purchase Invoice"]) or ( - ctx.doctype == "Material Request" and ctx.material_request_type == "Purchase" + elif ( + (ctx.doctype in ["Purchase Order", "Purchase Receipt", "Purchase Invoice"]) + or (ctx.doctype == "Material Request" and ctx.material_request_type == "Purchase") + or (ctx.doctype == "Supplier Quotation") ): ctx.uom = item.purchase_uom if item.purchase_uom else item.stock_uom else: From 13e4849c43bb979c713f7bd728db3fe2c4a473af Mon Sep 17 00:00:00 2001 From: SowmyaArunachalam Date: Wed, 14 Jan 2026 21:22:10 +0530 Subject: [PATCH 21/24] fix: disable asset repair when status is fully depreciated (cherry picked from commit 66fe1aa85d4014007f75a7023fa9235d694142b7) --- erpnext/assets/doctype/asset/asset.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index dc65883eb15..1c5c483bac4 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -116,14 +116,6 @@ frappe.ui.form.on("Asset", { __("Manage") ); - frm.add_custom_button( - __("Repair Asset"), - function () { - frm.trigger("create_asset_repair"); - }, - __("Manage") - ); - frm.add_custom_button( __("Split Asset"), function () { @@ -155,6 +147,14 @@ frappe.ui.form.on("Asset", { }, __("Manage") ); + + frm.add_custom_button( + __("Repair Asset"), + function () { + frm.trigger("create_asset_repair"); + }, + __("Manage") + ); } if (!frm.doc.calculate_depreciation) { From bf53133f945f42f678323caff1f4541101da23bd Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 23 Jan 2026 19:35:47 +0530 Subject: [PATCH 22/24] fix: Bin reserved qty for production for extra material transfer (cherry picked from commit f5378b6573ab3d536b360e224ab63839cb51d9c2) --- .../doctype/work_order/test_work_order.py | 47 +++++++++++++++++++ .../doctype/work_order/work_order.py | 3 ++ 2 files changed, 50 insertions(+) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 0840192ccfd..571e43a3d30 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -3725,6 +3725,53 @@ class TestWorkOrder(IntegrationTestCase): wo = make_wo_order_test_record(item="Top Level Parent") self.assertEqual([item.item_code for item in wo.required_items], expected) + def test_reserved_qty_for_pp_with_extra_material_transfer(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import ( + make_stock_entry as make_stock_entry_test_record, + ) + + rm_item_code = make_item( + "_Test Reserved Qty PP Item", + { + "is_stock_item": 1, + }, + ).name + + fg_item_code = make_item( + "_Test Reserved Qty PP FG Item", + { + "is_stock_item": 1, + }, + ).name + + make_stock_entry_test_record( + item_code=rm_item_code, target="_Test Warehouse - _TC", qty=10, basic_rate=100 + ) + + make_bom( + item=fg_item_code, + raw_materials=[rm_item_code], + ) + + wo_order = make_wo_order_test_record( + item=fg_item_code, + qty=1, + source_warehouse="_Test Warehouse - _TC", + skip_transfer=0, + target_warehouse="_Test Warehouse - _TC", + ) + + bin1_at_completion = get_bin(rm_item_code, "_Test Warehouse - _TC") + self.assertEqual(bin1_at_completion.reserved_qty_for_production, 1) + + s = frappe.get_doc(make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 1)) + s.items[0].qty += 2 # extra material transfer + s.submit() + + bin1_at_completion = get_bin(rm_item_code, "_Test Warehouse - _TC") + + self.assertEqual(bin1_at_completion.reserved_qty_for_production, 0) + def get_reserved_entries(voucher_no, warehouse=None): doctype = frappe.qb.DocType("Stock Reservation Entry") diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 57bda11fbc4..80cfc0c2a6e 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -2654,6 +2654,9 @@ def get_reserved_qty_for_production( qty_field = wo_item.required_qty else: qty_field = Case() + qty_field = qty_field.when( + ((wo.skip_transfer == 0) & (wo_item.transferred_qty > wo_item.required_qty)), 0.0 + ) qty_field = qty_field.when(wo.skip_transfer == 0, wo_item.required_qty - wo_item.transferred_qty) qty_field = qty_field.else_(wo_item.required_qty - wo_item.consumed_qty) From e9f3f0f4452baee839cfd1b1bca20daa5b9a9021 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 24 Jan 2026 12:03:34 +0530 Subject: [PATCH 23/24] fix: Ensure paid_amount is always numeric before calling allocate_amount_to_references (backport #50935) (#52036) fix: Ensure paid_amount is always numeric before calling allocate_amount_to_references (#50935) fix: ensure paid_amount is not null in allocate_party_amount_against_ref_docs (cherry picked from commit 50b3396064b0d067b333d28ac0c6b2cb9416c306) Co-authored-by: El-Shafei H. --- erpnext/accounts/doctype/payment_entry/payment_entry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 3020e4e6659..b65fff308b1 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -1104,7 +1104,7 @@ frappe.ui.form.on("Payment Entry", { allocate_party_amount_against_ref_docs: async function (frm, paid_amount, paid_amount_change) { await frm.call("allocate_amount_to_references", { - paid_amount: paid_amount, + paid_amount: flt(paid_amount), paid_amount_change: paid_amount_change, allocate_payment_amount: frappe.flags.allocate_payment_amount ?? false, }); From 8946f12677b8bb32d6a650dcc42c05fb229eb0f8 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 24 Jan 2026 06:48:16 +0000 Subject: [PATCH 24/24] fix: update country_wise_tax.json for Algerian Taxes (backport #51878) (#52038) fix: update country_wise_tax.json for Algerian Taxes (#51878) * Algeria chart of accounts Algeria chart of accounts * Update Algeria Chart Of Account * Algeria chart of account * Algeria Chart of Account Algeria Chart of Account * Modify Algeria tax entries in country_wise_tax.json Updated tax rates and account names for Algeria. * Rename account for Algeria tax from VAT to TVA Rename account for Algeria tax from VAT to TVA (cherry picked from commit e810cd8440b2753d3be8b48c1ae6cba524ca75e9) Co-authored-by: HALFWARE --- .../setup/setup_wizard/data/country_wise_tax.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/setup/setup_wizard/data/country_wise_tax.json b/erpnext/setup/setup_wizard/data/country_wise_tax.json index 343bc057f0c..7eb78c5499d 100644 --- a/erpnext/setup/setup_wizard/data/country_wise_tax.json +++ b/erpnext/setup/setup_wizard/data/country_wise_tax.json @@ -6,14 +6,14 @@ } }, "Algeria": { - "Algeria VAT 17%": { - "account_name": "VAT 17%", - "tax_rate": 17.00, + "Algeria TVA 19%": { + "account_name": "TVA 19%", + "tax_rate": 19.00, "default": 1 }, - "Algeria VAT 7%": { - "account_name": "VAT 7%", - "tax_rate": 7.00 + "Algeria TVA 9%": { + "account_name": "TVA 9%", + "tax_rate": 9.00 } },