From 0f9bf08685df67c7981eccd37c61e1ee945908e8 Mon Sep 17 00:00:00 2001 From: kavin-114 Date: Fri, 23 Jan 2026 02:12:00 +0530 Subject: [PATCH] fix(credit-note): set incoming rate as zero for expired batch (cherry picked from commit e78c750b4eb138818f5a1742d5b0ecc12dcb539a) --- .../controllers/sales_and_purchase_return.py | 39 ++++++++++++++++++- erpnext/controllers/selling_controller.py | 21 ++++++++-- .../selling_settings/selling_settings.py | 2 +- 3 files changed, 57 insertions(+), 5 deletions(-) diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index b4f618aecaa..0773dce71ae 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -12,7 +12,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.utils import get_combine_datetime, get_incoming_rate, get_valuation_method +from erpnext.stock.utils import get_combine_datetime, get_incoming_rate, get_valuation_method, getdate class StockOverReturnError(frappe.ValidationError): @@ -759,6 +759,29 @@ def get_rate_for_return( StockLedgerEntry = frappe.qb.DocType("Stock Ledger Entry") select_field = Abs(StockLedgerEntry.stock_value_difference / StockLedgerEntry.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") @@ -1276,3 +1299,17 @@ def get_sales_invoice_item_from_consolidated_invoice(return_against_pos_invoice, return result[0].name if result else None except Exception: return None + + +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 57aeea727bc..db54db5cf1f 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 @@ -536,16 +536,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, self.company) == "Moving Average" and self.get("is_return") and not item_details.has_serial_no diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.py b/erpnext/selling/doctype/selling_settings/selling_settings.py index 9427a514c09..239230de895 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.py +++ b/erpnext/selling/doctype/selling_settings/selling_settings.py @@ -44,7 +44,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: DF.Check + set_zero_rate_for_expired_batch: DF.Check so_required: DF.Literal["No", "Yes"] territory: DF.Link | None use_legacy_js_reactivity: DF.Check