Skip to content

Commit 547fbec

Browse files
Merge pull request #52176 from frappe/mergify/bp/version-15/pr-52140
Fix: Set Zero Rate for Standalone Credit Note with Expired Batch (backport #52007) (backport #52140)
2 parents d9bd429 + 65ed4e5 commit 547fbec

5 files changed

Lines changed: 127 additions & 6 deletions

File tree

erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4775,6 +4775,66 @@ def test_system_generated_exchange_gain_or_loss_je_after_repost(self):
47754775

47764776
self.assertEqual(q[0][0], 1)
47774777

4778+
@change_settings("Selling Settings", {"set_zero_rate_for_expired_batch": True})
4779+
def test_zero_valuation_for_standalone_credit_note_with_expired_batch(self):
4780+
item_code = "_Test Item for Expiry Batch Zero Valuation"
4781+
make_item_for_si(
4782+
item_code,
4783+
{
4784+
"is_stock_item": 1,
4785+
"has_batch_no": 1,
4786+
"has_expiry_date": 1,
4787+
"shelf_life_in_days": 2,
4788+
"create_new_batch": 1,
4789+
"batch_number_series": "TBATCH-EBZV.####",
4790+
},
4791+
)
4792+
4793+
se = make_stock_entry(
4794+
item_code=item_code,
4795+
qty=10,
4796+
target="_Test Warehouse - _TC",
4797+
rate=100,
4798+
)
4799+
4800+
# fetch batch no from bundle
4801+
batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)
4802+
4803+
si = create_sales_invoice(
4804+
posting_date=add_days(nowdate(), 3),
4805+
item=item_code,
4806+
qty=-10,
4807+
rate=100,
4808+
is_return=1,
4809+
update_stock=1,
4810+
use_serial_batch_fields=1,
4811+
do_not_save=1,
4812+
do_not_submit=1,
4813+
)
4814+
4815+
si.items[0].batch_no = batch_no
4816+
si.save()
4817+
si.submit()
4818+
4819+
si.reload()
4820+
# check zero incoming rate in voucher
4821+
self.assertEqual(si.items[0].incoming_rate, 0.0)
4822+
4823+
# chekc zero incoming rate in stock ledger
4824+
stock_ledger_entry = frappe.db.get_value(
4825+
"Stock Ledger Entry",
4826+
{
4827+
"voucher_type": "Sales Invoice",
4828+
"voucher_no": si.name,
4829+
"item_code": item_code,
4830+
"warehouse": "_Test Warehouse - _TC",
4831+
},
4832+
["incoming_rate", "valuation_rate"],
4833+
as_dict=True,
4834+
)
4835+
4836+
self.assertEqual(stock_ledger_entry.incoming_rate, 0.0)
4837+
47784838

47794839
def make_item_for_si(item_code, properties=None):
47804840
from erpnext.stock.doctype.item.test_item import make_item

erpnext/controllers/sales_and_purchase_return.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import erpnext
1212
from erpnext.stock.serial_batch_bundle import get_batches_from_bundle
1313
from erpnext.stock.serial_batch_bundle import get_serial_nos as get_serial_nos_from_bundle
14-
from erpnext.stock.utils import get_incoming_rate, get_valuation_method
14+
from erpnext.stock.utils import get_incoming_rate, get_valuation_method, getdate
1515

1616

1717
class StockOverReturnError(frappe.ValidationError):
@@ -683,6 +683,29 @@ def get_rate_for_return(
683683
else:
684684
select_field = "abs(stock_value_difference / actual_qty)"
685685

686+
item_details = frappe.get_cached_value("Item", item_code, ["has_batch_no", "has_expiry_date"], as_dict=1)
687+
set_zero_rate_for_expired_batch = frappe.db.get_single_value(
688+
"Selling Settings", "set_zero_rate_for_expired_batch"
689+
)
690+
691+
if (
692+
set_zero_rate_for_expired_batch
693+
and item_details.has_batch_no
694+
and item_details.has_expiry_date
695+
and not return_against
696+
and voucher_type in ["Sales Invoice", "Delivery Note"]
697+
):
698+
# set incoming_rate zero explicitly for standalone credit note with expired batch
699+
batch_no = frappe.db.get_value(f"{voucher_type} Item", voucher_detail_no, "batch_no")
700+
if batch_no and is_batch_expired(batch_no, sle.get("posting_date")):
701+
frappe.db.set_value(
702+
voucher_type + " Item",
703+
voucher_detail_no,
704+
"incoming_rate",
705+
0,
706+
)
707+
return 0
708+
686709
rate = flt(frappe.db.get_value("Stock Ledger Entry", filters, select_field))
687710
if not (rate and return_against) and voucher_type in ["Sales Invoice", "Delivery Note"]:
688711
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):
11521175
def get_payment_data(invoice):
11531176
payment = frappe.db.get_all("Sales Invoice Payment", {"parent": invoice}, ["mode_of_payment", "amount"])
11541177
return payment
1178+
1179+
1180+
def is_batch_expired(batch_no, posting_date):
1181+
"""
1182+
To check whether the batch is expired or not based on the posting date.
1183+
"""
1184+
expiry_date = frappe.db.get_value("Batch", batch_no, "expiry_date")
1185+
if not expiry_date:
1186+
return
1187+
1188+
if getdate(posting_date) > getdate(expiry_date):
1189+
return True
1190+
1191+
return False

