From eba73df88e28fe5239b215503b80a56533d31e18 Mon Sep 17 00:00:00 2001 From: marination <25857446+marination@users.noreply.github.com> Date: Fri, 14 Mar 2025 18:00:40 +0100 Subject: [PATCH] test: Purchase Order with Unit Price Items - chore: Fix error message in accounts controller (cherry picked from commit eea758f5b2c986b0efccdb6f387475b9800ff4e0) # Conflicts: # erpnext/buying/doctype/purchase_order/test_purchase_order.py # erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.py --- .../doctype/purchase_order/purchase_order.py | 8 +- .../purchase_order/test_purchase_order.py | 95 +++++++++++++++++++ .../test_request_for_quotation.py | 5 +- .../test_supplier_quotation.py | 5 + erpnext/controllers/accounts_controller.py | 6 +- 5 files changed, 112 insertions(+), 7 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 9ddbbb991d6..d883a41a197 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -723,8 +723,10 @@ def set_missing_values(source, target): @frappe.whitelist() def make_purchase_receipt(source_name, target_doc=None): + has_unit_price_items = frappe.db.get_value("Purchase Order", source_name, "has_unit_price_items") + def update_item(obj, target, source_parent): - target.qty = flt(obj.qty) - flt(obj.received_qty) + target.qty = flt(obj.qty) - flt(obj.received_qty) if not has_unit_price_items else 0 target.stock_qty = (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.conversion_factor) target.amount = (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate) target.base_amount = ( @@ -755,7 +757,9 @@ def make_purchase_receipt(source_name, target_doc=None): "wip_composite_asset": "wip_composite_asset", }, "postprocess": update_item, - "condition": lambda doc: abs(doc.received_qty) < abs(doc.qty) + "condition": lambda doc: ( + abs(doc.received_qty) < abs(doc.qty) if not has_unit_price_items else True + ) and doc.delivered_by_supplier != 1, }, "Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "reset_value": True}, diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 404c83cdb50..59143ed564e 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -5,7 +5,11 @@ import json import frappe +<<<<<<< HEAD from frappe.tests.utils import FrappeTestCase, change_settings +======= +from frappe.tests import IntegrationTestCase, UnitTestCase, change_settings +>>>>>>> eea758f5b2 (test: Purchase Order with Unit Price Items) from frappe.utils import add_days, flt, getdate, nowdate from frappe.utils.data import today @@ -44,6 +48,21 @@ class TestPurchaseOrder(FrappeTestCase): po.items[1].qty = 0 self.assertRaises(InvalidQtyError, po.save) +<<<<<<< HEAD +======= + # No error with qty=1 + po.items[1].qty = 1 + po.save() + self.assertEqual(po.items[1].qty, 1) + + def test_purchase_order_zero_qty(self): + po = create_purchase_order(qty=0, do_not_save=True) + + with change_settings("Buying Settings", {"allow_zero_qty_in_purchase_order": 1}): + po.save() + self.assertEqual(po.items[0].qty, 0) + +>>>>>>> eea758f5b2 (test: Purchase Order with Unit Price Items) def test_make_purchase_receipt(self): po = create_purchase_order(do_not_submit=True) self.assertRaises(frappe.ValidationError, make_purchase_receipt, po.name) @@ -1199,6 +1218,82 @@ class TestPurchaseOrder(FrappeTestCase): po.reload() self.assertEqual(po.per_billed, 100) + @IntegrationTestCase.change_settings("Buying Settings", {"allow_zero_qty_in_purchase_order": 1}) + def test_receive_zero_qty_purchase_order(self): + """ + Test the flow of a Unit Price PO and PR creation against it until completion. + Flow: + PO Qty 0 -> Receive +5 -> Receive +5 -> Update PO Qty +10 -> PO is 100% received + """ + po = create_purchase_order(qty=0) + pr = make_purchase_receipt(po.name) + + self.assertEqual(pr.items[0].qty, 0) + pr.items[0].qty = 5 + pr.submit() + + po.reload() + self.assertEqual(po.items[0].received_qty, 5) + # PO still has qty 0, so billed % should be unset + self.assertFalse(po.per_received) + self.assertEqual(po.status, "To Receive and Bill") + + # Test: PR can be made against PO as long PO qty is 0 OR PO qty > received qty + pr2 = make_purchase_receipt(po.name) + self.assertEqual(pr2.items[0].qty, 0) + pr2.items[0].qty = 5 + pr2.submit() + + po.reload() + self.assertEqual(po.items[0].received_qty, 10) + self.assertFalse(po.per_received) + self.assertEqual(po.status, "To Receive and Bill") + + # Update PO Item Qty to 10 after receipt of items + first_item_of_po = po.items[0] + trans_item = json.dumps( + [ + { + "item_code": first_item_of_po.item_code, + "rate": first_item_of_po.rate, + "qty": 10, + "docname": first_item_of_po.name, + } + ] + ) + update_child_qty_rate("Purchase Order", trans_item, po.name) + + # PO should be updated to 100% received + po.reload() + self.assertEqual(po.items[0].qty, 10) + self.assertEqual(po.per_received, 100.0) + self.assertEqual(po.status, "To Bill") + + @IntegrationTestCase.change_settings("Buying Settings", {"allow_zero_qty_in_purchase_order": 1}) + def test_bill_zero_qty_purchase_order(self): + po = create_purchase_order(qty=0) + + self.assertEqual(po.grand_total, 0) + self.assertFalse(po.per_billed) + self.assertEqual(po.items[0].qty, 0) + self.assertEqual(po.items[0].rate, 500) + + pi = make_pi_from_po(po.name) + self.assertEqual(pi.items[0].qty, 0) + self.assertEqual(pi.items[0].rate, 500) + + pi.items[0].qty = 5 + pi.submit() + + self.assertEqual(pi.grand_total, 2500) + + po.reload() + self.assertEqual(po.items[0].amount, 0) + self.assertEqual(po.items[0].billed_amt, 2500) + # PO still has qty 0, so billed % should be unset + self.assertFalse(po.per_billed) + self.assertEqual(po.status, "To Receive and Bill") + def create_po_for_sc_testing(): from erpnext.controllers.tests.test_subcontracting_controller import ( 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 6e21f4596f6..cd6d61ab926 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 @@ -55,6 +55,7 @@ class TestRequestforQuotation(IntegrationTestCase): with change_settings("Buying Settings", {"allow_zero_qty_in_request_for_quotation": 1}): rfq.save() + self.assertEqual(rfq.items[0].qty, 0) >>>>>>> 8f96c0b546 (test: Zero Qty in RFQ and Supplier Quotation) def test_quote_status(self): @@ -197,8 +198,8 @@ class TestRequestforQuotation(IntegrationTestCase): supplier_doc.reload() self.assertTrue(supplier_doc.portal_users[0].user) - @change_settings("Buying Settings", {"allow_zero_qty_in_request_for_quotation": 1}) - def test_map_supplier_quotation_from_zero_qty_rfq(self): + @IntegrationTestCase.change_settings("Buying Settings", {"allow_zero_qty_in_request_for_quotation": 1}) + def test_supplier_quotation_from_zero_qty_rfq(self): rfq = make_request_for_quotation(qty=0) sq = make_supplier_quotation_from_rfq(rfq.name, for_supplier=rfq.get("suppliers")[0].supplier) diff --git a/erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.py b/erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.py index 31bee01e775..87014c135f5 100644 --- a/erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.py +++ b/erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.py @@ -50,6 +50,7 @@ class TestPurchaseOrder(IntegrationTestCase): with change_settings("Buying Settings", {"allow_zero_qty_in_supplier_quotation": 1}): sq.save() + self.assertEqual(sq.items[0].qty, 0) def test_make_purchase_order(self): sq = frappe.copy_doc(self.globalTestRecords["Supplier Quotation"][0]).insert() @@ -73,10 +74,14 @@ class TestPurchaseOrder(IntegrationTestCase): po.insert() <<<<<<< HEAD +<<<<<<< HEAD test_records = frappe.get_test_records("Supplier Quotation") ======= @change_settings("Buying Settings", {"allow_zero_qty_in_supplier_quotation": 1}) +======= + @IntegrationTestCase.change_settings("Buying Settings", {"allow_zero_qty_in_supplier_quotation": 1}) +>>>>>>> eea758f5b2 (test: Purchase Order with Unit Price Items) def test_map_purchase_order_from_zero_qty_supplier_quotation(self): sq = frappe.copy_doc(self.globalTestRecords["Supplier Quotation"][0]) sq.items[0].qty = 0 diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index a3180388141..28d55973d74 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -3746,9 +3746,9 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil ) if amount_below_billed_amt and row_rate > 0.0: frappe.throw( - _("Row #{0}: Cannot set Rate if amount is greater than billed amount for Item {1}.").format( - child_item.idx, child_item.item_code - ) + _( + "Row #{0}: Cannot set Rate if the billed amount is greater than the amount for Item {1}." + ).format(child_item.idx, child_item.item_code) ) else: child_item.rate = row_rate