fix(invoice):validate return invoice qty (backport #46451) (#46481)

fix(invoice):validate return invoice qty (#46451)

* fix(invoice): validate return quantity when update stock is unchecked

* test: add unit test for validating fully returned invoice quantity

(cherry picked from commit ba96c86576)

Co-authored-by: Bhavansathru <122002510+Bhavan23@users.noreply.github.com>
This commit is contained in:
mergify[bot]
2025-03-12 17:23:26 +05:30
committed by GitHub
parent e61cc9b12e
commit d3a2350b3e
3 changed files with 41 additions and 10 deletions

View File

@@ -2669,6 +2669,20 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
)
self.assertEqual(actual, expected)
def test_prevents_fully_returned_invoice_with_zero_quantity(self):
from erpnext.controllers.sales_and_purchase_return import StockOverReturnError, make_return_doc
invoice = make_purchase_invoice(qty=10)
return_doc = make_return_doc(invoice.doctype, invoice.name)
return_doc.items[0].qty = -10
return_doc.save().submit()
return_doc = make_return_doc(invoice.doctype, invoice.name)
return_doc.items[0].qty = 0
self.assertRaises(StockOverReturnError, return_doc.save)
def set_advance_flag(company, flag, default_account):
frappe.db.set_value(

View File

@@ -4295,6 +4295,20 @@ class TestSalesInvoice(FrappeTestCase):
pos_return = make_sales_return(pos.name)
self.assertEqual(abs(pos_return.payments[0].amount), pos.payments[0].amount)
def test_prevents_fully_returned_invoice_with_zero_quantity(self):
from erpnext.controllers.sales_and_purchase_return import StockOverReturnError, make_return_doc
invoice = create_sales_invoice(qty=10)
return_doc = make_return_doc(invoice.doctype, invoice.name)
return_doc.items[0].qty = -10
return_doc.save().submit()
return_doc = make_return_doc(invoice.doctype, invoice.name)
return_doc.items[0].qty = 0
self.assertRaises(StockOverReturnError, return_doc.save)
def set_advance_flag(company, flag, default_account):
frappe.db.set_value(

View File

@@ -25,9 +25,6 @@ def validate_return(doc):
if doc.return_against:
validate_return_against(doc)
if doc.doctype in ("Sales Invoice", "Purchase Invoice") and not doc.update_stock:
return
validate_returned_items(doc)
@@ -118,7 +115,7 @@ def validate_returned_items(doc):
elif doc.doctype == "Delivery Note":
key = (d.item_code, d.get("dn_detail"))
if d.item_code and (flt(d.qty) < 0 or flt(d.get("received_qty")) < 0):
if d.item_code and (flt(d.qty) <= 0 or flt(d.get("received_qty")) <= 0):
if key not in valid_items:
frappe.msgprint(
_("Row # {0}: Returned Item {1} does not exist in {2} {3}").format(
@@ -160,6 +157,9 @@ def validate_returned_items(doc):
def validate_quantity(doc, key, args, ref, valid_items, already_returned_items):
fields = ["stock_qty"]
if (doc.doctype == "Purchase Invoice" or doc.doctype == "Sales Invoice") and not doc.update_stock:
fields = ["qty"]
if doc.doctype in ["Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"]:
if not args.get("return_qty_from_rejected_warehouse"):
fields.extend(["received_qty", "rejected_qty"])
@@ -169,13 +169,16 @@ def validate_quantity(doc, key, args, ref, valid_items, already_returned_items):
already_returned_data = already_returned_items.get(key) or {}
company_currency = erpnext.get_company_currency(doc.company)
stock_qty_precision = get_field_precision(
frappe.get_meta(doc.doctype + " Item").get_field("stock_qty"), company_currency
field_precision = get_field_precision(
frappe.get_meta(doc.doctype + " Item").get_field(
"stock_qty" if doc.get("update_stock", "") else "qty"
),
company_currency,
)
for column in fields:
returned_qty = (
flt(already_returned_data.get(column, 0), stock_qty_precision)
flt(already_returned_data.get(column, 0), field_precision)
if len(already_returned_data) > 0
else 0
)
@@ -190,17 +193,17 @@ def validate_quantity(doc, key, args, ref, valid_items, already_returned_items):
reference_qty = ref.get(column) * ref.get("conversion_factor", 1.0)
current_stock_qty = args.get(column) * args.get("conversion_factor", 1.0)
max_returnable_qty = flt(flt(reference_qty, stock_qty_precision) - returned_qty, stock_qty_precision)
max_returnable_qty = flt(flt(reference_qty, field_precision) - returned_qty, field_precision)
label = column.replace("_", " ").title()
if reference_qty:
if flt(args.get(column)) > 0:
frappe.throw(_("{0} must be negative in return document").format(label))
elif returned_qty >= reference_qty and args.get(column):
elif returned_qty >= reference_qty and args.get(column) >= 0:
frappe.throw(
_("Item {0} has already been returned").format(args.item_code), StockOverReturnError
)
elif abs(flt(current_stock_qty, stock_qty_precision)) > max_returnable_qty:
elif abs(flt(current_stock_qty, field_precision)) > max_returnable_qty:
frappe.throw(
_("Row # {0}: Cannot return more than {1} for Item {2}").format(
args.idx, max_returnable_qty, args.item_code