erpnext/controllers/selling_controller.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
from erpnext.accounts.party import render_address
1010
from erpnext.controllers.accounts_controller import get_taxes_and_charges
11-
from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
11+
from erpnext.controllers.sales_and_purchase_return import get_rate_for_return, is_batch_expired
1212
from erpnext.controllers.stock_controller import StockController
1313
from erpnext.stock.doctype.item.item import set_item_default
1414
from erpnext.stock.get_item_details import get_bin_details, get_conversion_factor
@@ -521,16 +521,31 @@ def set_incoming_rate(self):
521521
allow_at_arms_length_price = frappe.get_cached_value(
522522
"Stock Settings", None, "allow_internal_transfer_at_arms_length_price"
523523
)
524+
set_zero_rate_for_expired_batch = frappe.db.get_single_value(
525+
"Selling Settings", "set_zero_rate_for_expired_batch"
526+
)
527+
524528
items = self.get("items") + (self.get("packed_items") or [])
525529
for d in items:
526530
if not frappe.get_cached_value("Item", d.item_code, "is_stock_item"):
527531
continue
528532

529533
item_details = frappe.get_cached_value(
530-
"Item", d.item_code, ["has_serial_no", "has_batch_no"], as_dict=1
534+
"Item", d.item_code, ["has_serial_no", "has_batch_no", "has_expiry_date"], as_dict=1
531535
)
532536

533-
if not self.get("return_against") or (
537+
if (
538+
set_zero_rate_for_expired_batch
539+
and item_details.has_batch_no
540+
and item_details.has_expiry_date
541+
and self.get("is_return")
542+
and not self.get("return_against")
543+
and is_batch_expired(d.batch_no, self.get("posting_date"))
544+
):
545+
# set incoming rate as zero for stand-lone credit note with expired batch
546+
d.incoming_rate = 0
547+
548+
elif not self.get("return_against") or (
534549
get_valuation_method(d.item_code) == "Moving Average"
535550
and self.get("is_return")
536551
and not item_details.has_serial_no

erpnext/selling/doctype/selling_settings/selling_settings.json

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@
3535
"hide_tax_id",
3636
"enable_discount_accounting",
3737
"allow_zero_qty_in_quotation",
38-
"allow_zero_qty_in_sales_order"
38+
"allow_zero_qty_in_sales_order",
39+
"set_zero_rate_for_expired_batch"
3940
],
4041
"fields": [
4142
{
@@ -224,6 +225,13 @@
224225
"fieldname": "fallback_to_default_price_list",
225226
"fieldtype": "Check",
226227
"label": "Use Prices from Default Price List as Fallback"
228+
},
229+
{
230+
"default": "0",
231+
"description": "If enabled, system will set incoming rate as zero for stand-alone credit notes with expired batch item.",
232+
"fieldname": "set_zero_rate_for_expired_batch",
233+
"fieldtype": "Check",
234+
"label": "Set Incoming Rate as Zero for Expired Batch"
227235
}
228236
],
229237
"grid_page_length": 50,
@@ -232,7 +240,7 @@
232240
"index_web_pages_for_search": 1,
233241
"issingle": 1,
234242
"links": [],
235-
"modified": "2026-01-21 17:28:37.027837",
243+
"modified": "2026-01-24 00:04:33.105916",
236244
"modified_by": "Administrator",
237245
"module": "Selling",
238246
"name": "Selling Settings",

erpnext/selling/doctype/selling_settings/selling_settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ class SellingSettings(Document):
4141
role_to_override_stop_action: DF.Link | None
4242
sales_update_frequency: DF.Literal["Monthly", "Each Transaction", "Daily"]
4343
selling_price_list: DF.Link | None
44+
set_zero_rate_for_expired_batch: DF.Check
4445
so_required: DF.Literal["No", "Yes"]
4546
territory: DF.Link | None
4647
validate_selling_price: DF.Check

0 commit comments

Comments
 (0)