diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index a6d7df6971..5b83534caf 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -26,6 +26,11 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import ( get_taxes, 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.test_stock_entry import get_qty_after_transaction from erpnext.stock.tests.test_utils import StockTestMixin @@ -888,14 +893,20 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin): rejected_warehouse="_Test Rejected Warehouse - _TC", allow_zero_valuation_rate=1, ) + pi.load_from_db() + + serial_no = get_serial_nos_from_bundle(pi.get("items")[0].serial_and_batch_bundle)[0] + rejected_serial_no = get_serial_nos_from_bundle( + pi.get("items")[0].rejected_serial_and_batch_bundle + )[0] self.assertEqual( - frappe.db.get_value("Serial No", pi.get("items")[0].serial_no, "warehouse"), + frappe.db.get_value("Serial No", serial_no, "warehouse"), pi.get("items")[0].warehouse, ) self.assertEqual( - frappe.db.get_value("Serial No", pi.get("items")[0].rejected_serial_no, "warehouse"), + frappe.db.get_value("Serial No", rejected_serial_no, "warehouse"), pi.get("items")[0].rejected_warehouse, ) @@ -1652,7 +1663,7 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin): ) pi.load_from_db() - batch_no = pi.items[0].batch_no + batch_no = get_batch_from_bundle(pi.items[0].serial_and_batch_bundle) self.assertTrue(batch_no) frappe.db.set_value("Batch", batch_no, "expiry_date", add_days(nowdate(), -1)) @@ -1734,6 +1745,32 @@ def make_purchase_invoice(**args): pi.supplier_warehouse = args.supplier_warehouse or "_Test Warehouse 1 - _TC" pi.cost_center = args.parent_cost_center + bundle_id = None + if args.get("batch_no") or args.get("serial_no"): + batches = {} + qty = args.qty or 5 + item_code = args.item or args.item_code or "_Test Item" + if args.get("batch_no"): + batches = frappe._dict({args.batch_no: qty}) + + serial_nos = args.get("serial_no") or [] + + bundle_id = make_serial_batch_bundle( + frappe._dict( + { + "item_code": item_code, + "warehouse": args.warehouse or "_Test Warehouse - _TC", + "qty": qty, + "batches": batches, + "voucher_type": "Purchase Invoice", + "serial_nos": serial_nos, + "type_of_transaction": "Inward", + "posting_date": args.posting_date or today(), + "posting_time": args.posting_time, + } + ) + ).name + pi.append( "items", { @@ -1748,12 +1785,11 @@ def make_purchase_invoice(**args): "discount_account": args.discount_account or None, "discount_amount": args.discount_amount or 0, "conversion_factor": 1.0, - "serial_no": args.serial_no, + "serial_and_batch_bundle": bundle_id, "stock_uom": args.uom or "_Test UOM", "cost_center": args.cost_center or "_Test Cost Center - _TC", "project": args.project, "rejected_warehouse": args.rejected_warehouse or "", - "rejected_serial_no": args.rejected_serial_no or "", "asset_location": args.location or "", "allow_zero_valuation_rate": args.get("allow_zero_valuation_rate") or 0, }, @@ -1797,6 +1833,31 @@ def make_purchase_invoice_against_cost_center(**args): if args.supplier_warehouse: pi.supplier_warehouse = "_Test Warehouse 1 - _TC" + bundle_id = None + if args.get("batch_no") or args.get("serial_no"): + batches = {} + qty = args.qty or 5 + item_code = args.item or args.item_code or "_Test Item" + if args.get("batch_no"): + batches = frappe._dict({args.batch_no: qty}) + + serial_nos = args.get("serial_no") or [] + + bundle_id = make_serial_batch_bundle( + frappe._dict( + { + "item_code": item_code, + "warehouse": args.warehouse or "_Test Warehouse - _TC", + "qty": qty, + "batches": batches, + "voucher_type": "Purchase Receipt", + "serial_nos": serial_nos, + "posting_date": args.posting_date or today(), + "posting_time": args.posting_time, + } + ) + ).name + pi.append( "items", { @@ -1807,12 +1868,11 @@ def make_purchase_invoice_against_cost_center(**args): "rejected_qty": args.rejected_qty or 0, "rate": args.rate or 50, "conversion_factor": 1.0, - "serial_no": args.serial_no, + "serial_and_batch_bundle": bundle_id, "stock_uom": "_Test UOM", "cost_center": args.cost_center or "_Test Cost Center - _TC", "project": args.project, "rejected_warehouse": args.rejected_warehouse or "", - "rejected_serial_no": args.rejected_serial_no or "", }, ) if not args.do_not_save: diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index b58871ba7f..deb202d145 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -68,6 +68,7 @@ "serial_no", "col_br_wh", "rejected_warehouse", + "rejected_serial_and_batch_bundle", "batch_no", "rejected_serial_no", "manufacture_details", @@ -460,7 +461,8 @@ "fieldtype": "Text", "label": "Rejected Serial No", "no_copy": 1, - "print_hide": 1 + "print_hide": 1, + "read_only": 1 }, { "fieldname": "accounting", @@ -880,19 +882,28 @@ "label": "Apply TDS" }, { - "depends_on": "eval:!doc.is_fixed_asset", + "depends_on": "eval:parent.update_stock == 1", "fieldname": "serial_and_batch_bundle", "fieldtype": "Link", "label": "Serial and Batch Bundle", "no_copy": 1, "options": "Serial and Batch Bundle", "print_hide": 1 + }, + { + "depends_on": "eval:parent.update_stock == 1", + "fieldname": "rejected_serial_and_batch_bundle", + "fieldtype": "Link", + "label": "Rejected Serial and Batch Bundle", + "no_copy": 1, + "options": "Serial and Batch Bundle", + "print_hide": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2023-03-12 13:40:39.044607", + "modified": "2023-04-01 20:08:54.545160", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Item", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index e6037095ac..2075d57a35 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -266,8 +266,6 @@ class SalesInvoice(SellingController): self.update_billing_status_for_zero_amount_refdoc("Sales Order") self.check_credit_limit() - self.update_serial_no() - if not cint(self.is_pos) == 1 and not self.is_return: self.update_against_document_in_jv() @@ -351,7 +349,6 @@ class SalesInvoice(SellingController): if not self.is_return: self.update_billing_status_for_zero_amount_refdoc("Delivery Note") self.update_billing_status_for_zero_amount_refdoc("Sales Order") - self.update_serial_no(in_cancel=True) # Updating stock ledger should always be called after updating prevdoc status, # because updating reserved qty in bin depends upon updated delivered qty in SO @@ -1509,20 +1506,6 @@ class SalesInvoice(SellingController): self.set("write_off_amount", reference_doc.get("write_off_amount")) self.due_date = None - def update_serial_no(self, in_cancel=False): - """update Sales Invoice refrence in Serial No""" - invoice = None if (in_cancel or self.is_return) else self.name - if in_cancel and self.is_return: - invoice = self.return_against - - for item in self.items: - if not item.serial_no: - continue - - for serial_no in get_serial_nos(item.serial_no): - if serial_no and frappe.db.get_value("Serial No", serial_no, "item_code") == item.item_code: - frappe.db.set_value("Serial No", serial_no, "sales_invoice", invoice) - def validate_serial_numbers(self): """ validate serial number agains Delivery Note and Sales Invoice diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 48fef1892d..e503a77716 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -30,6 +30,11 @@ from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice from erpnext.stock.doctype.item.test_item import create_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.serial_no.serial_no import SerialNoWarehouseError from erpnext.stock.doctype.stock_entry.test_stock_entry import ( get_qty_after_transaction, @@ -1348,55 +1353,47 @@ class TestSalesInvoice(unittest.TestCase): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item se = make_serialized_item() - serial_nos = get_serial_nos(se.get("items")[0].serial_no) + se.load_from_db() + serial_nos = get_serial_nos_from_bundle(se.get("items")[0].serial_and_batch_bundle) si = frappe.copy_doc(test_records[0]) si.update_stock = 1 si.get("items")[0].item_code = "_Test Serialized Item With Series" si.get("items")[0].qty = 1 - si.get("items")[0].serial_no = serial_nos[0] + si.get("items")[0].warehouse = se.get("items")[0].t_warehouse + si.get("items")[0].serial_and_batch_bundle = make_serial_batch_bundle( + frappe._dict( + { + "item_code": si.get("items")[0].item_code, + "warehouse": si.get("items")[0].warehouse, + "company": si.company, + "qty": 1, + "voucher_type": "Stock Entry", + "serial_nos": [serial_nos[0]], + "posting_date": si.posting_date, + "posting_time": si.posting_time, + "type_of_transaction": "Outward", + "do_not_submit": True, + } + ) + ).name + si.insert() si.submit() self.assertFalse(frappe.db.get_value("Serial No", serial_nos[0], "warehouse")) - self.assertEqual( - frappe.db.get_value("Serial No", serial_nos[0], "delivery_document_no"), si.name - ) return si def test_serialized_cancel(self): - from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos - si = self.test_serialized() si.cancel() - serial_nos = get_serial_nos(si.get("items")[0].serial_no) + serial_nos = get_serial_nos_from_bundle(si.get("items")[0].serial_and_batch_bundle) self.assertEqual( frappe.db.get_value("Serial No", serial_nos[0], "warehouse"), "_Test Warehouse - _TC" ) - self.assertFalse(frappe.db.get_value("Serial No", serial_nos[0], "delivery_document_no")) - self.assertFalse(frappe.db.get_value("Serial No", serial_nos[0], "sales_invoice")) - - def test_serialize_status(self): - serial_no = frappe.get_doc( - { - "doctype": "Serial No", - "item_code": "_Test Serialized Item With Series", - "serial_no": make_autoname("SR", "Serial No"), - } - ) - serial_no.save() - - si = frappe.copy_doc(test_records[0]) - si.update_stock = 1 - si.get("items")[0].item_code = "_Test Serialized Item With Series" - si.get("items")[0].qty = 1 - si.get("items")[0].serial_no = serial_no.name - si.insert() - - self.assertRaises(SerialNoWarehouseError, si.submit) def test_serial_numbers_against_delivery_note(self): """ @@ -1404,20 +1401,22 @@ class TestSalesInvoice(unittest.TestCase): serial numbers are same """ from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note - from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item se = make_serialized_item() - serial_nos = get_serial_nos(se.get("items")[0].serial_no) + se.load_from_db() + serial_nos = get_serial_nos_from_bundle(se.get("items")[0].serial_and_batch_bundle)[0] - dn = create_delivery_note(item=se.get("items")[0].item_code, serial_no=serial_nos[0]) + dn = create_delivery_note(item=se.get("items")[0].item_code, serial_no=[serial_nos]) dn.submit() + dn.load_from_db() + + serial_nos = get_serial_nos_from_bundle(dn.get("items")[0].serial_and_batch_bundle)[0] + self.assertTrue(get_serial_nos_from_bundle(se.get("items")[0].serial_and_batch_bundle)[0]) si = make_sales_invoice(dn.name) si.save() - self.assertEqual(si.get("items")[0].serial_no, dn.get("items")[0].serial_no) - def test_return_sales_invoice(self): make_stock_entry(item_code="_Test Item", target="Stores - TCP1", qty=50, basic_rate=100) @@ -3174,7 +3173,7 @@ class TestSalesInvoice(unittest.TestCase): item_code="_Test Serialized Item With Series", update_stock=True, is_return=True, qty=-1 ) si.reload() - self.assertTrue(si.items[0].serial_no) + self.assertTrue(get_serial_nos_from_bundle(si.items[0].serial_and_batch_bundle)) def test_sales_invoice_with_disabled_account(self): try: @@ -3283,11 +3282,11 @@ class TestSalesInvoice(unittest.TestCase): pr = make_purchase_receipt(qty=1, item_code=item.name) - batch_no = pr.items[0].batch_no + batch_no = get_batch_from_bundle(pr.items[0].serial_and_batch_bundle) si = create_sales_invoice(qty=1, item_code=item.name, update_stock=1, batch_no=batch_no) si.load_from_db() - batch_no = si.items[0].batch_no + batch_no = get_batch_from_bundle(si.items[0].serial_and_batch_bundle) self.assertTrue(batch_no) frappe.db.set_value("Batch", batch_no, "expiry_date", add_days(today(), -1)) @@ -3386,6 +3385,32 @@ def create_sales_invoice(**args): si.naming_series = args.naming_series or "T-SINV-" si.cost_center = args.parent_cost_center + bundle_id = None + if si.update_stock and (args.get("batch_no") or args.get("serial_no")): + batches = {} + qty = args.qty or 1 + item_code = args.item or args.item_code or "_Test Item" + if args.get("batch_no"): + batches = frappe._dict({args.batch_no: qty}) + + serial_nos = args.get("serial_no") or [] + + bundle_id = make_serial_batch_bundle( + frappe._dict( + { + "item_code": item_code, + "warehouse": args.warehouse or "_Test Warehouse - _TC", + "qty": qty, + "batches": batches, + "voucher_type": "Purchase 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, + } + ) + ).name + si.append( "items", { @@ -3405,10 +3430,9 @@ def create_sales_invoice(**args): "discount_amount": args.discount_amount or 0, "asset": args.asset or None, "cost_center": args.cost_center or "_Test Cost Center - _TC", - "serial_no": args.serial_no, "conversion_factor": args.get("conversion_factor", 1), "incoming_rate": args.incoming_rate or 0, - "batch_no": args.batch_no or None, + "serial_and_batch_bundle": bundle_id, }, ) @@ -3418,6 +3442,8 @@ def create_sales_invoice(**args): si.submit() else: si.payment_schedule = [] + + si.load_from_db() else: si.payment_schedule = [] @@ -3452,7 +3478,6 @@ def create_sales_invoice_against_cost_center(**args): "income_account": "Sales - _TC", "expense_account": "Cost of Goods Sold - _TC", "cost_center": args.cost_center or "_Test Cost Center - _TC", - "serial_no": args.serial_no, }, ) 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 00fa1686c0..c67d6338c9 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 @@ -343,7 +343,7 @@ class TestLandedCostVoucher(FrappeTestCase): qty=1, rate=200, item_code=item_code, - serial_no=serial_no, + serial_no=[serial_no], ) serial_no_rate = frappe.db.get_value("Serial No", serial_no, "purchase_rate") @@ -353,7 +353,7 @@ class TestLandedCostVoucher(FrappeTestCase): item_code=item_code, company="_Test Company with perpetual inventory", warehouse="Stores - TCP1", - serial_no=serial_no, + serial_no=[serial_no], qty=1, rate=500, cost_center="Main - TCP1", diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py index 042395efac..9bb819aea0 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py @@ -24,6 +24,10 @@ def get_serial_nos_from_bundle(bundle): def make_serial_batch_bundle(kwargs): from erpnext.stock.serial_batch_bundle import SerialBatchCreation + type_of_transaction = "Inward" if kwargs.qty > 0 else "Outward" + if kwargs.get("type_of_transaction"): + type_of_transaction = kwargs.get("type_of_transaction") + sb = SerialBatchCreation( { "item_code": kwargs.item_code, @@ -36,7 +40,7 @@ def make_serial_batch_bundle(kwargs): "avg_rate": kwargs.rate, "batches": kwargs.batches, "serial_nos": kwargs.serial_nos, - "type_of_transaction": "Inward" if kwargs.qty > 0 else "Outward", + "type_of_transaction": type_of_transaction, "company": kwargs.company or "_Test Company", "do_not_submit": kwargs.do_not_submit, } diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 745cba67f8..083508e485 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -647,7 +647,7 @@ class TestStockEntry(FrappeTestCase): "do_not_submit": True, } ) - ) + ).name se.insert() se.submit() @@ -1789,7 +1789,7 @@ def make_serialized_item(**args): "do_not_submit": True, } ) - ) + ).name if args.cost_center: se.get("items")[0].cost_center = args.cost_center diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index fdc1ffc8a0..4694b29f9d 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -1142,7 +1142,6 @@ class update_entries_after(object): self.allow_zero_rate, currency=erpnext.get_company_currency(sle.company), company=sle.company, - batch_no=sle.batch_no, ) def get_sle_before_datetime(self, args):