From 1393100a6210a6badac930e6a074e1c1f47a096b Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Fri, 27 Jun 2025 11:33:03 +0530 Subject: [PATCH] fix: do not allow backdated transactions against serial numbers. (#48281) (cherry picked from commit 945bdabebb599992d13d177b6f4e035888bc0bf5) --- .../purchase_receipt/test_purchase_receipt.py | 57 +++++++++++++++++++ erpnext/stock/doctype/serial_no/serial_no.py | 34 +++++++++-- 2 files changed, 87 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index ef690cda67d..199a44b3734 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -2761,6 +2761,63 @@ class TestPurchaseReceipt(FrappeTestCase): pr.reload() self.assertEqual(pr.status, "To Bill") + def test_serial_no_exists_in_future(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + + item_doc = make_item( + "Test Serial No Item Exists in Future", + { + "is_purchase_item": 1, + "is_stock_item": 1, + "has_serial_no": 1, + "serial_no_series": "SN-SBNS-.#####", + }, + ) + + source_warehouse = "_Test Warehouse - _TC" + target_warehouse = "_Test Warehouse 1 - _TC" + if not frappe.db.exists("Warehouse", target_warehouse): + create_warehouse("_Test Warehouse 1") + + make_purchase_receipt( + item_code=item_doc.name, + qty=1, + rate=100, + serial_no="SN-SBNS-00001", + posting_date=add_days(today(), -2), + ) + + make_stock_entry( + item_code=item_doc.name, + qty=1, + rate=100, + to_warehouse=target_warehouse, + serial_no="SN-SBNS-00002", + posting_date=add_days(today(), -1), + ) + + make_stock_entry( + item_code=item_doc.name, + qty=1, + rate=100, + from_warehouse=source_warehouse, + to_warehouse=target_warehouse, + serial_no="SN-SBNS-00001", + posting_date=today(), + ) + + se = make_stock_entry( + item_code=item_doc.name, + qty=1, + rate=100, + from_warehouse=target_warehouse, + serial_no="SN-SBNS-00001", + posting_date=add_days(today(), -1), + do_not_submit=True, + ) + + self.assertRaises(frappe.ValidationError, se.submit) + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index bc4d4998d0b..2c87bb56035 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -8,7 +8,18 @@ import frappe from frappe import ValidationError, _ from frappe.model.naming import make_autoname from frappe.query_builder.functions import Coalesce -from frappe.utils import add_days, cint, cstr, flt, get_link_to_form, getdate, now, nowdate, safe_json_loads +from frappe.utils import ( + add_days, + cint, + cstr, + flt, + get_datetime, + get_link_to_form, + getdate, + now, + nowdate, + safe_json_loads, +) from erpnext.controllers.stock_controller import StockController from erpnext.stock.get_item_details import get_reserved_qty_for_so @@ -197,7 +208,7 @@ class SerialNo(StockController): for sle in frappe.db.sql( """ SELECT voucher_type, voucher_no, - posting_date, posting_time, incoming_rate, actual_qty, serial_no + posting_date, posting_time, incoming_rate, actual_qty, serial_no, posting_datetime FROM `tabStock Ledger Entry` WHERE @@ -251,8 +262,23 @@ class SerialNo(StockController): _("Cannot delete Serial No {0}, as it is used in stock transactions").format(self.name) ) - def update_serial_no_reference(self, serial_no=None): + def update_serial_no_reference(self, serial_no=None, sle=None): last_sle = self.get_last_sle(serial_no) + + _last_sle_dict = last_sle.get("last_sle") + if ( + _last_sle_dict + and sle.get("voucher_type") != "Stock Reconciliation" + and sle.get("voucher_no") != _last_sle_dict.get("voucher_no") + and get_datetime(sle.get("posting_datetime")) + < get_datetime(_last_sle_dict.get("posting_datetime")) + ): + frappe.throw( + _( + "You can not complete this transaction because a future transaction exists for the serial number {0}" + ).format(serial_no) + ) + self.set_purchase_details(last_sle.get("purchase_sle")) self.set_sales_details(last_sle.get("delivery_sle")) self.set_maintenance_status() @@ -770,7 +796,7 @@ def update_args_for_serial_no(serial_no_doc, serial_no, args, is_new=False): serial_no_doc.sales_order = None serial_no_doc.validate_item() - serial_no_doc.update_serial_no_reference(serial_no) + serial_no_doc.update_serial_no_reference(serial_no, sle=args) if is_new: serial_no_doc.db_insert()