diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index b91fddfc023..4ab95c7e140 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -299,7 +299,7 @@ def validate_so_serial_no(sr, sales_order,): be delivered""").format(sales_order, sr.item_code, sr.name)) def has_duplicate_serial_no(sn, sle): - if sn.warehouse: + if sn.warehouse and sle.voucher_type != 'Stock Reconciliation': return True if sn.company != sle.company: @@ -413,14 +413,17 @@ def update_serial_nos_after_submit(controller, parentfield): update_rejected_serial_nos = True if (controller.doctype in ("Purchase Receipt", "Purchase Invoice") and d.rejected_qty) else False accepted_serial_nos_updated = False + if controller.doctype == "Stock Entry": warehouse = d.t_warehouse qty = d.transfer_qty else: warehouse = d.warehouse - qty = d.stock_qty + qty = (d.qty if controller.doctype == "Stock Reconciliation" + else d.stock_qty) for sle in stock_ledger_entries: + print(accepted_serial_nos_updated, qty, sle.actual_qty) if sle.voucher_detail_no==d.name: if not accepted_serial_nos_updated and qty and abs(sle.actual_qty)==qty \ and sle.warehouse == warehouse and sle.serial_no != d.serial_no: diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 9aec76d7e66..cdf6068f110 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -36,6 +36,9 @@ class StockReconciliation(StockController): self.update_stock_ledger() self.make_gl_entries() + from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit + update_serial_nos_after_submit(self, "items") + def on_cancel(self): self.delete_and_repost_sle() self.make_gl_entries_on_cancel() @@ -48,7 +51,8 @@ class StockReconciliation(StockController): self.posting_date, self.posting_time, batch_no=item.batch_no) if ((item.qty==None or item.qty==item_dict.get("qty")) - and (item.valuation_rate==None or item.valuation_rate==item_dict.get("rate"))): + and (item.valuation_rate==None or item.valuation_rate==item_dict.get("rate")) + and item.serial_no == item_dict.get("serial_nos")): return False else: # set default as current rates @@ -64,7 +68,7 @@ class StockReconciliation(StockController): item.current_qty = item_dict.get("qty") item.current_valuation_rate = item_dict.get("rate") self.difference_amount += (flt(item.qty, item.precision("qty")) * \ - flt(item.valuation_rate or rate, item.precision("valuation_rate")) \ + flt(item.valuation_rate or item_dict.get("rate"), item.precision("valuation_rate")) \ - flt(item_dict.get("qty"), item.precision("qty")) * flt(item_dict.get("rate"), item.precision("valuation_rate"))) return True @@ -157,7 +161,7 @@ class StockReconciliation(StockController): validate_is_stock_item(item_code, item.is_stock_item, verbose=0) # item should not be serialized - if item.has_serial_no and not row.serial_no: + if item.has_serial_no and not row.serial_no and not item.serial_no_series: raise frappe.ValidationError(_("Serial nos are required for serialized item {0}").format(item_code)) # item managed batch-wise not allowed @@ -180,7 +184,8 @@ class StockReconciliation(StockController): sl_entries = [] for row in self.items: - if row.serial_no or row.batch_no: + item = frappe.get_doc("Item", row.item_code) + if item.has_serial_no or item.has_batch_no: self.get_sle_for_serialized_items(row, sl_entries) else: previous_sle = get_previous_sle({ @@ -213,6 +218,9 @@ class StockReconciliation(StockController): def get_sle_for_serialized_items(self, row, sl_entries): from erpnext.stock.stock_ledger import get_previous_sle + serial_nos = get_serial_nos(row.serial_no) + + # To issue existing serial nos if row.current_qty and (row.current_serial_no or row.batch_no): args = self.get_sle_for_items(row) @@ -230,7 +238,7 @@ class StockReconciliation(StockController): sl_entries.append(args) - for serial_no in get_serial_nos(row.serial_no): + for serial_no in serial_nos: args = self.get_sle_for_items(row, [serial_no]) previous_sle = get_previous_sle({ @@ -262,7 +270,7 @@ class StockReconciliation(StockController): sl_entries.append(args) - if self.docstatus == 1: + if self.docstatus == 1 and not row.remove_serial_no_from_stock: args = self.get_sle_for_items(row) args.update({ @@ -273,6 +281,15 @@ class StockReconciliation(StockController): sl_entries.append(args) + if serial_nos == get_serial_nos(row.current_serial_no): + # update valuation rate + self.update_valuation_rate_for_serial_nos(row, serial_nos) + + def update_valuation_rate_for_serial_nos(self, row, serial_nos): + valuation_rate = row.valuation_rate if self.docstatus == 1 else row.current_valuation_rate + for d in serial_nos: + frappe.db.set_value("Serial No", d, 'purchase_rate', valuation_rate) + def get_sle_for_items(self, row, serial_nos=None): """Insert Stock Ledger Entries""" @@ -287,6 +304,7 @@ class StockReconciliation(StockController): "posting_time": self.posting_time, "voucher_type": self.doctype, "voucher_no": self.name, + "voucher_detail_no": row.name, "company": self.company, "stock_uom": frappe.db.get_value("Item", row.item_code, "stock_uom"), "is_cancelled": "No" if self.docstatus != 2 else "Yes", @@ -432,7 +450,6 @@ def get_stock_balance_for(item_code, warehouse, if item_dict.get("has_batch_no"): qty = get_batch_qty(batch_no, warehouse) or 0 - print(qty, rate, batch_no, warehouse) return { 'qty': qty, 'rate': rate, @@ -457,7 +474,7 @@ def get_qty_rate_for_serial_nos(item_code, warehouse, posting_date, posting_time "serial_nos": serial_nos }) - rate = get_incoming_rate(args) + rate = get_incoming_rate(args, raise_error_if_no_rate=False) or 0 return qty, rate, serial_nos diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 2dc585b8d63..5ee8228edf7 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -13,9 +13,12 @@ from erpnext.stock.stock_ledger import get_previous_sle, update_entries_after from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import EmptyStockReconciliationItemsError, get_items from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.doctype.item.test_item import create_item +from erpnext.stock.utils import get_stock_balance, get_incoming_rate, get_available_serial_nos +from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos class TestStockReconciliation(unittest.TestCase): def setUp(self): + create_batch_or_serial_no_items() frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) self.insert_existing_sle() @@ -106,6 +109,83 @@ class TestStockReconciliation(unittest.TestCase): make_stock_entry(posting_date="2013-01-05", posting_time="07:00", item_code="_Test Item", target="_Test Warehouse - _TC", qty=15, basic_rate=1200) + def test_stock_reco_for_serialized_item(self): + set_perpetual_inventory() + + to_delete_records = [] + to_delete_serial_nos = [] + + # Add new serial nos + serial_item_code = "Stock-Reco-Serial-Item-1" + serial_warehouse = "_Test Warehouse for Stock Reco1 - _TC" + + sr = create_stock_reconciliation(item_code=serial_item_code, + warehouse = serial_warehouse, qty=5, rate=200) + + # print(sr.name) + serial_nos = get_serial_nos(sr.items[0].serial_no) + self.assertEqual(len(serial_nos), 5) + + args = { + "item_code": serial_item_code, + "warehouse": serial_warehouse, + "posting_date": nowdate(), + "posting_time": nowtime(), + "serial_no": sr.items[0].serial_no + } + + valuation_rate = get_incoming_rate(args) + self.assertEqual(valuation_rate, 200) + + to_delete_records.append(sr.name) + + sr = create_stock_reconciliation(item_code=serial_item_code, + warehouse = serial_warehouse, qty=5, rate=300, serial_no = '\n'.join(serial_nos)) + + # print(sr.name) + serial_nos1 = get_serial_nos(sr.items[0].serial_no) + self.assertEqual(len(serial_nos1), 5) + + args = { + "item_code": serial_item_code, + "warehouse": serial_warehouse, + "posting_date": nowdate(), + "posting_time": nowtime(), + "serial_no": sr.items[0].serial_no + } + + valuation_rate = get_incoming_rate(args) + self.assertEqual(valuation_rate, 300) + + to_delete_records.append(sr.name) + to_delete_records.reverse() + + for d in to_delete_records: + stock_doc = frappe.get_doc("Stock Reconciliation", d) + stock_doc.cancel() + frappe.delete_doc("Stock Reconciliation", stock_doc.name) + + for d in serial_nos + serial_nos1: + if frappe.db.exists("Serial No", d): + frappe.delete_doc("Serial No", d) + +def create_batch_or_serial_no_items(): + create_warehouse("_Test Warehouse for Stock Reco1", + {"is_group": 0, "parent_warehouse": "_Test Warehouse Group - _TC"}) + + serial_item_doc = create_item("Stock-Reco-Serial-Item-1", is_stock_item=1) + if not serial_item_doc.has_serial_no: + serial_item_doc.has_serial_no = 1 + serial_item_doc.serial_no_series = "SRSI.####" + serial_item_doc.save(ignore_permissions=True) + + batch_item_doc = create_item("Stock-Reco-batch-Item-1", is_stock_item=1) + if not batch_item_doc.has_batch_no: + batch_item_doc.has_batch_no = 1 + batch_item_doc.create_new_batch = 1 + serial_item_doc.batch_number_series = "BASR.#####" + batch_item_doc.save(ignore_permissions=True) + def create_stock_reconciliation(**args): args = frappe._dict(args) sr = frappe.new_doc("Stock Reconciliation") @@ -120,7 +200,10 @@ def create_stock_reconciliation(**args): "item_code": args.item_code or "_Test Item", "warehouse": args.warehouse or "_Test Warehouse - _TC", "qty": args.qty, - "valuation_rate": args.rate + "valuation_rate": args.rate, + "serial_no": args.serial_no, + "batch_no": args.batch_no, + "remove_serial_no_from_stock": args.remove_serial_no_from_stock or 0 }) try: @@ -140,3 +223,4 @@ def set_valuation_method(item_code, valuation_method): }, allow_negative_stock=1) test_dependencies = ["Item", "Warehouse"] + diff --git a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json index dce87e5ae3f..fa42c9c25c0 100644 --- a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json +++ b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json @@ -15,6 +15,7 @@ "amount", "serial_no_and_batch_section", "serial_no", + "remove_serial_no_from_stock", "column_break_11", "batch_no", "section_break_3", @@ -165,10 +166,16 @@ "fieldtype": "Link", "label": "Batch No", "options": "Batch" + }, + { + "default": "0", + "fieldname": "remove_serial_no_from_stock", + "fieldtype": "Check", + "label": "Remove Serial No from Stock" } ], "istable": 1, - "modified": "2019-05-24 12:34:50.018491", + "modified": "2019-06-01 03:16:38.459307", "modified_by": "Administrator", "module": "Stock", "name": "Stock Reconciliation Item", diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index ea8e8805a64..6ea322872ef 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -173,7 +173,7 @@ def get_incoming_rate(args, raise_error_if_no_rate=True): in_rate = get_valuation_rate(args.get('item_code'), args.get('warehouse'), args.get('voucher_type'), voucher_no, args.get('allow_zero_valuation'), currency=erpnext.get_company_currency(args.get('company')), company=args.get('company'), - raise_error_if_no_rate=True) + raise_error_if_no_rate=raise_error_if_no_rate) return in_rate