diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index e14f9e68355..bf393c0d29c 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -128,6 +128,7 @@ class POSInvoice(SalesInvoice): doc = frappe.get_doc("Serial and Batch Bundle", item.serial_and_batch_bundle) if doc.docstatus == 0: + doc.flags.ignore_voucher_validation = True doc.submit() def check_phone_payments(self): diff --git a/erpnext/stock/deprecated_serial_batch.py b/erpnext/stock/deprecated_serial_batch.py index 8b4279b08a1..b7c5d57d960 100644 --- a/erpnext/stock/deprecated_serial_batch.py +++ b/erpnext/stock/deprecated_serial_batch.py @@ -76,7 +76,6 @@ class DeprecatedBatchNoValuation: @deprecated def get_sle_for_batches(self): - batch_nos = list(self.batch_nos.keys()) sle = frappe.qb.DocType("Stock Ledger Entry") timestamp_condition = CombineDatetime(sle.posting_date, sle.posting_time) < CombineDatetime( @@ -88,7 +87,11 @@ class DeprecatedBatchNoValuation: == CombineDatetime(self.sle.posting_date, self.sle.posting_time) ) & (sle.creation < self.sle.creation) - return ( + batch_nos = self.batch_nos + if isinstance(self.batch_nos, dict): + batch_nos = list(self.batch_nos.keys()) + + query = ( frappe.qb.from_(sle) .select( sle.batch_no, @@ -97,11 +100,15 @@ class DeprecatedBatchNoValuation: ) .where( (sle.item_code == self.sle.item_code) - & (sle.name != self.sle.name) & (sle.warehouse == self.sle.warehouse) & (sle.batch_no.isin(batch_nos)) & (sle.is_cancelled == 0) ) .where(timestamp_condition) .groupby(sle.batch_no) - ).run(as_dict=True) + ) + + if self.sle.name: + query = query.where(sle.name != self.sle.name) + + return query.run(as_dict=True) diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index 84ab74a8c6b..88a037287f5 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -8,8 +8,8 @@ import frappe from frappe import _ from frappe.model.document import Document from frappe.model.naming import make_autoname, revert_series_if_last -from frappe.query_builder.functions import CombineDatetime, CurDate, Sum -from frappe.utils import cint, flt, get_link_to_form, nowtime +from frappe.query_builder.functions import CurDate, Sum +from frappe.utils import cint, flt, get_link_to_form from frappe.utils.data import add_days from frappe.utils.jinja import render_template @@ -179,44 +179,28 @@ def get_batch_qty( :param warehouse: Optional - give qty for this warehouse :param item_code: Optional - give qty for this item""" - sle = frappe.qb.DocType("Stock Ledger Entry") + from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import ( + get_auto_batch_nos, + ) - out = 0 - if batch_no and warehouse: - query = ( - frappe.qb.from_(sle) - .select(Sum(sle.actual_qty)) - .where((sle.is_cancelled == 0) & (sle.warehouse == warehouse) & (sle.batch_no == batch_no)) - ) + batchwise_qty = defaultdict(float) + kwargs = frappe._dict({ + "item_code": item_code, + "warehouse": warehouse, + "posting_date": posting_date, + "posting_time": posting_time, + "batch_no": batch_no + }) - if posting_date: - if posting_time is None: - posting_time = nowtime() + batches = get_auto_batch_nos(kwargs) - query = query.where( - CombineDatetime(sle.posting_date, sle.posting_time) - <= CombineDatetime(posting_date, posting_time) - ) + if not (batch_no and warehouse): + return batches - out = query.run(as_list=True)[0][0] or 0 + for batch in batches: + batchwise_qty[batch.get("batch_no")] += batch.get("qty") - if batch_no and not warehouse: - out = ( - frappe.qb.from_(sle) - .select(sle.warehouse, Sum(sle.actual_qty).as_("qty")) - .where((sle.is_cancelled == 0) & (sle.batch_no == batch_no)) - .groupby(sle.warehouse) - ).run(as_dict=True) - - if not batch_no and item_code and warehouse: - out = ( - frappe.qb.from_(sle) - .select(sle.batch_no, Sum(sle.actual_qty).as_("qty")) - .where((sle.is_cancelled == 0) & (sle.item_code == item_code) & (sle.warehouse == warehouse)) - .groupby(sle.batch_no) - ).run(as_dict=True) - - return out + return batchwise_qty[batch_no] @frappe.whitelist() @@ -366,3 +350,14 @@ def get_available_batches(kwargs): batchwise_qty[batch.get("batch_no")] += batch.get("qty") return batchwise_qty + + +def get_batch_no(bundle_id): + from erpnext.stock.serial_batch_bundle import get_batch_nos + + batches = defaultdict(float) + + for batch_id, d in get_batch_nos(bundle_id).items(): + batches[batch_id] += abs(d.get("qty")) + + return batches diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py index 271e2e02984..cf0d3f20c5a 100644 --- a/erpnext/stock/doctype/batch/test_batch.py +++ b/erpnext/stock/doctype/batch/test_batch.py @@ -10,15 +10,15 @@ from frappe.utils import cint, flt from frappe.utils.data import add_to_date, getdate from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice -from erpnext.stock.doctype.batch.batch import UnableToSelectBatchError, get_batch_no, get_batch_qty +from erpnext.stock.doctype.batch.batch import get_batch_no, get_batch_qty from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt -from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry -from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( - create_stock_reconciliation, +from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import ( + BatchNegativeStockError, ) +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.get_item_details import get_item_details -from erpnext.stock.stock_ledger import get_valuation_rate +from erpnext.stock.serial_batch_bundle import SerialBatchCreation class TestBatch(FrappeTestCase): @@ -49,8 +49,10 @@ class TestBatch(FrappeTestCase): ).insert() receipt.submit() - self.assertTrue(receipt.items[0].batch_no) - self.assertEqual(get_batch_qty(receipt.items[0].batch_no, receipt.items[0].warehouse), batch_qty) + receipt.load_from_db() + self.assertTrue(receipt.items[0].serial_and_batch_bundle) + batch_no = get_batch_from_bundle(receipt.items[0].serial_and_batch_bundle) + self.assertEqual(get_batch_qty(batch_no, receipt.items[0].warehouse), batch_qty) return receipt @@ -80,9 +82,12 @@ class TestBatch(FrappeTestCase): stock_entry.insert() stock_entry.submit() - self.assertTrue(stock_entry.items[0].batch_no) + stock_entry.load_from_db() + + bundle = stock_entry.items[0].serial_and_batch_bundle + self.assertTrue(bundle) self.assertEqual( - get_batch_qty(stock_entry.items[0].batch_no, stock_entry.items[0].t_warehouse), 90 + get_batch_qty(get_batch_from_bundle(bundle), stock_entry.items[0].t_warehouse), 90 ) def test_delivery_note(self): @@ -103,25 +108,35 @@ class TestBatch(FrappeTestCase): ).insert() delivery_note.submit() + receipt.load_from_db() + delivery_note.load_from_db() + # shipped from FEFO batch self.assertEqual( - delivery_note.items[0].batch_no, get_batch_no(item_code, receipt.items[0].warehouse, batch_qty) + get_batch_no(delivery_note.items[0].serial_and_batch_bundle), + get_batch_no(receipt.items[0].serial_and_batch_bundle), ) - def test_delivery_note_fail(self): + def test_batch_negative_stock_error(self): """Test automatic batch selection for outgoing items""" receipt = self.test_purchase_receipt(100) - delivery_note = frappe.get_doc( - dict( - doctype="Delivery Note", - customer="_Test Customer", - company=receipt.company, - items=[ - dict(item_code="ITEM-BATCH-1", qty=5000, rate=10, warehouse=receipt.items[0].warehouse) - ], - ) + + receipt.load_from_db() + batch_no = get_batch_from_bundle(receipt.items[0].serial_and_batch_bundle) + sn_doc = SerialBatchCreation( + { + "item_code": "ITEM-BATCH-1", + "warehouse": receipt.items[0].warehouse, + "voucher_type": "Delivery Note", + "qty": 5000, + "avg_rate": 10, + "batches": frappe._dict({batch_no: 90}), + "type_of_transaction": "Outward", + "company": receipt.company, + } ) - self.assertRaises(UnableToSelectBatchError, delivery_note.insert) + + self.assertRaises(BatchNegativeStockError, sn_doc.make_serial_and_batch_bundle) def test_stock_entry_outgoing(self): """Test automatic batch selection for outgoing stock entry""" @@ -149,9 +164,9 @@ class TestBatch(FrappeTestCase): stock_entry.insert() stock_entry.submit() - # assert same batch is selected self.assertEqual( - stock_entry.items[0].batch_no, get_batch_no(item_code, receipt.items[0].warehouse, batch_qty) + get_batch_no(stock_entry.items[0].serial_and_batch_bundle), + get_batch_no(receipt.items[0].serial_and_batch_bundle), ) def test_batch_split(self): @@ -201,6 +216,19 @@ class TestBatch(FrappeTestCase): ) batch.save() + sn_doc = SerialBatchCreation( + { + "item_code": item_name, + "warehouse": warehouse, + "voucher_type": "Stock Entry", + "qty": 90, + "avg_rate": 10, + "batches": frappe._dict({batch_name: 90}), + "type_of_transaction": "Inward", + "company": "_Test Company", + } + ).make_serial_and_batch_bundle() + stock_entry = frappe.get_doc( dict( doctype="Stock Entry", @@ -210,10 +238,10 @@ class TestBatch(FrappeTestCase): dict( item_code=item_name, qty=90, + serial_and_batch_bundle=sn_doc.name, t_warehouse=warehouse, cost_center="Main - _TC", rate=10, - batch_no=batch_name, allow_zero_valuation_rate=1, ) ], @@ -320,7 +348,8 @@ class TestBatch(FrappeTestCase): batches = {} for rate in rates: se = make_stock_entry(item_code=item_code, qty=10, rate=rate, target=warehouse) - batches[se.items[0].batch_no] = rate + batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle) + batches[batch_no] = rate LOW, HIGH = list(batches.keys()) @@ -341,7 +370,9 @@ class TestBatch(FrappeTestCase): sle = frappe.get_last_doc("Stock Ledger Entry", {"is_cancelled": 0, "voucher_no": se.name}) - stock_value_difference = sle.actual_qty * batches[sle.batch_no] + stock_value_difference = ( + sle.actual_qty * batches[get_batch_from_bundle(sle.serial_and_batch_bundle)] + ) self.assertAlmostEqual(sle.stock_value_difference, stock_value_difference) stock_value += stock_value_difference @@ -353,45 +384,6 @@ class TestBatch(FrappeTestCase): self.assertEqual(json.loads(sle.stock_queue), []) # queues don't apply on batched items - def test_moving_batch_valuation_rates(self): - item_code = "_TestBatchWiseVal" - warehouse = "_Test Warehouse - _TC" - self.make_batch_item(item_code) - - def assertValuation(expected): - actual = get_valuation_rate( - item_code, warehouse, "voucher_type", "voucher_no", batch_no=batch_no - ) - self.assertAlmostEqual(actual, expected) - - se = make_stock_entry(item_code=item_code, qty=100, rate=10, target=warehouse) - batch_no = se.items[0].batch_no - assertValuation(10) - - # consumption should never affect current valuation rate - make_stock_entry(item_code=item_code, qty=20, source=warehouse) - assertValuation(10) - - make_stock_entry(item_code=item_code, qty=30, source=warehouse) - assertValuation(10) - - # 50 * 10 = 500 current value, add more item with higher valuation - make_stock_entry(item_code=item_code, qty=50, rate=20, target=warehouse, batch_no=batch_no) - assertValuation(15) - - # consuming again shouldn't do anything - make_stock_entry(item_code=item_code, qty=20, source=warehouse) - assertValuation(15) - - # reset rate with stock reconiliation - create_stock_reconciliation( - item_code=item_code, warehouse=warehouse, qty=10, rate=25, batch_no=batch_no - ) - assertValuation(25) - - make_stock_entry(item_code=item_code, qty=20, rate=20, target=warehouse, batch_no=batch_no) - assertValuation((20 * 20 + 10 * 25) / (10 + 20)) - def test_update_batch_properties(self): item_code = "_TestBatchWiseVal" self.make_batch_item(item_code) @@ -430,6 +422,12 @@ class TestBatch(FrappeTestCase): self.assertEqual("BATCHEXISTING002", pr_2.items[0].batch_no) +def get_batch_from_bundle(bundle): + batches = get_batch_no(bundle) + + return list(batches.keys())[0] + + def create_batch(item_code, rate, create_item_price_for_batch): pi = make_purchase_invoice( company="_Test Company", diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index 0624ae94a71..6f152151059 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -9,7 +9,7 @@ import frappe from frappe import _, bold from frappe.model.document import Document from frappe.query_builder.functions import CombineDatetime, Sum -from frappe.utils import add_days, cint, flt, get_link_to_form, today +from frappe.utils import add_days, cint, flt, get_link_to_form, nowtime, today from erpnext.stock.serial_batch_bundle import BatchNoValuation, SerialNoValuation @@ -18,6 +18,10 @@ class SerialNoExistsInFutureTransactionError(frappe.ValidationError): pass +class BatchNegativeStockError(frappe.ValidationError): + pass + + class SerialandBatchBundle(Document): def validate(self): self.validate_serial_and_batch_no() @@ -81,7 +85,7 @@ class SerialandBatchBundle(Document): def set_incoming_rate_for_outward_transaction(self, row=None, save=False): sle = self.get_sle_for_outward_transaction(row) - if not sle.actual_qty: + if not sle.actual_qty and sle.qty: sle.actual_qty = sle.qty if self.has_serial_no: @@ -122,7 +126,7 @@ class SerialandBatchBundle(Document): of quantity {bold(available_qty)} in the warehouse {self.warehouse}""" - frappe.throw(_(msg)) + frappe.throw(_(msg), BatchNegativeStockError) def get_sle_for_outward_transaction(self, row): return frappe._dict( @@ -228,7 +232,13 @@ class SerialandBatchBundle(Document): if self.voucher_no and not frappe.db.exists(self.voucher_type, self.voucher_no): self.throw_error_message(f"The {self.voucher_type} # {self.voucher_no} does not exist") - if frappe.get_cached_value(self.voucher_type, self.voucher_no, "docstatus") != 1: + if self.flags.ignore_voucher_validation: + return + + if ( + self.docstatus == 1 + and frappe.get_cached_value(self.voucher_type, self.voucher_no, "docstatus") != 1 + ): self.throw_error_message(f"The {self.voucher_type} # {self.voucher_no} should be submit first.") def check_future_entries_exists(self): @@ -750,6 +760,16 @@ def get_available_batches(kwargs): .groupby(batch_ledger.batch_no) ) + if kwargs.get("posting_date"): + if kwargs.get("posting_time") is None: + kwargs.posting_time = nowtime() + + timestamp_condition = CombineDatetime( + stock_ledger_entry.posting_date, stock_ledger_entry.posting_time + ) <= CombineDatetime(kwargs.posting_date, kwargs.posting_time) + + query = query.where(timestamp_condition) + for field in ["warehouse", "item_code"]: if not kwargs.get(field): continue diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 8788e15a6c4..17e6d8376bf 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -405,28 +405,6 @@ frappe.ui.form.on('Stock Entry', { } }, - set_serial_no: function(frm, cdt, cdn, callback) { - var d = frappe.model.get_doc(cdt, cdn); - if(!d.item_code && !d.s_warehouse && !d.qty) return; - var args = { - 'item_code' : d.item_code, - 'warehouse' : cstr(d.s_warehouse), - 'stock_qty' : d.transfer_qty - }; - frappe.call({ - method: "erpnext.stock.get_item_details.get_serial_no", - args: {"args": args}, - callback: function(r) { - if (!r.exe && r.message){ - frappe.model.set_value(cdt, cdn, "serial_no", r.message); - } - if (callback) { - callback(); - } - } - }); - }, - make_retention_stock_entry: function(frm) { frappe.call({ method: "erpnext.stock.doctype.stock_entry.stock_entry.move_sample_to_retention_warehouse", @@ -682,9 +660,7 @@ frappe.ui.form.on('Stock Entry', { frappe.ui.form.on('Stock Entry Detail', { qty(frm, cdt, cdn) { - frm.events.set_serial_no(frm, cdt, cdn, () => { - frm.events.set_basic_rate(frm, cdt, cdn); - }); + frm.events.set_basic_rate(frm, cdt, cdn); }, conversion_factor(frm, cdt, cdn) { @@ -692,9 +668,7 @@ frappe.ui.form.on('Stock Entry Detail', { }, s_warehouse(frm, cdt, cdn) { - frm.events.set_serial_no(frm, cdt, cdn, () => { - frm.events.get_warehouse_details(frm, cdt, cdn); - }); + frm.events.get_warehouse_details(frm, cdt, cdn); // set allow_zero_valuation_rate to 0 if s_warehouse is selected. let item = frappe.get_doc(cdt, cdn); diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index fb5a93c191d..056a3aedcc5 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -747,7 +747,7 @@ class StockEntry(StockController): currency=erpnext.get_company_currency(self.company), company=self.company, raise_error_if_no_rate=raise_error_if_no_rate, - batch_no=d.batch_no, + serial_and_batch_bundle=d.serial_and_batch_bundle, ) # do not round off basic rate to avoid precision loss @@ -904,6 +904,9 @@ class StockEntry(StockController): return for row in self.items: + if not row.s_warehouse: + continue + if row.serial_and_batch_bundle or row.item_code not in serial_or_batch_items: continue @@ -915,7 +918,7 @@ class StockEntry(StockController): "posting_time": self.posting_time, "voucher_type": self.doctype, "voucher_detail_no": row.name, - "total_qty": row.qty, + "qty": row.qty * -1, "type_of_transaction": "Outward", "company": self.company, "do_not_submit": True, @@ -1437,10 +1440,8 @@ class StockEntry(StockController): "qty": args.get("qty"), "transfer_qty": args.get("qty"), "conversion_factor": 1, - "batch_no": "", "actual_qty": 0, "basic_rate": 0, - "serial_no": "", "has_serial_no": item.has_serial_no, "has_batch_no": item.has_batch_no, "sample_quantity": item.sample_quantity, diff --git a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py index 0f9001392df..674a49b01ed 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py @@ -52,6 +52,7 @@ def make_stock_entry(**args): :do_not_save: Optional flag :do_not_submit: Optional flag """ + from erpnext.stock.serial_batch_bundle import SerialBatchCreation def process_serial_numbers(serial_nos_list): serial_nos_list = [ @@ -131,16 +132,27 @@ def make_stock_entry(**args): # We can find out the serial number using the batch source document serial_number = args.serial_no + bundle_id = None if not args.serial_no and args.qty and args.batch_no: - serial_number_list = frappe.get_list( - doctype="Stock Ledger Entry", - fields=["serial_no"], - filters={"batch_no": args.batch_no, "warehouse": args.from_warehouse}, + batches = frappe._dict({args.batch_no: args.qty}) + + bundle_id = ( + SerialBatchCreation( + { + "item_code": args.item, + "warehouse": args.source or args.target, + "voucher_type": "Stock Entry", + "total_qty": args.qty * (-1 if args.source else 1), + "batches": batches, + "type_of_transaction": "Outward" if args.source else "Inward", + "company": s.company, + } + ) + .make_serial_and_batch_bundle() + .name ) - serial_number = process_serial_numbers(serial_number_list) args.serial_no = serial_number - s.append( "items", { @@ -148,6 +160,7 @@ def make_stock_entry(**args): "s_warehouse": args.source, "t_warehouse": args.target, "qty": args.qty, + "serial_and_batch_bundle": bundle_id, "basic_rate": args.rate or args.basic_rate, "conversion_factor": args.conversion_factor or 1.0, "transfer_qty": flt(args.qty) * (flt(args.conversion_factor) or 1.0), @@ -164,4 +177,7 @@ def make_stock_entry(**args): s.insert() if not args.do_not_submit: s.submit() + + s.load_from_db() + return s diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index c14df3b281b..7a6190ea771 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -67,7 +67,7 @@ class SerialBatchBundle: "voucher_type": self.sle.voucher_type, "voucher_no": self.sle.voucher_no, "voucher_detail_no": self.sle.voucher_detail_no, - "total_qty": self.sle.actual_qty, + "qty": self.sle.actual_qty, "avg_rate": self.sle.incoming_rate, "total_amount": flt(self.sle.actual_qty) * flt(self.sle.incoming_rate), "type_of_transaction": "Inward" if self.sle.actual_qty > 0 else "Outward", @@ -136,7 +136,6 @@ class SerialBatchBundle: and not self.sle.serial_and_batch_bundle and self.item_details.has_batch_no == 1 and self.item_details.create_new_batch - and self.item_details.batch_number_series ): self.make_serial_batch_no_bundle() elif not self.sle.is_cancelled: @@ -393,7 +392,7 @@ class BatchNoValuation(DeprecatedBatchNoValuation): self.calculate_valuation_rate() def calculate_avg_rate(self): - if self.sle.actual_qty > 0: + if flt(self.sle.actual_qty) > 0: self.stock_value_change = frappe.get_cached_value( "Serial and Batch Bundle", self.sle.serial_and_batch_bundle, "total_amount" ) @@ -414,7 +413,9 @@ class BatchNoValuation(DeprecatedBatchNoValuation): parent = frappe.qb.DocType("Serial and Batch Bundle") child = frappe.qb.DocType("Serial and Batch Entry") - batch_nos = list(self.batch_nos.keys()) + batch_nos = self.batch_nos + if isinstance(self.batch_nos, dict): + batch_nos = list(self.batch_nos.keys()) timestamp_condition = "" if self.sle.posting_date and self.sle.posting_time: @@ -433,7 +434,6 @@ class BatchNoValuation(DeprecatedBatchNoValuation): ) .where( (child.batch_no.isin(batch_nos)) - & (child.parent != self.sle.serial_and_batch_bundle) & (parent.warehouse == self.sle.warehouse) & (parent.item_code == self.sle.item_code) & (parent.docstatus == 1) @@ -443,8 +443,11 @@ class BatchNoValuation(DeprecatedBatchNoValuation): .groupby(child.batch_no) ) + if self.sle.serial_and_batch_bundle: + query = query.where(child.parent != self.sle.serial_and_batch_bundle) + if timestamp_condition: - query.where(timestamp_condition) + query = query.where(timestamp_condition) return query.run(as_dict=True) @@ -455,6 +458,9 @@ class BatchNoValuation(DeprecatedBatchNoValuation): return get_batch_nos(self.sle.serial_and_batch_bundle) def set_stock_value_difference(self): + if not self.sle.serial_and_batch_bundle: + return + self.stock_value_change = 0 for batch_no, ledger in self.batch_nos.items(): stock_value_change = self.batch_avg_rate[batch_no] * ledger.qty @@ -471,11 +477,10 @@ class BatchNoValuation(DeprecatedBatchNoValuation): self.wh_data.stock_value + self.stock_value_change ) + self.wh_data.qty_after_transaction += self.sle.actual_qty if self.wh_data.qty_after_transaction: self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction - self.wh_data.qty_after_transaction += self.sle.actual_qty - def get_incoming_rate(self): return abs(flt(self.stock_value_change) / flt(self.sle.actual_qty)) @@ -484,7 +489,8 @@ def get_batch_nos(serial_and_batch_bundle): entries = frappe.get_all( "Serial and Batch Entry", fields=["batch_no", "qty", "name"], - filters={"parent": serial_and_batch_bundle, "is_outward": 1}, + filters={"parent": serial_and_batch_bundle}, + order_by="idx", ) return {d.batch_no: d for d in entries} @@ -591,6 +597,12 @@ class SerialBatchCreation: setattr(self, "posting_date", today()) self.__dict__["posting_date"] = self.posting_date + if not self.get("actual_qty"): + qty = self.get("qty") or self.get("total_qty") + + setattr(self, "actual_qty", qty) + self.__dict__["actual_qty"] = self.actual_qty + def duplicate_package(self): if not self.serial_and_batch_bundle: return @@ -613,14 +625,14 @@ class SerialBatchCreation: if self.type_of_transaction == "Outward": self.set_auto_serial_batch_entries_for_outward() - elif self.type_of_transaction == "Inward": + elif self.type_of_transaction == "Inward" and not self.get("batches"): self.set_auto_serial_batch_entries_for_inward() self.set_serial_batch_entries(doc) - doc.set_incoming_rate() doc.save() if not hasattr(self, "do_not_submit") or not self.do_not_submit: + doc.flags.ignore_voucher_validation = True doc.submit() return doc @@ -633,7 +645,7 @@ class SerialBatchCreation: { "item_code": self.item_code, "warehouse": self.warehouse, - "qty": abs(self.total_qty), + "qty": abs(self.actual_qty), "based_on": frappe.db.get_single_value("Stock Settings", "pick_serial_and_batch_based_on"), } ) @@ -651,7 +663,7 @@ class SerialBatchCreation: if self.has_serial_no: self.serial_nos = self.get_auto_created_serial_nos() else: - self.batches = frappe._dict({self.batch_no: abs(self.total_qty)}) + self.batches = frappe._dict({self.batch_no: abs(self.actual_qty)}) def set_serial_batch_entries(self, doc): if self.get("serial_nos"): @@ -698,9 +710,9 @@ class SerialBatchCreation: return make_batch( frappe._dict( { - "item": self.item_code, - "reference_doctype": self.voucher_type, - "reference_name": self.voucher_no, + "item": self.get("item_code"), + "reference_doctype": self.get("voucher_type"), + "reference_name": self.get("voucher_no"), } ) ) @@ -709,7 +721,7 @@ class SerialBatchCreation: sr_nos = [] serial_nos_details = [] - for i in range(abs(cint(self.total_qty))): + for i in range(abs(cint(self.actual_qty))): serial_no = make_autoname(self.serial_no_series, "Serial No") sr_nos.append(serial_no) serial_nos_details.append( diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index e616ed030f6..aefc692496b 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -732,6 +732,7 @@ class update_entries_after(object): self.wh_data.stock_value = flt(self.wh_data.stock_value, self.currency_precision) if not self.wh_data.qty_after_transaction: self.wh_data.stock_value = 0.0 + stock_value_difference = self.wh_data.stock_value - self.wh_data.prev_stock_value self.wh_data.prev_stock_value = self.wh_data.stock_value @@ -1421,7 +1422,7 @@ def get_valuation_rate( currency=None, company=None, raise_error_if_no_rate=True, - batch_no=None, + serial_and_batch_bundle=None, ): if not company: @@ -1430,21 +1431,20 @@ def get_valuation_rate( last_valuation_rate = None # Get moving average rate of a specific batch number - if warehouse and batch_no and frappe.db.get_value("Batch", batch_no, "use_batchwise_valuation"): - last_valuation_rate = frappe.db.sql( - """ - select sum(stock_value_difference) / sum(actual_qty) - from `tabStock Ledger Entry` - where - item_code = %s - AND warehouse = %s - AND batch_no = %s - AND is_cancelled = 0 - AND NOT (voucher_no = %s AND voucher_type = %s) - """, - (item_code, warehouse, batch_no, voucher_no, voucher_type), + if warehouse and serial_and_batch_bundle: + batch_obj = BatchNoValuation( + sle=frappe._dict( + { + "item_code": item_code, + "warehouse": warehouse, + "actual_qty": -1, + "serial_and_batch_bundle": serial_and_batch_bundle, + } + ) ) + return batch_obj.get_incoming_rate() + # Get valuation rate from last sle for the same item and warehouse if not last_valuation_rate or last_valuation_rate[0][0] is None: last_valuation_rate = frappe.db.sql(