diff --git a/erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.py b/erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.py index ffbec9bd73e..7c2714ba671 100644 --- a/erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.py +++ b/erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.py @@ -34,7 +34,7 @@ class TestPurchaseOrder(IntegrationTestCase): self.assertEqual(sq.get("items")[1].rate, 300) self.assertEqual(sq.get("items")[1].description, "test") - def test_update_supplier_quotation_child_rate_disallow(self): + def test_update_supplier_quotation_child_rate(self): sq = frappe.copy_doc(self.globalTestRecords["Supplier Quotation"][0]) sq.submit() trans_item = json.dumps( @@ -47,6 +47,22 @@ class TestPurchaseOrder(IntegrationTestCase): }, ] ) + update_child_qty_rate("Supplier Quotation", trans_item, sq.name) + sq.reload() + self.assertEqual(sq.get("items")[0].rate, 300) + po = make_purchase_order(sq.name) + po.schedule_date = add_days(today(), 1) + po.submit() + trans_item = json.dumps( + [ + { + "item_code": sq.items[0].item_code, + "rate": 20, + "qty": sq.items[0].qty, + "docname": sq.items[0].name, + }, + ] + ) self.assertRaises( frappe.ValidationError, update_child_qty_rate, "Supplier Quotation", trans_item, sq.name ) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 6a75859cdb0..b0b5c4e25bb 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -3872,20 +3872,28 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil return frappe.db.get_single_value("Buying Settings", "allow_zero_qty_in_purchase_order") or False return False - def validate_quantity(child_item, new_data): + def validate_quantity_and_rate(child_item, new_data): if not flt(new_data.get("qty")) and not is_allowed_zero_qty(): frappe.throw( - _("Row #{0}: Quantity for Item {1} cannot be zero.").format( + _("Row #{0}:Quantity for Item {1} cannot be zero.").format( new_data.get("idx"), frappe.bold(new_data.get("item_code")) ), title=_("Invalid Qty"), ) - if parent_doctype == "Sales Order" and flt(new_data.get("qty")) < flt(child_item.delivered_qty): - frappe.throw(_("Cannot set quantity less than delivered quantity")) + qty_limits = { + "Sales Order": ("delivered_qty", _("Cannot set quantity less than delivered quantity")), + "Purchase Order": ("received_qty", _("Cannot set quantity less than received quantity")), + } - if parent_doctype == "Purchase Order" and flt(new_data.get("qty")) < flt(child_item.received_qty): - frappe.throw(_("Cannot set quantity less than received quantity")) + if parent_doctype in qty_limits: + qty_field, error_message = qty_limits[parent_doctype] + if flt(new_data.get("qty")) < flt(child_item.get(qty_field)): + frappe.throw( + _("Row #{0}:").format(new_data.get("idx")) + + error_message.format(frappe.bold(new_data.get("item_code"))), + title=_("Invalid Qty"), + ) if parent_doctype in ["Quotation", "Supplier Quotation"]: if (parent_doctype == "Quotation" and not ordered_items) or ( @@ -3898,7 +3906,15 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil if parent_doctype == "Quotation" else purchased_items.get(child_item.name) ) + if qty_to_check: + if not rate_unchanged: + frappe.throw( + _( + "Cannot update rate as item {0} is already ordered or purchased against this quotation" + ).format(frappe.bold(new_data.get("item_code"))) + ) + if flt(new_data.get("qty")) < qty_to_check: frappe.throw(_("Cannot reduce quantity than ordered or purchased quantity")) @@ -4017,10 +4033,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil ): continue - validate_quantity(child_item, d) - if parent_doctype in ["Quotation", "Supplier Quotation"]: - if not rate_unchanged: - frappe.throw(_("Rates cannot be modified for quoted items")) + validate_quantity_and_rate(child_item, d) if flt(child_item.get("qty")) != flt(d.get("qty")): any_qty_changed = True diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index 49f921d9865..ad8f2881873 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -59,8 +59,22 @@ class TestQuotation(IntegrationTestCase): qo.payment_schedule[0].due_date = add_days(qo.transaction_date, -2) self.assertRaises(frappe.ValidationError, qo.save) - def test_update_child_disallow_rate_change(self): - qo = make_quotation(qty=4) + def test_update_child_rate_change(self): + from erpnext.stock.doctype.item.test_item import make_item + + item_1 = make_item("_Test Item") + item_2 = make_item("_Test Item 1") + + item_list = [ + {"item_code": item_1.item_code, "warehouse": "_Test Warehouse - _TC", "qty": 10, "rate": 300}, + {"item_code": item_2.item_code, "warehouse": "_Test Warehouse - _TC", "qty": 5, "rate": 400}, + ] + + qo = make_quotation(item_list=item_list) + so = make_sales_order(qo.name, args={"filtered_children": [qo.items[0].name]}) + so.delivery_date = nowdate() + so.submit() + qo.reload() trans_item = json.dumps( [ { @@ -68,10 +82,35 @@ class TestQuotation(IntegrationTestCase): "rate": 5000, "qty": qo.items[0].qty, "docname": qo.items[0].name, - } + }, + { + "item_code": qo.items[1].item_code, + "rate": qo.items[1].rate, + "qty": qo.items[1].qty, + "docname": qo.items[1].name, + }, ] ) self.assertRaises(frappe.ValidationError, update_child_qty_rate, "Quotation", trans_item, qo.name) + trans_item = json.dumps( + [ + { + "item_code": qo.items[0].item_code, + "rate": qo.items[0].rate, + "qty": qo.items[0].qty, + "docname": qo.items[0].name, + }, + { + "item_code": qo.items[1].item_code, + "rate": 50, + "qty": qo.items[1].qty, + "docname": qo.items[1].name, + }, + ] + ) + update_child_qty_rate("Quotation", trans_item, qo.name) + qo.reload() + self.assertEqual(qo.items[1].rate, 50) def test_update_child_removing_item(self): qo = make_quotation(qty=10)