diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 86d1a6948dee..b097c0e6441e 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -3984,6 +3984,69 @@ def test_do_not_allow_to_inward_same_serial_no_multiple_times(self): frappe.db.set_single_value("Stock Settings", "allow_existing_serial_no", 1) + def test_seral_no_return_validation(self): + from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( + make_purchase_return, + ) + + sn_item_code = make_item( + "Test Serial No for Validation", {"has_serial_no": 1, "serial_no_series": "SN-TSNFVAL-.#####"} + ).name + + pr1 = make_purchase_receipt(item_code=sn_item_code, qty=5, rate=100, use_serial_batch_fields=1) + pr1_serial_nos = get_serial_nos_from_bundle(pr1.items[0].serial_and_batch_bundle) + + serial_no_pr = make_purchase_receipt( + item_code=sn_item_code, qty=5, rate=100, use_serial_batch_fields=1 + ) + serial_no_pr_serial_nos = get_serial_nos_from_bundle(serial_no_pr.items[0].serial_and_batch_bundle) + + sn_return = make_purchase_return(serial_no_pr.name) + sn_return.items[0].qty = -1 + sn_return.items[0].received_qty = -1 + sn_return.items[0].serial_no = pr1_serial_nos[0] + sn_return.save() + self.assertRaises(frappe.ValidationError, sn_return.submit) + + sn_return = make_purchase_return(serial_no_pr.name) + sn_return.items[0].qty = -1 + sn_return.items[0].received_qty = -1 + sn_return.items[0].serial_no = serial_no_pr_serial_nos[0] + sn_return.save() + sn_return.submit() + + def test_batch_no_return_validation(self): + from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( + make_purchase_return, + ) + + batch_item_code = make_item( + "Test Batch No for Validation", + {"has_batch_no": 1, "batch_number_series": "BT-TSNFVAL-.#####", "create_new_batch": 1}, + ).name + + pr1 = make_purchase_receipt(item_code=batch_item_code, qty=5, rate=100, use_serial_batch_fields=1) + batch_no = get_batch_from_bundle(pr1.items[0].serial_and_batch_bundle) + + batch_no_pr = make_purchase_receipt( + item_code=batch_item_code, qty=5, rate=100, use_serial_batch_fields=1 + ) + original_batch_no = get_batch_from_bundle(batch_no_pr.items[0].serial_and_batch_bundle) + + batch_return = make_purchase_return(batch_no_pr.name) + batch_return.items[0].qty = -1 + batch_return.items[0].received_qty = -1 + batch_return.items[0].batch_no = batch_no + batch_return.save() + self.assertRaises(frappe.ValidationError, batch_return.submit) + + batch_return = make_purchase_return(batch_no_pr.name) + batch_return.items[0].qty = -1 + batch_return.items[0].received_qty = -1 + batch_return.items[0].batch_no = original_batch_no + batch_return.save() + batch_return.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_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index a1f3135b70b8..dc2071b9ee07 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -252,8 +252,8 @@ def set_incoming_rate(self, parent=None, row=None, save=False, allow_negative_st ]: return - if return_aginst := self.get_return_aginst(parent=parent): - self.set_valuation_rate_for_return_entry(return_aginst, save) + if return_against := self.get_return_against(parent=parent): + self.set_valuation_rate_for_return_entry(return_against, save) elif self.type_of_transaction == "Outward": self.set_incoming_rate_for_outward_transaction( row, save, allow_negative_stock=allow_negative_stock @@ -261,9 +261,12 @@ def set_incoming_rate(self, parent=None, row=None, save=False, allow_negative_st else: self.set_incoming_rate_for_inward_transaction(row, save) - def set_valuation_rate_for_return_entry(self, return_aginst, save=False): - if valuation_details := self.get_valuation_rate_for_return_entry(return_aginst): + def set_valuation_rate_for_return_entry(self, return_against, save=False): + if valuation_details := self.get_valuation_rate_for_return_entry(return_against): for row in self.entries: + if valuation_details: + self.validate_returned_serial_batch_no(return_against, row, valuation_details) + if row.serial_no: valuation_rate = valuation_details["serial_nos"].get(row.serial_no) else: @@ -280,7 +283,22 @@ def set_valuation_rate_for_return_entry(self, return_aginst, save=False): } ) - def get_valuation_rate_for_return_entry(self, return_aginst): + def validate_returned_serial_batch_no(self, return_against, row, original_inv_details): + if row.serial_no and row.serial_no not in original_inv_details["serial_nos"]: + self.throw_error_message( + _( + "Serial No {0} is not present in the {1} {2}, hence you can't return it against the {1} {2}" + ).format(bold(row.serial_no), self.voucher_type, bold(return_against)) + ) + + if row.batch_no and row.batch_no not in original_inv_details["batches"]: + self.throw_error_message( + _( + "Batch No {0} is not present in the original {1} {2}, hence you can't return it against the {1} {2}" + ).format(bold(row.batch_no), self.voucher_type, bold(return_against)) + ) + + def get_valuation_rate_for_return_entry(self, return_against): valuation_details = frappe._dict( { "serial_nos": defaultdict(float), @@ -296,7 +314,7 @@ def get_valuation_rate_for_return_entry(self, return_aginst): "`tabSerial and Batch Entry`.`incoming_rate`", ], filters=[ - ["Serial and Batch Bundle", "voucher_no", "=", return_aginst], + ["Serial and Batch Bundle", "voucher_no", "=", return_against], ["Serial and Batch Entry", "docstatus", "=", 1], ["Serial and Batch Bundle", "is_cancelled", "=", 0], ["Serial and Batch Bundle", "item_code", "=", self.item_code], @@ -430,8 +448,8 @@ def get_sle_for_outward_transaction(self): return sle - def get_return_aginst(self, parent=None): - return_aginst = None + def get_return_against(self, parent=None): + return_against = None if parent and parent.get("is_return") and parent.get("return_against"): return parent.get("return_against") @@ -455,7 +473,7 @@ def get_return_aginst(self, parent=None): if voucher_details and voucher_details.get("is_return") and voucher_details.get("return_against"): return voucher_details.get("return_against") - return return_aginst + return return_against def set_incoming_rate_for_inward_transaction(self, row=None, save=False): valuation_field = "valuation_rate"