Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: validate returned serial nos and batches #44669

Merged
merged 1 commit into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
Original file line number Diff line number Diff line change
Expand Up @@ -4033,6 +4033,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -252,18 +252,21 @@ 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
)
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:
Expand All @@ -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),
Expand All @@ -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],
Expand Down Expand Up @@ -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")
Expand All @@ -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"
Expand Down
Loading