From 7280034abd80b3ce39c38f7a7f1b633843633453 Mon Sep 17 00:00:00 2001 From: Marc-Constantin Enke Date: Fri, 29 Aug 2025 11:07:37 +0200 Subject: [PATCH 01/24] fix: remove ignore_permissions (cherry picked from commit 7f55f421ab261d1dc94d9adad8640beaf3e1a198) --- erpnext/accounts/doctype/party_link/party_link.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/party_link/party_link.py b/erpnext/accounts/doctype/party_link/party_link.py index 16484fc4bb9..1ed837eada7 100644 --- a/erpnext/accounts/doctype/party_link/party_link.py +++ b/erpnext/accounts/doctype/party_link/party_link.py @@ -74,6 +74,6 @@ def create_party_link(primary_role, primary_party, secondary_party): party_link.secondary_role = "Customer" if primary_role == "Supplier" else "Supplier" party_link.secondary_party = secondary_party - party_link.save(ignore_permissions=True) + party_link.save() return party_link From 9db9dd6f35a7b4a6b60c3b908b2112597e5f24fa Mon Sep 17 00:00:00 2001 From: Marc-Constantin Enke Date: Fri, 29 Aug 2025 11:09:22 +0200 Subject: [PATCH 02/24] feat: add permission check for custom button (cherry picked from commit 00fd1d2f26f5f58649036edd4646038a730f9129) --- erpnext/buying/doctype/supplier/supplier.js | 5 ++++- erpnext/selling/doctype/customer/customer.js | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/erpnext/buying/doctype/supplier/supplier.js b/erpnext/buying/doctype/supplier/supplier.js index 0a316bd2974..cf3506a67d3 100644 --- a/erpnext/buying/doctype/supplier/supplier.js +++ b/erpnext/buying/doctype/supplier/supplier.js @@ -133,7 +133,10 @@ frappe.ui.form.on("Supplier", { __("Actions") ); - if (cint(frappe.defaults.get_default("enable_common_party_accounting"))) { + if ( + cint(frappe.defaults.get_default("enable_common_party_accounting")) && + frappe.model.can_create("Party Link") + ) { frm.add_custom_button( __("Link with Customer"), function () { diff --git a/erpnext/selling/doctype/customer/customer.js b/erpnext/selling/doctype/customer/customer.js index aa8bea55094..598452276cc 100644 --- a/erpnext/selling/doctype/customer/customer.js +++ b/erpnext/selling/doctype/customer/customer.js @@ -175,7 +175,10 @@ frappe.ui.form.on("Customer", { __("Actions") ); - if (cint(frappe.defaults.get_default("enable_common_party_accounting"))) { + if ( + cint(frappe.defaults.get_default("enable_common_party_accounting")) && + frappe.model.can_create("Party Link") + ) { frm.add_custom_button( __("Link with Supplier"), function () { From b8a07a437bb7aad5f811b29c116dfbcf14e2b7ed Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 2 Sep 2025 17:35:03 +0000 Subject: [PATCH 03/24] test: use valid IBANs in party matching test case (backport #49432) (#49433) Co-authored-by: barredterra <14891507+barredterra@users.noreply.github.com> --- .../bank_transaction/test_auto_match_party.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/bank_transaction/test_auto_match_party.py b/erpnext/accounts/doctype/bank_transaction/test_auto_match_party.py index 36ef1fca074..14d4eb3a6ed 100644 --- a/erpnext/accounts/doctype/bank_transaction/test_auto_match_party.py +++ b/erpnext/accounts/doctype/bank_transaction/test_auto_match_party.py @@ -7,6 +7,9 @@ from frappe.utils import nowdate from erpnext.accounts.doctype.bank_transaction.test_bank_transaction import create_bank_account +IBAN_1 = "DE02000000003716541159" +IBAN_2 = "DE02500105170137075030" + class TestAutoMatchParty(FrappeTestCase): @classmethod @@ -22,24 +25,24 @@ class TestAutoMatchParty(FrappeTestCase): frappe.db.set_single_value("Accounts Settings", "enable_fuzzy_matching", 0) def test_match_by_account_number(self): - create_supplier_for_match(account_no="000000003716541159") + create_supplier_for_match(account_no=IBAN_1[11:]) doc = create_bank_transaction( withdrawal=1200, transaction_id="562213b0ca1bf838dab8f2c6a39bbc3b", - account_no="000000003716541159", - iban="DE02000000003716541159", + account_no=IBAN_1[11:], + iban=IBAN_1, ) self.assertEqual(doc.party_type, "Supplier") self.assertEqual(doc.party, "John Doe & Co.") def test_match_by_iban(self): - create_supplier_for_match(iban="DE02000000003716541159") + create_supplier_for_match(iban=IBAN_1) doc = create_bank_transaction( withdrawal=1200, transaction_id="c5455a224602afaa51592a9d9250600d", - account_no="000000003716541159", - iban="DE02000000003716541159", + account_no=IBAN_1[11:], + iban=IBAN_1, ) self.assertEqual(doc.party_type, "Supplier") @@ -51,7 +54,7 @@ class TestAutoMatchParty(FrappeTestCase): withdrawal=1200, transaction_id="1f6f661f347ff7b1ea588665f473adb1", party_name="Ella Jackson", - iban="DE04000000003716545346", + iban=IBAN_2, ) self.assertEqual(doc.party_type, "Supplier") self.assertEqual(doc.party, "Jackson Ella W.") From 43e9ab9cd16570fa32c68888af3a69b88cd7d9a3 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 3 Sep 2025 13:07:08 +0530 Subject: [PATCH 04/24] refactor: PR 49320 (cherry picked from commit 991413608b1cabe58abdf15cdddd0c4205737273) --- .../doctype/work_order/work_order.js | 28 +++++++++---------- .../stock/doctype/stock_entry/stock_entry.py | 6 ++-- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index 2f189ba4c46..2d67c7bd490 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -116,20 +116,6 @@ frappe.ui.form.on("Work Order", { frm.set_indicator_formatter("operation", function (doc) { return frm.doc.qty == doc.completed_qty ? "green" : "orange"; }); - - if (frm.doc.docstatus == 0 && frm.doc.bom_no) { - frappe.call({ - method: "erpnext.manufacturing.doctype.work_order.work_order.check_if_scrap_warehouse_mandatory", - args: { - bom_no: frm.doc.bom_no, - }, - callback: function (r) { - if (r.message["set_scrap_wh_mandatory"]) { - frm.toggle_reqd("scrap_warehouse", true); - } - }, - }); - } }, onload: function (frm) { @@ -144,6 +130,20 @@ frappe.ui.form.on("Work Order", { }); erpnext.work_order.set_default_warehouse(frm); } + + if (frm.doc.docstatus == 0 && frm.doc.bom_no) { + frappe.call({ + method: "erpnext.manufacturing.doctype.work_order.work_order.check_if_scrap_warehouse_mandatory", + args: { + bom_no: frm.doc.bom_no, + }, + callback: function (r) { + if (r.message["set_scrap_wh_mandatory"]) { + frm.toggle_reqd("scrap_warehouse", true); + } + }, + }); + } }, source_warehouse: function (frm) { diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 0d1175b2fe0..6bdfeef74a9 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -2123,14 +2123,16 @@ class StockEntry(StockController): "Work Order", self.work_order, "allow_alternative_item" ) + skip_transfer, from_wip_warehouse = frappe.get_value( + "Work Order", self.work_order, ["skip_transfer", "from_wip_warehouse"] + ) item.from_warehouse = ( frappe.get_value( "Work Order Item", {"parent": self.work_order, "item_code": item.item_code}, "source_warehouse", ) - if frappe.get_value("Work Order", self.work_order, "skip_transfer") - and not frappe.get_value("Work Order", self.work_order, "from_wip_warehouse") + if skip_transfer and not from_wip_warehouse else self.from_warehouse or item.source_warehouse or item.default_warehouse ) if item.item_code in used_alternative_items: From ae3dd5b83165562d5ab8cee9284dbc5d1cd97c48 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 3 Sep 2025 15:58:30 +0530 Subject: [PATCH 05/24] fix: incorrect batch qty (cherry picked from commit 000135a3d4ed833c0d1363d2514294ca249f2d0e) --- .../test_stock_reconciliation.py | 74 +++++++++++++++++++ erpnext/stock/stock_ledger.py | 14 +++- 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 51cf4a34943..5b59030c945 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -1590,6 +1590,80 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin): self.assertFalse(status == "Active") + def test_stock_reconciliation_for_batch_with_backward(self): + # Make stock inward for 10 -> Stock Reco for 20 after two days + # Make backdated delivery note for 10 qty between stock inward and stock reco + # Check the state of the current serial and batch bundle in the stock reco + # The state should be cancelled + + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + + item_code = "Test Stock Reco for Batch with Backward" + + self.make_item( + item_code, {"has_batch_no": 1, "create_new_batch": 1, "batch_number_series": "BCN-CB.#####"} + ) + + warehouse = "_Test Warehouse - _TC" + + se = make_stock_entry( + posting_date=add_days(nowdate(), -2), + posting_time="02:00", + item_code=item_code, + target=warehouse, + qty=10, + basic_rate=100, + ) + + batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle) + + sr = create_stock_reconciliation( + item_code=item_code, + warehouse=warehouse, + qty=20, + rate=200, + use_serial_batch_fields=1, + batch_no=batch_no, + posting_date=nowdate(), + posting_time="03:00", + ) + + current_sabb = sr.items[0].current_serial_and_batch_bundle + + self.assertTrue(frappe.db.get_value("Serial and Batch Bundle", current_sabb, "docstatus") == 1) + + self.assertTrue( + frappe.db.get_value( + "Stock Ledger Entry", {"serial_and_batch_bundle": current_sabb, "is_cancelled": 0}, "name" + ) + ) + self.assertTrue(sr.items[0].current_serial_and_batch_bundle) + self.assertTrue(sr.items[0].current_qty) + self.assertTrue(sr.items[0].current_qty == 10) + + se = make_stock_entry( + posting_date=add_days(nowdate(), -1), + posting_time="02:00", + item_code=item_code, + source=warehouse, + qty=10, + basic_rate=100, + use_serial_batch_fields=1, + batch_no=batch_no, + ) + + sr.reload() + self.assertFalse(sr.items[0].current_serial_and_batch_bundle) + self.assertTrue(sr.items[0].current_qty == 0) + + self.assertFalse(frappe.db.get_value("Serial and Batch Bundle", current_sabb, "docstatus") == 1) + + self.assertFalse( + frappe.db.get_value( + "Stock Ledger Entry", {"serial_and_batch_bundle": current_sabb, "is_cancelled": 0}, "name" + ) + ) + def create_batch_item_with_batch(item_name, batch_id): batch_item_doc = create_item(item_name, is_stock_item=1) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 61a19a95bd4..950206b665b 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -974,10 +974,12 @@ class update_entries_after: self.wh_data.valuation_rate = self.get_fallback_rate(sle) def reset_actual_qty_for_stock_reco(self, sle): - doc = frappe.get_cached_doc("Stock Reconciliation", sle.voucher_no) + doc = frappe.get_doc("Stock Reconciliation", sle.voucher_no) doc.recalculate_current_qty(sle.voucher_detail_no, sle.creation, sle.actual_qty > 0) if sle.actual_qty < 0: + doc.reload() + sle.actual_qty = ( flt(frappe.db.get_value("Stock Reconciliation Item", sle.voucher_detail_no, "current_qty")) * -1 @@ -986,6 +988,16 @@ class update_entries_after: if abs(sle.actual_qty) == 0.0: sle.is_cancelled = 1 + if sle.serial_and_batch_bundle: + for row in doc.items: + if row.name == sle.voucher_detail_no: + row.db_set("current_serial_and_batch_bundle", "") + + sabb_doc = frappe.get_doc("Serial and Batch Bundle", sle.serial_and_batch_bundle) + sabb_doc.voucher_detail_no = None + sabb_doc.voucher_no = None + sabb_doc.cancel() + if sle.serial_and_batch_bundle and frappe.get_cached_value("Item", sle.item_code, "has_serial_no"): self.update_serial_no_status(sle) From d302ca7ea0e7aecb0093f6075b92143f0ba6d368 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 3 Sep 2025 19:03:08 +0530 Subject: [PATCH 06/24] fix: incorrect stock value in the report (cherry picked from commit 5824b5effdd8911d16032d7bb0a9278d74a965fe) --- erpnext/stock/report/stock_ledger/stock_ledger.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py index bbc85c5a4ad..b9275417847 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.py +++ b/erpnext/stock/report/stock_ledger/stock_ledger.py @@ -72,6 +72,7 @@ def execute(filters=None): batch_balance_dict[sle.batch_no] = [0, 0] batch_balance_dict[sle.batch_no][0] += sle.actual_qty + batch_balance_dict[sle.batch_no][1] += stock_value if filters.get("segregate_serial_batch_bundle"): actual_qty = batch_balance_dict[sle.batch_no][0] From affe09ee0bcc9ddab5f98638dadbfacaf08a1686 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 4 Sep 2025 00:48:45 +0530 Subject: [PATCH 07/24] fix: non batch-wise valuation for batch item (cherry picked from commit 11b82ba00822a883193f4a8d119da572ef9b19f5) --- erpnext/stock/deprecated_serial_batch.py | 5 +++ .../serial_and_batch_bundle.py | 33 ++++++++++++++++++- erpnext/stock/serial_batch_bundle.py | 14 ++++---- erpnext/stock/stock_ledger.py | 9 +++++ erpnext/stock/utils.py | 1 + 5 files changed, 53 insertions(+), 9 deletions(-) diff --git a/erpnext/stock/deprecated_serial_batch.py b/erpnext/stock/deprecated_serial_batch.py index 825b1fe8d7e..19e77b3e068 100644 --- a/erpnext/stock/deprecated_serial_batch.py +++ b/erpnext/stock/deprecated_serial_batch.py @@ -1,4 +1,5 @@ import datetime +import json from collections import defaultdict import frappe @@ -197,6 +198,9 @@ class DeprecatedBatchNoValuation: @deprecated def set_balance_value_for_non_batchwise_valuation_batches(self): self.last_sle = self.get_last_sle_for_non_batch() + if self.last_sle and self.last_sle.stock_value: + self.stock_queue = json.loads(self.last_sle.stock_queue or "[]") or [] + self.set_balance_value_from_sl_entries() self.set_balance_value_from_bundle() @@ -271,6 +275,7 @@ class DeprecatedBatchNoValuation: .select( sle.stock_value, sle.qty_after_transaction, + sle.stock_queue, ) .where( (sle.item_code == self.sle.item_code) 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 a453457f919..e7d7446cea6 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 @@ -3,6 +3,7 @@ import collections import csv +import json from collections import Counter, defaultdict import frappe @@ -29,6 +30,7 @@ from erpnext.stock.serial_batch_bundle import ( get_batches_from_bundle, ) from erpnext.stock.serial_batch_bundle import get_serial_nos as get_serial_nos_from_bundle +from erpnext.stock.valuation import FIFOValuation class SerialNoExistsInFutureTransactionError(frappe.ValidationError): @@ -463,6 +465,8 @@ class SerialandBatchBundle(Document): ) def set_incoming_rate_for_outward_transaction(self, row=None, save=False, allow_negative_stock=False): + from erpnext.stock.utils import get_valuation_method + sle = self.get_sle_for_outward_transaction() if self.has_serial_no: @@ -479,13 +483,40 @@ class SerialandBatchBundle(Document): warehouse=self.warehouse, ) + stock_queue = [] + if hasattr(sn_obj, "stock_queue") and sn_obj.stock_queue: + stock_queue = parse_json(sn_obj.stock_queue) + + val_method = get_valuation_method(self.item_code) + for d in self.entries: available_qty = 0 if self.has_serial_no: d.incoming_rate = abs(sn_obj.serial_no_incoming_rate.get(d.serial_no, 0.0)) else: - d.incoming_rate = abs(flt(sn_obj.batch_avg_rate.get(d.batch_no))) + actual_qty = d.qty + if ( + stock_queue + and val_method == "FIFO" + and d.batch_no in sn_obj.non_batchwise_valuation_batches + ): + if actual_qty < 0: + stock_queue = FIFOValuation(stock_queue) + _prev_qty, prev_stock_value = stock_queue.get_total_stock_and_value() + + stock_queue.remove_stock(qty=abs(actual_qty)) + _qty, stock_value = stock_queue.get_total_stock_and_value() + + stock_value_difference = stock_value - prev_stock_value + d.incoming_rate = abs(flt(stock_value_difference) / abs(flt(actual_qty))) + stock_queue = stock_queue.state + else: + d.incoming_rate = abs(flt(sn_obj.batch_avg_rate.get(d.batch_no))) + stock_queue.append([d.qty, d.incoming_rate]) + d.stock_queue = json.dumps(stock_queue) + else: + d.incoming_rate = abs(flt(sn_obj.batch_avg_rate.get(d.batch_no))) available_qty = flt(sn_obj.available_qty.get(d.batch_no), d.precision("qty")) if self.docstatus == 1: diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index 2a1fe92ae28..0cf0a940cf3 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -674,6 +674,7 @@ class BatchNoValuation(DeprecatedBatchNoValuation): for key, value in kwargs.items(): setattr(self, key, value) + self.stock_queue = [] self.batch_nos = self.get_batch_nos() self.prepare_batches() self.calculate_avg_rate() @@ -770,15 +771,12 @@ class BatchNoValuation(DeprecatedBatchNoValuation): self.non_batchwise_valuation_batches = self.batches return - if get_valuation_method(self.sle.item_code) == "FIFO": - self.batchwise_valuation_batches = self.batches - else: - batches = frappe.get_all( - "Batch", filters={"name": ("in", self.batches), "use_batchwise_valuation": 1}, fields=["name"] - ) + batches = frappe.get_all( + "Batch", filters={"name": ("in", self.batches), "use_batchwise_valuation": 1}, fields=["name"] + ) - for batch in batches: - self.batchwise_valuation_batches.append(batch.name) + for batch in batches: + self.batchwise_valuation_batches.append(batch.name) self.non_batchwise_valuation_batches = list(set(self.batches) - set(self.batchwise_valuation_batches)) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 950206b665b..a4d8695784d 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -1042,6 +1042,15 @@ class update_entries_after: doc.set_incoming_rate(save=True, allow_negative_stock=self.allow_negative_stock) doc.calculate_qty_and_amount(save=True) + if stock_queue := frappe.get_all( + "Serial and Batch Entry", + filters={"parent": sle.serial_and_batch_bundle, "stock_queue": ("is", "set")}, + pluck="stock_queue", + order_by="idx desc", + limit=1, + ): + self.wh_data.stock_queue = json.loads(stock_queue[0]) if stock_queue else [] + self.wh_data.stock_value = round_off_if_near_zero(self.wh_data.stock_value + doc.total_amount) self.wh_data.qty_after_transaction += flt(doc.total_qty, self.flt_precision) if flt(self.wh_data.qty_after_transaction, self.flt_precision): diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 781fc81445c..58ecb24db48 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -373,6 +373,7 @@ def get_avg_purchase_rate(serial_nos): ) +@frappe.request_cache def get_valuation_method(item_code): """get valuation method from item or default""" val_method = frappe.db.get_value("Item", item_code, "valuation_method", cache=True) From 6bdf11f5c90d5842b2760bd635350d9d37f0258c Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Thu, 4 Sep 2025 07:18:03 +0530 Subject: [PATCH 08/24] chore: fix test case (cherry picked from commit ac8637d5a0a2c7312be1e368166d5b55e182f445) --- erpnext/stock/deprecated_serial_batch.py | 2 +- .../test_serial_and_batch_bundle.py | 105 ++++++++++-------- 2 files changed, 58 insertions(+), 49 deletions(-) diff --git a/erpnext/stock/deprecated_serial_batch.py b/erpnext/stock/deprecated_serial_batch.py index 19e77b3e068..d3eb178d778 100644 --- a/erpnext/stock/deprecated_serial_batch.py +++ b/erpnext/stock/deprecated_serial_batch.py @@ -198,7 +198,7 @@ class DeprecatedBatchNoValuation: @deprecated def set_balance_value_for_non_batchwise_valuation_batches(self): self.last_sle = self.get_last_sle_for_non_batch() - if self.last_sle and self.last_sle.stock_value: + if self.last_sle and self.last_sle.stock_queue: self.stock_queue = json.loads(self.last_sle.stock_queue or "[]") or [] self.set_balance_value_from_sl_entries() diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py index 5fdc3d56ebd..4ae2b3ce4b5 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py @@ -203,7 +203,10 @@ class TestSerialandBatchBundle(FrappeTestCase): batch_item_code, { "has_batch_no": 1, + "batch_number_series": "TEST-OLD-BAT-VAL-.#####", + "create_new_batch": 1, "is_stock_item": 1, + "valuation_method": "FIFO", }, ) @@ -256,57 +259,63 @@ class TestSerialandBatchBundle(FrappeTestCase): doc.submit() doc.reload() - bundle_doc = make_serial_batch_bundle( - { - "item_code": batch_item_code, - "warehouse": "_Test Warehouse - _TC", - "voucher_type": "Stock Entry", - "posting_date": today(), - "posting_time": nowtime(), - "qty": -10, - "batches": frappe._dict({batch_id: 10}), - "type_of_transaction": "Outward", - "do_not_submit": True, - } - ) - - bundle_doc.reload() - for row in bundle_doc.entries: - self.assertEqual(flt(row.stock_value_difference, 2), -1666.67) - - bundle_doc.flags.ignore_permissions = True - bundle_doc.flags.ignore_mandatory = True - bundle_doc.flags.ignore_links = True - bundle_doc.flags.ignore_validate = True - bundle_doc.submit() - - bundle_doc = make_serial_batch_bundle( - { - "item_code": batch_item_code, - "warehouse": "_Test Warehouse - _TC", - "voucher_type": "Stock Entry", - "posting_date": today(), - "posting_time": nowtime(), - "qty": -20, - "batches": frappe._dict({batch_id: 20}), - "type_of_transaction": "Outward", - "do_not_submit": True, - } - ) - - bundle_doc.reload() - for row in bundle_doc.entries: - self.assertEqual(flt(row.stock_value_difference, 2), -3333.33) - - bundle_doc.flags.ignore_permissions = True - bundle_doc.flags.ignore_mandatory = True - bundle_doc.flags.ignore_links = True - bundle_doc.flags.ignore_validate = True - bundle_doc.submit() - frappe.flags.ignore_serial_batch_bundle_validation = False frappe.flags.use_serial_and_batch_fields = False + se = make_stock_entry( + item_code=batch_item_code, + source="_Test Warehouse - _TC", + qty=10, + use_serial_batch_fields=True, + batch_no=batch_id, + ) + + sle = frappe.db.get_value( + "Stock Ledger Entry", + {"item_code": batch_item_code, "is_cancelled": 0, "voucher_no": se.name}, + ["stock_value_difference", "stock_queue"], + as_dict=True, + ) + + self.assertEqual(flt(sle.stock_value_difference), 1000.00 * -1) + self.assertEqual(json.loads(sle.stock_queue), [[20, 200]]) + + se = make_stock_entry( + item_code=batch_item_code, + target="_Test Warehouse - _TC", + qty=10, + rate=100, + use_serial_batch_fields=True, + ) + + sle = frappe.db.get_value( + "Stock Ledger Entry", + {"item_code": batch_item_code, "is_cancelled": 0, "voucher_no": se.name}, + ["stock_value_difference", "stock_queue"], + as_dict=True, + ) + + self.assertEqual(flt(sle.stock_value_difference), 1000.00) + self.assertEqual(json.loads(sle.stock_queue), [[20, 200]]) + + se = make_stock_entry( + item_code=batch_item_code, + source="_Test Warehouse - _TC", + qty=30, + use_serial_batch_fields=False, + ) + + sle = frappe.db.get_value( + "Stock Ledger Entry", + {"item_code": batch_item_code, "is_cancelled": 0, "voucher_no": se.name}, + ["stock_value_difference", "stock_queue", "stock_value"], + as_dict=True, + ) + + self.assertEqual(flt(sle.stock_value_difference), 5000.00 * -1) + self.assertFalse(json.loads(sle.stock_queue or "[]")) + self.assertEqual(flt(sle.stock_value), 0.0) + def test_old_serial_no_valuation(self): from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt From 845d3464b4feaf8d059ffd226e83a6a0bad4b058 Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Fri, 5 Sep 2025 01:54:56 +0530 Subject: [PATCH 09/24] fix: depreciate asset with remaining amount when depreciation amount exceeds current asset value --- .../asset_depreciation_schedule.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py index 1e3c1ffc598..9e8bab0721a 100644 --- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py +++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py @@ -369,6 +369,10 @@ class AssetDepreciationSchedule(Document): original_schedule_date=schedule_date, ) depreciation_amount = flt(depreciation_amount, asset_doc.precision("gross_purchase_amount")) + + if depreciation_amount > row.value_after_depreciation - row.expected_value_after_useful_life: + depreciation_amount = row.value_after_depreciation - row.expected_value_after_useful_life + if depreciation_amount > 0: self.add_depr_schedule_row(date_of_disposal, depreciation_amount, n) @@ -654,6 +658,7 @@ def _get_pro_rata_amt( total_days = get_total_days(original_schedule_date or to_date, 12) else: total_days = get_total_days(original_schedule_date or to_date, row.frequency_of_depreciation) + print(total_days, days, depreciation_amount) return (depreciation_amount * flt(days)) / flt(total_days), days, months From da039e5bf0c3de29d9cd1e3a9a781946a457a124 Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Fri, 5 Sep 2025 01:58:20 +0530 Subject: [PATCH 10/24] chore: remove print statement --- .../asset_depreciation_schedule.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py index 9e8bab0721a..26d3f93856f 100644 --- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py +++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py @@ -369,7 +369,7 @@ class AssetDepreciationSchedule(Document): original_schedule_date=schedule_date, ) depreciation_amount = flt(depreciation_amount, asset_doc.precision("gross_purchase_amount")) - + if depreciation_amount > row.value_after_depreciation - row.expected_value_after_useful_life: depreciation_amount = row.value_after_depreciation - row.expected_value_after_useful_life @@ -658,7 +658,7 @@ def _get_pro_rata_amt( total_days = get_total_days(original_schedule_date or to_date, 12) else: total_days = get_total_days(original_schedule_date or to_date, row.frequency_of_depreciation) - print(total_days, days, depreciation_amount) + return (depreciation_amount * flt(days)) / flt(total_days), days, months From 28b3856bc91be91a837d17cb32a11d429f89ab27 Mon Sep 17 00:00:00 2001 From: Sanket322 Date: Thu, 10 Apr 2025 13:03:11 +0530 Subject: [PATCH 11/24] fix: add Address and Contact in Add Column (cherry picked from commit d9ca7e755fa14d5446188e98408322c0190fdcc4) --- .../address_and_contacts/address_and_contacts.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/report/address_and_contacts/address_and_contacts.py b/erpnext/selling/report/address_and_contacts/address_and_contacts.py index 5d0e706930f..3742342c7a3 100644 --- a/erpnext/selling/report/address_and_contacts/address_and_contacts.py +++ b/erpnext/selling/report/address_and_contacts/address_and_contacts.py @@ -5,8 +5,9 @@ import frappe field_map = { - "Contact": ["first_name", "last_name", "phone", "mobile_no", "email_id", "is_primary_contact"], + "Contact": ["name", "first_name", "last_name", "phone", "mobile_no", "email_id", "is_primary_contact"], "Address": [ + "name", "address_line1", "address_line2", "city", @@ -29,6 +30,12 @@ def get_columns(filters): return [ f"{party_type}:Link/{party_type}", f"{frappe.unscrub(str(party_type_value))}::150", + { + "label": "Address", + "fieldtype": "Link", + "options": "Address", + "hidden": 1, + }, "Address Line 1", "Address Line 2", "Postal Code", @@ -36,6 +43,7 @@ def get_columns(filters): "State", "Country", "Is Primary Address:Check", + {"label": "Contact", "fieldtype": "Link", "options": "Contact", "hidden": 1}, "First Name", "Last Name", "Phone", From 4f1f46a4d41ebaf2af805e7eae2ba333b95a1864 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 15 Apr 2025 17:32:46 +0530 Subject: [PATCH 12/24] chore: translatable labels (cherry picked from commit 39174f9dc0f7399c56a60254b84044d5b0feacf8) --- .../report/address_and_contacts/address_and_contacts.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/report/address_and_contacts/address_and_contacts.py b/erpnext/selling/report/address_and_contacts/address_and_contacts.py index 3742342c7a3..65fef8b5ac6 100644 --- a/erpnext/selling/report/address_and_contacts/address_and_contacts.py +++ b/erpnext/selling/report/address_and_contacts/address_and_contacts.py @@ -3,6 +3,7 @@ import frappe +from frappe import _ field_map = { "Contact": ["name", "first_name", "last_name", "phone", "mobile_no", "email_id", "is_primary_contact"], @@ -31,7 +32,7 @@ def get_columns(filters): f"{party_type}:Link/{party_type}", f"{frappe.unscrub(str(party_type_value))}::150", { - "label": "Address", + "label": _("Address"), "fieldtype": "Link", "options": "Address", "hidden": 1, @@ -43,7 +44,7 @@ def get_columns(filters): "State", "Country", "Is Primary Address:Check", - {"label": "Contact", "fieldtype": "Link", "options": "Contact", "hidden": 1}, + {"label": _("Contact"), "fieldtype": "Link", "options": "Contact", "hidden": 1}, "First Name", "Last Name", "Phone", From 07241aa54a4697fe026864c96529b0c36dd7b842 Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Sat, 6 Sep 2025 02:01:28 +0530 Subject: [PATCH 13/24] fix: skip 'Bank Account' creation on setup (cherry picked from commit 47d4319f83b7c9f4c161261ae00b89608e38cff8) --- erpnext/setup/demo.py | 2 +- erpnext/setup/setup_wizard/operations/install_fixtures.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/setup/demo.py b/erpnext/setup/demo.py index 68d9fdfec5c..b21fb96546a 100644 --- a/erpnext/setup/demo.py +++ b/erpnext/setup/demo.py @@ -75,7 +75,7 @@ def create_demo_company(): frappe.db.set_single_value("Global Defaults", "demo_company", new_company.name) frappe.db.set_default("company", new_company.name) - bank_account = create_bank_account({"company_name": new_company.name}) + bank_account = create_bank_account({"company_name": new_company.name}, demo=True) frappe.db.set_value("Company", new_company.name, "default_bank_account", bank_account.name) return new_company.name diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index c78e9b5b60d..8e9f2ee24fc 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -513,8 +513,10 @@ def update_stock_settings(): stock_settings.save() -def create_bank_account(args): +def create_bank_account(args, demo=False): if not args.get("bank_account"): + if not demo: + return args["bank_account"] = _("Bank Account") company_name = args.get("company_name") From 6ab287f19c947e6cf91a5988a24f93717206b4a5 Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Sat, 6 Sep 2025 02:38:53 +0530 Subject: [PATCH 14/24] fix: renamed temporary bank account to 'Demo Bank Account' (cherry picked from commit efeda90cad0da096b47b2afb460a7f6081aa80a7) --- erpnext/setup/setup_wizard/operations/install_fixtures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index 8e9f2ee24fc..0f3356ffa50 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -517,7 +517,7 @@ def create_bank_account(args, demo=False): if not args.get("bank_account"): if not demo: return - args["bank_account"] = _("Bank Account") + args["bank_account"] = _("Demo Bank Account") company_name = args.get("company_name") bank_account_group = frappe.db.get_value( From 921f317423879f91c5cf199675f92990b0abbbbb Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 5 Sep 2025 18:18:55 +0530 Subject: [PATCH 15/24] fix: batch qty calculation performance issue (cherry picked from commit 1a262483a49a5f24bfa0fa3e3c7b0af941e6e2c0) --- erpnext/controllers/stock_controller.py | 4 +- .../test_serial_and_batch_bundle.py | 7 ++- erpnext/stock/serial_batch_bundle.py | 50 +++++++++++-------- 3 files changed, 37 insertions(+), 24 deletions(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 8c2b4db3fc9..3b5dbf2ee81 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -957,7 +957,9 @@ class StockController(AccountsController): from erpnext.stock.stock_ledger import make_sl_entries make_sl_entries(sl_entries, allow_negative_stock, via_landed_cost_voucher) - update_batch_qty(self.doctype, self.name, via_landed_cost_voucher=via_landed_cost_voucher) + update_batch_qty( + self.doctype, self.name, self.docstatus, via_landed_cost_voucher=via_landed_cost_voucher + ) def make_gl_entries_on_cancel(self, from_repost=False): if not from_repost: diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py index 4ae2b3ce4b5..b81fbc8bc55 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py @@ -222,7 +222,12 @@ class TestSerialandBatchBundle(FrappeTestCase): ).insert(ignore_permissions=True) self.assertTrue(batch_doc.use_batchwise_valuation) - batch_doc.db_set("use_batchwise_valuation", 0) + batch_doc.db_set( + { + "use_batchwise_valuation": 0, + "batch_qty": 30, + } + ) stock_queue = [] qty_after_transaction = 0 diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index 0cf0a940cf3..44df6fdb6bc 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -1326,40 +1326,40 @@ def get_serial_nos_batch(serial_nos): ) -def update_batch_qty(voucher_type, voucher_no, via_landed_cost_voucher=False): - from erpnext.stock.doctype.batch.batch import get_available_batches - - batches = get_distinct_batches(voucher_type, voucher_no) +def update_batch_qty(voucher_type, voucher_no, docstatus, via_landed_cost_voucher=False): + batches = get_batchwise_qty(voucher_type, voucher_no) if not batches: return precision = frappe.get_precision("Batch", "batch_qty") - batch_data = get_available_batches( - frappe._dict({"batch_no": batches, "consider_negative_batches": 1, "based_on_warehouse": True}) - ) - batchwise_qty = defaultdict(float) + for batch, qty in batches.items(): + current_qty = get_batch_current_qty(batch) + current_qty += flt(qty, precision) * (-1 if docstatus == 2 else 1) - for (batch_no, warehouse), qty in batch_data.items(): - if not via_landed_cost_voucher and flt(qty, precision) < 0: - throw_negative_batch_validation(batch_no, warehouse, qty) + if not via_landed_cost_voucher and current_qty < 0: + throw_negative_batch_validation(batch, current_qty) - batchwise_qty[batch_no] += qty - - for batch_no in batches: - qty = flt(batchwise_qty.get(batch_no, 0), precision) - frappe.db.set_value("Batch", batch_no, "batch_qty", qty) + frappe.db.set_value("Batch", batch, "batch_qty", current_qty) -def throw_negative_batch_validation(batch_no, warehouse, qty): +def get_batch_current_qty(batch): + doctype = frappe.qb.DocType("Batch") + query = frappe.qb.from_(doctype).select(doctype.batch_qty).where(doctype.name == batch).for_update() + batch_qty = query.run() + + return flt(batch_qty[0][0]) if batch_qty else 0.0 + + +def throw_negative_batch_validation(batch_no, qty): frappe.throw( - _("The Batch {0} has negative quantity {1} in warehouse {2}. Please correct the quantity.").format( - bold(batch_no), bold(qty), bold(warehouse) + _("The Batch {0} has negative quantity {1}. Please correct the quantity.").format( + bold(batch_no), bold(qty) ), title=_("Negative Batch Quantity"), ) -def get_distinct_batches(voucher_type, voucher_no): +def get_batchwise_qty(voucher_type, voucher_no): bundles = frappe.get_all( "Serial and Batch Bundle", filters={"voucher_no": voucher_no, "voucher_type": voucher_type}, @@ -1368,9 +1368,15 @@ def get_distinct_batches(voucher_type, voucher_no): if not bundles: return - return frappe.get_all( + batches = frappe.get_all( "Serial and Batch Entry", filters={"parent": ("in", bundles), "batch_no": ("is", "set")}, + fields=["batch_no", "SUM(qty) as qty"], group_by="batch_no", - pluck="batch_no", + as_list=1, ) + + if not batches: + return frappe._dict({}) + + return frappe._dict(batches) From 7bb92255c5209d7c758e1857a858e649735f8b52 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Sun, 14 Sep 2025 10:53:38 +0530 Subject: [PATCH 16/24] fix: SABB document status validation (cherry picked from commit c0236191aacc1261fa5056bde0f4e370c0063e64) --- erpnext/stock/serial_batch_bundle.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index 44df6fdb6bc..7f4f0b2e945 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -354,6 +354,10 @@ class SerialBatchBundle: doc.flags.ignore_voucher_validation = True doc.submit() + for row in doc.entries: + if row.docstatus == 0: + frappe.throw(_("Serial and Batch Bundle {0} is not submitted").format(bold(doc.name))) + def set_warehouse_and_status_in_serial_nos(self): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos as get_parsed_serial_nos From 7a8cd47259e31ee60e2e33f4e5e3896fb211f3ab Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Sun, 14 Sep 2025 11:22:14 +0530 Subject: [PATCH 17/24] fix: precision issue for valuation rate calculation (cherry picked from commit c92a06d77d82c02ba6755ba0e4d0f79cbc31bdcc) --- erpnext/stock/stock_ledger.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index a4d8695784d..9f64786d110 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -883,10 +883,7 @@ class update_entries_after: self.wh_data.valuation_rate ) - if ( - sle.actual_qty < 0 - and flt(self.wh_data.qty_after_transaction, self.flt_precision) != 0 - ): + if flt(self.wh_data.qty_after_transaction, self.flt_precision) != 0: self.wh_data.valuation_rate = flt( self.wh_data.stock_value, self.currency_precision ) / flt(self.wh_data.qty_after_transaction, self.flt_precision) From 0b2f53fefef1f995132faeb74b2d3195e8caebaf Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Sun, 14 Sep 2025 11:55:08 +0530 Subject: [PATCH 18/24] fix: validation for document status (cherry picked from commit 96e2e356b6c0a5331c441fe71cda1dd2f0f9f3b0) --- erpnext/stock/serial_batch_bundle.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index 7f4f0b2e945..d4defccc452 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -307,6 +307,18 @@ class SerialBatchBundle: if docstatus == 0: self.submit_serial_and_batch_bundle() + if ( + frappe.db.count( + "Serial and Batch Entry", {"parent": self.sle.serial_and_batch_bundle, "docstatus": 0} + ) + > 0 + ): + frappe.throw( + _("Serial and Batch Bundle {0} is not submitted").format( + bold(self.sle.serial_and_batch_bundle) + ) + ) + if self.item_details.has_serial_no == 1: self.set_warehouse_and_status_in_serial_nos() @@ -354,10 +366,6 @@ class SerialBatchBundle: doc.flags.ignore_voucher_validation = True doc.submit() - for row in doc.entries: - if row.docstatus == 0: - frappe.throw(_("Serial and Batch Bundle {0} is not submitted").format(bold(doc.name))) - def set_warehouse_and_status_in_serial_nos(self): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos as get_parsed_serial_nos From edd3f5da1ce9bd2ca9bd46707e94ad37348073bc Mon Sep 17 00:00:00 2001 From: KerollesFathy Date: Wed, 10 Sep 2025 17:57:30 +0000 Subject: [PATCH 19/24] fix: correct typo in asset movement purpose validation (cherry picked from commit 56da3bd2e4d92f57e2bbf6562051aeedc17239ac) --- erpnext/assets/doctype/asset_movement/asset_movement.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.py b/erpnext/assets/doctype/asset_movement/asset_movement.py index db4e7510670..5514c6a512a 100644 --- a/erpnext/assets/doctype/asset_movement/asset_movement.py +++ b/erpnext/assets/doctype/asset_movement/asset_movement.py @@ -79,7 +79,7 @@ class AssetMovement(Document): ) def validate_employee(self, d): - if self.purpose == "Tranfer and Issue": + if self.purpose == "Transfer and Issue": if not d.from_employee: frappe.throw(_("From Employee is required while issuing Asset {0}").format(d.asset)) From 97765ce8bd7fc31269259db817b101d6638cad2e Mon Sep 17 00:00:00 2001 From: KerollesFathy Date: Wed, 10 Sep 2025 18:00:44 +0000 Subject: [PATCH 20/24] fix: correct grammatical errors in asset movement validation messages (cherry picked from commit 5f083d55b5aa255e6fb963be8828779750335347) --- erpnext/assets/doctype/asset_movement/asset_movement.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.py b/erpnext/assets/doctype/asset_movement/asset_movement.py index 5514c6a512a..611f2c44cfd 100644 --- a/erpnext/assets/doctype/asset_movement/asset_movement.py +++ b/erpnext/assets/doctype/asset_movement/asset_movement.py @@ -61,7 +61,7 @@ class AssetMovement(Document): if d.source_location: if current_location != d.source_location: frappe.throw( - _("Asset {0} does not belongs to the location {1}").format(d.asset, d.source_location) + _("Asset {0} does not belong to the location {1}").format(d.asset, d.source_location) ) else: d.source_location = current_location @@ -75,7 +75,7 @@ class AssetMovement(Document): frappe.throw(_("Target Location is required while receiving Asset {0}").format(d.asset)) if d.to_employee and frappe.db.get_value("Employee", d.to_employee, "company") != self.company: frappe.throw( - _("Employee {0} does not belongs to the company {1}").format(d.to_employee, self.company) + _("Employee {0} does not belong to the company {1}").format(d.to_employee, self.company) ) def validate_employee(self, d): @@ -88,7 +88,7 @@ class AssetMovement(Document): if current_custodian != d.from_employee: frappe.throw( - _("Asset {0} does not belongs to the custodian {1}").format(d.asset, d.from_employee) + _("Asset {0} does not belong to the custodian {1}").format(d.asset, d.from_employee) ) if not d.to_employee: @@ -96,7 +96,7 @@ class AssetMovement(Document): if d.to_employee and frappe.db.get_value("Employee", d.to_employee, "company") != self.company: frappe.throw( - _("Employee {0} does not belongs to the company {1}").format(d.to_employee, self.company) + _("Employee {0} does not belong to the company {1}").format(d.to_employee, self.company) ) def on_submit(self): From 44869f02b4308f5b8befc81647658a5790d79546 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 15 Sep 2025 10:59:24 +0530 Subject: [PATCH 21/24] fix: do not allow backdated entries if stock reco exists in future for serial or batch (cherry picked from commit 335dcc976cab3f26778916d9be4b113fd66b8e8c) # Conflicts: # erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json --- .../serial_and_batch_bundle.json | 10 +- .../serial_and_batch_bundle.py | 127 +++++++++++---- .../test_stock_reconciliation.py | 148 +++--------------- erpnext/stock/stock_ledger.py | 2 +- 4 files changed, 125 insertions(+), 162 deletions(-) diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json index 02ecc11f44a..5dc59718d0b 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json @@ -108,7 +108,8 @@ "in_list_view": 1, "label": "Voucher Type", "options": "DocType", - "reqd": 1 + "reqd": 1, + "search_index": 1 }, { "fieldname": "voucher_no", @@ -195,8 +196,7 @@ "fieldtype": "Select", "label": "Type of Transaction", "options": "\nInward\nOutward\nMaintenance\nAsset Repair", - "reqd": 1, - "search_index": 1 + "reqd": 1 }, { "default": "0", @@ -256,7 +256,11 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], +<<<<<<< HEAD "modified": "2025-02-17 18:22:36.056205", +======= + "modified": "2025-09-15 14:37:26.441742", +>>>>>>> 335dcc976c (fix: do not allow backdated entries if stock reco exists in future for serial or batch) "modified_by": "Administrator", "module": "Stock", "name": "Serial and Batch Bundle", 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 e7d7446cea6..cf46867d64e 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 @@ -118,8 +118,8 @@ class SerialandBatchBundle(Document): self.allow_existing_serial_nos() if not self.flags.ignore_validate_serial_batch or frappe.flags.in_test: self.validate_serial_nos_duplicate() - self.check_future_entries_exists() + self.check_future_entries_exists() self.set_is_outward() self.calculate_total_qty() self.set_warehouse() @@ -228,7 +228,7 @@ class SerialandBatchBundle(Document): return if self.voucher_type == "Stock Reconciliation": - serial_nos = self.get_serial_nos_for_validate() + serial_nos, batches = self.get_serial_nos_for_validate() else: serial_nos = [d.serial_no for d in self.entries if d.serial_no] @@ -716,15 +716,22 @@ class SerialandBatchBundle(Document): if self.flags and self.flags.via_landed_cost_voucher: return - if not self.has_serial_no: - return + serial_nos = [] + batches = [] if self.voucher_type == "Stock Reconciliation": - serial_nos = self.get_serial_nos_for_validate(is_cancelled=is_cancelled) + serial_nos, batches = self.get_serial_nos_for_validate(is_cancelled=is_cancelled) else: + batches = [d.batch_no for d in self.entries if d.batch_no] + + if ( + self.voucher_type != "Stock Reconciliation" + and not self.flags.ignore_validate_serial_batch + and self.has_serial_no + ): serial_nos = [d.serial_no for d in self.entries if d.serial_no] - if not serial_nos: + if self.has_batch_no and not self.has_serial_no and not batches: return parent = frappe.qb.DocType("Serial and Batch Bundle") @@ -740,65 +747,117 @@ class SerialandBatchBundle(Document): .on(parent.name == child.parent) .select( child.serial_no, + child.batch_no, parent.voucher_type, parent.voucher_no, ) .where( - (child.serial_no.isin(serial_nos)) - & (child.parent != self.name) + (child.parent != self.name) & (parent.item_code == self.item_code) & (parent.docstatus == 1) & (parent.is_cancelled == 0) & (parent.type_of_transaction.isin(["Inward", "Outward"])) ) .where(timestamp_condition) - ).run(as_dict=True) + ) + + if self.has_batch_no and not self.has_serial_no: + future_entries = future_entries.where(parent.voucher_type == "Stock Reconciliation") + + if serial_nos: + future_entries = future_entries.where( + (child.serial_no.isin(serial_nos)) + | ((parent.warehouse == self.warehouse) & (parent.voucher_type == "Stock Reconciliation")) + ) + elif self.has_serial_no: + future_entries = future_entries.where( + (parent.warehouse == self.warehouse) & (parent.voucher_type == "Stock Reconciliation") + ) + elif batches: + future_entries = future_entries.where(child.batch_no.isin(batches)) + + future_entries = future_entries.run(as_dict=True) if future_entries: - msg = """The serial nos has been used in the future - transactions so you need to cancel them first. - The list of serial nos and their respective - transactions are as below.""" + if self.has_serial_no: + title = "Serial No Exists In Future Transaction(s)" + else: + title = "Batches Exists In Future Transaction(s)" + + msg = """Since the stock reconciliation exists + for future dates, cancel it first. For Serial/Batch, + if you want to make a backdated transaction, + avoid using stock reconciliation. + For more details about the transaction, + please refer to the list below. + """ msg += "

    " for d in future_entries: - msg += f"
  • {d.serial_no} in {get_link_to_form(d.voucher_type, d.voucher_no)}
  • " + if self.has_serial_no: + msg += f"
  • {d.serial_no} in {get_link_to_form(d.voucher_type, d.voucher_no)}
  • " + else: + msg += f"
  • {d.batch_no} in {get_link_to_form(d.voucher_type, d.voucher_no)}
  • " msg += "
" - title = "Serial No Exists In Future Transaction(s)" - frappe.throw(_(msg), title=_(title), exc=SerialNoExistsInFutureTransactionError) def get_serial_nos_for_validate(self, is_cancelled=False): serial_nos = [d.serial_no for d in self.entries if d.serial_no] - skip_serial_nos = self.get_skip_serial_nos_for_stock_reconciliation(is_cancelled=is_cancelled) - serial_nos = list(set(sorted(serial_nos)) - set(sorted(skip_serial_nos))) + batches = [d.batch_no for d in self.entries if d.batch_no] - return serial_nos + skip_serial_nos, skip_batches = self.get_skip_serial_nos_for_stock_reconciliation( + is_cancelled=is_cancelled + ) + + serial_nos = list(set(sorted(serial_nos)) - set(sorted(skip_serial_nos))) + batch_nos = list(set(sorted(batches)) - set(sorted(skip_batches))) + + return serial_nos, batch_nos def get_skip_serial_nos_for_stock_reconciliation(self, is_cancelled=False): data = get_stock_reco_details(self.voucher_detail_no) + if not data: - return [] + return [], [] + + current_serial_nos = set() + serial_nos = set() + current_batches = set() + batches = set() if data.current_serial_no: current_serial_nos = set(parse_serial_nos(data.current_serial_no)) serial_nos = set(parse_serial_nos(data.serial_no)) if data.serial_no else set([]) - return list(serial_nos.intersection(current_serial_nos)) + return list(serial_nos.intersection(current_serial_nos)), [] + + elif data.batch_no and data.current_qty == data.qty: + return [], [data.batch_no] + elif data.current_serial_and_batch_bundle: - current_serial_nos = set(get_serial_nos_from_bundle(data.current_serial_and_batch_bundle)) + if self.has_serial_no: + current_serial_nos = set(get_serial_nos_from_bundle(data.current_serial_and_batch_bundle)) + else: + current_batches = set(get_batches_from_bundle(data.current_serial_and_batch_bundle)) + if is_cancelled: - return current_serial_nos + return list(current_serial_nos), list(current_batches) - serial_nos = ( - set(get_serial_nos_from_bundle(data.serial_and_batch_bundle)) - if data.serial_and_batch_bundle - else set([]) + if self.has_serial_no: + serial_nos = ( + set(get_serial_nos_from_bundle(data.serial_and_batch_bundle)) + if data.serial_and_batch_bundle + else set([]) + ) + elif self.has_batch_no and data.serial_and_batch_bundle: + batches = set(get_batches_from_bundle(data.serial_and_batch_bundle)) + + return list(serial_nos.intersection(current_serial_nos)), list( + batches.intersection(current_batches) ) - return list(serial_nos.intersection(current_serial_nos)) - return [] + return [], [] def reset_qty(self, row, qty_field=None): qty_field = self.get_qty_field(row, qty_field=qty_field) @@ -2673,6 +2732,14 @@ def get_stock_reco_details(voucher_detail_no): return frappe.db.get_value( "Stock Reconciliation Item", voucher_detail_no, - ["current_serial_no", "serial_no", "serial_and_batch_bundle", "current_serial_and_batch_bundle"], + [ + "current_serial_no", + "serial_no", + "serial_and_batch_bundle", + "current_serial_and_batch_bundle", + "batch_no", + "qty", + "current_qty", + ], as_dict=True, ) diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 5b59030c945..afbfcdd6062 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -696,7 +696,7 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin): batch_no = get_batch_from_bundle(se1.items[0].serial_and_batch_bundle) # Removed 50 Qty, Balace Qty 50 - se2 = make_stock_entry( + make_stock_entry( item_code=item_code, batch_no=batch_no, posting_time="10:00:00", @@ -729,33 +729,13 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin): batch_no=batch_no, posting_time="12:00:00", source=warehouse, - qty=50, + qty=52, basic_rate=700, ) self.assertFalse(frappe.db.exists("Repost Item Valuation", {"voucher_no": stock_reco.name})) - # Cancel the backdated Stock Entry se2, - # Since Stock Reco entry in the future the Balace Qty should remain as it's (50) - - se2.cancel() - - sle = frappe.get_all( - "Stock Ledger Entry", - filters={"item_code": item_code, "warehouse": warehouse, "is_cancelled": 0}, - fields=["qty_after_transaction", "actual_qty", "voucher_type", "voucher_no"], - order_by="posting_time desc, creation desc", - ) - - self.assertEqual(flt(sle[0].qty_after_transaction), flt(50.0)) - - sle = frappe.get_all( - "Stock Ledger Entry", - filters={"is_cancelled": 0, "voucher_no": stock_reco.name, "actual_qty": ("<", 0)}, - fields=["actual_qty"], - ) - - self.assertEqual(flt(sle[0].actual_qty), flt(-100.0)) + self.assertRaises(frappe.ValidationError, stock_reco.cancel) def test_update_stock_reconciliation_while_reposting(self): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry @@ -905,27 +885,16 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin): self.assertAlmostEqual(d.stock_value_difference, 500.0) # Step - 3: Create a Purchase Receipt before the first Purchase Receipt - make_purchase_receipt( - item_code=item_code, warehouse=warehouse, qty=10, rate=200, posting_date=add_days(nowdate(), -5) + pr = make_purchase_receipt( + item_code=item_code, + warehouse=warehouse, + qty=10, + rate=200, + posting_date=add_days(nowdate(), -5), + do_not_submit=True, ) - data = frappe.get_all( - "Stock Ledger Entry", - fields=["serial_no", "actual_qty", "stock_value_difference"], - filters={"voucher_no": sr1.name, "is_cancelled": 0}, - order_by="creation", - ) - - for d in data: - if d.actual_qty < 0: - self.assertEqual(d.actual_qty, -20.0) - self.assertAlmostEqual(d.stock_value_difference, -3000.0) - else: - self.assertEqual(d.actual_qty, 5.0) - self.assertAlmostEqual(d.stock_value_difference, 500.0) - - active_serial_no = frappe.get_all("Serial No", filters={"status": "Active", "item_code": item_code}) - self.assertEqual(len(active_serial_no), 5) + self.assertRaises(frappe.ValidationError, pr.submit) def test_balance_qty_for_batch_with_backdated_stock_reco_and_future_entries(self): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry @@ -1463,6 +1432,7 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin): sr = create_stock_reconciliation( item_code=item_code, + posting_date=add_days(nowdate(), -2), warehouse=warehouse, qty=10, rate=100, @@ -1482,9 +1452,9 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin): self.assertTrue(len(stock_ledgers) == 1) - make_stock_entry( + se = make_stock_entry( item_code=item_code, - target=warehouse, + source=warehouse, qty=10, basic_rate=100, use_serial_batch_fields=1, @@ -1496,23 +1466,19 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin): item_code=item_code, warehouse=warehouse, qty=10, - rate=100, + rate=200, use_serial_batch_fields=1, batch_no=batch_no, posting_date=add_days(nowdate(), -1), ) - stock_ledgers = frappe.get_all( + stock_ledger = frappe.get_all( "Stock Ledger Entry", - filters={"voucher_no": sr.name, "is_cancelled": 0}, - pluck="name", + filters={"voucher_no": se.name, "is_cancelled": 0}, + fields=["stock_value_difference"], ) - sr.reload() - self.assertEqual(sr.items[0].current_qty, 10) - self.assertEqual(sr.items[0].current_valuation_rate, 100) - - self.assertTrue(len(stock_ledgers) == 2) + self.assertEqual(stock_ledger[0].stock_value_difference, 2000.0 * -1) def test_serial_no_backdated_stock_reco(self): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry @@ -1564,7 +1530,7 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin): self.assertTrue(status == "Active") - make_stock_entry( + se = make_stock_entry( item_code=serial_item, source=warehouse, qty=1, @@ -1590,80 +1556,6 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin): self.assertFalse(status == "Active") - def test_stock_reconciliation_for_batch_with_backward(self): - # Make stock inward for 10 -> Stock Reco for 20 after two days - # Make backdated delivery note for 10 qty between stock inward and stock reco - # Check the state of the current serial and batch bundle in the stock reco - # The state should be cancelled - - from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry - - item_code = "Test Stock Reco for Batch with Backward" - - self.make_item( - item_code, {"has_batch_no": 1, "create_new_batch": 1, "batch_number_series": "BCN-CB.#####"} - ) - - warehouse = "_Test Warehouse - _TC" - - se = make_stock_entry( - posting_date=add_days(nowdate(), -2), - posting_time="02:00", - item_code=item_code, - target=warehouse, - qty=10, - basic_rate=100, - ) - - batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle) - - sr = create_stock_reconciliation( - item_code=item_code, - warehouse=warehouse, - qty=20, - rate=200, - use_serial_batch_fields=1, - batch_no=batch_no, - posting_date=nowdate(), - posting_time="03:00", - ) - - current_sabb = sr.items[0].current_serial_and_batch_bundle - - self.assertTrue(frappe.db.get_value("Serial and Batch Bundle", current_sabb, "docstatus") == 1) - - self.assertTrue( - frappe.db.get_value( - "Stock Ledger Entry", {"serial_and_batch_bundle": current_sabb, "is_cancelled": 0}, "name" - ) - ) - self.assertTrue(sr.items[0].current_serial_and_batch_bundle) - self.assertTrue(sr.items[0].current_qty) - self.assertTrue(sr.items[0].current_qty == 10) - - se = make_stock_entry( - posting_date=add_days(nowdate(), -1), - posting_time="02:00", - item_code=item_code, - source=warehouse, - qty=10, - basic_rate=100, - use_serial_batch_fields=1, - batch_no=batch_no, - ) - - sr.reload() - self.assertFalse(sr.items[0].current_serial_and_batch_bundle) - self.assertTrue(sr.items[0].current_qty == 0) - - self.assertFalse(frappe.db.get_value("Serial and Batch Bundle", current_sabb, "docstatus") == 1) - - self.assertFalse( - frappe.db.get_value( - "Stock Ledger Entry", {"serial_and_batch_bundle": current_sabb, "is_cancelled": 0}, "name" - ) - ) - def create_batch_item_with_batch(item_name, batch_id): batch_item_doc = create_item(item_name, is_stock_item=1) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 9f64786d110..3030bde7ba2 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -817,7 +817,7 @@ class update_entries_after: if ( sle.voucher_type == "Stock Reconciliation" - and (sle.batch_no or sle.serial_no or sle.serial_and_batch_bundle) + and (sle.serial_and_batch_bundle) and sle.voucher_detail_no and not self.args.get("sle_id") and sle.is_cancelled == 0 From 572af6e08a06698c9874a31fe657693e8d4d262b Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Mon, 15 Sep 2025 15:43:38 +0530 Subject: [PATCH 22/24] chore: fix conflicts --- .../serial_and_batch_bundle/serial_and_batch_bundle.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json index 5dc59718d0b..0a24bfb5645 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json @@ -256,11 +256,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], -<<<<<<< HEAD - "modified": "2025-02-17 18:22:36.056205", -======= "modified": "2025-09-15 14:37:26.441742", ->>>>>>> 335dcc976c (fix: do not allow backdated entries if stock reco exists in future for serial or batch) "modified_by": "Administrator", "module": "Stock", "name": "Serial and Batch Bundle", From ee7da999a3d4294585249b91dbbdcf491dd56e82 Mon Sep 17 00:00:00 2001 From: ravibharathi656 Date: Mon, 15 Sep 2025 10:11:23 +0530 Subject: [PATCH 23/24] fix(sales invoice): fetch tax id from customer (cherry picked from commit df329964dd2e531ddbdb7ab53153a24014c36d3d) --- erpnext/accounts/doctype/sales_invoice/sales_invoice.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 816a6bfeded..8cc9598d915 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -281,6 +281,7 @@ "read_only": 1 }, { + "fetch_from": "customer.tax_id", "fieldname": "tax_id", "fieldtype": "Data", "hide_days": 1, @@ -2198,7 +2199,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2025-08-04 19:20:28.732039", + "modified": "2025-09-09 14:48:59.472826", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", From 39eeafd0d0119f74bda69674ffbbc74580321b85 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 16 Sep 2025 11:57:43 +0530 Subject: [PATCH 24/24] fix: set basic rate on selection of the batch (cherry picked from commit bebb8ae1ea7b66707e7ccc4b9a2c8a1415e0969a) --- erpnext/stock/doctype/stock_entry/stock_entry.js | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index ea2ec897d1d..0989d610b4a 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -962,6 +962,7 @@ frappe.ui.form.on("Stock Entry Detail", { }); } + frm.events.set_basic_rate(frm, cdt, cdn); validate_sample_quantity(frm, cdt, cdn); },