diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json index fb7104d3891..6d90268e574 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.json +++ b/erpnext/buying/doctype/buying_settings/buying_settings.json @@ -25,6 +25,9 @@ "disable_last_purchase_rate", "show_pay_button", "use_transaction_date_exchange_rate", + "allow_zero_qty_in_request_for_quotation", + "allow_zero_qty_in_supplier_quotation", + "allow_zero_qty_in_purchase_order", "subcontract", "backflush_raw_materials_of_subcontract_based_on", "column_break_11", @@ -207,14 +210,33 @@ "fieldtype": "Select", "label": "Update frequency of Project", "options": "Each Transaction\nManual" + }, + { + "default": "0", + "fieldname": "allow_zero_qty_in_purchase_order", + "fieldtype": "Check", + "label": "Allow 0 Qty in Purchase Order (Unit Price Items)" + }, + { + "default": "0", + "fieldname": "allow_zero_qty_in_request_for_quotation", + "fieldtype": "Check", + "label": "Allow 0 Qty in Request for Quotation (Unit Price Items)" + }, + { + "default": "0", + "fieldname": "allow_zero_qty_in_supplier_quotation", + "fieldtype": "Check", + "label": "Allow 0 Qty in Supplier Quotation (Unit Price Items)" } ], + "grid_page_length": 50, "icon": "fa fa-cog", "idx": 1, "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2024-03-27 13:06:43.375495", + "modified": "2025-03-03 17:32:25.939482", "modified_by": "Administrator", "module": "Buying", "name": "Buying Settings", @@ -260,6 +282,7 @@ "role": "Purchase User" } ], + "row_format": "Dynamic", "sort_field": "creation", "sort_order": "DESC", "states": [], diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.py b/erpnext/buying/doctype/buying_settings/buying_settings.py index ec9b88888b7..4dde7c8dabf 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.py +++ b/erpnext/buying/doctype/buying_settings/buying_settings.py @@ -18,6 +18,9 @@ class BuyingSettings(Document): from frappe.types import DF allow_multiple_items: DF.Check + allow_zero_qty_in_purchase_order: DF.Check + allow_zero_qty_in_request_for_quotation: DF.Check + allow_zero_qty_in_supplier_quotation: DF.Check auto_create_purchase_receipt: DF.Check auto_create_subcontracting_order: DF.Check backflush_raw_materials_of_subcontract_based_on: DF.Literal[ diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index 2ee6cf7f00c..ea383bfc190 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -26,7 +26,15 @@ frappe.ui.form.on("Purchase Order", { } frm.set_indicator_formatter("item_code", function (doc) { - return doc.qty <= doc.received_qty ? "green" : "orange"; + let color; + if (!doc.qty && frm.doc.has_unit_price_items) { + color = "yellow"; + } else if (doc.qty <= doc.received_qty) { + color = "green"; + } else { + color = "orange"; + } + return color; }); frm.set_query("expense_account", "items", function () { @@ -63,6 +71,10 @@ frappe.ui.form.on("Purchase Order", { } }); } + + if (frm.doc.docstatus == 0) { + erpnext.set_unit_price_items_note(frm); + } }, supplier: function (frm) { diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index cac6854b89e..30cf3118a45 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -24,6 +24,7 @@ "apply_tds", "tax_withholding_category", "is_subcontracted", + "has_unit_price_items", "supplier_warehouse", "amended_from", "accounting_dimensions_section", @@ -1282,13 +1283,22 @@ "oldfieldtype": "Select", "options": "Not Initiated\nInitiated\nPartially Paid\nFully Paid", "print_hide": 1 + }, + { + "default": "0", + "fieldname": "has_unit_price_items", + "fieldtype": "Check", + "hidden": 1, + "label": "Has Unit Price Items", + "no_copy": 1 } ], + "grid_page_length": 50, "icon": "fa fa-file-text", "idx": 105, "is_submittable": 1, "links": [], - "modified": "2024-03-27 13:10:24.518785", + "modified": "2025-03-03 16:48:08.697520", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", @@ -1335,6 +1345,7 @@ "write": 1 } ], + "row_format": "Dynamic", "search_fields": "status, transaction_date, supplier, grand_total", "show_name_in_global_search": 1, "sort_field": "creation", diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 5ee650ead60..a6b2d15d4a3 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -95,6 +95,7 @@ class PurchaseOrder(BuyingController): from_date: DF.Date | None grand_total: DF.Currency group_same_items: DF.Check + has_unit_price_items: DF.Check ignore_pricing_rule: DF.Check in_words: DF.Data | None incoterm: DF.Link | None @@ -189,6 +190,10 @@ class PurchaseOrder(BuyingController): self.set_onload("supplier_tds", supplier_tds) self.set_onload("can_update_items", self.can_update_items()) + def before_validate(self): + self.set_has_unit_price_items() + self.flags.allow_zero_qty = self.has_unit_price_items + def validate(self): super().validate() @@ -225,6 +230,17 @@ class PurchaseOrder(BuyingController): ) self.reset_default_field_value("set_warehouse", "items", "warehouse") + def set_has_unit_price_items(self): + """ + If permitted in settings and any item has 0 qty, the PO has unit price items. + """ + if not frappe.db.get_single_value("Buying Settings", "allow_zero_qty_in_purchase_order"): + return + + self.has_unit_price_items = any( + not row.qty for row in self.get("items") if (row.item_code and not row.qty) + ) + def validate_with_previous_doc(self): mri_compare_fields = [["project", "="], ["item_code", "="]] if self.is_subcontracted: diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js index 75be5574353..784dda1a72b 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js @@ -28,6 +28,10 @@ frappe.ui.form.on("Request for Quotation", { is_group: 0, }, })); + + frm.set_indicator_formatter("item_code", function (doc) { + return !doc.qty && frm.doc.has_unit_price_items ? "yellow" : ""; + }); }, onload: function (frm) { @@ -155,6 +159,10 @@ frappe.ui.form.on("Request for Quotation", { frm.page.set_inner_btn_group_as_primary(__("Create")); } + + if (frm.doc.docstatus === 0) { + erpnext.set_unit_price_items_note(frm); + } }, make_supplier_quotation: function (frm) { diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json index 424bccd80f1..fee11e627b4 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json @@ -16,6 +16,7 @@ "transaction_date", "schedule_date", "status", + "has_unit_price_items", "amended_from", "suppliers_section", "suppliers", @@ -306,13 +307,22 @@ "fieldtype": "Text Editor", "label": "Billing Address Details", "read_only": 1 + }, + { + "default": "0", + "fieldname": "has_unit_price_items", + "fieldtype": "Check", + "hidden": 1, + "label": "Has Unit Price Items", + "no_copy": 1 } ], + "grid_page_length": 50, "icon": "fa fa-shopping-cart", "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2024-03-27 13:10:33.030915", + "modified": "2025-03-03 16:48:39.856779", "modified_by": "Administrator", "module": "Buying", "name": "Request for Quotation", @@ -377,6 +387,7 @@ "role": "All" } ], + "row_format": "Dynamic", "search_fields": "status, transaction_date", "show_name_in_global_search": 1, "sort_field": "creation", diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py index ee6c7a89cae..6cbab414773 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -42,6 +42,7 @@ class RequestforQuotation(BuyingController): billing_address_display: DF.TextEditor | None company: DF.Link email_template: DF.Link | None + has_unit_price_items: DF.Check incoterm: DF.Link | None items: DF.Table[RequestforQuotationItem] letter_head: DF.Link | None @@ -61,6 +62,10 @@ class RequestforQuotation(BuyingController): vendor: DF.Link | None # end: auto-generated types + def before_validate(self): + self.set_has_unit_price_items() + self.flags.allow_zero_qty = self.has_unit_price_items + def validate(self): self.validate_duplicate_supplier() self.validate_supplier_list() @@ -73,6 +78,17 @@ class RequestforQuotation(BuyingController): # after amend and save, status still shows as cancelled, until submit self.db_set("status", "Draft") + def set_has_unit_price_items(self): + """ + If permitted in settings and any item has 0 qty, the RFQ has unit price items. + """ + if not frappe.db.get_single_value("Buying Settings", "allow_zero_qty_in_request_for_quotation"): + return + + self.has_unit_price_items = any( + not row.qty for row in self.get("items") if (row.item_code and not row.qty) + ) + def validate_duplicate_supplier(self): supplier_list = [d.supplier for d in self.suppliers] if len(supplier_list) != len(set(supplier_list)): diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js index de37ec20bdc..26f439ba03c 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js @@ -11,6 +11,11 @@ erpnext.buying.SupplierQuotationController = class SupplierQuotationController e Quotation: "Quotation", }; + const me = this; + this.frm.set_indicator_formatter("item_code", function (doc) { + return !doc.qty && me.frm.doc.has_unit_price_items ? "yellow" : ""; + }); + super.setup(); } @@ -30,6 +35,8 @@ erpnext.buying.SupplierQuotationController = class SupplierQuotationController e this.frm.page.set_inner_btn_group_as_primary(__("Create")); this.frm.add_custom_button(__("Quotation"), this.make_quotation.bind(this), __("Create")); } else if (this.frm.doc.docstatus === 0) { + erpnext.set_unit_price_items_note(this.frm); + this.frm.add_custom_button( __("Material Request"), function () { diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json index 210c7308ceb..2f4cae69e01 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json @@ -19,6 +19,7 @@ "transaction_date", "valid_till", "quotation_number", + "has_unit_price_items", "amended_from", "accounting_dimensions_section", "cost_center", @@ -921,14 +922,23 @@ "fieldname": "accounting_dimensions_section", "fieldtype": "Section Break", "label": "Accounting Dimensions" + }, + { + "default": "0", + "fieldname": "has_unit_price_items", + "fieldtype": "Check", + "hidden": 1, + "label": "Has Unit Price Items", + "no_copy": 1 } ], + "grid_page_length": 50, "icon": "fa fa-shopping-cart", "idx": 29, "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2024-03-28 10:20:30.231915", + "modified": "2025-03-03 17:39:38.459977", "modified_by": "Administrator", "module": "Buying", "name": "Supplier Quotation", @@ -989,6 +999,7 @@ "write": 1 } ], + "row_format": "Dynamic", "search_fields": "status, transaction_date, supplier,grand_total", "show_name_in_global_search": 1, "sort_field": "creation", diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py index 0829b27151f..719608c1991 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py @@ -60,6 +60,7 @@ class SupplierQuotation(BuyingController): discount_amount: DF.Currency grand_total: DF.Currency group_same_items: DF.Check + has_unit_price_items: DF.Check ignore_pricing_rule: DF.Check in_words: DF.Data | None incoterm: DF.Link | None @@ -103,6 +104,10 @@ class SupplierQuotation(BuyingController): valid_till: DF.Date | None # end: auto-generated types + def before_validate(self): + self.set_has_unit_price_items() + self.flags.allow_zero_qty = self.has_unit_price_items + def validate(self): super().validate() @@ -129,6 +134,17 @@ class SupplierQuotation(BuyingController): def on_trash(self): pass + def set_has_unit_price_items(self): + """ + If permitted in settings and any item has 0 qty, the SQ has unit price items. + """ + if not frappe.db.get_single_value("Buying Settings", "allow_zero_qty_in_supplier_quotation"): + return + + self.has_unit_price_items = any( + not row.qty for row in self.get("items") if (row.item_code and not row.qty) + ) + def validate_with_previous_doc(self): super().validate_with_previous_doc( { diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 0bec99e06fb..dae29510fff 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -2755,3 +2755,13 @@ erpnext.apply_putaway_rule = (frm, purpose=null) => { } }); }; + +erpnext.set_unit_price_items_note = (frm) => { + if (frm.doc.has_unit_price_items && !frm.is_new()) { + frm.dashboard.set_headline_alert( + __("The {0} contains Unit Price Items with 0 Qty.", [__(frm.doc.doctype)]), + "yellow", + true + ); + } +}; diff --git a/erpnext/selling/doctype/quotation/quotation.js b/erpnext/selling/doctype/quotation/quotation.js index 508eb51fec3..c4c0febdcd0 100644 --- a/erpnext/selling/doctype/quotation/quotation.js +++ b/erpnext/selling/doctype/quotation/quotation.js @@ -46,7 +46,7 @@ frappe.ui.form.on("Quotation", { frm.trigger("set_dynamic_field_label"); if (frm.doc.docstatus === 0) { - frm.trigger("set_unit_price_items_note"); + erpnext.set_unit_price_items_note(frm); } let sbb_field = frm.get_docfield("packed_items", "serial_and_batch_bundle"); @@ -72,16 +72,6 @@ frappe.ui.form.on("Quotation", { set_label: function (frm) { frm.fields_dict.customer_address.set_label(__(frm.doc.quotation_to + " Address")); }, - - set_unit_price_items_note: function (frm) { - if (frm.doc.has_unit_price_items) { - frm.dashboard.set_headline_alert( - __("The Quotation contains Unit Price Items with 0 Qty."), - "yellow", - true - ); - } - }, }); erpnext.selling.QuotationController = class QuotationController extends erpnext.selling.SellingController { diff --git a/erpnext/selling/doctype/quotation/quotation.json b/erpnext/selling/doctype/quotation/quotation.json index 1b9a8c7d62c..3e38f126726 100644 --- a/erpnext/selling/doctype/quotation/quotation.json +++ b/erpnext/selling/doctype/quotation/quotation.json @@ -1096,14 +1096,15 @@ "fieldname": "has_unit_price_items", "fieldtype": "Check", "hidden": 1, - "label": "Has Unit Price Items" + "label": "Has Unit Price Items", + "no_copy": 1 } ], "icon": "fa fa-shopping-cart", "idx": 82, "is_submittable": 1, "links": [], - "modified": "2025-02-28 18:52:44.063265", + "modified": "2025-03-03 16:49:20.050303", "modified_by": "Administrator", "module": "Selling", "name": "Quotation", diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 10061a71518..a62c4715f38 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -106,7 +106,7 @@ frappe.ui.form.on("Sales Order", { } if (frm.doc.docstatus === 0) { - frm.trigger("set_unit_price_items_note"); + erpnext.set_unit_price_items_note(frm); if (frm.doc.is_internal_customer) { frm.events.get_items_from_internal_purchase_order(frm); @@ -560,16 +560,6 @@ frappe.ui.form.on("Sales Order", { }; frappe.set_route("query-report", "Reserved Stock"); }, - - set_unit_price_items_note: function (frm) { - if (frm.doc.has_unit_price_items && !frm.is_new()) { - frm.dashboard.set_headline_alert( - __("The Sales Order contains Unit Price Items with 0 Qty."), - "yellow", - true - ); - } - }, }); frappe.ui.form.on("Sales Order Item", { diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index 4cf7816e177..2f481028c1b 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -1679,14 +1679,15 @@ "fieldname": "has_unit_price_items", "fieldtype": "Check", "hidden": 1, - "label": "Has Unit Price Items" + "label": "Has Unit Price Items", + "no_copy": 1 } ], "icon": "fa fa-file-text", "idx": 105, "is_submittable": 1, "links": [], - "modified": "2025-02-28 18:52:01.932669", + "modified": "2025-03-03 16:49:00.676927", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order", diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json index 9589246a98f..7406b473f98 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.json +++ b/erpnext/selling/doctype/selling_settings/selling_settings.json @@ -34,8 +34,8 @@ "hide_tax_id", "enable_discount_accounting", "enable_cutoff_date_on_bulk_delivery_note_creation", - "allow_zero_qty_in_sales_order", "allow_zero_qty_in_quotation", + "allow_zero_qty_in_sales_order", "experimental_section", "use_server_side_reactivity" ], @@ -227,13 +227,13 @@ "default": "0", "fieldname": "allow_zero_qty_in_sales_order", "fieldtype": "Check", - "label": "Allow 0 Qty in Sales Order (Unit Price Contract)" + "label": "Allow 0 Qty in Sales Order (Unit Price Items)" }, { "default": "0", "fieldname": "allow_zero_qty_in_quotation", "fieldtype": "Check", - "label": "Allow 0 Qty in Quotation (Unit Price Contract)" + "label": "Allow 0 Qty in Quotation (Unit Price Items)" } ], "grid_page_length": 50, @@ -242,7 +242,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2025-02-28 18:19:46.436595", + "modified": "2025-03-03 16:39:16.360823", "modified_by": "Administrator", "module": "Selling", "name": "Selling Settings",