test: Add, expand and refine test-cases for zero-quantity transactions.
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
index e43ea6e..8ff9f90 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
@@ -14,7 +14,7 @@
from erpnext.buying.doctype.purchase_order.purchase_order import get_mapped_purchase_invoice
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
-from erpnext.controllers.accounts_controller import get_payment_terms
+from erpnext.controllers.accounts_controller import InvalidQtyError, get_payment_terms
from erpnext.controllers.buying_controller import QtyMismatchError
from erpnext.exceptions import InvalidCurrency
from erpnext.projects.doctype.project.test_project import make_project
@@ -51,6 +51,16 @@
def tearDown(self):
frappe.db.rollback()
+ def test_purchase_invoice_qty(self):
+ pi = make_purchase_invoice(qty=0, do_not_save=True)
+ with self.assertRaises(InvalidQtyError):
+ pi.save()
+
+ # No error with qty=1
+ pi.items[0].qty = 1
+ pi.save()
+ self.assertEqual(pi.items[0].qty, 1)
+
def test_purchase_invoice_received_qty(self):
"""
1. Test if received qty is validated against accepted + rejected
@@ -2094,7 +2104,7 @@
bundle_id = None
if args.get("batch_no") or args.get("serial_no"):
batches = {}
- qty = args.qty or 5
+ qty = args.qty if args.qty is not None else 5
item_code = args.item or args.item_code or "_Test Item"
if args.get("batch_no"):
batches = frappe._dict({args.batch_no: qty})
@@ -2122,7 +2132,7 @@
{
"item_code": args.item or args.item_code or "_Test Item",
"warehouse": args.warehouse or "_Test Warehouse - _TC",
- "qty": args.qty or 5,
+ "qty": args.qty if args.qty is not None else 5,
"received_qty": args.received_qty or 0,
"rejected_qty": args.rejected_qty or 0,
"rate": args.rate or 50,
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index e9b71dd..01b5e28 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -23,7 +23,7 @@
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
get_depr_schedule,
)
-from erpnext.controllers.accounts_controller import update_invoice_status
+from erpnext.controllers.accounts_controller import InvalidQtyError, update_invoice_status
from erpnext.controllers.taxes_and_totals import get_itemised_tax_breakup_data
from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency
from erpnext.selling.doctype.customer.test_customer import get_customer_dict
@@ -72,6 +72,16 @@
def tearDownClass(self):
unlink_payment_on_cancel_of_invoice(0)
+ def test_sales_invoice_qty(self):
+ si = create_sales_invoice(qty=0, do_not_save=True)
+ with self.assertRaises(InvalidQtyError):
+ si.save()
+
+ # No error with qty=1
+ si.items[0].qty = 1
+ si.save()
+ self.assertEqual(si.items[0].qty, 1)
+
def test_timestamp_change(self):
w = frappe.copy_doc(test_records[0])
w.docstatus = 0
@@ -3629,7 +3639,7 @@
bundle_id = None
if si.update_stock and (args.get("batch_no") or args.get("serial_no")):
batches = {}
- qty = args.qty or 1
+ qty = args.qty if args.qty is not None else 1
item_code = args.item or args.item_code or "_Test Item"
if args.get("batch_no"):
batches = frappe._dict({args.batch_no: qty})
@@ -3661,7 +3671,7 @@
"description": args.description or "_Test Item",
"warehouse": args.warehouse or "_Test Warehouse - _TC",
"target_warehouse": args.target_warehouse,
- "qty": args.qty or 1,
+ "qty": args.qty if args.qty is not None else 1,
"uom": args.uom or "Nos",
"stock_uom": args.uom or "Nos",
"rate": args.rate if args.get("rate") is not None else 100,
diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
index f80a00a..9b382bb 100644
--- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
@@ -29,6 +29,8 @@
class TestPurchaseOrder(FrappeTestCase):
def test_purchase_order_qty(self):
po = create_purchase_order(qty=1, do_not_save=True)
+
+ # NonNegativeError with qty=-1
po.append(
"items",
{
@@ -39,9 +41,15 @@
)
self.assertRaises(frappe.NonNegativeError, po.save)
+ # InvalidQtyError with qty=0
po.items[1].qty = 0
self.assertRaises(InvalidQtyError, po.save)
+ # No error with qty=1
+ po.items[1].qty = 1
+ po.save()
+ self.assertEqual(po.items[1].qty, 1)
+
def test_make_purchase_receipt(self):
po = create_purchase_order(do_not_submit=True)
self.assertRaises(frappe.ValidationError, make_purchase_receipt, po.name)
@@ -1108,7 +1116,7 @@
"item_code": args.item or args.item_code or "_Test Item",
"warehouse": args.warehouse or "_Test Warehouse - _TC",
"from_warehouse": args.from_warehouse,
- "qty": args.qty or 10,
+ "qty": args.qty if args.qty is not None else 10,
"rate": args.rate or 500,
"schedule_date": add_days(nowdate(), 1),
"include_exploded_items": args.get("include_exploded_items", 1),
diff --git a/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py
index 42fa1d9..05a604f 100644
--- a/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py
+++ b/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py
@@ -14,6 +14,7 @@
get_pdf,
make_supplier_quotation_from_rfq,
)
+from erpnext.controllers.accounts_controller import InvalidQtyError
from erpnext.crm.doctype.opportunity.opportunity import make_request_for_quotation as make_rfq
from erpnext.crm.doctype.opportunity.test_opportunity import make_opportunity
from erpnext.stock.doctype.item.test_item import make_item
@@ -21,6 +22,16 @@
class TestRequestforQuotation(FrappeTestCase):
+ def test_rfq_qty(self):
+ rfq = make_request_for_quotation(qty=0, do_not_save=True)
+ with self.assertRaises(InvalidQtyError):
+ rfq.save()
+
+ # No error with qty=1
+ rfq.items[0].qty = 1
+ rfq.save()
+ self.assertEqual(rfq.items[0].qty, 1)
+
def test_quote_status(self):
rfq = make_request_for_quotation()
@@ -161,14 +172,17 @@
"description": "_Test Item",
"uom": args.uom or "_Test UOM",
"stock_uom": args.stock_uom or "_Test UOM",
- "qty": args.qty or 5,
+ "qty": args.qty if args.qty is not None else 5,
"conversion_factor": args.conversion_factor or 1.0,
"warehouse": args.warehouse or "_Test Warehouse - _TC",
"schedule_date": nowdate(),
},
)
- rfq.submit()
+ if not args.do_not_save:
+ rfq.insert()
+ if not args.do_not_submit:
+ rfq.submit()
return rfq
diff --git a/erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.py b/erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.py
index 13c851c..3346570 100644
--- a/erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.py
+++ b/erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.py
@@ -5,8 +5,21 @@
import frappe
from frappe.tests.utils import FrappeTestCase
+from erpnext.controllers.accounts_controller import InvalidQtyError
+
class TestPurchaseOrder(FrappeTestCase):
+ def test_supplier_quotation_qty(self):
+ sq = frappe.copy_doc(test_records[0])
+ sq.items[0].qty = 0
+ with self.assertRaises(InvalidQtyError):
+ sq.save()
+
+ # No error with qty=1
+ sq.items[0].qty = 1
+ sq.save()
+ self.assertEqual(sq.items[0].qty, 1)
+
def test_make_purchase_order(self):
from erpnext.buying.doctype.supplier_quotation.supplier_quotation import make_purchase_order
diff --git a/erpnext/buying/utils.py b/erpnext/buying/utils.py
index e904af0..8b7b694 100644
--- a/erpnext/buying/utils.py
+++ b/erpnext/buying/utils.py
@@ -42,12 +42,15 @@
def validate_for_items(doc) -> None:
+ from erpnext.controllers.accounts_controller import InvalidQtyError
+
items = []
for d in doc.get("items"):
if not d.qty:
if doc.doctype == "Purchase Receipt" and d.rejected_qty:
continue
- frappe.throw(_("Please enter quantity for Item {0}").format(d.item_code))
+ message = _("Please enter quantity for Item {0}").format(d.item_code)
+ frappe.throw(message, InvalidQtyError)
set_stock_levels(row=d) # update with latest quantities
item = validate_item_and_get_basic_data(row=d)
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index fdadb30..d6e3ee2 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -7,7 +7,7 @@
from frappe.utils import cint, flt, get_link_to_form, nowtime
from erpnext.accounts.party import render_address
-from erpnext.controllers.accounts_controller import get_taxes_and_charges
+from erpnext.controllers.accounts_controller import InvalidQtyError, get_taxes_and_charges
from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
from erpnext.controllers.stock_controller import StockController
from erpnext.stock.doctype.item.item import set_item_default
@@ -296,7 +296,8 @@
il = []
for d in self.get("items"):
if d.qty is None:
- frappe.throw(_("Row {0}: Qty is mandatory").format(d.idx))
+ message = _("Row {0}: Qty is mandatory").format(d.idx)
+ frappe.throw(message, InvalidQtyError)
if self.has_product_bundle(d.item_code):
for p in self.get("packed_items"):
diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py
index 051b475..3611bb4 100644
--- a/erpnext/manufacturing/doctype/bom/test_bom.py
+++ b/erpnext/manufacturing/doctype/bom/test_bom.py
@@ -28,6 +28,28 @@
class TestBOM(FrappeTestCase):
@timeout
+ def test_bom_qty(self):
+ from erpnext.stock.doctype.item.test_item import make_item
+
+ # No error.
+ bom = frappe.new_doc("BOM")
+ item = make_item(properties={"is_stock_item": 1})
+ bom.item = fg_item.item_code
+ bom.quantity = 1
+ bom.append(
+ "items",
+ {
+ "item_code": bom_item.item_code,
+ "qty": 0,
+ "uom": bom_item.stock_uom,
+ "stock_uom": bom_item.stock_uom,
+ "rate": 100.0,
+ },
+ )
+ bom.save()
+ self.assertEqual(bom.items[0].qty, 0)
+
+ @timeout
def test_get_items(self):
from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict
diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py
index 590cd3d..ecb7d09 100644
--- a/erpnext/selling/doctype/quotation/test_quotation.py
+++ b/erpnext/selling/doctype/quotation/test_quotation.py
@@ -5,10 +5,22 @@
from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, add_months, flt, getdate, nowdate
+from erpnext.controllers.accounts_controller import InvalidQtyError
+
test_dependencies = ["Product Bundle"]
class TestQuotation(FrappeTestCase):
+ def test_quotation_qty(self):
+ qo = make_quotation(qty=0, do_not_save=True)
+ with self.assertRaises(InvalidQtyError):
+ qo.save()
+
+ # No error with qty=1
+ qo.items[0].qty = 1
+ qo.save()
+ self.assertEqual(qo.items[0].qty, 1)
+
def test_make_quotation_without_terms(self):
quotation = make_quotation(do_not_save=1)
self.assertFalse(quotation.get("payment_schedule"))
@@ -629,7 +641,7 @@
{
"item_code": args.item or args.item_code or "_Test Item",
"warehouse": args.warehouse,
- "qty": args.qty or 10,
+ "qty": args.qty if args.qty is not None else 10,
"uom": args.uom or None,
"rate": args.rate or 100,
},
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index a518597..a6c86a6 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -9,7 +9,7 @@
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, flt, getdate, nowdate, today
-from erpnext.controllers.accounts_controller import update_child_qty_rate
+from erpnext.controllers.accounts_controller import InvalidQtyError, update_child_qty_rate
from erpnext.maintenance.doctype.maintenance_schedule.test_maintenance_schedule import (
make_maintenance_schedule,
)
@@ -80,6 +80,29 @@
)
update_child_qty_rate("Sales Order", trans_item, so.name)
+ def test_sales_order_qty(self):
+ so = make_sales_order(qty=1, do_not_save=True)
+
+ # NonNegativeError with qty=-1
+ so.append(
+ "items",
+ {
+ "item_code": "_Test Item",
+ "qty": -1,
+ "rate": 10,
+ },
+ )
+ self.assertRaises(frappe.NonNegativeError, so.save)
+
+ # InvalidQtyError with qty=0
+ so.items[1].qty = 0
+ self.assertRaises(InvalidQtyError, so.save)
+
+ # No error with qty=1
+ so.items[1].qty = 1
+ so.save()
+ self.assertEqual(so.items[0].qty, 1)
+
def test_make_material_request(self):
so = make_sales_order(do_not_submit=True)
@@ -2015,7 +2038,7 @@
{
"item_code": args.item or args.item_code or "_Test Item",
"warehouse": args.warehouse,
- "qty": args.qty or 10,
+ "qty": args.qty if args.qty is not None else 10,
"uom": args.uom or None,
"price_list_rate": args.price_list_rate or None,
"discount_percentage": args.discount_percentage or None,
diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
index 9465574..376b970 100644
--- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
@@ -10,6 +10,7 @@
from erpnext.accounts.doctype.account.test_account import get_inventory_account
from erpnext.accounts.utils import get_balance_on
+from erpnext.controllers.accounts_controller import InvalidQtyError
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
from erpnext.selling.doctype.sales_order.test_sales_order import (
automatically_fetch_payment_terms,
@@ -42,6 +43,16 @@
class TestDeliveryNote(FrappeTestCase):
+ def test_delivery_note_qty(self):
+ dn = create_delivery_note(qty=0, do_not_save=True)
+ with self.assertRaises(InvalidQtyError):
+ dn.save()
+
+ # No error with qty=1
+ dn.items[0].qty = 1
+ dn.save()
+ self.assertEqual(dn.items[0].qty, 1)
+
def test_over_billing_against_dn(self):
frappe.db.set_single_value("Stock Settings", "allow_negative_stock", 1)
@@ -1287,7 +1298,7 @@
if dn.is_return:
type_of_transaction = "Inward"
- qty = args.get("qty") or 1
+ qty = args.qty if args.get("qty") is not None else 1
qty *= -1 if type_of_transaction == "Outward" else 1
batches = {}
if args.get("batch_no"):
@@ -1315,7 +1326,7 @@
{
"item_code": args.item or args.item_code or "_Test Item",
"warehouse": args.warehouse or "_Test Warehouse - _TC",
- "qty": args.qty or 1,
+ "qty": args.qty if args.get("qty") is not None else 1,
"rate": args.rate if args.get("rate") is not None else 100,
"conversion_factor": 1.0,
"serial_and_batch_bundle": bundle_id,
diff --git a/erpnext/stock/doctype/material_request/test_material_request.py b/erpnext/stock/doctype/material_request/test_material_request.py
index e5aff38..3e44049 100644
--- a/erpnext/stock/doctype/material_request/test_material_request.py
+++ b/erpnext/stock/doctype/material_request/test_material_request.py
@@ -9,6 +9,7 @@
from frappe.tests.utils import FrappeTestCase
from frappe.utils import flt, today
+from erpnext.controllers.accounts_controller import InvalidQtyError
from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.material_request.material_request import (
make_in_transit_stock_entry,
@@ -20,6 +21,17 @@
class TestMaterialRequest(FrappeTestCase):
+ def test_material_request_qty(self):
+ mr = frappe.copy_doc(test_records[0])
+ mr.items[0].qty = 0
+ with self.assertRaises(InvalidQtyError):
+ mr.insert()
+
+ # No error with qty=1
+ mr.items[0].qty = 1
+ mr.save()
+ self.assertEqual(mr.items[0].qty, 1)
+
def test_make_purchase_order(self):
mr = frappe.copy_doc(test_records[0]).insert()
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index 146cbff..57ba5bb 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -8,6 +8,7 @@
import erpnext
from erpnext.accounts.doctype.account.test_account import get_inventory_account
+from erpnext.controllers.accounts_controller import InvalidQtyError
from erpnext.controllers.buying_controller import QtyMismatchError
from erpnext.stock.doctype.item.test_item import create_item, make_item
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice
@@ -29,6 +30,23 @@
def setUp(self):
frappe.db.set_single_value("Buying Settings", "allow_multiple_items", 1)
+ def test_purchase_receipt_qty(self):
+ pr = make_purchase_receipt(qty=0, rejected_qty=0, do_not_save=True)
+ with self.assertRaises(InvalidQtyError):
+ pr.save()
+
+ # No error with qty=1
+ pr.items[0].qty = 1
+ pr.save()
+ self.assertEqual(pr.items[0].qty, 1)
+
+ # No error with rejected_qty=1
+ pr.items[0].rejected_warehouse = "_Test Rejected Warehouse - _TC"
+ pr.items[0].rejected_qty = 1
+ pr.items[0].qty = 0
+ pr.save()
+ self.assertEqual(pr.items[0].rejected_qty, 1)
+
def test_purchase_receipt_received_qty(self):
"""
1. Test if received qty is validated against accepted + rejected
@@ -2348,7 +2366,8 @@
pr.is_return = args.is_return
pr.return_against = args.return_against
pr.apply_putaway_rule = args.apply_putaway_rule
- qty = args.qty or 5
+
+ qty = args.qty if args.qty is not None else 5
rejected_qty = args.rejected_qty or 0
received_qty = args.received_qty or flt(rejected_qty) + flt(qty)
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index 3baafd7..37a80be 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -24,6 +24,7 @@
import erpnext
from erpnext.accounts.general_ledger import process_gl_map
+from erpnext.controllers.accounts_controller import InvalidQtyError
from erpnext.controllers.taxes_and_totals import init_landed_taxes_and_totals
from erpnext.manufacturing.doctype.bom.bom import add_additional_cost, validate_bom_no
from erpnext.setup.doctype.brand.brand import get_brand_defaults
@@ -390,7 +391,8 @@
def set_transfer_qty(self):
for item in self.get("items"):
if not flt(item.qty):
- frappe.throw(_("Row {0}: Qty is mandatory").format(item.idx), title=_("Zero quantity"))
+ message = _("Row {0}: Qty is mandatory").format(item.idx)
+ frappe.throw(message, InvalidQtyError, title=_("Zero quantity"))
if not flt(item.conversion_factor):
frappe.throw(_("Row {0}: UOM Conversion Factor is mandatory").format(item.idx))
item.transfer_qty = flt(
diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
index eb1c7a8..5ebf7c9 100644
--- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
@@ -8,6 +8,7 @@
from frappe.utils import add_days, add_to_date, flt, nowdate, nowtime, today
from erpnext.accounts.doctype.account.test_account import get_inventory_account
+from erpnext.controllers.accounts_controller import InvalidQtyError
from erpnext.stock.doctype.item.test_item import (
create_item,
make_item,
@@ -54,6 +55,18 @@
frappe.db.rollback()
frappe.set_user("Administrator")
+ def test_stock_entry_qty(self):
+ item_code = "_Test Item 2"
+ warehouse = "_Test Warehouse - _TC"
+ se = make_stock_entry(item_code=item_code, target=warehouse, qty=0, do_not_save=True)
+ with self.assertRaises(InvalidQtyError):
+ se.save()
+
+ # No error with qty=1
+ se.items[0].qty = 1
+ se.save()
+ self.assertEqual(se.items[0].qty, 1)
+
def test_fifo(self):
frappe.db.set_single_value("Stock Settings", "allow_negative_stock", 1)
item_code = "_Test Item 2"