mirror of
https://github.com/frappe/erpnext.git
synced 2026-03-17 14:02:10 +00:00
* fix: do not allow to inward same serial nos multiple times (#44617)
(cherry picked from commit 616bb383c5)
# Conflicts:
# erpnext/patches.txt
* chore: fix conflicts
---------
Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
This commit is contained in:
@@ -384,3 +384,4 @@ erpnext.patches.v15_0.update_task_assignee_email_field_in_asset_maintenance_log
|
|||||||
erpnext.patches.v15_0.update_sub_voucher_type_in_gl_entries
|
erpnext.patches.v15_0.update_sub_voucher_type_in_gl_entries
|
||||||
erpnext.patches.v14_0.update_stock_uom_in_work_order_item
|
erpnext.patches.v14_0.update_stock_uom_in_work_order_item
|
||||||
erpnext.patches.v15_0.set_is_exchange_gain_loss_in_payment_entry_deductions
|
erpnext.patches.v15_0.set_is_exchange_gain_loss_in_payment_entry_deductions
|
||||||
|
erpnext.patches.v15_0.enable_allow_existing_serial_no
|
||||||
|
|||||||
6
erpnext/patches/v15_0/enable_allow_existing_serial_no.py
Normal file
6
erpnext/patches/v15_0/enable_allow_existing_serial_no.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
if frappe.get_all("Company", filters={"country": "India"}, limit=1):
|
||||||
|
frappe.db.set_single_value("Stock Settings", "allow_existing_serial_no", 1)
|
||||||
@@ -3948,6 +3948,42 @@ class TestPurchaseReceipt(FrappeTestCase):
|
|||||||
self.assertEqual(return_pr.per_billed, 100)
|
self.assertEqual(return_pr.per_billed, 100)
|
||||||
self.assertEqual(return_pr.status, "Completed")
|
self.assertEqual(return_pr.status, "Completed")
|
||||||
|
|
||||||
|
def test_do_not_allow_to_inward_same_serial_no_multiple_times(self):
|
||||||
|
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||||
|
|
||||||
|
frappe.db.set_single_value("Stock Settings", "allow_existing_serial_no", 0)
|
||||||
|
|
||||||
|
item_code = make_item(
|
||||||
|
"Test Do Not Allow INWD Item 123", {"has_serial_no": 1, "serial_no_series": "SN-TDAISN-.#####"}
|
||||||
|
).name
|
||||||
|
|
||||||
|
pr = make_purchase_receipt(item_code=item_code, qty=1, rate=100, use_serial_batch_fields=1)
|
||||||
|
serial_no = get_serial_nos_from_bundle(pr.items[0].serial_and_batch_bundle)[0]
|
||||||
|
|
||||||
|
status = frappe.db.get_value("Serial No", serial_no, "status")
|
||||||
|
self.assertTrue(status == "Active")
|
||||||
|
|
||||||
|
make_stock_entry(
|
||||||
|
item_code=item_code,
|
||||||
|
source=pr.items[0].warehouse,
|
||||||
|
qty=1,
|
||||||
|
serial_no=serial_no,
|
||||||
|
use_serial_batch_fields=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
status = frappe.db.get_value("Serial No", serial_no, "status")
|
||||||
|
self.assertFalse(status == "Active")
|
||||||
|
|
||||||
|
pr = make_purchase_receipt(
|
||||||
|
item_code=item_code, qty=1, rate=100, use_serial_batch_fields=1, do_not_submit=1
|
||||||
|
)
|
||||||
|
pr.items[0].serial_no = serial_no
|
||||||
|
pr.save()
|
||||||
|
|
||||||
|
self.assertRaises(frappe.exceptions.ValidationError, pr.submit)
|
||||||
|
|
||||||
|
frappe.db.set_single_value("Stock Settings", "allow_existing_serial_no", 1)
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -89,6 +89,10 @@ class SerialandBatchBundle(Document):
|
|||||||
self.validate_serial_and_batch_no()
|
self.validate_serial_and_batch_no()
|
||||||
self.validate_duplicate_serial_and_batch_no()
|
self.validate_duplicate_serial_and_batch_no()
|
||||||
self.validate_voucher_no()
|
self.validate_voucher_no()
|
||||||
|
|
||||||
|
if self.docstatus == 0:
|
||||||
|
self.allow_existing_serial_nos()
|
||||||
|
|
||||||
if self.type_of_transaction == "Maintenance":
|
if self.type_of_transaction == "Maintenance":
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -102,6 +106,42 @@ class SerialandBatchBundle(Document):
|
|||||||
self.set_incoming_rate()
|
self.set_incoming_rate()
|
||||||
self.calculate_qty_and_amount()
|
self.calculate_qty_and_amount()
|
||||||
|
|
||||||
|
def allow_existing_serial_nos(self):
|
||||||
|
if self.type_of_transaction == "Outward" or not self.has_serial_no:
|
||||||
|
return
|
||||||
|
|
||||||
|
if frappe.db.get_single_value("Stock Settings", "allow_existing_serial_no"):
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.voucher_type not in ["Purchase Receipt", "Purchase Invoice", "Stock Entry"]:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.voucher_type == "Stock Entry" and frappe.get_cached_value(
|
||||||
|
"Stock Entry", self.voucher_no, "purpose"
|
||||||
|
) in ["Material Transfer", "Send to Subcontractor", "Material Transfer for Manufacture"]:
|
||||||
|
return
|
||||||
|
|
||||||
|
serial_nos = [d.serial_no for d in self.entries if d.serial_no]
|
||||||
|
|
||||||
|
data = frappe.get_all(
|
||||||
|
"Serial and Batch Entry",
|
||||||
|
filters={"serial_no": ("in", serial_nos), "docstatus": 1, "qty": ("<", 0)},
|
||||||
|
fields=["serial_no", "parent"],
|
||||||
|
)
|
||||||
|
|
||||||
|
note = "<br><br> <b>Note</b>:<br>"
|
||||||
|
for row in data:
|
||||||
|
frappe.throw(
|
||||||
|
_(
|
||||||
|
"You can't process the serial number {0} as it has already been used in the SABB {1}. {2} if you want to inward same serial number multiple times then enabled 'Allow existing Serial No to be Manufactured/Received again' in the {3}"
|
||||||
|
).format(
|
||||||
|
row.serial_no,
|
||||||
|
get_link_to_form("Serial and Batch Bundle", row.parent),
|
||||||
|
note,
|
||||||
|
get_link_to_form("Stock Settings", "Stock Settings"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def reset_serial_batch_bundle(self):
|
def reset_serial_batch_bundle(self):
|
||||||
if self.is_new() and self.amended_from:
|
if self.is_new() and self.amended_from:
|
||||||
for field in ["is_cancelled", "is_rejected"]:
|
for field in ["is_cancelled", "is_rejected"]:
|
||||||
@@ -136,7 +176,12 @@ class SerialandBatchBundle(Document):
|
|||||||
return
|
return
|
||||||
|
|
||||||
serial_nos = [d.serial_no for d in self.entries if d.serial_no]
|
serial_nos = [d.serial_no for d in self.entries if d.serial_no]
|
||||||
kwargs = {"item_code": self.item_code, "warehouse": self.warehouse}
|
kwargs = {
|
||||||
|
"item_code": self.item_code,
|
||||||
|
"warehouse": self.warehouse,
|
||||||
|
"check_serial_nos": True,
|
||||||
|
"serial_nos": serial_nos,
|
||||||
|
}
|
||||||
if self.voucher_type == "POS Invoice":
|
if self.voucher_type == "POS Invoice":
|
||||||
kwargs["ignore_voucher_nos"] = [self.voucher_no]
|
kwargs["ignore_voucher_nos"] = [self.voucher_no]
|
||||||
|
|
||||||
@@ -177,6 +222,7 @@ class SerialandBatchBundle(Document):
|
|||||||
"posting_date": self.posting_date,
|
"posting_date": self.posting_date,
|
||||||
"posting_time": self.posting_time,
|
"posting_time": self.posting_time,
|
||||||
"serial_nos": serial_nos,
|
"serial_nos": serial_nos,
|
||||||
|
"check_serial_nos": True,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1683,7 +1729,7 @@ def get_serial_nos_based_on_posting_date(kwargs, ignore_serial_nos):
|
|||||||
serial_nos = set()
|
serial_nos = set()
|
||||||
data = get_stock_ledgers_for_serial_nos(kwargs)
|
data = get_stock_ledgers_for_serial_nos(kwargs)
|
||||||
|
|
||||||
bundle_wise_serial_nos = get_bundle_wise_serial_nos(data)
|
bundle_wise_serial_nos = get_bundle_wise_serial_nos(data, kwargs)
|
||||||
for d in data:
|
for d in data:
|
||||||
if d.serial_and_batch_bundle:
|
if d.serial_and_batch_bundle:
|
||||||
if sns := bundle_wise_serial_nos.get(d.serial_and_batch_bundle):
|
if sns := bundle_wise_serial_nos.get(d.serial_and_batch_bundle):
|
||||||
@@ -1707,16 +1753,21 @@ def get_serial_nos_based_on_posting_date(kwargs, ignore_serial_nos):
|
|||||||
return serial_nos
|
return serial_nos
|
||||||
|
|
||||||
|
|
||||||
def get_bundle_wise_serial_nos(data):
|
def get_bundle_wise_serial_nos(data, kwargs):
|
||||||
bundle_wise_serial_nos = defaultdict(list)
|
bundle_wise_serial_nos = defaultdict(list)
|
||||||
bundles = [d.serial_and_batch_bundle for d in data if d.serial_and_batch_bundle]
|
bundles = [d.serial_and_batch_bundle for d in data if d.serial_and_batch_bundle]
|
||||||
if not bundles:
|
if not bundles:
|
||||||
return bundle_wise_serial_nos
|
return bundle_wise_serial_nos
|
||||||
|
|
||||||
|
filters = {"parent": ("in", bundles), "docstatus": 1, "serial_no": ("is", "set")}
|
||||||
|
|
||||||
|
if kwargs.get("check_serial_nos") and kwargs.get("serial_nos"):
|
||||||
|
filters["serial_no"] = ("in", kwargs.get("serial_nos"))
|
||||||
|
|
||||||
bundle_data = frappe.get_all(
|
bundle_data = frappe.get_all(
|
||||||
"Serial and Batch Entry",
|
"Serial and Batch Entry",
|
||||||
fields=["serial_no", "parent"],
|
fields=["serial_no", "parent"],
|
||||||
filters={"parent": ("in", bundles), "docstatus": 1, "serial_no": ("is", "set")},
|
filters=filters,
|
||||||
)
|
)
|
||||||
|
|
||||||
for d in bundle_data:
|
for d in bundle_data:
|
||||||
@@ -2277,6 +2328,8 @@ def get_ledgers_from_serial_batch_bundle(**kwargs) -> list[frappe._dict]:
|
|||||||
|
|
||||||
|
|
||||||
def get_stock_ledgers_for_serial_nos(kwargs):
|
def get_stock_ledgers_for_serial_nos(kwargs):
|
||||||
|
from erpnext.stock.utils import get_combine_datetime
|
||||||
|
|
||||||
stock_ledger_entry = frappe.qb.DocType("Stock Ledger Entry")
|
stock_ledger_entry = frappe.qb.DocType("Stock Ledger Entry")
|
||||||
|
|
||||||
query = (
|
query = (
|
||||||
@@ -2287,15 +2340,16 @@ def get_stock_ledgers_for_serial_nos(kwargs):
|
|||||||
stock_ledger_entry.serial_and_batch_bundle,
|
stock_ledger_entry.serial_and_batch_bundle,
|
||||||
)
|
)
|
||||||
.where(stock_ledger_entry.is_cancelled == 0)
|
.where(stock_ledger_entry.is_cancelled == 0)
|
||||||
|
.orderby(stock_ledger_entry.posting_datetime)
|
||||||
)
|
)
|
||||||
|
|
||||||
if kwargs.get("posting_date"):
|
if kwargs.get("posting_date"):
|
||||||
if kwargs.get("posting_time") is None:
|
if kwargs.get("posting_time") is None:
|
||||||
kwargs.posting_time = nowtime()
|
kwargs.posting_time = nowtime()
|
||||||
|
|
||||||
timestamp_condition = CombineDatetime(
|
timestamp_condition = stock_ledger_entry.posting_datetime <= get_combine_datetime(
|
||||||
stock_ledger_entry.posting_date, stock_ledger_entry.posting_time
|
kwargs.posting_date, kwargs.posting_time
|
||||||
) <= CombineDatetime(kwargs.posting_date, kwargs.posting_time)
|
)
|
||||||
|
|
||||||
query = query.where(timestamp_condition)
|
query = query.where(timestamp_condition)
|
||||||
|
|
||||||
|
|||||||
@@ -49,12 +49,13 @@
|
|||||||
"do_not_use_batchwise_valuation",
|
"do_not_use_batchwise_valuation",
|
||||||
"auto_create_serial_and_batch_bundle_for_outward",
|
"auto_create_serial_and_batch_bundle_for_outward",
|
||||||
"pick_serial_and_batch_based_on",
|
"pick_serial_and_batch_based_on",
|
||||||
|
"naming_series_prefix",
|
||||||
"column_break_mhzc",
|
"column_break_mhzc",
|
||||||
"disable_serial_no_and_batch_selector",
|
"disable_serial_no_and_batch_selector",
|
||||||
"use_naming_series",
|
"use_naming_series",
|
||||||
"naming_series_prefix",
|
|
||||||
"use_serial_batch_fields",
|
"use_serial_batch_fields",
|
||||||
"do_not_update_serial_batch_on_creation_of_auto_bundle",
|
"do_not_update_serial_batch_on_creation_of_auto_bundle",
|
||||||
|
"allow_existing_serial_no",
|
||||||
"stock_planning_tab",
|
"stock_planning_tab",
|
||||||
"auto_material_request",
|
"auto_material_request",
|
||||||
"auto_indent",
|
"auto_indent",
|
||||||
@@ -460,6 +461,12 @@
|
|||||||
"fieldname": "over_picking_allowance",
|
"fieldname": "over_picking_allowance",
|
||||||
"fieldtype": "Percent",
|
"fieldtype": "Percent",
|
||||||
"label": "Over Picking Allowance"
|
"label": "Over Picking Allowance"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "1",
|
||||||
|
"fieldname": "allow_existing_serial_no",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Allow existing Serial No to be Manufactured/Received again"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "icon-cog",
|
"icon": "icon-cog",
|
||||||
@@ -467,7 +474,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-07-29 14:55:19.093508",
|
"modified": "2024-12-09 17:52:36.030456",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Stock Settings",
|
"name": "Stock Settings",
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ class StockSettings(Document):
|
|||||||
|
|
||||||
action_if_quality_inspection_is_not_submitted: DF.Literal["Stop", "Warn"]
|
action_if_quality_inspection_is_not_submitted: DF.Literal["Stop", "Warn"]
|
||||||
action_if_quality_inspection_is_rejected: DF.Literal["Stop", "Warn"]
|
action_if_quality_inspection_is_rejected: DF.Literal["Stop", "Warn"]
|
||||||
|
allow_existing_serial_no: DF.Check
|
||||||
allow_from_dn: DF.Check
|
allow_from_dn: DF.Check
|
||||||
allow_from_pr: DF.Check
|
allow_from_pr: DF.Check
|
||||||
allow_internal_transfer_at_arms_length_price: DF.Check
|
allow_internal_transfer_at_arms_length_price: DF.Check
|
||||||
|
|||||||
Reference in New Issue
Block a user