Merge pull request #48290 from frappe/mergify/bp/version-14/pr-48281

fix: do not allow backdated transactions against serial numbers. (backport #48281)
This commit is contained in:
rohitwaghchaure
2025-06-27 11:55:38 +05:30
committed by GitHub
2 changed files with 87 additions and 4 deletions

View File

@@ -2761,6 +2761,63 @@ class TestPurchaseReceipt(FrappeTestCase):
pr.reload() pr.reload()
self.assertEqual(pr.status, "To Bill") 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(): def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier

View File

@@ -8,7 +8,18 @@ import frappe
from frappe import ValidationError, _ from frappe import ValidationError, _
from frappe.model.naming import make_autoname from frappe.model.naming import make_autoname
from frappe.query_builder.functions import Coalesce 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.controllers.stock_controller import StockController
from erpnext.stock.get_item_details import get_reserved_qty_for_so 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( for sle in frappe.db.sql(
""" """
SELECT voucher_type, voucher_no, 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 FROM
`tabStock Ledger Entry` `tabStock Ledger Entry`
WHERE WHERE
@@ -251,8 +262,23 @@ class SerialNo(StockController):
_("Cannot delete Serial No {0}, as it is used in stock transactions").format(self.name) _("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 = 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_purchase_details(last_sle.get("purchase_sle"))
self.set_sales_details(last_sle.get("delivery_sle")) self.set_sales_details(last_sle.get("delivery_sle"))
self.set_maintenance_status() 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.sales_order = None
serial_no_doc.validate_item() 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: if is_new:
serial_no_doc.db_insert() serial_no_doc.db_insert()