From b2d8a4419903830220b3aa7228de641e59606e67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Oliver=20S=C3=BCnderhauf?= <46800703+bosue@users.noreply.github.com> Date: Sun, 3 Dec 2023 15:10:00 +0100 Subject: [PATCH] test: Add, expand and refine test-cases for zero-quantity transactions. --- .../purchase_invoice/test_purchase_invoice.py | 16 ++++++++--- .../sales_invoice/test_sales_invoice.py | 16 ++++++++--- .../purchase_order/test_purchase_order.py | 10 ++++++- .../test_request_for_quotation.py | 18 +++++++++++-- .../test_supplier_quotation.py | 13 +++++++++ erpnext/buying/utils.py | 5 +++- erpnext/controllers/selling_controller.py | 5 ++-- erpnext/manufacturing/doctype/bom/test_bom.py | 22 +++++++++++++++ .../doctype/quotation/test_quotation.py | 14 +++++++++- .../doctype/sales_order/test_sales_order.py | 27 +++++++++++++++++-- .../delivery_note/test_delivery_note.py | 15 +++++++++-- .../material_request/test_material_request.py | 12 +++++++++ .../purchase_receipt/test_purchase_receipt.py | 21 ++++++++++++++- .../stock/doctype/stock_entry/stock_entry.py | 4 ++- .../doctype/stock_entry/test_stock_entry.py | 13 +++++++++ 15 files changed, 192 insertions(+), 19 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index e43ea6ecbe..8ff9f9002c 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.accounts.doctype.payment_entry.payment_entry import get_payment_ent 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 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): 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 @@ def make_purchase_invoice(**args): 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 @@ def make_purchase_invoice(**args): { "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 e9b71ddffd..01b5e28ea4 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.test_asset import create_asset, create_asset_d 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 @@ class TestSalesInvoice(FrappeTestCase): 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 @@ def create_sales_invoice(**args): 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 @@ def create_sales_invoice(**args): "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 f80a00a95f..9b382bbd7e 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 @@ from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( 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 @@ class TestPurchaseOrder(FrappeTestCase): ) 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 @@ def create_purchase_order(**args): "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 42fa1d923e..05a604f0cc 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 @@ from erpnext.buying.doctype.request_for_quotation.request_for_quotation import ( 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 @@ from erpnext.templates.pages.rfq import check_supplier_has_docname_access 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 @@ def make_request_for_quotation(**args) -> "RequestforQuotation": "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 13c851c735..33465700f4 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 e904af0dce..8b7b6940ca 100644 --- a/erpnext/buying/utils.py +++ b/erpnext/buying/utils.py @@ -42,12 +42,15 @@ def update_last_purchase_rate(doc, is_submit) -> None: 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 fdadb30e93..d6e3ee25a9 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -7,7 +7,7 @@ from frappe import _, bold, throw 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 @@ class SellingController(StockController): 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 051b475bcc..3611bb469d 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -27,6 +27,28 @@ test_dependencies = ["Item", "Quality Inspection Template"] 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 590cd3d0cf..ecb7d097b8 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -5,10 +5,22 @@ import frappe 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 @@ def make_quotation(**args): { "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 a518597aa6..a6c86a670d 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.core.doctype.user_permission.test_user_permission import create_user 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 @@ class TestSalesOrder(FrappeTestCase): ) 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 @@ def make_sales_order(**args): { "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 94655747e4..376b970222 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 frappe.utils import add_days, cstr, flt, nowdate, nowtime, today 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 @@ from erpnext.stock.stock_ledger import get_previous_sle 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 @@ def create_delivery_note(**args): 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 @@ def create_delivery_note(**args): { "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 e5aff38c52..3e440497f0 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 @@ import frappe 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 @@ from erpnext.stock.doctype.material_request.material_request import ( 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 146cbff1aa..57ba5bb0a5 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 @@ from pypika import functions as fn 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 @@ class TestPurchaseReceipt(FrappeTestCase): 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 @@ def make_purchase_receipt(**args): 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 3baafd77ba..37a80be0e3 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -24,6 +24,7 @@ from frappe.utils import ( 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 @@ class StockEntry(StockController): 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 eb1c7a85eb..5ebf7c92da 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.tests.utils import FrappeTestCase, change_settings 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 @@ class TestStockEntry(FrappeTestCase): 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"