From ad8c8cb0e81b322fbbeb837d5765444629cd70ee Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 29 Jan 2026 14:30:22 +0530 Subject: [PATCH] Merge pull request #52140 from frappe/mergify/bp/version-15-hotfix/pr-52007 Fix: Set Zero Rate for Standalone Credit Note with Expired Batch (backport #52007) --- .../sales_invoice/test_sales_invoice.py | 60 +++++++++++++++++++ .../controllers/sales_and_purchase_return.py | 39 +++++++++++- erpnext/controllers/selling_controller.py | 21 ++++++- .../selling_settings/selling_settings.json | 12 +++- .../selling_settings/selling_settings.py | 1 + 5 files changed, 127 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index bd8af98b16f..f9b6ab4f07c 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -4775,6 +4775,66 @@ class TestSalesInvoice(FrappeTestCase): self.assertEqual(q[0][0], 1) + @change_settings("Selling Settings", {"set_zero_rate_for_expired_batch": True}) + def test_zero_valuation_for_standalone_credit_note_with_expired_batch(self): + item_code = "_Test Item for Expiry Batch Zero Valuation" + make_item_for_si( + item_code, + { + "is_stock_item": 1, + "has_batch_no": 1, + "has_expiry_date": 1, + "shelf_life_in_days": 2, + "create_new_batch": 1, + "batch_number_series": "TBATCH-EBZV.####", + }, + ) + + se = make_stock_entry( + item_code=item_code, + qty=10, + target="_Test Warehouse - _TC", + rate=100, + ) + + # fetch batch no from bundle + batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle) + + si = create_sales_invoice( + posting_date=add_days(nowdate(), 3), + item=item_code, + qty=-10, + rate=100, + is_return=1, + update_stock=1, + use_serial_batch_fields=1, + do_not_save=1, + do_not_submit=1, + ) + + si.items[0].batch_no = batch_no + si.save() + si.submit() + + si.reload() + # check zero incoming rate in voucher + self.assertEqual(si.items[0].incoming_rate, 0.0) + + # chekc zero incoming rate in stock ledger + stock_ledger_entry = frappe.db.get_value( + "Stock Ledger Entry", + { + "voucher_type": "Sales Invoice", + "voucher_no": si.name, + "item_code": item_code, + "warehouse": "_Test Warehouse - _TC", + }, + ["incoming_rate", "valuation_rate"], + as_dict=True, + ) + + self.assertEqual(stock_ledger_entry.incoming_rate, 0.0) + def make_item_for_si(item_code, properties=None): from erpnext.stock.doctype.item.test_item import make_item diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index c774c3de341..d3d5cb808f3 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -11,7 +11,7 @@ from frappe.utils import cint, flt, format_datetime, get_datetime import erpnext 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.utils import get_incoming_rate, get_valuation_method +from erpnext.stock.utils import get_incoming_rate, get_valuation_method, getdate class StockOverReturnError(frappe.ValidationError): @@ -683,6 +683,29 @@ def get_rate_for_return( else: select_field = "abs(stock_value_difference / actual_qty)" + item_details = frappe.get_cached_value("Item", item_code, ["has_batch_no", "has_expiry_date"], as_dict=1) + set_zero_rate_for_expired_batch = frappe.db.get_single_value( + "Selling Settings", "set_zero_rate_for_expired_batch" + ) + + if ( + set_zero_rate_for_expired_batch + and item_details.has_batch_no + and item_details.has_expiry_date + and not return_against + and voucher_type in ["Sales Invoice", "Delivery Note"] + ): + # set incoming_rate zero explicitly for standalone credit note with expired batch + batch_no = frappe.db.get_value(f"{voucher_type} Item", voucher_detail_no, "batch_no") + if batch_no and is_batch_expired(batch_no, sle.get("posting_date")): + frappe.db.set_value( + voucher_type + " Item", + voucher_detail_no, + "incoming_rate", + 0, + ) + return 0 + rate = flt(frappe.db.get_value("Stock Ledger Entry", filters, select_field)) if not (rate and return_against) and voucher_type in ["Sales Invoice", "Delivery Note"]: rate = frappe.db.get_value(f"{voucher_type} Item", voucher_detail_no, "incoming_rate") @@ -1152,3 +1175,17 @@ def get_available_serial_nos(serial_nos, warehouse): def get_payment_data(invoice): payment = frappe.db.get_all("Sales Invoice Payment", {"parent": invoice}, ["mode_of_payment", "amount"]) return payment + + +def is_batch_expired(batch_no, posting_date): + """ + To check whether the batch is expired or not based on the posting date. + """ + expiry_date = frappe.db.get_value("Batch", batch_no, "expiry_date") + if not expiry_date: + return + + if getdate(posting_date) > getdate(expiry_date): + return True + + return False diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 3667a7a7e76..5c2dc7491c2 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -8,7 +8,7 @@ from frappe.utils import cint, flt, get_link_to_form, nowtime from erpnext.accounts.party import render_address from erpnext.controllers.accounts_controller import get_taxes_and_charges -from erpnext.controllers.sales_and_purchase_return import get_rate_for_return +from erpnext.controllers.sales_and_purchase_return import get_rate_for_return, is_batch_expired from erpnext.controllers.stock_controller import StockController from erpnext.stock.doctype.item.item import set_item_default from erpnext.stock.get_item_details import get_bin_details, get_conversion_factor @@ -521,16 +521,31 @@ class SellingController(StockController): allow_at_arms_length_price = frappe.get_cached_value( "Stock Settings", None, "allow_internal_transfer_at_arms_length_price" ) + set_zero_rate_for_expired_batch = frappe.db.get_single_value( + "Selling Settings", "set_zero_rate_for_expired_batch" + ) + items = self.get("items") + (self.get("packed_items") or []) for d in items: if not frappe.get_cached_value("Item", d.item_code, "is_stock_item"): continue item_details = frappe.get_cached_value( - "Item", d.item_code, ["has_serial_no", "has_batch_no"], as_dict=1 + "Item", d.item_code, ["has_serial_no", "has_batch_no", "has_expiry_date"], as_dict=1 ) - if not self.get("return_against") or ( + if ( + set_zero_rate_for_expired_batch + and item_details.has_batch_no + and item_details.has_expiry_date + and self.get("is_return") + and not self.get("return_against") + and is_batch_expired(d.batch_no, self.get("posting_date")) + ): + # set incoming rate as zero for stand-lone credit note with expired batch + d.incoming_rate = 0 + + elif not self.get("return_against") or ( get_valuation_method(d.item_code) == "Moving Average" and self.get("is_return") and not item_details.has_serial_no diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json index d2f8945eb29..5bef5bc55a2 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.json +++ b/erpnext/selling/doctype/selling_settings/selling_settings.json @@ -35,7 +35,8 @@ "hide_tax_id", "enable_discount_accounting", "allow_zero_qty_in_quotation", - "allow_zero_qty_in_sales_order" + "allow_zero_qty_in_sales_order", + "set_zero_rate_for_expired_batch" ], "fields": [ { @@ -224,6 +225,13 @@ "fieldname": "fallback_to_default_price_list", "fieldtype": "Check", "label": "Use Prices from Default Price List as Fallback" + }, + { + "default": "0", + "description": "If enabled, system will set incoming rate as zero for stand-alone credit notes with expired batch item.", + "fieldname": "set_zero_rate_for_expired_batch", + "fieldtype": "Check", + "label": "Set Incoming Rate as Zero for Expired Batch" } ], "grid_page_length": 50, @@ -232,7 +240,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2026-01-21 17:28:37.027837", + "modified": "2026-01-24 00:04:33.105916", "modified_by": "Administrator", "module": "Selling", "name": "Selling Settings", diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.py b/erpnext/selling/doctype/selling_settings/selling_settings.py index f15fdc7041d..cad8385ab73 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.py +++ b/erpnext/selling/doctype/selling_settings/selling_settings.py @@ -41,6 +41,7 @@ class SellingSettings(Document): role_to_override_stop_action: DF.Link | None sales_update_frequency: DF.Literal["Monthly", "Each Transaction", "Daily"] selling_price_list: DF.Link | None + set_zero_rate_for_expired_batch: DF.Check so_required: DF.Literal["No", "Yes"] territory: DF.Link | None validate_selling_price: DF.Check