mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-19 04:59:18 +00:00
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)
(cherry picked from commit ad8c8cb0e8)
This commit is contained in:
@@ -4775,6 +4775,66 @@ class TestSalesInvoice(FrappeTestCase):
|
|||||||
|
|
||||||
self.assertEqual(q[0][0], 1)
|
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):
|
def make_item_for_si(item_code, properties=None):
|
||||||
from erpnext.stock.doctype.item.test_item import make_item
|
from erpnext.stock.doctype.item.test_item import make_item
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from frappe.utils import cint, flt, format_datetime, get_datetime
|
|||||||
import erpnext
|
import erpnext
|
||||||
from erpnext.stock.serial_batch_bundle import get_batches_from_bundle
|
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.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):
|
class StockOverReturnError(frappe.ValidationError):
|
||||||
@@ -683,6 +683,29 @@ def get_rate_for_return(
|
|||||||
else:
|
else:
|
||||||
select_field = "abs(stock_value_difference / actual_qty)"
|
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))
|
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"]:
|
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")
|
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):
|
def get_payment_data(invoice):
|
||||||
payment = frappe.db.get_all("Sales Invoice Payment", {"parent": invoice}, ["mode_of_payment", "amount"])
|
payment = frappe.db.get_all("Sales Invoice Payment", {"parent": invoice}, ["mode_of_payment", "amount"])
|
||||||
return payment
|
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
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from frappe.utils import cint, flt, get_link_to_form, nowtime
|
|||||||
|
|
||||||
from erpnext.accounts.party import render_address
|
from erpnext.accounts.party import render_address
|
||||||
from erpnext.controllers.accounts_controller import get_taxes_and_charges
|
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.controllers.stock_controller import StockController
|
||||||
from erpnext.stock.doctype.item.item import set_item_default
|
from erpnext.stock.doctype.item.item import set_item_default
|
||||||
from erpnext.stock.get_item_details import get_bin_details, get_conversion_factor
|
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(
|
allow_at_arms_length_price = frappe.get_cached_value(
|
||||||
"Stock Settings", None, "allow_internal_transfer_at_arms_length_price"
|
"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 [])
|
items = self.get("items") + (self.get("packed_items") or [])
|
||||||
for d in items:
|
for d in items:
|
||||||
if not frappe.get_cached_value("Item", d.item_code, "is_stock_item"):
|
if not frappe.get_cached_value("Item", d.item_code, "is_stock_item"):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
item_details = frappe.get_cached_value(
|
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"
|
get_valuation_method(d.item_code) == "Moving Average"
|
||||||
and self.get("is_return")
|
and self.get("is_return")
|
||||||
and not item_details.has_serial_no
|
and not item_details.has_serial_no
|
||||||
|
|||||||
@@ -35,7 +35,8 @@
|
|||||||
"hide_tax_id",
|
"hide_tax_id",
|
||||||
"enable_discount_accounting",
|
"enable_discount_accounting",
|
||||||
"allow_zero_qty_in_quotation",
|
"allow_zero_qty_in_quotation",
|
||||||
"allow_zero_qty_in_sales_order"
|
"allow_zero_qty_in_sales_order",
|
||||||
|
"set_zero_rate_for_expired_batch"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -224,6 +225,13 @@
|
|||||||
"fieldname": "fallback_to_default_price_list",
|
"fieldname": "fallback_to_default_price_list",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Use Prices from Default Price List as Fallback"
|
"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,
|
"grid_page_length": 50,
|
||||||
@@ -232,7 +240,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2026-01-21 17:28:37.027837",
|
"modified": "2026-01-24 00:04:33.105916",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Selling",
|
"module": "Selling",
|
||||||
"name": "Selling Settings",
|
"name": "Selling Settings",
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ class SellingSettings(Document):
|
|||||||
role_to_override_stop_action: DF.Link | None
|
role_to_override_stop_action: DF.Link | None
|
||||||
sales_update_frequency: DF.Literal["Monthly", "Each Transaction", "Daily"]
|
sales_update_frequency: DF.Literal["Monthly", "Each Transaction", "Daily"]
|
||||||
selling_price_list: DF.Link | None
|
selling_price_list: DF.Link | None
|
||||||
|
set_zero_rate_for_expired_batch: DF.Check
|
||||||
so_required: DF.Literal["No", "Yes"]
|
so_required: DF.Literal["No", "Yes"]
|
||||||
territory: DF.Link | None
|
territory: DF.Link | None
|
||||||
validate_selling_price: DF.Check
|
validate_selling_price: DF.Check
|
||||||
|
|||||||
Reference in New Issue
Block a user