diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 192f9cc81d2..cf5dbccd54f 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -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( diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 99867e94fcb..f428eb50a3c 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -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( diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index a9007208a2a..74c5e34ecfa 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -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