From eab2d25c6aeef71c1f149cb60a846ab25c0b6555 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 27 Aug 2020 20:34:51 +0530 Subject: [PATCH 1/4] fix: Unlink and delete batch created from stock reco on cancel --- erpnext/controllers/stock_controller.py | 3 ++- .../stock/doctype/stock_reconciliation/stock_reconciliation.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 2f275bb3c91..8cad82c3e25 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -242,10 +242,11 @@ class StockController(AccountsController): _(self.doctype), self.name, item.get("item_code"))) def delete_auto_created_batches(self): + from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos for d in self.items: if not d.batch_no: continue - serial_nos = [sr.name for sr in frappe.get_all("Serial No", {'batch_no': d.batch_no})] + serial_nos = get_serial_nos(d.serial_no) if serial_nos: frappe.db.set_value("Serial No", { 'name': ['in', serial_nos] }, "batch_no", None) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 0dc87767dde..00dc7725039 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -45,6 +45,7 @@ class StockReconciliation(StockController): def on_cancel(self): self.delete_and_repost_sle() self.make_gl_entries_on_cancel() + self.delete_auto_created_batches() def remove_items_with_no_change(self): """Remove items if qty or rate is not changed""" From 6b785cf52202ed639527efda16ebaac37a6a4ab4 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 2 Sep 2020 15:45:02 +0530 Subject: [PATCH 2/4] chore: Tests for Stock Entry --- .../doctype/stock_entry/test_stock_entry.py | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 2afabe1480d..6098038e74e 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -483,6 +483,100 @@ class TestStockEntry(unittest.TestCase): serial_no = get_serial_nos(se.get("items")[0].serial_no)[0] self.assertFalse(frappe.db.get_value("Serial No", serial_no, "warehouse")) + def test_serial_batch_item_stock_entry(self): + """ + Behaviour: 1) Submit Stock Entry (Receipt) with Serial & Batched Item + 2) Cancel same Stock Entry + Expected Result: 1) Batch is created with Reference in Serial No + 2) Batch is deleted and Serial No is Inactive + """ + from erpnext.stock.doctype.batch.batch import get_batch_qty + + item = frappe.db.exists("Item", {'item_name': 'Batched and Serialised Item'}) + if not item: + item = create_item("Batched and Serialised Item") + item.has_batch_no = 1 + item.create_new_batch = 1 + item.has_serial_no = 1 + item.batch_number_series = "B-BATCH-.##" + item.serial_no_series = "S-.####" + item.save() + else: + item = frappe.get_doc("Item", {'item_name': 'Batched and Serialised Item'}) + + se = make_stock_entry(item_code=item.item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=100) + batch_no = se.items[0].batch_no + serial_no = get_serial_nos(se.items[0].serial_no)[0] + batch_qty = get_batch_qty(batch_no, "_Test Warehouse - _TC", item.item_code) + + batch_in_serial_no = frappe.db.get_value("Serial No", serial_no, "batch_no") + self.assertEqual(batch_in_serial_no, batch_no) + + self.assertEqual(batch_qty, 1) + + se.cancel() + + batch_in_serial_no = frappe.db.get_value("Serial No", serial_no, "batch_no") + self.assertEqual(batch_in_serial_no, None) + + self.assertEqual(frappe.db.get_value("Serial No", serial_no, "status"), "Inactive") + self.assertEqual(frappe.db.exists("Batch", batch_no), None) + + def test_serial_batch_item_qty_deduction(self): + """ + Behaviour: Create 2 Stock Entries, both adding Serial Nos to same batch + Expected Result: 1) Cancelling first Stock Entry (origin transaction of created batch) + should throw a Link Exists Error + 2) Cancelling second Stock Entry should make Serial Nos that are, linked to mentioned batch + and in that transaction only, Inactive. + """ + from erpnext.stock.doctype.batch.batch import get_batch_qty + + item = frappe.db.exists("Item", {'item_name': 'Batched and Serialised Item'}) + if not item: + item = create_item("Batched and Serialised Item") + item.has_batch_no = 1 + item.create_new_batch = 1 + item.has_serial_no = 1 + item.batch_number_series = "B-BATCH-.##" + item.serial_no_series = "S-.####" + item.save() + else: + item = frappe.get_doc("Item", {'item_name': 'Batched and Serialised Item'}) + + se1 = make_stock_entry(item_code=item.item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=100) + batch_no = se1.items[0].batch_no + serial_no1 = get_serial_nos(se1.items[0].serial_no)[0] + + # Check Source (Origin) Document of Batch + self.assertEqual(frappe.db.get_value("Batch", batch_no, "reference_name"), se1.name) + + se2 = make_stock_entry(item_code=item.item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=100, + batch_no=batch_no) + serial_no2 = get_serial_nos(se2.items[0].serial_no)[0] + + batch_qty = get_batch_qty(batch_no, "_Test Warehouse - _TC", item.item_code) + self.assertEqual(batch_qty, 2) + frappe.db.commit() + + # Cancelling Origin Document + self.assertRaises(frappe.LinkExistsError, se1.cancel) + frappe.db.rollback() + + se2.cancel() + + # Check decrease in Batch Qty + batch_qty = get_batch_qty(batch_no, "_Test Warehouse - _TC", item.item_code) + self.assertEqual(batch_qty, 1) + + # Check if Serial No from Stock Entry 1 is intact + self.assertEqual(frappe.db.get_value("Serial No", serial_no1, "batch_no"), batch_no) + self.assertEqual(frappe.db.get_value("Serial No", serial_no1, "status"), "Active") + + # Check id Serial No from Stock Entry 2 is Unlinked and Inactive + self.assertEqual(frappe.db.get_value("Serial No", serial_no2, "batch_no"), None) + self.assertEqual(frappe.db.get_value("Serial No", serial_no2, "status"), "Inactive") + def test_warehouse_company_validation(self): company = frappe.db.get_value('Warehouse', '_Test Warehouse 2 - _TC1', 'company') set_perpetual_inventory(0, company) From 9b6326f5d40f2e3a2677c7a81aee5bbbe861076f Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 2 Sep 2020 19:21:16 +0530 Subject: [PATCH 3/4] chore: Tests for Stock Reconciliation --- .../doctype/stock_entry/test_stock_entry.py | 7 +- .../test_stock_reconciliation.py | 117 +++++++++++++++++- 2 files changed, 115 insertions(+), 9 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 6098038e74e..84f535912d4 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -16,6 +16,7 @@ from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.accounts.doctype.account.test_account import get_inventory_account from erpnext.stock.doctype.stock_entry.stock_entry import move_sample_to_retention_warehouse, make_stock_in_entry from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import OpeningEntryAccountError +from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from six import iteritems def get_sle(**args): @@ -526,7 +527,7 @@ class TestStockEntry(unittest.TestCase): """ Behaviour: Create 2 Stock Entries, both adding Serial Nos to same batch Expected Result: 1) Cancelling first Stock Entry (origin transaction of created batch) - should throw a Link Exists Error + should throw a LinkExistsError 2) Cancelling second Stock Entry should make Serial Nos that are, linked to mentioned batch and in that transaction only, Inactive. """ @@ -559,7 +560,7 @@ class TestStockEntry(unittest.TestCase): self.assertEqual(batch_qty, 2) frappe.db.commit() - # Cancelling Origin Document + # Cancelling Origin Document of Batch self.assertRaises(frappe.LinkExistsError, se1.cancel) frappe.db.rollback() @@ -573,7 +574,7 @@ class TestStockEntry(unittest.TestCase): self.assertEqual(frappe.db.get_value("Serial No", serial_no1, "batch_no"), batch_no) self.assertEqual(frappe.db.get_value("Serial No", serial_no1, "status"), "Active") - # Check id Serial No from Stock Entry 2 is Unlinked and Inactive + # Check if Serial No from Stock Entry 2 is Unlinked and Inactive self.assertEqual(frappe.db.get_value("Serial No", serial_no2, "batch_no"), None) self.assertEqual(frappe.db.get_value("Serial No", serial_no2, "status"), "Inactive") diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 51d027f22ef..bbef4abeb2d 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -170,7 +170,7 @@ class TestStockReconciliation(unittest.TestCase): warehouse = "_Test Warehouse for Stock Reco2 - _TC" sr = create_stock_reconciliation(item_code=item_code, - warehouse = warehouse, qty=5, rate=200, do_not_submit=1) + warehouse = warehouse, qty=5, rate=200, do_not_save=1, do_not_submit=1) sr.save(ignore_permissions=True) sr.submit() @@ -204,6 +204,109 @@ class TestStockReconciliation(unittest.TestCase): stock_doc = frappe.get_doc("Stock Reconciliation", d) stock_doc.cancel() + def test_stock_reco_for_serial_and_batch_item(self): + set_perpetual_inventory() + + item = frappe.db.exists("Item", {'item_name': 'Batched and Serialised Item'}) + if not item: + item = create_item("Batched and Serialised Item") + item.has_batch_no = 1 + item.create_new_batch = 1 + item.has_serial_no = 1 + item.batch_number_series = "B-BATCH-.##" + item.serial_no_series = "S-.####" + item.save() + else: + item = frappe.get_doc("Item", {'item_name': 'Batched and Serialised Item'}) + + warehouse = "_Test Warehouse for Stock Reco2 - _TC" + + sr = create_stock_reconciliation(item_code=item.item_code, + warehouse = warehouse, qty=1, rate=100) + + batch_no = sr.items[0].batch_no + + serial_nos = get_serial_nos(sr.items[0].serial_no) + self.assertEqual(len(serial_nos), 1) + self.assertEqual(frappe.db.get_value("Serial No", serial_nos[0], "batch_no"), batch_no) + + sr.cancel() + + self.assertEqual(frappe.db.get_value("Serial No", serial_nos[0], "status"), "Inactive") + self.assertEqual(frappe.db.exists("Batch", batch_no), None) + + if frappe.db.exists("Serial No", serial_nos[0]): + frappe.delete_doc("Serial No", serial_nos[0]) + + def test_stock_reco_for_serial_and_batch_item_with_future_dependent_entry(self): + """ + Behaviour: 1) Create Stock Reconciliation, which will be the origin document + of a new batch having a serial no + 2) Create a Stock Entry that adds a serial no to the same batch following this + Stock Reconciliation + 3) Cancel Stock Reconciliation + 4) Cancel Stock Entry + Expected Result: 3) Cancelling the Stock Reco throws a LinkExistsError since + Stock Entry is dependent on the batch involved + 4) Serial No only in the Stock Entry is Inactive and Batch qty decreases + """ + from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry + from erpnext.stock.doctype.batch.batch import get_batch_qty + + set_perpetual_inventory() + + item = frappe.db.exists("Item", {'item_name': 'Batched and Serialised Item'}) + if not item: + item = create_item("Batched and Serialised Item") + item.has_batch_no = 1 + item.create_new_batch = 1 + item.has_serial_no = 1 + item.batch_number_series = "B-BATCH-.##" + item.serial_no_series = "S-.####" + item.save() + else: + item = frappe.get_doc("Item", {'item_name': 'Batched and Serialised Item'}) + + warehouse = "_Test Warehouse for Stock Reco2 - _TC" + + stock_reco = create_stock_reconciliation(item_code=item.item_code, + warehouse = warehouse, qty=1, rate=100) + batch_no = stock_reco.items[0].batch_no + serial_no = get_serial_nos(stock_reco.items[0].serial_no)[0] + + stock_entry = make_stock_entry(item_code=item.item_code, target=warehouse, qty=1, basic_rate=100, + batch_no=batch_no) + serial_no_2 = get_serial_nos(stock_entry.items[0].serial_no)[0] + + # Check Batch qty after 2 transactions + batch_qty = get_batch_qty(batch_no, warehouse, item.item_code) + self.assertEqual(batch_qty, 2) + frappe.db.commit() + + # Cancelling Origin Document of Batch + self.assertRaises(frappe.LinkExistsError, stock_reco.cancel) + frappe.db.rollback() + + stock_entry.cancel() + + # Check Batch qty after cancellation + batch_qty = get_batch_qty(batch_no, warehouse, item.item_code) + self.assertEqual(batch_qty, 1) + + # Check if Serial No from Stock Reconcilation is intact + self.assertEqual(frappe.db.get_value("Serial No", serial_no, "batch_no"), batch_no) + self.assertEqual(frappe.db.get_value("Serial No", serial_no, "status"), "Active") + + # Check if Serial No from Stock Entry is Unlinked and Inactive + self.assertEqual(frappe.db.get_value("Serial No", serial_no_2, "batch_no"), None) + self.assertEqual(frappe.db.get_value("Serial No", serial_no_2, "status"), "Inactive") + + stock_reco.load_from_db() + stock_reco.cancel() + + for sn in (serial_no, serial_no_2): + if frappe.db.exists("Serial No", sn): + frappe.delete_doc("Serial No", sn) def insert_existing_sle(warehouse): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry @@ -260,11 +363,13 @@ def create_stock_reconciliation(**args): "batch_no": args.batch_no }) - try: - if not args.do_not_submit: - sr.submit() - except EmptyStockReconciliationItemsError: - pass + if not args.do_not_save: + sr.insert() + try: + if not args.do_not_submit: + sr.submit() + except EmptyStockReconciliationItemsError: + pass return sr def set_valuation_method(item_code, valuation_method): From 44717e3b4a0d4f0ee70cf8ce2ec9a705ad71c75c Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 3 Sep 2020 14:41:05 +0530 Subject: [PATCH 4/4] fix: Item Alternative Test --- .../doctype/item_alternative/test_item_alternative.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/item_alternative/test_item_alternative.py b/erpnext/stock/doctype/item_alternative/test_item_alternative.py index f045e4f9114..61d90392364 100644 --- a/erpnext/stock/doctype/item_alternative/test_item_alternative.py +++ b/erpnext/stock/doctype/item_alternative/test_item_alternative.py @@ -13,6 +13,7 @@ from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_ord from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt, make_rm_stock_entry import unittest from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory +from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import EmptyStockReconciliationItemsError class TestItemAlternative(unittest.TestCase): def setUp(self): @@ -110,8 +111,11 @@ def make_items(): if not frappe.db.exists('Item', item_code): create_item(item_code) - create_stock_reconciliation(item_code="Test FG A RW 1", - warehouse='_Test Warehouse - _TC', qty=10, rate=2000) + try: + create_stock_reconciliation(item_code="Test FG A RW 1", + warehouse='_Test Warehouse - _TC', qty=10, rate=2000) + except EmptyStockReconciliationItemsError: + pass if frappe.db.exists('Item', 'Test FG A RW 1'): doc = frappe.get_doc('Item', 'Test FG A RW 1')