diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json index ae854f29343..a92eb23a23d 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,37 @@ "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": [], +<<<<<<< HEAD "modified": "2024-01-31 13:34:18.101256", +======= + "modified": "2025-03-03 17:32:25.939482", +>>>>>>> e403d3f153 (feat: Unit Price Items in Buying (RFQ, SQ, PO)) "modified_by": "Administrator", "module": "Buying", "name": "Buying Settings", @@ -260,7 +286,12 @@ "role": "Purchase User" } ], +<<<<<<< HEAD "sort_field": "modified", +======= + "row_format": "Dynamic", + "sort_field": "creation", +>>>>>>> e403d3f153 (feat: Unit Price Items in Buying (RFQ, SQ, PO)) "sort_order": "DESC", "states": [], "track_changes": 1 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 0b7c9de467a..7925d59d25a 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 7c9362ebaf9..bc63e8756ad 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", @@ -1280,11 +1281,20 @@ "print_hide": 1 }, { +<<<<<<< HEAD "fieldname": "dispatch_address_display", "fieldtype": "Text Editor", "label": "Dispatch Address Details", "print_hide": 1, "read_only": 1 +======= + "default": "0", + "fieldname": "has_unit_price_items", + "fieldtype": "Check", + "hidden": 1, + "label": "Has Unit Price Items", + "no_copy": 1 +>>>>>>> e403d3f153 (feat: Unit Price Items in Buying (RFQ, SQ, PO)) } ], "grid_page_length": 50, @@ -1292,7 +1302,11 @@ "idx": 105, "is_submittable": 1, "links": [], +<<<<<<< HEAD "modified": "2025-04-09 16:54:08.836106", +======= + "modified": "2025-03-03 16:48:08.697520", +>>>>>>> e403d3f153 (feat: Unit Price Items in Buying (RFQ, SQ, PO)) "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index a18d9fce186..9ddbbb991d6 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -97,6 +97,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 @@ -191,6 +192,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() @@ -223,6 +228,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 96597bd9753..e88a98759d0 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) { @@ -163,6 +167,10 @@ frappe.ui.form.on("Request for Quotation", { __("View") ); } + + if (frm.doc.docstatus === 0) { + erpnext.set_unit_price_items_note(frm); + } }, show_supplier_quotation_comparison(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 fd73f77ff8f..056535af17b 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,26 @@ "fieldtype": "Small Text", "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": [], +<<<<<<< HEAD "modified": "2023-11-06 12:45:28.898706", +======= + "modified": "2025-03-03 16:48:39.856779", +>>>>>>> e403d3f153 (feat: Unit Price Items in Buying (RFQ, SQ, PO)) "modified_by": "Administrator", "module": "Buying", "name": "Request for Quotation", @@ -377,6 +391,7 @@ "role": "All" } ], + "row_format": "Dynamic", "search_fields": "status, transaction_date", "show_name_in_global_search": 1, "sort_field": "modified", 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 bef41394742..e9bc9694a70 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.SmallText | 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() @@ -72,6 +77,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 c1698710135..fccca81f8ce 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(); } @@ -26,6 +31,8 @@ erpnext.buying.SupplierQuotationController = class SupplierQuotationController e cur_frm.page.set_inner_btn_group_as_primary(__("Create")); cur_frm.add_custom_button(__("Quotation"), this.make_quotation, __("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 4a8cd8bf9e6..6682c7e3585 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": "modified", diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py index 215022e18a6..5557f1a80ae 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 1fcdd459a3f..886ddd2acc0 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -2773,3 +2773,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 07869ab339b..6cb676ac58e 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 e087256d8d4..5bf8f474625 100644 --- a/erpnext/selling/doctype/quotation/quotation.json +++ b/erpnext/selling/doctype/quotation/quotation.json @@ -1091,18 +1091,23 @@ "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": [], +<<<<<<< HEAD <<<<<<< HEAD "modified": "2024-11-26 12:43:29.293637", ======= "modified": "2025-02-28 18:52:44.063265", >>>>>>> c1e4e7af28 (feat: Unit Price Contract) +======= + "modified": "2025-03-03 16:49:20.050303", +>>>>>>> e403d3f153 (feat: Unit Price Items in Buying (RFQ, SQ, PO)) "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 6c8cc7ac1df..ca3d2ac824b 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); @@ -539,16 +539,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 5fc808798e2..801648b4479 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -1656,14 +1656,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 01b7701f488..0e857430b5e 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.json +++ b/erpnext/selling/doctype/selling_settings/selling_settings.json @@ -37,8 +37,8 @@ ======= "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" >>>>>>> c1e4e7af28 (feat: Unit Price Contract) @@ -233,14 +233,18 @@ "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", +<<<<<<< HEAD "label": "Allow 0 Qty in Quotation (Unit Price Contract)" >>>>>>> c1e4e7af28 (feat: Unit Price Contract) +======= + "label": "Allow 0 Qty in Quotation (Unit Price Items)" +>>>>>>> e403d3f153 (feat: Unit Price Items in Buying (RFQ, SQ, PO)) } ], "grid_page_length": 50, @@ -249,11 +253,15 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], +<<<<<<< HEAD <<<<<<< HEAD "modified": "2023-10-25 14:03:03.966701", ======= "modified": "2025-02-28 18:19:46.436595", >>>>>>> c1e4e7af28 (feat: Unit Price Contract) +======= + "modified": "2025-03-03 16:39:16.360823", +>>>>>>> e403d3f153 (feat: Unit Price Items in Buying (RFQ, SQ, PO)) "modified_by": "Administrator", "module": "Selling", "name": "Selling Settings",