From 4b6444e93bf39029f943a3fc5c8ffa9430646bd0 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 15 Jul 2025 14:00:04 +0530 Subject: [PATCH] fix: system was allowing credit notes with serial numbers for any customer (cherry picked from commit e0730758343f3a53e58b7874e891369d62f2174f) # Conflicts: # erpnext/stock/doctype/delivery_note/delivery_note.py # erpnext/stock/doctype/serial_no/serial_no.json --- .../doctype/sales_invoice/sales_invoice.py | 1 + .../controllers/sales_and_purchase_return.py | 11 +++++++ erpnext/controllers/selling_controller.py | 29 +++++++++++++++++++ .../doctype/delivery_note/delivery_note.py | 6 ++++ .../stock/doctype/serial_no/serial_no.json | 15 ++++++++++ erpnext/stock/doctype/serial_no/serial_no.py | 1 + erpnext/stock/serial_batch_bundle.py | 7 ++++- 7 files changed, 69 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index b12f51e8a4b..829428eec86 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -461,6 +461,7 @@ class SalesInvoice(SellingController): self.make_bundle_for_sales_purchase_return(table_name) self.make_bundle_using_old_serial_batch_fields(table_name) + self.validate_standalone_serial_nos_customer() self.update_stock_reservation_entries() self.update_stock_ledger() diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index cb359dc5a71..011f21fe388 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -36,6 +36,17 @@ def validate_return_against(doc): party_type = "customer" if doc.doctype in ("Sales Invoice", "Delivery Note") else "supplier" + if ref_doc.get(party_type) != doc.get(party_type): + frappe.throw( + _("The {0} {1} does not match with the {0} {2} in the {3} {4}").format( + doc.meta.get_label(party_type), + doc.get(party_type), + ref_doc.get(party_type), + ref_doc.doctype, + ref_doc.name, + ) + ) + if ( ref_doc.company == doc.company and ref_doc.get(party_type) == doc.get(party_type) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 9089b5a2829..5f7cfb165d4 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -57,6 +57,35 @@ class SellingController(StockController): if self.get(table_field): self.set_serial_and_batch_bundle(table_field) + def validate_standalone_serial_nos_customer(self): + if not self.is_return or self.return_against: + return + + if self.doctype in ["Sales Invoice", "Delivery Note"]: + bundle_ids = [d.serial_and_batch_bundle for d in self.get("items") if d.serial_and_batch_bundle] + if not bundle_ids: + return + + serial_nos = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": ("in", bundle_ids)}, + pluck="serial_no", + ) + + if serial_nos := frappe.get_all( + "Serial No", + filters={"name": ("in", serial_nos), "customer": ("is", "set")}, + fields=["name", "customer"], + ): + for sn in serial_nos: + if sn.customer and sn.customer != self.customer: + frappe.throw( + _( + "Serial No {0} is already assigned to customer {1}. Can only be returned against the customer {1}" + ).format(frappe.bold(sn.name), frappe.bold(sn.customer)), + title=_("Serial No Already Assigned"), + ) + def set_missing_values(self, for_validate=False): super().set_missing_values(for_validate) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 37e2737391a..605cf2edb85 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -466,6 +466,12 @@ class DeliveryNote(SellingController): self.make_bundle_for_sales_purchase_return(table_name) self.make_bundle_using_old_serial_batch_fields(table_name) +<<<<<<< HEAD +======= + self.validate_standalone_serial_nos_customer() + self.update_stock_reservation_entries() + +>>>>>>> e073075834 (fix: system was allowing credit notes with serial numbers for any customer) # Updating stock ledger should always be called after updating prevdoc status, # because updating reserved qty in bin depends upon updated delivered qty in SO self.update_stock_ledger() diff --git a/erpnext/stock/doctype/serial_no/serial_no.json b/erpnext/stock/doctype/serial_no/serial_no.json index 89c77d46b1e..021847d5fa5 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.json +++ b/erpnext/stock/doctype/serial_no/serial_no.json @@ -15,6 +15,7 @@ "batch_no", "warehouse", "purchase_rate", + "customer", "column_break1", "status", "item_name", @@ -267,12 +268,25 @@ "label": "Creation Document No", "no_copy": 1, "read_only": 1 + }, + { + "fieldname": "customer", + "fieldtype": "Link", + "label": "Customer", + "no_copy": 1, + "options": "Customer", + "print_hide": 1, + "read_only": 1 } ], "icon": "fa fa-barcode", "idx": 1, "links": [], +<<<<<<< HEAD "modified": "2025-01-15 16:22:49.873889", +======= + "modified": "2025-07-15 13:36:21.938700", +>>>>>>> e073075834 (fix: system was allowing credit notes with serial numbers for any customer) "modified_by": "Administrator", "module": "Stock", "name": "Serial No", @@ -310,6 +324,7 @@ "role": "Stock User" } ], + "row_format": "Dynamic", "search_fields": "item_code", "show_name_in_global_search": 1, "sort_field": "modified", diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index 928313576f1..896323d6529 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -40,6 +40,7 @@ class SerialNo(StockController): batch_no: DF.Link | None brand: DF.Link | None company: DF.Link + customer: DF.Link | None description: DF.Text | None employee: DF.Link | None item_code: DF.Link diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index 8d9634db965..0fbf8475103 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -377,6 +377,10 @@ class SerialBatchBundle: ]: status = "Consumed" + customer = None + if sle.voucher_type in ["Sales Invoice", "Delivery Note"] and sle.actual_qty < 0: + customer = frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "customer") + sn_table = frappe.qb.DocType("Serial No") query = ( @@ -387,10 +391,11 @@ class SerialBatchBundle: "Active" if warehouse else status - if (sn_table.purchase_document_no != sle.voucher_no and sle.is_cancelled != 1) + if (sn_table.purchase_document_no != sle.voucher_no or sle.is_cancelled != 1) else "Inactive", ) .set(sn_table.company, sle.company) + .set(sn_table.customer, customer) .where(sn_table.name.isin(serial_nos)) )