From f8bf4aa7c81fbbbbc0d8c2326c725fb51ed47e91 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Sun, 2 Apr 2023 13:13:42 +0530 Subject: [PATCH] fix: travis for work order, pos invoice and landed cost voucher --- .../doctype/pos_invoice/test_pos_invoice.py | 178 ++++++++------ .../sales_invoice/test_sales_invoice.py | 3 +- .../controllers/sales_and_purchase_return.py | 26 +- .../doctype/work_order/test_work_order.py | 222 ++++++++++-------- .../doctype/work_order/work_order.py | 19 +- .../doctype/sales_order/test_sales_order.py | 106 --------- erpnext/stock/deprecated_serial_batch.py | 1 + .../delivery_note/test_delivery_note.py | 8 + .../test_landed_cost_voucher.py | 89 +++++-- .../serial_and_batch_bundle.py | 29 ++- .../stock/doctype/serial_no/serial_no.json | 13 +- .../stock/doctype/stock_entry/stock_entry.py | 2 +- .../doctype/stock_entry/test_stock_entry.py | 2 + erpnext/stock/serial_batch_bundle.py | 28 ++- 14 files changed, 406 insertions(+), 320 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py index 3132fdd259a..9685d99f357 100644 --- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py @@ -5,12 +5,18 @@ import copy import unittest import frappe +from frappe import _ from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice 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.serial_and_batch_bundle.test_serial_and_batch_bundle import ( + get_batch_from_bundle, + get_serial_nos_from_bundle, + make_serial_batch_bundle, +) from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry @@ -249,7 +255,7 @@ class TestPOSInvoice(unittest.TestCase): expense_account="Cost of Goods Sold - _TC", ) - serial_nos = get_serial_nos(se.get("items")[0].serial_no) + serial_nos = get_serial_nos_from_bundle(se.get("items")[0].serial_and_batch_bundle) pos = create_pos_invoice( company="_Test Company", @@ -260,11 +266,11 @@ class TestPOSInvoice(unittest.TestCase): expense_account="Cost of Goods Sold - _TC", cost_center="Main - _TC", item=se.get("items")[0].item_code, + serial_no=[serial_nos[0]], rate=1000, do_not_save=1, ) - pos.get("items")[0].serial_no = serial_nos[0] pos.append( "payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000, "default": 1} ) @@ -276,7 +282,9 @@ class TestPOSInvoice(unittest.TestCase): pos_return.insert() pos_return.submit() - self.assertEqual(pos_return.get("items")[0].serial_no, serial_nos[0]) + self.assertEqual( + get_serial_nos_from_bundle(pos_return.get("items")[0].serial_and_batch_bundle)[0], serial_nos[0] + ) def test_partial_pos_returns(self): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos @@ -289,7 +297,7 @@ class TestPOSInvoice(unittest.TestCase): expense_account="Cost of Goods Sold - _TC", ) - serial_nos = get_serial_nos(se.get("items")[0].serial_no) + serial_nos = get_serial_nos_from_bundle(se.get("items")[0].serial_and_batch_bundle) pos = create_pos_invoice( company="_Test Company", @@ -300,12 +308,12 @@ class TestPOSInvoice(unittest.TestCase): expense_account="Cost of Goods Sold - _TC", cost_center="Main - _TC", item=se.get("items")[0].item_code, + serial_no=serial_nos, qty=2, rate=1000, do_not_save=1, ) - pos.get("items")[0].serial_no = serial_nos[0] + "\n" + serial_nos[1] pos.append( "payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000, "default": 1} ) @@ -317,14 +325,27 @@ class TestPOSInvoice(unittest.TestCase): # partial return 1 pos_return1.get("items")[0].qty = -1 - pos_return1.get("items")[0].serial_no = serial_nos[0] + + bundle_id = frappe.get_doc( + "Serial and Batch Bundle", pos_return1.get("items")[0].serial_and_batch_bundle + ) + + bundle_id.remove(bundle_id.entries[1]) + bundle_id.save() + + bundle_id.load_from_db() + + serial_no = bundle_id.entries[0].serial_no + self.assertEqual(serial_no, serial_nos[0]) + pos_return1.insert() pos_return1.submit() # partial return 2 pos_return2 = make_sales_return(pos.name) self.assertEqual(pos_return2.get("items")[0].qty, -1) - self.assertEqual(pos_return2.get("items")[0].serial_no, serial_nos[1]) + serial_no = get_serial_nos_from_bundle(pos_return2.get("items")[0].serial_and_batch_bundle)[0] + self.assertEqual(serial_no, serial_nos[1]) def test_pos_change_amount(self): pos = create_pos_invoice( @@ -368,7 +389,7 @@ class TestPOSInvoice(unittest.TestCase): expense_account="Cost of Goods Sold - _TC", ) - serial_nos = get_serial_nos(se.get("items")[0].serial_no) + serial_nos = get_serial_nos_from_bundle(se.get("items")[0].serial_and_batch_bundle) pos = create_pos_invoice( company="_Test Company", @@ -380,10 +401,10 @@ class TestPOSInvoice(unittest.TestCase): cost_center="Main - _TC", item=se.get("items")[0].item_code, rate=1000, + serial_no=[serial_nos[0]], do_not_save=1, ) - pos.get("items")[0].serial_no = serial_nos[0] pos.append( "payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 1000} ) @@ -401,10 +422,10 @@ class TestPOSInvoice(unittest.TestCase): cost_center="Main - _TC", item=se.get("items")[0].item_code, rate=1000, + serial_no=[serial_nos[0]], do_not_save=1, ) - pos2.get("items")[0].serial_no = serial_nos[0] pos2.append( "payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 1000} ) @@ -423,7 +444,7 @@ class TestPOSInvoice(unittest.TestCase): expense_account="Cost of Goods Sold - _TC", ) - serial_nos = get_serial_nos(se.get("items")[0].serial_no) + serial_nos = get_serial_nos_from_bundle(se.get("items")[0].serial_and_batch_bundle) si = create_sales_invoice( company="_Test Company", @@ -435,11 +456,11 @@ class TestPOSInvoice(unittest.TestCase): cost_center="Main - _TC", item=se.get("items")[0].item_code, rate=1000, + update_stock=1, + serial_no=[serial_nos[0]], do_not_save=1, ) - si.get("items")[0].serial_no = serial_nos[0] - si.update_stock = 1 si.insert() si.submit() @@ -453,10 +474,10 @@ class TestPOSInvoice(unittest.TestCase): cost_center="Main - _TC", item=se.get("items")[0].item_code, rate=1000, + serial_no=[serial_nos[0]], do_not_save=1, ) - pos2.get("items")[0].serial_no = serial_nos[0] pos2.append( "payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 1000} ) @@ -473,7 +494,7 @@ class TestPOSInvoice(unittest.TestCase): cost_center="Main - _TC", expense_account="Cost of Goods Sold - _TC", ) - serial_nos = se.get("items")[0].serial_no + "wrong" + serial_nos = get_serial_nos_from_bundle(se.get("items")[0].serial_and_batch_bundle)[0] + "wrong" pos = create_pos_invoice( company="_Test Company", @@ -486,14 +507,13 @@ class TestPOSInvoice(unittest.TestCase): item=se.get("items")[0].item_code, rate=1000, qty=2, + serial_nos=[serial_nos], do_not_save=1, ) pos.get("items")[0].has_serial_no = 1 - pos.get("items")[0].serial_no = serial_nos - pos.insert() - self.assertRaises(frappe.ValidationError, pos.submit) + self.assertRaises(frappe.ValidationError, pos.insert) def test_value_error_on_serial_no_validation(self): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item @@ -504,7 +524,7 @@ class TestPOSInvoice(unittest.TestCase): cost_center="Main - _TC", expense_account="Cost of Goods Sold - _TC", ) - serial_nos = se.get("items")[0].serial_no + serial_nos = get_serial_nos_from_bundle(se.get("items")[0].serial_and_batch_bundle) # make a pos invoice pos = create_pos_invoice( @@ -517,11 +537,11 @@ class TestPOSInvoice(unittest.TestCase): cost_center="Main - _TC", item=se.get("items")[0].item_code, rate=1000, + serial_no=[serial_nos[0]], qty=1, do_not_save=1, ) pos.get("items")[0].has_serial_no = 1 - pos.get("items")[0].serial_no = serial_nos.split("\n")[0] pos.set("payments", []) pos.append( "payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000, "default": 1} @@ -547,12 +567,12 @@ class TestPOSInvoice(unittest.TestCase): cost_center="Main - _TC", item=se.get("items")[0].item_code, rate=1000, + serial_no=[serial_nos[0]], qty=1, do_not_save=1, ) pos2.get("items")[0].has_serial_no = 1 - pos2.get("items")[0].serial_no = serial_nos.split("\n")[0] # Value error should not be triggered on validation pos2.save() @@ -748,16 +768,16 @@ class TestPOSInvoice(unittest.TestCase): self.assertEqual(rounded_total, 400) def test_pos_batch_item_qty_validation(self): + from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import ( + BatchNegativeStockError, + ) from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( create_batch_item_with_batch, ) + from erpnext.stock.serial_batch_bundle import SerialBatchCreation create_batch_item_with_batch("_BATCH ITEM", "TestBatch 01") item = frappe.get_doc("Item", "_BATCH ITEM") - batch = frappe.get_doc("Batch", "TestBatch 01") - batch.submit() - item.batch_no = "TestBatch 01" - item.save() se = make_stock_entry( target="_Test Warehouse - _TC", @@ -767,16 +787,28 @@ class TestPOSInvoice(unittest.TestCase): batch_no="TestBatch 01", ) - pos_inv1 = create_pos_invoice(item=item.name, rate=300, qty=1, do_not_submit=1) - pos_inv1.items[0].batch_no = "TestBatch 01" + pos_inv1 = create_pos_invoice( + item=item.name, rate=300, qty=1, do_not_submit=1, batch_no="TestBatch 01" + ) pos_inv1.save() pos_inv1.submit() pos_inv2 = create_pos_invoice(item=item.name, rate=300, qty=2, do_not_submit=1) - pos_inv2.items[0].batch_no = "TestBatch 01" - pos_inv2.save() - self.assertRaises(frappe.ValidationError, pos_inv2.submit) + sn_doc = SerialBatchCreation( + { + "item_code": item.name, + "warehouse": pos_inv2.items[0].warehouse, + "voucher_type": "Delivery Note", + "qty": 2, + "avg_rate": 300, + "batches": frappe._dict({"TestBatch 01": 2}), + "type_of_transaction": "Outward", + "company": pos_inv2.company, + } + ) + + self.assertRaises(BatchNegativeStockError, sn_doc.make_serial_and_batch_bundle) # teardown pos_inv1.reload() @@ -785,9 +817,6 @@ class TestPOSInvoice(unittest.TestCase): pos_inv2.reload() pos_inv2.delete() se.cancel() - batch.reload() - batch.cancel() - batch.delete() def test_ignore_pricing_rule(self): from erpnext.accounts.doctype.pricing_rule.test_pricing_rule import make_pricing_rule @@ -838,18 +867,18 @@ class TestPOSInvoice(unittest.TestCase): frappe.db.savepoint("before_test_delivered_serial_no_case") try: se = make_serialized_item() - serial_no = get_serial_nos(se.get("items")[0].serial_no)[0] + serial_no = get_serial_nos_from_bundle(se.get("items")[0].serial_and_batch_bundle)[0] - dn = create_delivery_note(item_code="_Test Serialized Item With Series", serial_no=serial_no) + dn = create_delivery_note(item_code="_Test Serialized Item With Series", serial_no=[serial_no]) + delivered_serial_no = get_serial_nos_from_bundle(dn.get("items")[0].serial_and_batch_bundle)[0] - delivery_document_no = frappe.db.get_value("Serial No", serial_no, "delivery_document_no") - self.assertEquals(delivery_document_no, dn.name) + self.assertEqual(serial_no, delivered_serial_no) init_user_and_profile() pos_inv = create_pos_invoice( item_code="_Test Serialized Item With Series", - serial_no=serial_no, + serial_no=[serial_no], qty=1, rate=100, do_not_submit=True, @@ -861,42 +890,6 @@ class TestPOSInvoice(unittest.TestCase): frappe.db.rollback(save_point="before_test_delivered_serial_no_case") frappe.set_user("Administrator") - def test_returned_serial_no_case(self): - from erpnext.accounts.doctype.pos_invoice_merge_log.test_pos_invoice_merge_log import ( - init_user_and_profile, - ) - from erpnext.stock.doctype.serial_no.serial_no import get_pos_reserved_serial_nos - from erpnext.stock.doctype.serial_no.test_serial_no import get_serial_nos - from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item - - frappe.db.savepoint("before_test_returned_serial_no_case") - try: - se = make_serialized_item() - serial_no = get_serial_nos(se.get("items")[0].serial_no)[0] - - init_user_and_profile() - - pos_inv = create_pos_invoice( - item_code="_Test Serialized Item With Series", - serial_no=serial_no, - qty=1, - rate=100, - ) - - pos_return = make_sales_return(pos_inv.name) - pos_return.flags.ignore_validate = True - pos_return.insert() - pos_return.submit() - - pos_reserved_serial_nos = get_pos_reserved_serial_nos( - {"item_code": "_Test Serialized Item With Series", "warehouse": "_Test Warehouse - _TC"} - ) - self.assertTrue(serial_no not in pos_reserved_serial_nos) - - finally: - frappe.db.rollback(save_point="before_test_returned_serial_no_case") - frappe.set_user("Administrator") - def create_pos_invoice(**args): args = frappe._dict(args) @@ -926,6 +919,40 @@ def create_pos_invoice(**args): pos_inv.set_missing_values() + bundle_id = None + if args.get("batch_no") or args.get("serial_no"): + type_of_transaction = args.type_of_transaction or "Outward" + + if pos_inv.is_return: + type_of_transaction = "Inward" + + qty = args.get("qty") or 1 + qty *= -1 if type_of_transaction == "Outward" else 1 + batches = {} + if args.get("batch_no"): + batches = frappe._dict({args.batch_no: qty}) + + bundle_id = make_serial_batch_bundle( + frappe._dict( + { + "item_code": args.item or args.item_code or "_Test Item", + "warehouse": args.warehouse or "_Test Warehouse - _TC", + "qty": qty, + "batches": batches, + "voucher_type": "Delivery Note", + "serial_nos": args.serial_no, + "posting_date": pos_inv.posting_date, + "posting_time": pos_inv.posting_time, + "type_of_transaction": type_of_transaction, + "do_not_submit": True, + } + ) + ).name + + if not bundle_id: + msg = f"Serial No {args.serial_no} not available for Item {args.item}" + frappe.throw(_(msg)) + pos_inv.append( "items", { @@ -936,8 +963,7 @@ def create_pos_invoice(**args): "income_account": args.income_account or "Sales - _TC", "expense_account": args.expense_account or "Cost of Goods Sold - _TC", "cost_center": args.cost_center or "_Test Cost Center - _TC", - "serial_no": args.serial_no, - "batch_no": args.batch_no, + "serial_and_batch_bundle": bundle_id, }, ) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index e503a777164..9fa7a86cb33 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -3402,11 +3402,12 @@ def create_sales_invoice(**args): "warehouse": args.warehouse or "_Test Warehouse - _TC", "qty": qty, "batches": batches, - "voucher_type": "Purchase Invoice", + "voucher_type": "Sales Invoice", "serial_nos": serial_nos, "type_of_transaction": "Outward" if not args.is_return else "Inward", "posting_date": si.posting_date or today(), "posting_time": si.posting_time, + "do_not_submit": True, } ) ).name diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index ef5898a45dc..34e3b131c5d 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -393,8 +393,15 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None): from erpnext.stock.serial_batch_bundle import SerialBatchCreation target_doc.qty = -1 * source_doc.qty + item_details = frappe.get_cached_value( + "Item", source_doc.item_code, ["has_batch_no", "has_serial_no"], as_dict=1 + ) + returned_serial_nos = [] if source_doc.get("serial_and_batch_bundle"): + if item_details.has_serial_no: + returned_serial_nos = get_returned_serial_nos(source_doc, source_parent) + type_of_transaction = "Inward" if ( frappe.db.get_value( @@ -410,6 +417,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None): "serial_and_batch_bundle": source_doc.serial_and_batch_bundle, "returned_against": source_doc.name, "item_code": source_doc.item_code, + "returned_serial_nos": returned_serial_nos, } ) @@ -418,6 +426,11 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None): target_doc.serial_and_batch_bundle = cls_obj.serial_and_batch_bundle if source_doc.get("rejected_serial_and_batch_bundle"): + if item_details.has_serial_no: + returned_serial_nos = get_returned_serial_nos( + source_doc, source_parent, serial_no_field="rejected_serial_and_batch_bundle" + ) + type_of_transaction = "Inward" if ( frappe.db.get_value( @@ -433,6 +446,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None): "serial_and_batch_bundle": source_doc.rejected_serial_and_batch_bundle, "returned_against": source_doc.name, "item_code": source_doc.item_code, + "returned_serial_nos": returned_serial_nos, } ) @@ -649,8 +663,11 @@ def get_filters( return filters -def get_returned_serial_nos(child_doc, parent_doc, serial_no_field="serial_no"): - from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos +def get_returned_serial_nos(child_doc, parent_doc, serial_no_field=None): + from erpnext.stock.serial_batch_bundle import get_serial_nos + + if not serial_no_field: + serial_no_field = "serial_and_batch_bundle" return_ref_field = frappe.scrub(child_doc.doctype) if child_doc.doctype == "Delivery Note Item": @@ -667,7 +684,10 @@ def get_returned_serial_nos(child_doc, parent_doc, serial_no_field="serial_no"): [parent_doc.doctype, "docstatus", "=", 1], ] + ids = [] for row in frappe.get_all(parent_doc.doctype, fields=fields, filters=filters): - serial_nos.extend(get_serial_nos(row.get(serial_no_field))) + ids.append(row.get("serial_and_batch_bundle")) + + serial_nos.extend(get_serial_nos(ids)) return serial_nos diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index bb53c8c225c..49ce6b95fd9 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -22,6 +22,11 @@ from erpnext.manufacturing.doctype.work_order.work_order import ( ) from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order from erpnext.stock.doctype.item.test_item import create_item, make_item +from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import ( + get_batch_from_bundle, + get_serial_nos_from_bundle, + make_serial_batch_bundle, +) from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.doctype.stock_entry import test_stock_entry from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse @@ -672,8 +677,11 @@ class TestWorkOrder(FrappeTestCase): if row.is_finished_item: self.assertEqual(row.item_code, fg_item) self.assertEqual(row.qty, 10) - self.assertTrue(row.batch_no in batches) - batches.remove(row.batch_no) + + bundle_id = frappe.get_doc("Serial and Batch Bundle", row.serial_and_batch_bundle) + for bundle_row in bundle_id.get("entries"): + self.assertTrue(bundle_row.batch_no in batches) + batches.remove(bundle_row.batch_no) ste1.submit() @@ -682,8 +690,12 @@ class TestWorkOrder(FrappeTestCase): for row in ste1.get("items"): if row.is_finished_item: self.assertEqual(row.item_code, fg_item) - self.assertEqual(row.qty, 10) - remaining_batches.append(row.batch_no) + self.assertEqual(row.qty, 20) + + bundle_id = frappe.get_doc("Serial and Batch Bundle", row.serial_and_batch_bundle) + for bundle_row in bundle_id.get("entries"): + self.assertTrue(bundle_row.batch_no in batches) + remaining_batches.append(bundle_row.batch_no) self.assertEqual(sorted(remaining_batches), sorted(batches)) @@ -1168,18 +1180,28 @@ class TestWorkOrder(FrappeTestCase): try: wo_order = make_wo_order_test_record(item=fg_item, qty=2, skip_transfer=True) - serial_nos = wo_order.serial_no + serial_nos = self.get_serial_nos_for_fg(wo_order.name) + stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 10)) stock_entry.set_work_order_details() stock_entry.set_serial_no_batch_for_finished_good() for row in stock_entry.items: if row.item_code == fg_item: - self.assertTrue(row.serial_no) - self.assertEqual(sorted(get_serial_nos(row.serial_no)), sorted(get_serial_nos(serial_nos))) + self.assertTrue(row.serial_and_batch_bundle) + self.assertEqual( + sorted(get_serial_nos_from_bundle(row.serial_and_batch_bundle)), sorted(serial_nos) + ) except frappe.MandatoryError: self.fail("Batch generation causing failing in Work Order") + def get_serial_nos_for_fg(self, work_order): + serial_nos = [] + for row in frappe.get_all("Serial No", filters={"work_order": work_order}): + serial_nos.append(row.name) + + return serial_nos + @change_settings( "Manufacturing Settings", {"backflush_raw_materials_based_on": "Material Transferred for Manufacture"}, @@ -1272,63 +1294,66 @@ class TestWorkOrder(FrappeTestCase): fg_item = "Test FG Item with Batch Raw Materials" ste_doc = test_stock_entry.make_stock_entry( - item_code=batch_item, target="Stores - _TC", qty=2, basic_rate=100, do_not_save=True - ) - - ste_doc.append( - "items", - { - "item_code": batch_item, - "item_name": batch_item, - "description": batch_item, - "basic_rate": 100, - "t_warehouse": "Stores - _TC", - "qty": 2, - "uom": "Nos", - "stock_uom": "Nos", - "conversion_factor": 1, - }, + item_code=batch_item, target="Stores - _TC", qty=4, basic_rate=100, do_not_save=True ) # Inward raw materials in Stores warehouse ste_doc.insert() ste_doc.submit() + ste_doc.load_from_db() - batch_list = sorted([row.batch_no for row in ste_doc.items]) + batch_no = get_batch_from_bundle(ste_doc.items[0].serial_and_batch_bundle) wo_doc = make_wo_order_test_record(production_item=fg_item, qty=4) transferred_ste_doc = frappe.get_doc( make_stock_entry(wo_doc.name, "Material Transfer for Manufacture", 4) ) - transferred_ste_doc.items[0].qty = 2 - transferred_ste_doc.items[0].batch_no = batch_list[0] + transferred_ste_doc.items[0].qty = 4 + transferred_ste_doc.items[0].serial_and_batch_bundle = make_serial_batch_bundle( + frappe._dict( + { + "item_code": batch_item, + "warehouse": "Stores - _TC", + "company": transferred_ste_doc.company, + "qty": 4, + "voucher_type": "Stock Entry", + "batches": frappe._dict({batch_no: 4}), + "posting_date": transferred_ste_doc.posting_date, + "posting_time": transferred_ste_doc.posting_time, + "type_of_transaction": "Outward", + "do_not_submit": True, + } + ) + ).name - new_row = copy.deepcopy(transferred_ste_doc.items[0]) - new_row.name = "" - new_row.batch_no = batch_list[1] - - # Transferred two batches from Stores to WIP Warehouse - transferred_ste_doc.append("items", new_row) transferred_ste_doc.submit() + transferred_ste_doc.load_from_db() # First Manufacture stock entry manufacture_ste_doc1 = frappe.get_doc(make_stock_entry(wo_doc.name, "Manufacture", 1)) + manufacture_ste_doc1.submit() + manufacture_ste_doc1.load_from_db() # Batch no should be same as transferred Batch no - self.assertEqual(manufacture_ste_doc1.items[0].batch_no, batch_list[0]) + self.assertEqual( + get_batch_from_bundle(manufacture_ste_doc1.items[0].serial_and_batch_bundle), batch_no + ) self.assertEqual(manufacture_ste_doc1.items[0].qty, 1) - manufacture_ste_doc1.submit() - # Second Manufacture stock entry manufacture_ste_doc2 = frappe.get_doc(make_stock_entry(wo_doc.name, "Manufacture", 2)) + manufacture_ste_doc2.submit() + manufacture_ste_doc2.load_from_db() - # Batch no should be same as transferred Batch no - self.assertEqual(manufacture_ste_doc2.items[0].batch_no, batch_list[0]) - self.assertEqual(manufacture_ste_doc2.items[0].qty, 1) - self.assertEqual(manufacture_ste_doc2.items[1].batch_no, batch_list[1]) - self.assertEqual(manufacture_ste_doc2.items[1].qty, 1) + self.assertTrue(manufacture_ste_doc2.items[0].serial_and_batch_bundle) + bundle_doc = frappe.get_doc( + "Serial and Batch Bundle", manufacture_ste_doc2.items[0].serial_and_batch_bundle + ) + + for d in bundle_doc.entries: + self.assertEqual(d.batch_no, batch_no) + self.assertEqual(abs(d.qty), 2) def test_backflushed_serial_no_raw_materials_based_on_transferred(self): frappe.db.set_value( @@ -1386,76 +1411,79 @@ class TestWorkOrder(FrappeTestCase): fg_item = "Test FG Item with Serial & Batch No Raw Materials" ste_doc = test_stock_entry.make_stock_entry( - item_code=sn_batch_item, target="Stores - _TC", qty=2, basic_rate=100, do_not_save=True - ) - - ste_doc.append( - "items", - { - "item_code": sn_batch_item, - "item_name": sn_batch_item, - "description": sn_batch_item, - "basic_rate": 100, - "t_warehouse": "Stores - _TC", - "qty": 2, - "uom": "Nos", - "stock_uom": "Nos", - "conversion_factor": 1, - }, + item_code=sn_batch_item, target="Stores - _TC", qty=4, basic_rate=100, do_not_save=True ) # Inward raw materials in Stores warehouse ste_doc.insert() ste_doc.submit() + ste_doc.load_from_db() - batch_dict = {row.batch_no: get_serial_nos(row.serial_no) for row in ste_doc.items} - batches = list(batch_dict.keys()) + serial_nos = [] + for row in ste_doc.items: + bundle_doc = frappe.get_doc("Serial and Batch Bundle", row.serial_and_batch_bundle) + + for d in bundle_doc.entries: + serial_nos.append(d.serial_no) wo_doc = make_wo_order_test_record(production_item=fg_item, qty=4) transferred_ste_doc = frappe.get_doc( make_stock_entry(wo_doc.name, "Material Transfer for Manufacture", 4) ) - transferred_ste_doc.items[0].qty = 2 - transferred_ste_doc.items[0].batch_no = batches[0] - transferred_ste_doc.items[0].serial_no = "\n".join(batch_dict.get(batches[0])) + transferred_ste_doc.items[0].qty = 4 + transferred_ste_doc.items[0].serial_and_batch_bundle = make_serial_batch_bundle( + frappe._dict( + { + "item_code": transferred_ste_doc.get("items")[0].item_code, + "warehouse": transferred_ste_doc.get("items")[0].s_warehouse, + "company": transferred_ste_doc.company, + "qty": 4, + "type_of_transaction": "Outward", + "voucher_type": "Stock Entry", + "serial_nos": serial_nos, + "posting_date": transferred_ste_doc.posting_date, + "posting_time": transferred_ste_doc.posting_time, + "do_not_submit": True, + } + ) + ).name - new_row = copy.deepcopy(transferred_ste_doc.items[0]) - new_row.name = "" - new_row.batch_no = batches[1] - new_row.serial_no = "\n".join(batch_dict.get(batches[1])) - - # Transferred two batches from Stores to WIP Warehouse - transferred_ste_doc.append("items", new_row) transferred_ste_doc.submit() + transferred_ste_doc.load_from_db() # First Manufacture stock entry manufacture_ste_doc1 = frappe.get_doc(make_stock_entry(wo_doc.name, "Manufacture", 1)) + manufacture_ste_doc1.submit() + manufacture_ste_doc1.load_from_db() # Batch no & Serial Nos should be same as transferred Batch no & Serial Nos - batch_no = manufacture_ste_doc1.items[0].batch_no - self.assertEqual( - get_serial_nos(manufacture_ste_doc1.items[0].serial_no)[0], batch_dict.get(batch_no)[0] - ) - self.assertEqual(manufacture_ste_doc1.items[0].qty, 1) + bundle = manufacture_ste_doc1.items[0].serial_and_batch_bundle + self.assertTrue(bundle) - manufacture_ste_doc1.submit() + bundle_doc = frappe.get_doc("Serial and Batch Bundle", bundle) + for d in bundle_doc.entries: + self.assertTrue(d.serial_no) + self.assertTrue(d.batch_no) + batch_no = frappe.get_cached_value("Serial No", d.serial_no, "batch_no") + self.assertEqual(d.batch_no, batch_no) + serial_nos.remove(d.serial_no) # Second Manufacture stock entry - manufacture_ste_doc2 = frappe.get_doc(make_stock_entry(wo_doc.name, "Manufacture", 2)) + manufacture_ste_doc2 = frappe.get_doc(make_stock_entry(wo_doc.name, "Manufacture", 3)) + manufacture_ste_doc2.submit() + manufacture_ste_doc2.load_from_db() - # Batch no & Serial Nos should be same as transferred Batch no & Serial Nos - batch_no = manufacture_ste_doc2.items[0].batch_no - self.assertEqual( - get_serial_nos(manufacture_ste_doc2.items[0].serial_no)[0], batch_dict.get(batch_no)[1] - ) - self.assertEqual(manufacture_ste_doc2.items[0].qty, 1) + bundle = manufacture_ste_doc2.items[0].serial_and_batch_bundle + self.assertTrue(bundle) - batch_no = manufacture_ste_doc2.items[1].batch_no - self.assertEqual( - get_serial_nos(manufacture_ste_doc2.items[1].serial_no)[0], batch_dict.get(batch_no)[0] - ) - self.assertEqual(manufacture_ste_doc2.items[1].qty, 1) + bundle_doc = frappe.get_doc("Serial and Batch Bundle", bundle) + for d in bundle_doc.entries: + self.assertTrue(d.serial_no) + self.assertTrue(d.batch_no) + serial_nos.remove(d.serial_no) + + self.assertFalse(serial_nos) def test_non_consumed_material_return_against_work_order(self): frappe.db.set_value( @@ -1490,27 +1518,35 @@ class TestWorkOrder(FrappeTestCase): for row in ste_doc.items: row.qty += 2 row.transfer_qty += 2 - nste_doc = test_stock_entry.make_stock_entry( + test_stock_entry.make_stock_entry( item_code=row.item_code, target="Stores - _TC", qty=row.qty, basic_rate=100 ) - row.batch_no = nste_doc.items[0].batch_no - row.serial_no = nste_doc.items[0].serial_no - ste_doc.save() ste_doc.submit() ste_doc.load_from_db() # Create a stock entry to manufacture the item + print("remove 2 qty from each item") ste_doc = frappe.get_doc(make_stock_entry(wo_doc.name, "Manufacture", 5)) for row in ste_doc.items: if row.s_warehouse and not row.t_warehouse: row.qty -= 2 row.transfer_qty -= 2 - if row.serial_no: - serial_nos = get_serial_nos(row.serial_no) - row.serial_no = "\n".join(serial_nos[0:5]) + if not row.serial_and_batch_bundle: + continue + + bundle_id = row.serial_and_batch_bundle + bundle_doc = frappe.get_doc("Serial and Batch Bundle", bundle_id) + if bundle_doc.has_serial_no: + bundle_doc.set("entries", bundle_doc.entries[0:5]) + else: + for bundle_row in bundle_doc.entries: + bundle_row.qty += 2 + + bundle_doc.save() + bundle_doc.load_from_db() ste_doc.save() ste_doc.submit() diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 529513931b7..3265b8f1d4c 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -1364,10 +1364,10 @@ def split_qty_based_on_batch_size(wo_doc, row, qty): def get_serial_nos_for_job_card(row, wo_doc): - if not wo_doc.serial_no: + if not wo_doc.has_serial_no: return - serial_nos = get_serial_nos(wo_doc.serial_no) + serial_nos = get_serial_nos_for_work_order(wo_doc.name, wo_doc.production_item) used_serial_nos = [] for d in frappe.get_all( "Job Card", @@ -1380,6 +1380,21 @@ def get_serial_nos_for_job_card(row, wo_doc): row.serial_no = "\n".join(serial_nos[0 : cint(row.job_card_qty)]) +def get_serial_nos_for_work_order(work_order, production_item): + serial_nos = [] + for d in frappe.get_all( + "Serial No", + fields=["name"], + filters={ + "work_order": work_order, + "item_code": production_item, + }, + ): + serial_nos.append(d.name) + + return serial_nos + + def validate_operation_data(row): if row.get("qty") <= 0: frappe.throw( diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 8d1dd0725f7..e58bc739495 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -1254,112 +1254,6 @@ class TestSalesOrder(FrappeTestCase): ) self.assertEqual(wo_qty[0][0], so_item_name.get(item)) - def test_serial_no_based_delivery(self): - frappe.set_value("Stock Settings", None, "automatically_set_serial_nos_based_on_fifo", 1) - item = make_item( - "_Reserved_Serialized_Item", - { - "is_stock_item": 1, - "maintain_stock": 1, - "has_serial_no": 1, - "serial_no_series": "SI.####", - "valuation_rate": 500, - "item_defaults": [{"default_warehouse": "_Test Warehouse - _TC", "company": "_Test Company"}], - }, - ) - frappe.db.sql("""delete from `tabSerial No` where item_code=%s""", (item.item_code)) - make_item( - "_Test Item A", - { - "maintain_stock": 1, - "valuation_rate": 100, - "item_defaults": [{"default_warehouse": "_Test Warehouse - _TC", "company": "_Test Company"}], - }, - ) - make_item( - "_Test Item B", - { - "maintain_stock": 1, - "valuation_rate": 200, - "item_defaults": [{"default_warehouse": "_Test Warehouse - _TC", "company": "_Test Company"}], - }, - ) - from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom - - make_bom(item=item.item_code, rate=1000, raw_materials=["_Test Item A", "_Test Item B"]) - - so = make_sales_order( - **{ - "item_list": [ - { - "item_code": item.item_code, - "ensure_delivery_based_on_produced_serial_no": 1, - "qty": 1, - "rate": 1000, - } - ] - } - ) - so.submit() - from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record - - work_order = make_wo_order_test_record(item=item.item_code, qty=1, do_not_save=True) - work_order.fg_warehouse = "_Test Warehouse - _TC" - work_order.sales_order = so.name - work_order.submit() - make_stock_entry(item_code=item.item_code, target="_Test Warehouse - _TC", qty=1) - item_serial_no = frappe.get_doc("Serial No", {"item_code": item.item_code}) - from erpnext.manufacturing.doctype.work_order.work_order import ( - make_stock_entry as make_production_stock_entry, - ) - - se = frappe.get_doc(make_production_stock_entry(work_order.name, "Manufacture", 1)) - se.submit() - reserved_serial_no = se.get("items")[2].serial_no - serial_no_so = frappe.get_value("Serial No", reserved_serial_no, "sales_order") - self.assertEqual(serial_no_so, so.name) - dn = make_delivery_note(so.name) - dn.save() - self.assertEqual(reserved_serial_no, dn.get("items")[0].serial_no) - item_line = dn.get("items")[0] - item_line.serial_no = item_serial_no.name - item_line = dn.get("items")[0] - item_line.serial_no = reserved_serial_no - dn.submit() - dn.load_from_db() - dn.cancel() - si = make_sales_invoice(so.name) - si.update_stock = 1 - si.save() - self.assertEqual(si.get("items")[0].serial_no, reserved_serial_no) - item_line = si.get("items")[0] - item_line.serial_no = item_serial_no.name - self.assertRaises(frappe.ValidationError, dn.submit) - item_line = si.get("items")[0] - item_line.serial_no = reserved_serial_no - self.assertTrue(si.submit) - si.submit() - si.load_from_db() - si.cancel() - si = make_sales_invoice(so.name) - si.update_stock = 0 - si.submit() - from erpnext.accounts.doctype.sales_invoice.sales_invoice import ( - make_delivery_note as make_delivery_note_from_invoice, - ) - - dn = make_delivery_note_from_invoice(si.name) - dn.save() - dn.submit() - self.assertEqual(dn.get("items")[0].serial_no, reserved_serial_no) - dn.load_from_db() - dn.cancel() - si.load_from_db() - si.cancel() - se.load_from_db() - se.cancel() - self.assertFalse(frappe.db.exists("Serial No", {"sales_order": so.name})) - def test_advance_payment_entry_unlink_against_sales_order(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry diff --git a/erpnext/stock/deprecated_serial_batch.py b/erpnext/stock/deprecated_serial_batch.py index ae1bf1469e4..1e1d8fdeca9 100644 --- a/erpnext/stock/deprecated_serial_batch.py +++ b/erpnext/stock/deprecated_serial_batch.py @@ -211,6 +211,7 @@ class DeprecatedBatchNoValuation: & (bundle_child.batch_no.isnotnull()) & (batch.use_batchwise_valuation == 0) & (bundle.is_cancelled == 0) + & (bundle.type_of_transaction.isin(["Inward", "Outward"])) ) .where(timestamp_condition) ) diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 099a96bda02..ff2d70501cf 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -512,6 +512,7 @@ class TestDeliveryNote(FrappeTestCase): def test_return_for_serialized_items(self): se = make_serialized_item() + serial_no = [get_serial_nos_from_bundle(se.get("items")[0].serial_and_batch_bundle)[0]] dn = create_delivery_note( @@ -1215,6 +1216,9 @@ def create_delivery_note(**args): if args.get("batch_no") or args.get("serial_no"): type_of_transaction = args.type_of_transaction or "Outward" + if dn.is_return: + type_of_transaction = "Inward" + qty = args.get("qty") or 1 qty *= -1 if type_of_transaction == "Outward" else 1 batches = {} @@ -1233,6 +1237,7 @@ def create_delivery_note(**args): "posting_date": dn.posting_date, "posting_time": dn.posting_time, "type_of_transaction": type_of_transaction, + "do_not_submit": True, } ) ).name @@ -1257,6 +1262,9 @@ def create_delivery_note(**args): dn.insert() if not args.do_not_submit: dn.submit() + + dn.load_from_db() + return dn diff --git a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py index c67d6338c93..03ff12cae05 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py @@ -4,7 +4,7 @@ import frappe from frappe.tests.utils import FrappeTestCase -from frappe.utils import add_days, add_to_date, flt, now +from frappe.utils import add_days, add_to_date, flt, now, nowtime, today from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice @@ -15,6 +15,12 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import ( get_gl_entries, make_purchase_receipt, ) +from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import ( + get_batch_from_bundle, + get_serial_nos_from_bundle, + make_serial_batch_bundle, +) +from erpnext.stock.serial_batch_bundle import SerialNoValuation class TestLandedCostVoucher(FrappeTestCase): @@ -297,9 +303,8 @@ class TestLandedCostVoucher(FrappeTestCase): self.assertEqual(expected_values[gle.account][1], gle.credit) def test_landed_cost_voucher_for_serialized_item(self): - frappe.db.sql( - "delete from `tabSerial No` where name in ('SN001', 'SN002', 'SN003', 'SN004', 'SN005')" - ) + frappe.db.set_value("Item", "_Test Serialized Item", "serial_no_series", "SNJJ.###") + pr = make_purchase_receipt( company="_Test Company with perpetual inventory", warehouse="Stores - TCP1", @@ -310,17 +315,42 @@ class TestLandedCostVoucher(FrappeTestCase): ) pr.items[0].item_code = "_Test Serialized Item" - pr.items[0].serial_no = "SN001\nSN002\nSN003\nSN004\nSN005" pr.submit() + pr.load_from_db() - serial_no_rate = frappe.db.get_value("Serial No", "SN001", "purchase_rate") + serial_no = get_serial_nos_from_bundle(pr.items[0].serial_and_batch_bundle)[0] + + sn_obj = SerialNoValuation( + sle=frappe._dict( + { + "posting_date": today(), + "posting_time": nowtime(), + "item_code": "_Test Serialized Item", + "warehouse": "Stores - TCP1", + "serial_nos": [serial_no], + } + ) + ) + + serial_no_rate = sn_obj.get_incoming_rate_of_serial_no(serial_no) create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company) - serial_no = frappe.db.get_value("Serial No", "SN001", ["warehouse", "purchase_rate"], as_dict=1) + sn_obj = SerialNoValuation( + sle=frappe._dict( + { + "posting_date": today(), + "posting_time": nowtime(), + "item_code": "_Test Serialized Item", + "warehouse": "Stores - TCP1", + "serial_nos": [serial_no], + } + ) + ) - self.assertEqual(serial_no.purchase_rate - serial_no_rate, 5.0) - self.assertEqual(serial_no.warehouse, "Stores - TCP1") + new_serial_no_rate = sn_obj.get_incoming_rate_of_serial_no(serial_no) + + self.assertEqual(new_serial_no_rate - serial_no_rate, 5.0) def test_serialized_lcv_delivered(self): """In some cases you'd want to deliver before you can know all the @@ -337,6 +367,15 @@ class TestLandedCostVoucher(FrappeTestCase): item_code = "_Test Serialized Item" warehouse = "Stores - TCP1" + if not frappe.db.exists("Serial No", serial_no): + frappe.get_doc( + { + "doctype": "Serial No", + "item_code": item_code, + "serial_no": serial_no, + } + ).insert() + pr = make_purchase_receipt( company="_Test Company with perpetual inventory", warehouse=warehouse, @@ -346,7 +385,19 @@ class TestLandedCostVoucher(FrappeTestCase): serial_no=[serial_no], ) - serial_no_rate = frappe.db.get_value("Serial No", serial_no, "purchase_rate") + sn_obj = SerialNoValuation( + sle=frappe._dict( + { + "posting_date": today(), + "posting_time": nowtime(), + "item_code": "_Test Serialized Item", + "warehouse": "Stores - TCP1", + "serial_nos": [serial_no], + } + ) + ) + + serial_no_rate = sn_obj.get_incoming_rate_of_serial_no(serial_no) # deliver it before creating LCV dn = create_delivery_note( @@ -362,14 +413,24 @@ class TestLandedCostVoucher(FrappeTestCase): charges = 10 create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company, charges=charges) - new_purchase_rate = serial_no_rate + charges - serial_no = frappe.db.get_value( - "Serial No", serial_no, ["warehouse", "purchase_rate"], as_dict=1 + sn_obj = SerialNoValuation( + sle=frappe._dict( + { + "posting_date": today(), + "posting_time": nowtime(), + "item_code": "_Test Serialized Item", + "warehouse": "Stores - TCP1", + "serial_nos": [serial_no], + } + ) ) - self.assertEqual(serial_no.purchase_rate, new_purchase_rate) + new_serial_no_rate = sn_obj.get_incoming_rate_of_serial_no(serial_no) + + # Since the serial no is already delivered the rate must be zero + self.assertFalse(new_serial_no_rate) stock_value_difference = frappe.db.get_value( "Stock Ledger Entry", 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 e1135163501..cfb03f0389e 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 @@ -52,9 +52,11 @@ class SerialandBatchBundle(Document): return serial_nos = [d.serial_no for d in self.entries if d.serial_no] - available_serial_nos = get_available_serial_nos( - frappe._dict({"item_code": self.item_code, "warehouse": self.warehouse}) - ) + kwargs = {"item_code": self.item_code, "warehouse": self.warehouse} + if self.voucher_type == "POS Invoice": + kwargs["ignore_voucher_no"] = self.voucher_no + + available_serial_nos = get_available_serial_nos(frappe._dict(kwargs)) serial_no_warehouse = {} for data in available_serial_nos: @@ -149,12 +151,9 @@ class SerialandBatchBundle(Document): d.incoming_rate = abs(sn_obj.batch_avg_rate.get(d.batch_no)) available_qty = flt(sn_obj.available_qty.get(d.batch_no)) + flt(d.qty) - self.validate_negative_batch(d.batch_no, available_qty) - if self.has_batch_no: - d.stock_value_difference = flt(d.qty) * flt(d.incoming_rate) - + d.stock_value_difference = flt(d.qty) * flt(d.incoming_rate) if save: d.db_set( {"incoming_rate": d.incoming_rate, "stock_value_difference": d.stock_value_difference} @@ -198,6 +197,9 @@ class SerialandBatchBundle(Document): if self.voucher_type in ["Sales Invoice", "Delivery Note"]: valuation_field = "incoming_rate" + if self.voucher_type == "POS Invoice": + valuation_field = "rate" + rate = row.get(valuation_field) if row else 0.0 precision = frappe.get_precision(self.child_table, valuation_field) or 2 @@ -325,6 +327,7 @@ class SerialandBatchBundle(Document): & (parent.item_code == self.item_code) & (parent.docstatus == 1) & (parent.is_cancelled == 0) + & (parent.type_of_transaction.isin(["Inward", "Outward"])) ) .where(timestamp_condition) ).run(as_dict=True) @@ -830,6 +833,7 @@ def get_reserved_serial_nos_for_pos(kwargs): ["POS Invoice", "consolidated_invoice", "is", "not set"], ["POS Invoice", "docstatus", "=", 1], ["POS Invoice Item", "item_code", "=", kwargs.item_code], + ["POS Invoice", "name", "!=", kwargs.ignore_voucher_no], ], ) @@ -839,7 +843,10 @@ def get_reserved_serial_nos_for_pos(kwargs): if pos_invoice.serial_and_batch_bundle ] - for d in get_serial_batch_ledgers(ids, docstatus=1, name=ids): + if not ids: + return [] + + for d in get_serial_batch_ledgers(kwargs.item_code, docstatus=1, name=ids): ignore_serial_nos.append(d.serial_no) # Will be deprecated in v16 @@ -1010,7 +1017,11 @@ def get_ledgers_from_serial_batch_bundle(**kwargs) -> List[frappe._dict]: bundle_table.posting_date, bundle_table.posting_time, ) - .where((bundle_table.docstatus == 1) & (bundle_table.is_cancelled == 0)) + .where( + (bundle_table.docstatus == 1) + & (bundle_table.is_cancelled == 0) + & (bundle_table.type_of_transaction.isin(["Inward", "Outward"])) + ) ) for key, val in kwargs.items(): diff --git a/erpnext/stock/doctype/serial_no/serial_no.json b/erpnext/stock/doctype/serial_no/serial_no.json index 8dba69832da..ed1b0af30a6 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.json +++ b/erpnext/stock/doctype/serial_no/serial_no.json @@ -39,9 +39,7 @@ "more_info", "company", "column_break_2cmm", - "work_order", - "section_break_fgyk", - "serial_no_details" + "work_order" ], "fields": [ { @@ -226,11 +224,6 @@ "fieldtype": "Section Break", "label": "More Information" }, - { - "fieldname": "serial_no_details", - "fieldtype": "Text Editor", - "label": "Serial No Details" - }, { "fieldname": "company", "fieldtype": "Link", @@ -282,10 +275,6 @@ { "fieldname": "column_break_2cmm", "fieldtype": "Column Break" - }, - { - "fieldname": "section_break_fgyk", - "fieldtype": "Section Break" } ], "icon": "fa fa-barcode", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index f0cf1750dd0..e686e58c1d5 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -2789,7 +2789,7 @@ def get_available_materials(work_order) -> dict: if row.batch_nos: for batch_no, qty in row.batch_nos.items(): - item_data.batch_details[batch_no] -= qty + item_data.batch_details[batch_no] += qty if row.serial_no: for serial_no in get_serial_nos(row.serial_no): diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 083508e4853..08dcded7382 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -1803,6 +1803,8 @@ def make_serialized_item(**args): se.set_stock_entry_type() se.insert() se.submit() + + se.load_from_db() return se diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index 728394e798a..06fe0f1ec28 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -257,6 +257,9 @@ class SerialBatchBundle: def get_serial_nos(serial_and_batch_bundle): filters = {"parent": serial_and_batch_bundle} + if isinstance(serial_and_batch_bundle, list): + filters = {"parent": ("in", serial_and_batch_bundle)} + entries = frappe.get_all("Serial and Batch Entry", fields=["serial_no"], filters=filters) return [d.serial_no for d in entries] @@ -306,7 +309,7 @@ class SerialNoValuation(DeprecatedSerialNoValuation): (bundle.is_cancelled == 0) & (bundle.docstatus == 1) & (bundle_child.serial_no.isin(serial_nos)) - & (bundle.type_of_transaction != "Maintenance") + & (bundle.type_of_transaction.isin(["Inward", "Outward"])) & (bundle.item_code == self.sle.item_code) & (bundle_child.warehouse == self.sle.warehouse) ) @@ -314,7 +317,7 @@ class SerialNoValuation(DeprecatedSerialNoValuation): ) # Important to exclude the current voucher - if self.sle.voucher_type == "Stock Reconciliation" and self.sle.voucher_no: + if self.sle.voucher_no: query = query.where(bundle.voucher_no != self.sle.voucher_no) if self.sle.posting_date: @@ -370,6 +373,9 @@ class SerialNoValuation(DeprecatedSerialNoValuation): def get_incoming_rate(self): return abs(flt(self.stock_value_change) / flt(self.sle.actual_qty)) + def get_incoming_rate_of_serial_no(self, serial_no): + return self.serial_no_incoming_rate.get(serial_no, 0.0) + def is_rejected(voucher_type, voucher_detail_no, warehouse): if voucher_type in ["Purchase Receipt", "Purchase Invoice"]: @@ -437,7 +443,7 @@ class BatchNoValuation(DeprecatedBatchNoValuation): & (parent.item_code == self.sle.item_code) & (parent.docstatus == 1) & (parent.is_cancelled == 0) - & (parent.type_of_transaction != "Maintenance") + & (parent.type_of_transaction.isin(["Inward", "Outward"])) ) .groupby(child.batch_no) ) @@ -644,6 +650,10 @@ class SerialBatchCreation: id = self.serial_and_batch_bundle package = frappe.get_doc("Serial and Batch Bundle", id) new_package = frappe.copy_doc(package) + + if self.get("returned_serial_nos"): + self.remove_returned_serial_nos(new_package) + new_package.docstatus = 0 new_package.type_of_transaction = self.type_of_transaction new_package.returned_against = self.get("returned_against") @@ -651,6 +661,15 @@ class SerialBatchCreation: self.serial_and_batch_bundle = new_package.name + def remove_returned_serial_nos(self, package): + remove_list = [] + for d in package.entries: + if d.serial_no in self.returned_serial_nos: + remove_list.append(d) + + for d in remove_list: + package.remove(d) + def make_serial_and_batch_bundle(self): doc = frappe.new_doc("Serial and Batch Bundle") valid_columns = doc.meta.get_valid_columns() @@ -664,6 +683,9 @@ class SerialBatchCreation: self.set_auto_serial_batch_entries_for_inward() self.set_serial_batch_entries(doc) + if not doc.get("entries"): + return frappe._dict({}) + doc.save() if not hasattr(self, "do_not_submit") or not self.do_not_submit: