mirror of
https://github.com/frappe/erpnext.git
synced 2026-09-16 18:24:10 +00:00
fix(stock): value stock entry rows as of the posting date (#59042)
This commit is contained in:
@@ -34,7 +34,7 @@ from erpnext.stock.get_item_details import (
|
|||||||
get_default_cost_center,
|
get_default_cost_center,
|
||||||
)
|
)
|
||||||
from erpnext.stock.stock_ledger import get_previous_sle, get_valuation_rate
|
from erpnext.stock.stock_ledger import get_previous_sle, get_valuation_rate
|
||||||
from erpnext.stock.utils import get_incoming_rate
|
from erpnext.stock.utils import get_combine_datetime, get_incoming_rate
|
||||||
|
|
||||||
from .services.disassemble import DisassembleStockEntry
|
from .services.disassemble import DisassembleStockEntry
|
||||||
from .services.manufacturing import (
|
from .services.manufacturing import (
|
||||||
@@ -736,6 +736,18 @@ class StockEntry(StockController, SubcontractingInwardController):
|
|||||||
raise_error_if_no_rate=raise_error_if_no_rate,
|
raise_error_if_no_rate=raise_error_if_no_rate,
|
||||||
batch_no=d.batch_no,
|
batch_no=d.batch_no,
|
||||||
serial_and_batch_bundle=d.serial_and_batch_bundle,
|
serial_and_batch_bundle=d.serial_and_batch_bundle,
|
||||||
|
posting_datetime=get_combine_datetime(self.posting_date, self.posting_time),
|
||||||
|
creation=self.first_sle_creation,
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def first_sle_creation(self):
|
||||||
|
"""Creation of this entry's earliest ledger entry, if it has posted any yet."""
|
||||||
|
return frappe.db.get_value(
|
||||||
|
"Stock Ledger Entry",
|
||||||
|
{"voucher_no": self.name, "voucher_type": self.doctype, "is_cancelled": 0},
|
||||||
|
"creation",
|
||||||
|
order_by="creation asc",
|
||||||
)
|
)
|
||||||
|
|
||||||
def _notify_zero_valuation_rate(self, items):
|
def _notify_zero_valuation_rate(self, items):
|
||||||
|
|||||||
@@ -56,6 +56,19 @@ def get_sle(**args):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def stock_entry_row(item_code, qty, **kwargs):
|
||||||
|
stock_uom = frappe.db.get_value("Item", item_code, "stock_uom")
|
||||||
|
return {
|
||||||
|
"item_code": item_code,
|
||||||
|
"qty": qty,
|
||||||
|
"transfer_qty": qty,
|
||||||
|
"uom": stock_uom,
|
||||||
|
"stock_uom": stock_uom,
|
||||||
|
"conversion_factor": 1,
|
||||||
|
**kwargs,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class TestStockEntry(ERPNextTestSuite):
|
class TestStockEntry(ERPNextTestSuite):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.load_test_records("Stock Entry")
|
self.load_test_records("Stock Entry")
|
||||||
@@ -1609,32 +1622,25 @@ class TestStockEntry(ERPNextTestSuite):
|
|||||||
|
|
||||||
make_stock_entry(item_code=rm_item, target="_Test Warehouse - _TC", qty=10, basic_rate=100)
|
make_stock_entry(item_code=rm_item, target="_Test Warehouse - _TC", qty=10, basic_rate=100)
|
||||||
|
|
||||||
def row(item_code, qty, **kwargs):
|
|
||||||
stock_uom = frappe.db.get_value("Item", item_code, "stock_uom")
|
|
||||||
return {
|
|
||||||
"item_code": item_code,
|
|
||||||
"qty": qty,
|
|
||||||
"transfer_qty": qty,
|
|
||||||
"uom": stock_uom,
|
|
||||||
"stock_uom": stock_uom,
|
|
||||||
"conversion_factor": 1,
|
|
||||||
**kwargs,
|
|
||||||
}
|
|
||||||
|
|
||||||
entry = frappe.new_doc("Stock Entry")
|
entry = frappe.new_doc("Stock Entry")
|
||||||
entry.company = "_Test Company"
|
entry.company = "_Test Company"
|
||||||
entry.purpose = "Manufacture"
|
entry.purpose = "Manufacture"
|
||||||
entry.set_stock_entry_type()
|
entry.set_stock_entry_type()
|
||||||
entry.fg_completed_qty = 1
|
entry.fg_completed_qty = 1
|
||||||
entry.append("items", row(rm_item, 10, s_warehouse="_Test Warehouse - _TC"))
|
entry.append("items", stock_entry_row(rm_item, 10, s_warehouse="_Test Warehouse - _TC"))
|
||||||
entry.append("items", row(fg_item, 1, t_warehouse="_Test Warehouse 1 - _TC", is_finished_item=1))
|
|
||||||
entry.append(
|
entry.append(
|
||||||
"items",
|
"items",
|
||||||
row(scrap_item, 2, t_warehouse="_Test Warehouse 1 - _TC", secondary_item_type="Scrap"),
|
stock_entry_row(fg_item, 1, t_warehouse="_Test Warehouse 1 - _TC", is_finished_item=1),
|
||||||
)
|
)
|
||||||
entry.append(
|
entry.append(
|
||||||
"items",
|
"items",
|
||||||
row(
|
stock_entry_row(
|
||||||
|
scrap_item, 2, t_warehouse="_Test Warehouse 1 - _TC", secondary_item_type="Scrap"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
entry.append(
|
||||||
|
"items",
|
||||||
|
stock_entry_row(
|
||||||
manual_item,
|
manual_item,
|
||||||
1,
|
1,
|
||||||
t_warehouse="_Test Warehouse 1 - _TC",
|
t_warehouse="_Test Warehouse 1 - _TC",
|
||||||
@@ -1663,6 +1669,88 @@ class TestStockEntry(ERPNextTestSuite):
|
|||||||
manual_row.valuation_type = "% of Component Cost"
|
manual_row.valuation_type = "% of Component Cost"
|
||||||
self.assertRaises(frappe.ValidationError, entry.save)
|
self.assertRaises(frappe.ValidationError, entry.save)
|
||||||
|
|
||||||
|
@ERPNextTestSuite.change_settings("Stock Reposting Settings", {"item_based_reposting": 0})
|
||||||
|
def test_repost_values_costed_out_row_as_of_posting_date(self):
|
||||||
|
from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import repost_sl_entries
|
||||||
|
|
||||||
|
fg_item = make_item(properties={"is_stock_item": 1}).name
|
||||||
|
rm_item = make_item(properties={"is_stock_item": 1, "valuation_method": "Moving Average"}).name
|
||||||
|
scrap_item = make_item(properties={"is_stock_item": 1}).name
|
||||||
|
|
||||||
|
make_stock_entry(
|
||||||
|
item_code=rm_item,
|
||||||
|
target="_Test Warehouse - _TC",
|
||||||
|
qty=10,
|
||||||
|
basic_rate=100,
|
||||||
|
posting_date=add_days(today(), -10),
|
||||||
|
)
|
||||||
|
make_stock_entry(
|
||||||
|
item_code=scrap_item,
|
||||||
|
target="_Test Warehouse 1 - _TC",
|
||||||
|
qty=5,
|
||||||
|
basic_rate=50,
|
||||||
|
posting_date=add_days(today(), -10),
|
||||||
|
)
|
||||||
|
|
||||||
|
entry = frappe.new_doc("Stock Entry")
|
||||||
|
entry.company = "_Test Company"
|
||||||
|
entry.purpose = "Manufacture"
|
||||||
|
entry.set_stock_entry_type()
|
||||||
|
entry.set_posting_time = 1
|
||||||
|
entry.posting_date = add_days(today(), -5)
|
||||||
|
entry.posting_time = "10:00:00"
|
||||||
|
entry.fg_completed_qty = 1
|
||||||
|
entry.append("items", stock_entry_row(rm_item, 10, s_warehouse="_Test Warehouse - _TC"))
|
||||||
|
entry.append(
|
||||||
|
"items",
|
||||||
|
stock_entry_row(fg_item, 1, t_warehouse="_Test Warehouse 1 - _TC", is_finished_item=1),
|
||||||
|
)
|
||||||
|
entry.append(
|
||||||
|
"items",
|
||||||
|
stock_entry_row(
|
||||||
|
scrap_item, 2, t_warehouse="_Test Warehouse 1 - _TC", secondary_item_type="Scrap"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
entry.insert()
|
||||||
|
entry.submit()
|
||||||
|
|
||||||
|
self.assertEqual(entry.items[2].basic_rate, 50)
|
||||||
|
self.assertEqual(entry.items[1].basic_rate, 900)
|
||||||
|
|
||||||
|
make_stock_entry(
|
||||||
|
item_code=scrap_item,
|
||||||
|
target="_Test Warehouse 1 - _TC",
|
||||||
|
qty=10,
|
||||||
|
basic_rate=5000,
|
||||||
|
posting_date=add_days(today(), -1),
|
||||||
|
)
|
||||||
|
|
||||||
|
make_stock_entry(
|
||||||
|
item_code=scrap_item,
|
||||||
|
target="_Test Warehouse 1 - _TC",
|
||||||
|
qty=10,
|
||||||
|
basic_rate=9000,
|
||||||
|
posting_date=add_days(today(), -5),
|
||||||
|
posting_time="10:00:00",
|
||||||
|
)
|
||||||
|
|
||||||
|
backdated_receipt = make_stock_entry(
|
||||||
|
item_code=rm_item,
|
||||||
|
target="_Test Warehouse - _TC",
|
||||||
|
qty=10,
|
||||||
|
basic_rate=200,
|
||||||
|
posting_date=add_days(today(), -8),
|
||||||
|
)
|
||||||
|
repost = frappe.db.get_value(
|
||||||
|
"Repost Item Valuation", {"voucher_no": backdated_receipt.name, "docstatus": 1}, "name"
|
||||||
|
)
|
||||||
|
repost_sl_entries(frappe.get_doc("Repost Item Valuation", repost))
|
||||||
|
|
||||||
|
entry.load_from_db()
|
||||||
|
|
||||||
|
self.assertEqual(entry.items[2].basic_rate, 50)
|
||||||
|
self.assertEqual(entry.items[1].basic_rate, 1400)
|
||||||
|
|
||||||
def test_valuation_rate_lookup_without_voucher_no(self):
|
def test_valuation_rate_lookup_without_voucher_no(self):
|
||||||
from erpnext.stock.stock_ledger import get_valuation_rate
|
from erpnext.stock.stock_ledger import get_valuation_rate
|
||||||
|
|
||||||
|
|||||||
@@ -2155,6 +2155,15 @@ def get_sle_by_voucher_detail_no(voucher_detail_no):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_prior_ledger_condition(table, posting_datetime, creation):
|
||||||
|
"""Restrict a ledger lookup to the entries that precede a voucher in ledger order."""
|
||||||
|
if creation:
|
||||||
|
return (table.posting_datetime < posting_datetime) | (
|
||||||
|
(table.posting_datetime == posting_datetime) & (table.creation < creation)
|
||||||
|
)
|
||||||
|
return table.posting_datetime <= posting_datetime
|
||||||
|
|
||||||
|
|
||||||
def get_valuation_rate(
|
def get_valuation_rate(
|
||||||
item_code,
|
item_code,
|
||||||
warehouse,
|
warehouse,
|
||||||
@@ -2167,6 +2176,8 @@ def get_valuation_rate(
|
|||||||
raise_error_if_no_rate=True,
|
raise_error_if_no_rate=True,
|
||||||
batch_no=None,
|
batch_no=None,
|
||||||
serial_and_batch_bundle=None,
|
serial_and_batch_bundle=None,
|
||||||
|
posting_datetime=None,
|
||||||
|
creation=None,
|
||||||
):
|
):
|
||||||
from erpnext.stock.serial_batch_bundle import BatchNoValuation
|
from erpnext.stock.serial_batch_bundle import BatchNoValuation
|
||||||
|
|
||||||
@@ -2189,6 +2200,9 @@ def get_valuation_rate(
|
|||||||
# Comparing against a None voucher_no yields NULL, which filters out every row
|
# Comparing against a None voucher_no yields NULL, which filters out every row
|
||||||
query = query.where((table.voucher_no != voucher_no) | (table.voucher_type != voucher_type))
|
query = query.where((table.voucher_no != voucher_no) | (table.voucher_type != voucher_type))
|
||||||
|
|
||||||
|
if posting_datetime:
|
||||||
|
query = query.where(get_prior_ledger_condition(table, posting_datetime, creation))
|
||||||
|
|
||||||
last_valuation_rate = query.run()
|
last_valuation_rate = query.run()
|
||||||
if last_valuation_rate and last_valuation_rate[0][0] is not None:
|
if last_valuation_rate and last_valuation_rate[0][0] is not None:
|
||||||
return flt(last_valuation_rate[0][0])
|
return flt(last_valuation_rate[0][0])
|
||||||
@@ -2236,6 +2250,11 @@ def get_valuation_rate(
|
|||||||
~((sle_entry.voucher_no == voucher_no) & (sle_entry.voucher_type == voucher_type))
|
~((sle_entry.voucher_no == voucher_no) & (sle_entry.voucher_type == voucher_type))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if posting_datetime:
|
||||||
|
last_sle_query = last_sle_query.where(
|
||||||
|
get_prior_ledger_condition(sle_entry, posting_datetime, creation)
|
||||||
|
)
|
||||||
|
|
||||||
if last_valuation_rate := last_sle_query.run():
|
if last_valuation_rate := last_sle_query.run():
|
||||||
return flt(last_valuation_rate[0][0])
|
return flt(last_valuation_rate[0][0])
|
||||||
|
|
||||||
@@ -2671,13 +2690,7 @@ def get_stock_value_difference(
|
|||||||
elif voucher_no:
|
elif voucher_no:
|
||||||
query = query.where(table.voucher_no != voucher_no)
|
query = query.where(table.voucher_no != voucher_no)
|
||||||
|
|
||||||
if creation:
|
query = query.where(get_prior_ledger_condition(table, posting_datetime, creation))
|
||||||
query = query.where(
|
|
||||||
(table.posting_datetime < posting_datetime)
|
|
||||||
| ((table.posting_datetime == posting_datetime) & (table.creation < creation))
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
query = query.where(table.posting_datetime <= posting_datetime)
|
|
||||||
|
|
||||||
difference_amount = query.run()
|
difference_amount = query.run()
|
||||||
return flt(difference_amount[0][0]) if difference_amount else 0
|
return flt(difference_amount[0][0]) if difference_amount else 0
|
||||||
|
|||||||
Reference in New Issue
Block a user