diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 827f204e40e..3b4d32b7305 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -456,6 +456,16 @@ class SellingController(StockController): raise_error_if_no_rate=False, ) + if ( + not d.incoming_rate + and self.get("return_against") + and self.get("is_return") + and get_valuation_method(d.item_code) == "Moving Average" + ): + d.incoming_rate = get_rate_for_return( + self.doctype, self.name, d.item_code, self.return_against, item_row=d + ) + # For internal transfers use incoming rate as the valuation rate if self.is_internal_transfer(): if self.doctype == "Delivery Note" or self.get("update_stock"): diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index e9b07e76b2d..afd49c99033 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -1449,6 +1449,446 @@ class TestDeliveryNote(FrappeTestCase): self.assertEqual(so.items[0].rate, rate) self.assertEqual(dn.items[0].rate, so.items[0].rate) +<<<<<<< HEAD +======= + def test_use_serial_batch_fields_for_packed_items(self): + bundle_item = make_item("Test _Packed Product Bundle Item ", {"is_stock_item": 0}) + serial_item = make_item( + "Test _Packed Serial Item ", + {"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "SN-TESTSERIAL-.#####"}, + ) + batch_item = make_item( + "Test _Packed Batch Item ", + { + "is_stock_item": 1, + "has_batch_no": 1, + "batch_number_series": "BATCH-TESTSERIAL-.#####", + "create_new_batch": 1, + }, + ) + make_product_bundle(parent=bundle_item.name, items=[serial_item.name, batch_item.name]) + + item_details = {} + for item in [serial_item, batch_item]: + se = make_stock_entry(item_code=item.name, target="_Test Warehouse - _TC", qty=5, basic_rate=100) + item_details[item.name] = se.items[0].serial_and_batch_bundle + + dn = create_delivery_note(item_code=bundle_item.name, qty=1, do_not_submit=True) + serial_no = "" + for row in dn.packed_items: + row.use_serial_batch_fields = 1 + + if row.item_code == serial_item.name: + serial_and_batch_bundle = item_details[serial_item.name] + row.serial_no = get_serial_nos_from_bundle(serial_and_batch_bundle)[3] + serial_no = row.serial_no + else: + serial_and_batch_bundle = item_details[batch_item.name] + row.batch_no = get_batch_from_bundle(serial_and_batch_bundle) + + dn.submit() + dn.load_from_db() + + for row in dn.packed_items: + self.assertTrue(row.serial_no or row.batch_no) + self.assertTrue(row.serial_and_batch_bundle) + + if row.serial_no: + self.assertEqual(row.serial_no, serial_no) + + def test_delivery_note_legacy_serial_no_valuation(self): + from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return + from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos + + frappe.flags.ignore_serial_batch_bundle_validation = True + sn_item = "Old Serial NO Item Valuation Test - 2" + make_item( + sn_item, + { + "has_serial_no": 1, + "serial_no_series": "SN-SOVOSN-.####", + "is_stock_item": 1, + }, + ) + + serial_nos = [ + "SN-SOVOSN-1234", + "SN-SOVOSN-2234", + ] + + for sn in serial_nos: + if not frappe.db.exists("Serial No", sn): + sn_doc = frappe.get_doc( + { + "doctype": "Serial No", + "item_code": sn_item, + "serial_no": sn, + } + ) + sn_doc.insert() + + warehouse = "_Test Warehouse - _TC" + company = frappe.db.get_value("Warehouse", warehouse, "company") + se_doc = make_stock_entry( + item_code=sn_item, + company=company, + target="_Test Warehouse - _TC", + qty=2, + basic_rate=150, + do_not_submit=1, + use_serial_batch_fields=0, + ) + se_doc.submit() + + se_doc.items[0].db_set("serial_no", "\n".join(serial_nos)) + + sle_data = frappe.get_all( + "Stock Ledger Entry", + filters={"voucher_no": se_doc.name, "voucher_type": "Stock Entry"}, + )[0] + + sle_doc = frappe.get_doc("Stock Ledger Entry", sle_data.name) + self.assertFalse(sle_doc.serial_no) + sle_doc.db_set("serial_no", "\n".join(serial_nos)) + sle_doc.reload() + self.assertTrue(sle_doc.serial_no) + self.assertFalse(sle_doc.is_cancelled) + + for sn in serial_nos: + sn_doc = frappe.get_doc("Serial No", sn) + sn_doc.db_set( + { + "status": "Active", + "warehouse": warehouse, + } + ) + + self.assertEqual(sorted(get_serial_nos(se_doc.items[0].serial_no)), sorted(serial_nos)) + frappe.flags.ignore_serial_batch_bundle_validation = False + + se_doc = make_stock_entry( + item_code=sn_item, + company=company, + target="_Test Warehouse - _TC", + qty=2, + basic_rate=200, + ) + + serial_nos.extend(get_serial_nos_from_bundle(se_doc.items[0].serial_and_batch_bundle)) + + dn = create_delivery_note( + item_code=sn_item, + qty=3, + rate=500, + warehouse=warehouse, + company=company, + expense_account="Cost of Goods Sold - _TC", + cost_center="Main - _TC", + use_serial_batch_fields=1, + serial_no="\n".join(serial_nos[0:3]), + ) + + dn.reload() + + sle_data = frappe.get_all( + "Stock Ledger Entry", + filters={"voucher_no": dn.name, "voucher_type": "Delivery Note"}, + fields=["stock_value_difference", "actual_qty"], + )[0] + + self.assertEqual(sle_data.actual_qty, 3 * -1) + self.assertEqual(sle_data.stock_value_difference, 500.0 * -1) + + dn = create_delivery_note( + item_code=sn_item, + qty=1, + rate=500, + warehouse=warehouse, + company=company, + expense_account="Cost of Goods Sold - _TC", + cost_center="Main - _TC", + use_serial_batch_fields=1, + serial_no=serial_nos[-1], + ) + + dn.reload() + + sle_data = frappe.get_all( + "Stock Ledger Entry", + filters={"voucher_no": dn.name, "voucher_type": "Delivery Note"}, + fields=["stock_value_difference", "actual_qty"], + )[0] + + self.assertEqual(sle_data.actual_qty, 1 * -1) + self.assertEqual(sle_data.stock_value_difference, 200.0 * -1) + + def test_sales_return_batch_no_for_batched_item_in_dn(self): + from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return + + item_code = make_item( + "Test Batched Item for Sales Return 11", + properties={ + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "B11-TESTBATCH.#####", + "is_stock_item": 1, + }, + ).name + + se = make_stock_entry(item_code=item_code, target="_Test Warehouse - _TC", qty=5, basic_rate=100) + + batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle) + dn = create_delivery_note( + item_code=item_code, + qty=5, + rate=500, + use_serial_batch_fields=0, + batch_no=batch_no, + ) + + dn_return = make_sales_return(dn.name) + dn_return.save().submit() + returned_batch_no = get_batch_from_bundle(dn_return.items[0].serial_and_batch_bundle) + self.assertEqual(batch_no, returned_batch_no) + + def test_partial_sales_return_batch_no_for_batched_item_in_dn(self): + from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return + + item_code = make_item( + "Test Partial Batched Item for Sales Return 11", + properties={ + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "BPART11-TESTBATCH.#####", + "is_stock_item": 1, + }, + ).name + + se = make_stock_entry(item_code=item_code, target="_Test Warehouse - _TC", qty=5, basic_rate=100) + + batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle) + dn = create_delivery_note( + item_code=item_code, + qty=5, + rate=500, + use_serial_batch_fields=0, + batch_no=batch_no, + ) + + dn_return = make_sales_return(dn.name) + dn_return.items[0].qty = 3 * -1 + dn_return.save().submit() + + returned_batch_no = get_batch_from_bundle(dn_return.items[0].serial_and_batch_bundle) + self.assertEqual(batch_no, returned_batch_no) + sabb_qty = frappe.db.get_value( + "Serial and Batch Bundle", dn_return.items[0].serial_and_batch_bundle, "total_qty" + ) + self.assertEqual(sabb_qty, 3) + + dn_return = make_sales_return(dn.name) + dn_return.items[0].qty = 2 * -1 + dn_return.save().submit() + + returned_batch_no = get_batch_from_bundle(dn_return.items[0].serial_and_batch_bundle) + self.assertEqual(batch_no, returned_batch_no) + + sabb_qty = frappe.db.get_value( + "Serial and Batch Bundle", dn_return.items[0].serial_and_batch_bundle, "total_qty" + ) + self.assertEqual(sabb_qty, 2) + + def test_sales_return_serial_no_for_serial_item_in_dn(self): + from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return + + item_code = make_item( + "Test Serial Item for Sales Return 11", + properties={ + "has_serial_no": 1, + "serial_no_series": "SNN11-TESTBATCH.#####", + "is_stock_item": 1, + }, + ).name + + se = make_stock_entry(item_code=item_code, target="_Test Warehouse - _TC", qty=5, basic_rate=100) + + serial_nos = get_serial_nos_from_bundle(se.items[0].serial_and_batch_bundle) + dn = create_delivery_note( + item_code=item_code, + qty=5, + rate=500, + use_serial_batch_fields=0, + serial_no=serial_nos, + ) + + dn_return = make_sales_return(dn.name) + dn_return.save().submit() + returned_serial_nos = get_serial_nos_from_bundle(dn_return.items[0].serial_and_batch_bundle) + self.assertEqual(serial_nos, returned_serial_nos) + + def test_same_posting_date_and_posting_time(self): + item_code = make_item( + "Test Same Posting Datetime Item", + properties={ + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "SS-ART11-TESTBATCH.#####", + "is_stock_item": 1, + }, + ).name + + se = make_stock_entry( + item_code=item_code, + target="_Test Warehouse - _TC", + qty=100, + basic_rate=50, + posting_date=add_days(nowdate(), -1), + ) + + batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle) + + posting_date = today() + posting_time = nowtime() + dn1 = create_delivery_note( + posting_date=posting_date, + posting_time=posting_time, + item_code=item_code, + rate=300, + qty=25, + batch_no=batch_no, + use_serial_batch_fields=1, + ) + + dn2 = create_delivery_note( + posting_date=posting_date, + posting_time=posting_time, + item_code=item_code, + rate=300, + qty=25, + batch_no=batch_no, + use_serial_batch_fields=1, + ) + + dn3 = create_delivery_note( + posting_date=posting_date, + posting_time=posting_time, + item_code=item_code, + rate=300, + qty=25, + batch_no=batch_no, + use_serial_batch_fields=1, + ) + + dn4 = create_delivery_note( + posting_date=posting_date, + posting_time=posting_time, + item_code=item_code, + rate=300, + qty=25, + batch_no=batch_no, + use_serial_batch_fields=1, + ) + + for dn in [dn1, dn2, dn3, dn4]: + sles = frappe.get_all( + "Stock Ledger Entry", + fields=["stock_value_difference", "actual_qty"], + filters={"is_cancelled": 0, "voucher_no": dn.name, "docstatus": 1}, + ) + + for sle in sles: + self.assertEqual(sle.actual_qty, 25.0 * -1) + self.assertEqual(sle.stock_value_difference, 25.0 * 50 * -1) + + dn5 = create_delivery_note( + posting_date=posting_date, + posting_time=posting_time, + item_code=item_code, + rate=300, + qty=25, + batch_no=batch_no, + use_serial_batch_fields=1, + do_not_submit=True, + ) + + self.assertRaises(frappe.ValidationError, dn5.submit) + + def test_warranty_expiry_date_for_serial_item(self): + item_code = make_item( + "Test Warranty Expiry Date Item", + properties={ + "has_serial_no": 1, + "serial_no_series": "TWE.#####", + "is_stock_item": 1, + "warranty_period": 100, + }, + ).name + + se = make_stock_entry( + item_code=item_code, + target="_Test Warehouse - _TC", + qty=2, + basic_rate=50, + posting_date=nowdate(), + ) + + serial_nos = get_serial_nos_from_bundle(se.items[0].serial_and_batch_bundle) + create_delivery_note( + item_code=item_code, + qty=2, + rate=300, + use_serial_batch_fields=0, + serial_no=serial_nos, + ) + + for row in serial_nos: + sn = frappe.get_doc("Serial No", row) + self.assertEqual(getdate(sn.warranty_expiry_date), getdate(add_days(nowdate(), 100))) + self.assertEqual(sn.status, "Delivered") + self.assertEqual(sn.warranty_period, 100) + + def test_batch_return_dn(self): + from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return + + item_code = make_item( + "Test Batch Return DN Item 1", + properties={ + "has_batch_no": 1, + "valuation_method": "Moving Average", + "create_new_batch": 1, + "batch_number_series": "TBRDN1-.#####", + "is_stock_item": 1, + }, + ).name + + se = make_stock_entry(item_code=item_code, target="_Test Warehouse - _TC", qty=5, basic_rate=100) + + batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle) + dn = create_delivery_note( + item_code=item_code, + qty=5, + rate=500, + use_serial_batch_fields=1, + batch_no=batch_no, + ) + + dn_return = make_sales_return(dn.name) + dn_return.save().submit() + + self.assertEqual(dn_return.items[0].qty, 5 * -1) + + returned_batch_no = get_batch_from_bundle(dn_return.items[0].serial_and_batch_bundle) + self.assertEqual(batch_no, returned_batch_no) + + stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_no": dn_return.name, "voucher_type": "Delivery Note"}, + "stock_value_difference", + ) + + self.assertEqual(stock_value_difference, 100.0 * 5) + +>>>>>>> 6087a57b0c (fix: zero incoming rate for delivery note return (#43642)) def create_delivery_note(**args): dn = frappe.new_doc("Delivery Note") diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 8ebc2aa9c04..a81b57168de 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -775,6 +775,15 @@ class update_entries_after: } ) + if not rate and sle.voucher_type in ["Delivery Note", "Sales Invoice"]: + rate = get_rate_for_return( + sle.voucher_type, + sle.voucher_no, + sle.item_code, + voucher_detail_no=sle.voucher_detail_no, + sle=sle, + ) + else: rate = get_rate_for_return( sle.voucher_type,