mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-25 17:48:30 +00:00
* fix: provision to enable do not use batch-wise valuation (#42186)
fix: provision to enable do not use batchwise valuation
(cherry picked from commit f06ba0cc36)
# Conflicts:
# erpnext/stock/doctype/stock_settings/stock_settings.json
* chore: fix conflicts
---------
Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
This commit is contained in:
@@ -369,3 +369,4 @@ erpnext.patches.v15_0.rename_number_of_depreciations_booked_to_opening_booked_de
|
|||||||
erpnext.patches.v15_0.update_warehouse_field_in_asset_repair_consumed_item_doctype
|
erpnext.patches.v15_0.update_warehouse_field_in_asset_repair_consumed_item_doctype
|
||||||
erpnext.patches.v15_0.update_asset_repair_field_in_stock_entry
|
erpnext.patches.v15_0.update_asset_repair_field_in_stock_entry
|
||||||
erpnext.patches.v15_0.update_total_number_of_booked_depreciations
|
erpnext.patches.v15_0.update_total_number_of_booked_depreciations
|
||||||
|
erpnext.patches.v15_0.do_not_use_batchwise_valuation
|
||||||
|
|||||||
15
erpnext/patches/v15_0/do_not_use_batchwise_valuation.py
Normal file
15
erpnext/patches/v15_0/do_not_use_batchwise_valuation.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
valuation_method = frappe.db.get_single_value("Stock Settings", "valuation_method")
|
||||||
|
if valuation_method in ["FIFO", "LIFO"]:
|
||||||
|
return
|
||||||
|
|
||||||
|
if frappe.get_all("Batch", filters={"use_batchwise_valuation": 1}, limit=1):
|
||||||
|
return
|
||||||
|
|
||||||
|
if frappe.get_all("Item", filters={"has_batch_no": 1, "valuation_method": "FIFO"}, limit=1):
|
||||||
|
return
|
||||||
|
|
||||||
|
frappe.db.set_single_value("Stock Settings", "do_not_use_batchwise_valuation", 1)
|
||||||
@@ -158,6 +158,10 @@ class Batch(Document):
|
|||||||
|
|
||||||
def set_batchwise_valuation(self):
|
def set_batchwise_valuation(self):
|
||||||
if self.is_new():
|
if self.is_new():
|
||||||
|
if frappe.db.get_single_value("Stock Settings", "do_not_use_batchwise_valuation"):
|
||||||
|
self.use_batchwise_valuation = 0
|
||||||
|
return
|
||||||
|
|
||||||
self.use_batchwise_valuation = 1
|
self.use_batchwise_valuation = 1
|
||||||
|
|
||||||
def before_save(self):
|
def before_save(self):
|
||||||
|
|||||||
@@ -1654,7 +1654,7 @@ class TestDeliveryNote(FrappeTestCase):
|
|||||||
{
|
{
|
||||||
"is_stock_item": 1,
|
"is_stock_item": 1,
|
||||||
"has_batch_no": 1,
|
"has_batch_no": 1,
|
||||||
"batch_no_series": "BATCH-TESTSERIAL-.#####",
|
"batch_number_series": "BATCH-TESTSERIAL-.#####",
|
||||||
"create_new_batch": 1,
|
"create_new_batch": 1,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -648,7 +648,7 @@ class TestPickList(FrappeTestCase):
|
|||||||
def test_picklist_for_batch_item(self):
|
def test_picklist_for_batch_item(self):
|
||||||
warehouse = "_Test Warehouse - _TC"
|
warehouse = "_Test Warehouse - _TC"
|
||||||
item = make_item(
|
item = make_item(
|
||||||
properties={"is_stock_item": 1, "has_batch_no": 1, "batch_no_series": "PICKLT-.######"}
|
properties={"is_stock_item": 1, "has_batch_no": 1, "batch_number_series": "PICKLT-.######"}
|
||||||
).name
|
).name
|
||||||
|
|
||||||
# create batch
|
# create batch
|
||||||
|
|||||||
@@ -3170,6 +3170,65 @@ class TestPurchaseReceipt(FrappeTestCase):
|
|||||||
lcv.save().submit()
|
lcv.save().submit()
|
||||||
return lcv
|
return lcv
|
||||||
|
|
||||||
|
def test_do_not_use_batchwise_valuation_rate(self):
|
||||||
|
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||||
|
|
||||||
|
item_code = "Test Item for Do Not Use Batchwise Valuation"
|
||||||
|
make_item(
|
||||||
|
item_code,
|
||||||
|
properties={
|
||||||
|
"is_stock_item": 1,
|
||||||
|
"has_batch_no": 1,
|
||||||
|
"create_new_batch": 1,
|
||||||
|
"batch_number_series": "TIDNBV-.#####",
|
||||||
|
"valuation_method": "Moving Average",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# 1st pr for 100 rate
|
||||||
|
pr = make_purchase_receipt(
|
||||||
|
item_code=item_code,
|
||||||
|
qty=1,
|
||||||
|
rate=100,
|
||||||
|
posting_date=add_days(today(), -2),
|
||||||
|
)
|
||||||
|
|
||||||
|
make_purchase_receipt(
|
||||||
|
item_code=item_code,
|
||||||
|
qty=1,
|
||||||
|
rate=200,
|
||||||
|
posting_date=add_days(today(), -1),
|
||||||
|
)
|
||||||
|
|
||||||
|
dn = create_delivery_note(
|
||||||
|
item_code=item_code,
|
||||||
|
qty=1,
|
||||||
|
rate=300,
|
||||||
|
posting_date=today(),
|
||||||
|
use_serial_batch_fields=1,
|
||||||
|
batch_no=get_batch_from_bundle(pr.items[0].serial_and_batch_bundle),
|
||||||
|
)
|
||||||
|
dn.reload()
|
||||||
|
bundle = dn.items[0].serial_and_batch_bundle
|
||||||
|
|
||||||
|
valuation_rate = frappe.db.get_value("Serial and Batch Bundle", bundle, "avg_rate")
|
||||||
|
self.assertEqual(valuation_rate, 100)
|
||||||
|
|
||||||
|
doc = frappe.get_doc("Stock Settings")
|
||||||
|
doc.do_not_use_batchwise_valuation = 1
|
||||||
|
doc.flags.ignore_validate = True
|
||||||
|
doc.save()
|
||||||
|
|
||||||
|
pr.repost_future_sle_and_gle(force=True)
|
||||||
|
|
||||||
|
valuation_rate = frappe.db.get_value("Serial and Batch Bundle", bundle, "avg_rate")
|
||||||
|
self.assertEqual(valuation_rate, 150)
|
||||||
|
|
||||||
|
doc = frappe.get_doc("Stock Settings")
|
||||||
|
doc.do_not_use_batchwise_valuation = 0
|
||||||
|
doc.flags.ignore_validate = True
|
||||||
|
doc.save()
|
||||||
|
|
||||||
|
|
||||||
def prepare_data_for_internal_transfer():
|
def prepare_data_for_internal_transfer():
|
||||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
||||||
|
|||||||
@@ -228,6 +228,16 @@ class SerialandBatchBundle(Document):
|
|||||||
def get_serial_nos(self):
|
def get_serial_nos(self):
|
||||||
return [d.serial_no for d in self.entries if d.serial_no]
|
return [d.serial_no for d in self.entries if d.serial_no]
|
||||||
|
|
||||||
|
def update_valuation_rate(self, valuation_rate=None, save=False):
|
||||||
|
for row in self.entries:
|
||||||
|
row.incoming_rate = valuation_rate
|
||||||
|
row.stock_value_difference = flt(row.qty) * flt(valuation_rate)
|
||||||
|
|
||||||
|
if save:
|
||||||
|
row.db_set(
|
||||||
|
{"incoming_rate": row.incoming_rate, "stock_value_difference": row.stock_value_difference}
|
||||||
|
)
|
||||||
|
|
||||||
def set_incoming_rate_for_outward_transaction(self, row=None, save=False, allow_negative_stock=False):
|
def set_incoming_rate_for_outward_transaction(self, row=None, save=False, allow_negative_stock=False):
|
||||||
sle = self.get_sle_for_outward_transaction()
|
sle = self.get_sle_for_outward_transaction()
|
||||||
|
|
||||||
|
|||||||
@@ -653,7 +653,7 @@ class TestSerialandBatchBundle(FrappeTestCase):
|
|||||||
"is_stock_item": 1,
|
"is_stock_item": 1,
|
||||||
"has_batch_no": 1,
|
"has_batch_no": 1,
|
||||||
"create_new_batch": 1,
|
"create_new_batch": 1,
|
||||||
"batch_no_series": "PSNBI-TSNVL-.#####",
|
"batch_number_series": "PSNBI-TSNVL-.#####",
|
||||||
"has_serial_no": 1,
|
"has_serial_no": 1,
|
||||||
"serial_no_series": "SN-PSNBI-TSNVL-.#####",
|
"serial_no_series": "SN-PSNBI-TSNVL-.#####",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -44,6 +44,7 @@
|
|||||||
"auto_reserve_serial_and_batch",
|
"auto_reserve_serial_and_batch",
|
||||||
"serial_and_batch_item_settings_tab",
|
"serial_and_batch_item_settings_tab",
|
||||||
"section_break_7",
|
"section_break_7",
|
||||||
|
"do_not_use_batchwise_valuation",
|
||||||
"auto_create_serial_and_batch_bundle_for_outward",
|
"auto_create_serial_and_batch_bundle_for_outward",
|
||||||
"pick_serial_and_batch_based_on",
|
"pick_serial_and_batch_based_on",
|
||||||
"column_break_mhzc",
|
"column_break_mhzc",
|
||||||
@@ -437,6 +438,14 @@
|
|||||||
"fieldname": "do_not_update_serial_batch_on_creation_of_auto_bundle",
|
"fieldname": "do_not_update_serial_batch_on_creation_of_auto_bundle",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Do Not Update Serial / Batch on Creation of Auto Bundle"
|
"label": "Do Not Update Serial / Batch on Creation of Auto Bundle"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"depends_on": "eval:doc.valuation_method === \"Moving Average\"",
|
||||||
|
"description": "If enabled, the system will use the moving average valuation method to calculate the valuation rate for the batched items and will not consider the individual batch-wise incoming rate.",
|
||||||
|
"fieldname": "do_not_use_batchwise_valuation",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Do Not Use Batch-wise Valuation"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "icon-cog",
|
"icon": "icon-cog",
|
||||||
@@ -444,7 +453,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-02-25 16:32:01.084453",
|
"modified": "2024-07-04 12:45:09.811280",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Stock Settings",
|
"name": "Stock Settings",
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ class StockSettings(Document):
|
|||||||
default_warehouse: DF.Link | None
|
default_warehouse: DF.Link | None
|
||||||
disable_serial_no_and_batch_selector: DF.Check
|
disable_serial_no_and_batch_selector: DF.Check
|
||||||
do_not_update_serial_batch_on_creation_of_auto_bundle: DF.Check
|
do_not_update_serial_batch_on_creation_of_auto_bundle: DF.Check
|
||||||
|
do_not_use_batchwise_valuation: DF.Check
|
||||||
enable_stock_reservation: DF.Check
|
enable_stock_reservation: DF.Check
|
||||||
item_group: DF.Link | None
|
item_group: DF.Link | None
|
||||||
item_naming_by: DF.Literal["Item Code", "Naming Series"]
|
item_naming_by: DF.Literal["Item Code", "Naming Series"]
|
||||||
@@ -98,6 +99,22 @@ class StockSettings(Document):
|
|||||||
self.validate_stock_reservation()
|
self.validate_stock_reservation()
|
||||||
self.change_precision_for_for_sales()
|
self.change_precision_for_for_sales()
|
||||||
self.change_precision_for_purchase()
|
self.change_precision_for_purchase()
|
||||||
|
self.validate_use_batch_wise_valuation()
|
||||||
|
|
||||||
|
def validate_use_batch_wise_valuation(self):
|
||||||
|
if not self.do_not_use_batchwise_valuation:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.valuation_method == "FIFO":
|
||||||
|
frappe.throw(_("Cannot disable batch wise valuation for FIFO valuation method."))
|
||||||
|
|
||||||
|
if frappe.get_all(
|
||||||
|
"Item", filters={"valuation_method": "FIFO", "is_stock_item": 1, "has_batch_no": 1}, limit=1
|
||||||
|
):
|
||||||
|
frappe.throw(_("Can't disable batch wise valuation for items with FIFO valuation method."))
|
||||||
|
|
||||||
|
if frappe.get_all("Batch", filters={"use_batchwise_valuation": 1}, limit=1):
|
||||||
|
frappe.throw(_("Can't disable batch wise valuation for active batches."))
|
||||||
|
|
||||||
def validate_warehouses(self):
|
def validate_warehouses(self):
|
||||||
warehouse_fields = ["default_warehouse", "sample_retention_warehouse"]
|
warehouse_fields = ["default_warehouse", "sample_retention_warehouse"]
|
||||||
|
|||||||
@@ -530,6 +530,10 @@ class update_entries_after:
|
|||||||
self.allow_zero_rate = allow_zero_rate
|
self.allow_zero_rate = allow_zero_rate
|
||||||
self.via_landed_cost_voucher = via_landed_cost_voucher
|
self.via_landed_cost_voucher = via_landed_cost_voucher
|
||||||
self.item_code = args.get("item_code")
|
self.item_code = args.get("item_code")
|
||||||
|
self.use_moving_avg_for_batch = frappe.db.get_single_value(
|
||||||
|
"Stock Settings", "do_not_use_batchwise_valuation"
|
||||||
|
)
|
||||||
|
|
||||||
self.allow_negative_stock = allow_negative_stock or is_negative_stock_allowed(
|
self.allow_negative_stock = allow_negative_stock or is_negative_stock_allowed(
|
||||||
item_code=self.item_code
|
item_code=self.item_code
|
||||||
)
|
)
|
||||||
@@ -745,7 +749,7 @@ class update_entries_after:
|
|||||||
if sle.get(dimension.get("fieldname")):
|
if sle.get(dimension.get("fieldname")):
|
||||||
has_dimensions = True
|
has_dimensions = True
|
||||||
|
|
||||||
if sle.serial_and_batch_bundle:
|
if sle.serial_and_batch_bundle and (not self.use_moving_avg_for_batch or sle.has_serial_no):
|
||||||
self.calculate_valuation_for_serial_batch_bundle(sle)
|
self.calculate_valuation_for_serial_batch_bundle(sle)
|
||||||
elif sle.serial_no and not self.args.get("sle_id"):
|
elif sle.serial_no and not self.args.get("sle_id"):
|
||||||
# Only run in reposting
|
# Only run in reposting
|
||||||
@@ -765,7 +769,12 @@ class update_entries_after:
|
|||||||
# Only run in reposting
|
# Only run in reposting
|
||||||
self.update_batched_values(sle)
|
self.update_batched_values(sle)
|
||||||
else:
|
else:
|
||||||
if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no and not has_dimensions:
|
if (
|
||||||
|
sle.voucher_type == "Stock Reconciliation"
|
||||||
|
and not sle.batch_no
|
||||||
|
and not sle.has_batch_no
|
||||||
|
and not has_dimensions
|
||||||
|
):
|
||||||
# assert
|
# assert
|
||||||
self.wh_data.valuation_rate = sle.valuation_rate
|
self.wh_data.valuation_rate = sle.valuation_rate
|
||||||
self.wh_data.qty_after_transaction = sle.qty_after_transaction
|
self.wh_data.qty_after_transaction = sle.qty_after_transaction
|
||||||
@@ -806,6 +815,15 @@ class update_entries_after:
|
|||||||
sle.doctype = "Stock Ledger Entry"
|
sle.doctype = "Stock Ledger Entry"
|
||||||
frappe.get_doc(sle).db_update()
|
frappe.get_doc(sle).db_update()
|
||||||
|
|
||||||
|
if (
|
||||||
|
sle.serial_and_batch_bundle
|
||||||
|
and self.valuation_method == "Moving Average"
|
||||||
|
and self.use_moving_avg_for_batch
|
||||||
|
and (sle.batch_no or sle.has_batch_no)
|
||||||
|
):
|
||||||
|
valuation_rate = flt(stock_value_difference) / flt(sle.actual_qty)
|
||||||
|
self.update_valuation_rate_in_serial_and_batch_bundle(sle, valuation_rate)
|
||||||
|
|
||||||
if not self.args.get("sle_id") or (
|
if not self.args.get("sle_id") or (
|
||||||
sle.serial_and_batch_bundle and sle.auto_created_serial_and_batch_bundle
|
sle.serial_and_batch_bundle and sle.auto_created_serial_and_batch_bundle
|
||||||
):
|
):
|
||||||
@@ -916,6 +934,21 @@ class update_entries_after:
|
|||||||
self.wh_data.qty_after_transaction, precision
|
self.wh_data.qty_after_transaction, precision
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def update_valuation_rate_in_serial_and_batch_bundle(self, sle, valuation_rate):
|
||||||
|
# Only execute if the item has batch_no and the valuation method is moving average
|
||||||
|
if not frappe.db.exists("Serial and Batch Bundle", sle.serial_and_batch_bundle):
|
||||||
|
return
|
||||||
|
|
||||||
|
doc = frappe.get_cached_doc("Serial and Batch Bundle", sle.serial_and_batch_bundle)
|
||||||
|
doc.update_valuation_rate(valuation_rate, save=True)
|
||||||
|
doc.calculate_qty_and_amount(save=True)
|
||||||
|
|
||||||
|
def get_outgoing_rate_for_batched_item(self, sle):
|
||||||
|
if self.wh_data.qty_after_transaction == 0:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
return flt(self.wh_data.stock_value) / flt(self.wh_data.qty_after_transaction)
|
||||||
|
|
||||||
def validate_negative_stock(self, sle):
|
def validate_negative_stock(self, sle):
|
||||||
"""
|
"""
|
||||||
validate negative stock for entries current datetime onwards
|
validate negative stock for entries current datetime onwards
|
||||||
|
|||||||
@@ -244,6 +244,8 @@ def get_incoming_rate(args, raise_error_if_no_rate=True):
|
|||||||
"Item", args.get("item_code"), ["has_serial_no", "has_batch_no"], as_dict=1
|
"Item", args.get("item_code"), ["has_serial_no", "has_batch_no"], as_dict=1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
use_moving_avg_for_batch = frappe.db.get_single_value("Stock Settings", "do_not_use_batchwise_valuation")
|
||||||
|
|
||||||
if isinstance(args, dict):
|
if isinstance(args, dict):
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
|
|
||||||
@@ -257,7 +259,12 @@ def get_incoming_rate(args, raise_error_if_no_rate=True):
|
|||||||
|
|
||||||
return sn_obj.get_incoming_rate()
|
return sn_obj.get_incoming_rate()
|
||||||
|
|
||||||
elif item_details and item_details.has_batch_no and args.get("serial_and_batch_bundle"):
|
elif (
|
||||||
|
item_details
|
||||||
|
and item_details.has_batch_no
|
||||||
|
and args.get("serial_and_batch_bundle")
|
||||||
|
and not use_moving_avg_for_batch
|
||||||
|
):
|
||||||
args.actual_qty = args.qty
|
args.actual_qty = args.qty
|
||||||
batch_obj = BatchNoValuation(
|
batch_obj = BatchNoValuation(
|
||||||
sle=args,
|
sle=args,
|
||||||
@@ -274,7 +281,7 @@ def get_incoming_rate(args, raise_error_if_no_rate=True):
|
|||||||
sn_obj = SerialNoValuation(sle=args, warehouse=args.get("warehouse"), item_code=args.get("item_code"))
|
sn_obj = SerialNoValuation(sle=args, warehouse=args.get("warehouse"), item_code=args.get("item_code"))
|
||||||
|
|
||||||
return sn_obj.get_incoming_rate()
|
return sn_obj.get_incoming_rate()
|
||||||
elif args.get("batch_no") and not args.get("serial_and_batch_bundle"):
|
elif args.get("batch_no") and not args.get("serial_and_batch_bundle") and not use_moving_avg_for_batch:
|
||||||
args.actual_qty = args.qty
|
args.actual_qty = args.qty
|
||||||
args.batch_nos = frappe._dict({args.batch_no: args})
|
args.batch_nos = frappe._dict({args.batch_no: args})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user