diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index 2c6a5238c7..07ac326446 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -53,7 +53,7 @@ class SerialNo(StockController): if not self.get("__islocal"): item_code, warehouse = frappe.db.get_value("Serial No", self.name, ["item_code", "warehouse"]) - if item_code != self.item_code: + if not self.via_stock_ledger and item_code != self.item_code: frappe.throw(_("Item Code cannot be changed for Serial No."), SerialNoCannotCannotChangeError) if not self.via_stock_ledger and warehouse != self.warehouse: @@ -205,9 +205,10 @@ def validate_serial_no(sle, item_det): sr = frappe.get_doc("Serial No", serial_no) if sr.item_code!=sle.item_code: - frappe.throw(_("Serial No {0} does not belong to Item {1}").format(serial_no, - sle.item_code), SerialNoItemError) - + if not allow_serial_nos_with_different_item(serial_no, sle): + frappe.throw(_("Serial No {0} does not belong to Item {1}").format(serial_no, + sle.item_code), SerialNoItemError) + if sr.warehouse and sle.actual_qty > 0: frappe.throw(_("Serial No {0} has already been received").format(sr.name), SerialNoDuplicateError) @@ -228,7 +229,24 @@ def validate_serial_no(sle, item_det): elif sle.actual_qty < 0 or not item_det.serial_no_series: frappe.throw(_("Serial Nos Required for Serialized Item {0}").format(sle.item_code), SerialNoRequiredError) - + +def allow_serial_nos_with_different_item(sle_serial_no, sle): + """ + Allows same serial nos for raw materials and finished goods + in Manufacture / Repack type Stock Entry + """ + allow_serial_nos = False + if sle.voucher_type=="Stock Entry" and sle.actual_qty > 0: + stock_entry = frappe.get_doc("Stock Entry", sle.voucher_no) + if stock_entry.purpose in ("Repack", "Manufacture"): + for d in stock_entry.get("items"): + if d.serial_no and (d.s_warehouse if sle.is_cancelled=="No" else d.t_warehouse): + serial_nos = get_serial_nos(d.serial_no) + if sle_serial_no in serial_nos: + allow_serial_nos = True + + return allow_serial_nos + def update_serial_nos(sle, item_det): if sle.is_cancelled == "No" and not sle.serial_no and sle.actual_qty > 0 \ and item_det.has_serial_no == 1 and item_det.serial_no_series: @@ -245,6 +263,7 @@ def update_serial_nos(sle, item_det): if frappe.db.exists("Serial No", serial_no): sr = frappe.get_doc("Serial No", serial_no) sr.via_stock_ledger = True + sr.item_code = sle.item_code sr.warehouse = sle.warehouse if sle.actual_qty > 0 else None sr.save(ignore_permissions=True) elif sle.actual_qty > 0: diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 18d600d4d8..868053e5a8 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -359,14 +359,17 @@ class StockEntry(StockController): def update_stock_ledger(self): sl_entries = [] + + # make sl entries for source warehouse first, then do for target warehouse for d in self.get('items'): - if cstr(d.s_warehouse) and self.docstatus == 1: + if cstr(d.s_warehouse): sl_entries.append(self.get_sl_entries(d, { "warehouse": cstr(d.s_warehouse), "actual_qty": -flt(d.transfer_qty), "incoming_rate": 0 })) - + + for d in self.get('items'): if cstr(d.t_warehouse): sl_entries.append(self.get_sl_entries(d, { "warehouse": cstr(d.t_warehouse), @@ -374,15 +377,18 @@ class StockEntry(StockController): "incoming_rate": flt(d.valuation_rate) })) - # On cancellation, make stock ledger entry for - # target warehouse first, to update serial no values properly + # On cancellation, make stock ledger entry for + # target warehouse first, to update serial no values properly - if cstr(d.s_warehouse) and self.docstatus == 2: - sl_entries.append(self.get_sl_entries(d, { - "warehouse": cstr(d.s_warehouse), - "actual_qty": -flt(d.transfer_qty), - "incoming_rate": 0 - })) + # if cstr(d.s_warehouse) and self.docstatus == 2: + # sl_entries.append(self.get_sl_entries(d, { + # "warehouse": cstr(d.s_warehouse), + # "actual_qty": -flt(d.transfer_qty), + # "incoming_rate": 0 + # })) + + if self.docstatus == 2: + sl_entries.reverse() self.make_sl_entries(sl_entries, self.amended_from and 'Yes' or 'No') diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 62c55fde50..252deaad36 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -566,6 +566,27 @@ class TestStockEntry(unittest.TestCase): stock_entry = frappe.get_doc(make_stock_entry(production_order.name, "Manufacture", 1)) stock_entry.insert() self.assertTrue("_Test Variant Item-S" in [d.item_code for d in stock_entry.items]) + + def test_same_serial_nos_in_repack_or_manufacture_entries(self): + s1 = make_serialized_item(target_warehouse="_Test Warehouse - _TC") + serial_nos = s1.get("items")[0].serial_no + + s2 = make_stock_entry(item_code="_Test Serialized Item With Series", source="_Test Warehouse - _TC", + qty=2, basic_rate=100, purpose="Repack", serial_no=serial_nos, do_not_save=True) + + s2.append("items", { + "item_code": "_Test Serialized Item", + "t_warehouse": "_Test Warehouse - _TC", + "qty": 2, + "basic_rate": 120, + "expense_account": "Stock Adjustment - _TC", + "conversion_factor": 1.0, + "cost_center": "_Test Cost Center - _TC", + "serial_no": serial_nos + }) + + s2.submit() + s2.cancel() def make_serialized_item(item_code=None, serial_no=None, target_warehouse=None): se = frappe.copy_doc(test_records[0]) @@ -616,7 +637,8 @@ def make_stock_entry(**args): "basic_rate": args.basic_rate, "expense_account": args.expense_account or "Stock Adjustment - _TC", "conversion_factor": 1.0, - "cost_center": "_Test Cost Center - _TC" + "cost_center": "_Test Cost Center - _TC", + "serial_no": args.serial_no }) if not args.do_not_save: