Skip to content

Commit e403d3f

Browse files
committed
feat: Unit Price Items in Buying (RFQ, SQ, PO)
- chore: Extract `set_unit_price_items_note` into a util
1 parent 71f65ba commit e403d3f

File tree

17 files changed

+161
-35
lines changed

17 files changed

+161
-35
lines changed

erpnext/buying/doctype/buying_settings/buying_settings.json

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
"disable_last_purchase_rate",
2626
"show_pay_button",
2727
"use_transaction_date_exchange_rate",
28+
"allow_zero_qty_in_request_for_quotation",
29+
"allow_zero_qty_in_supplier_quotation",
30+
"allow_zero_qty_in_purchase_order",
2831
"subcontract",
2932
"backflush_raw_materials_of_subcontract_based_on",
3033
"column_break_11",
@@ -207,14 +210,33 @@
207210
"fieldtype": "Select",
208211
"label": "Update frequency of Project",
209212
"options": "Each Transaction\nManual"
213+
},
214+
{
215+
"default": "0",
216+
"fieldname": "allow_zero_qty_in_purchase_order",
217+
"fieldtype": "Check",
218+
"label": "Allow 0 Qty in Purchase Order (Unit Price Items)"
219+
},
220+
{
221+
"default": "0",
222+
"fieldname": "allow_zero_qty_in_request_for_quotation",
223+
"fieldtype": "Check",
224+
"label": "Allow 0 Qty in Request for Quotation (Unit Price Items)"
225+
},
226+
{
227+
"default": "0",
228+
"fieldname": "allow_zero_qty_in_supplier_quotation",
229+
"fieldtype": "Check",
230+
"label": "Allow 0 Qty in Supplier Quotation (Unit Price Items)"
210231
}
211232
],
233+
"grid_page_length": 50,
212234
"icon": "fa fa-cog",
213235
"idx": 1,
214236
"index_web_pages_for_search": 1,
215237
"issingle": 1,
216238
"links": [],
217-
"modified": "2024-03-27 13:06:43.375495",
239+
"modified": "2025-03-03 17:32:25.939482",
218240
"modified_by": "Administrator",
219241
"module": "Buying",
220242
"name": "Buying Settings",
@@ -260,6 +282,7 @@
260282
"role": "Purchase User"
261283
}
262284
],
285+
"row_format": "Dynamic",
263286
"sort_field": "creation",
264287
"sort_order": "DESC",
265288
"states": [],

erpnext/buying/doctype/buying_settings/buying_settings.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ class BuyingSettings(Document):
1818
from frappe.types import DF
1919

2020
allow_multiple_items: DF.Check
21+
allow_zero_qty_in_purchase_order: DF.Check
22+
allow_zero_qty_in_request_for_quotation: DF.Check
23+
allow_zero_qty_in_supplier_quotation: DF.Check
2124
auto_create_purchase_receipt: DF.Check
2225
auto_create_subcontracting_order: DF.Check
2326
backflush_raw_materials_of_subcontract_based_on: DF.Literal[

erpnext/buying/doctype/purchase_order/purchase_order.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,15 @@ frappe.ui.form.on("Purchase Order", {
2626
}
2727

2828
frm.set_indicator_formatter("item_code", function (doc) {
29-
return doc.qty <= doc.received_qty ? "green" : "orange";
29+
let color;
30+
if (!doc.qty && frm.doc.has_unit_price_items) {
31+
color = "yellow";
32+
} else if (doc.qty <= doc.received_qty) {
33+
color = "green";
34+
} else {
35+
color = "orange";
36+
}
37+
return color;
3038
});
3139

3240
frm.set_query("expense_account", "items", function () {
@@ -63,6 +71,10 @@ frappe.ui.form.on("Purchase Order", {
6371
}
6472
});
6573
}
74+
75+
if (frm.doc.docstatus == 0) {
76+
erpnext.set_unit_price_items_note(frm);
77+
}
6678
},
6779

6880
supplier: function (frm) {

erpnext/buying/doctype/purchase_order/purchase_order.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"apply_tds",
2525
"tax_withholding_category",
2626
"is_subcontracted",
27+
"has_unit_price_items",
2728
"supplier_warehouse",
2829
"amended_from",
2930
"accounting_dimensions_section",
@@ -1282,13 +1283,22 @@
12821283
"oldfieldtype": "Select",
12831284
"options": "Not Initiated\nInitiated\nPartially Paid\nFully Paid",
12841285
"print_hide": 1
1286+
},
1287+
{
1288+
"default": "0",
1289+
"fieldname": "has_unit_price_items",
1290+
"fieldtype": "Check",
1291+
"hidden": 1,
1292+
"label": "Has Unit Price Items",
1293+
"no_copy": 1
12851294
}
12861295
],
1296+
"grid_page_length": 50,
12871297
"icon": "fa fa-file-text",
12881298
"idx": 105,
12891299
"is_submittable": 1,
12901300
"links": [],
1291-
"modified": "2024-03-27 13:10:24.518785",
1301+
"modified": "2025-03-03 16:48:08.697520",
12921302
"modified_by": "Administrator",
12931303
"module": "Buying",
12941304
"name": "Purchase Order",
@@ -1335,6 +1345,7 @@
13351345
"write": 1
13361346
}
13371347
],
1348+
"row_format": "Dynamic",
13381349
"search_fields": "status, transaction_date, supplier, grand_total",
13391350
"show_name_in_global_search": 1,
13401351
"sort_field": "creation",

erpnext/buying/doctype/purchase_order/purchase_order.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ class PurchaseOrder(BuyingController):
9595
from_date: DF.Date | None
9696
grand_total: DF.Currency
9797
group_same_items: DF.Check
98+
has_unit_price_items: DF.Check
9899
ignore_pricing_rule: DF.Check
99100
in_words: DF.Data | None
100101
incoterm: DF.Link | None
@@ -189,6 +190,10 @@ def onload(self):
189190
self.set_onload("supplier_tds", supplier_tds)
190191
self.set_onload("can_update_items", self.can_update_items())
191192

193+
def before_validate(self):
194+
self.set_has_unit_price_items()
195+
self.flags.allow_zero_qty = self.has_unit_price_items
196+
192197
def validate(self):
193198
super().validate()
194199

@@ -225,6 +230,17 @@ def validate(self):
225230
)
226231
self.reset_default_field_value("set_warehouse", "items", "warehouse")
227232

233+
def set_has_unit_price_items(self):
234+
"""
235+
If permitted in settings and any item has 0 qty, the PO has unit price items.
236+
"""
237+
if not frappe.db.get_single_value("Buying Settings", "allow_zero_qty_in_purchase_order"):
238+
return
239+
240+
self.has_unit_price_items = any(
241+
not row.qty for row in self.get("items") if (row.item_code and not row.qty)
242+
)
243+
228244
def validate_with_previous_doc(self):
229245
mri_compare_fields = [["project", "="], ["item_code", "="]]
230246
if self.is_subcontracted:

erpnext/buying/doctype/request_for_quotation/request_for_quotation.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ frappe.ui.form.on("Request for Quotation", {
2828
is_group: 0,
2929
},
3030
}));
31+
32+
frm.set_indicator_formatter("item_code", function (doc) {
33+
return !doc.qty && frm.doc.has_unit_price_items ? "yellow" : "";
34+
});
3135
},
3236

3337
onload: function (frm) {
@@ -155,6 +159,10 @@ frappe.ui.form.on("Request for Quotation", {
155159

156160
frm.page.set_inner_btn_group_as_primary(__("Create"));
157161
}
162+
163+
if (frm.doc.docstatus === 0) {
164+
erpnext.set_unit_price_items_note(frm);
165+
}
158166
},
159167

160168
make_supplier_quotation: function (frm) {

erpnext/buying/doctype/request_for_quotation/request_for_quotation.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"transaction_date",
1717
"schedule_date",
1818
"status",
19+
"has_unit_price_items",
1920
"amended_from",
2021
"suppliers_section",
2122
"suppliers",
@@ -306,13 +307,22 @@
306307
"fieldtype": "Text Editor",
307308
"label": "Billing Address Details",
308309
"read_only": 1
310+
},
311+
{
312+
"default": "0",
313+
"fieldname": "has_unit_price_items",
314+
"fieldtype": "Check",
315+
"hidden": 1,
316+
"label": "Has Unit Price Items",
317+
"no_copy": 1
309318
}
310319
],
320+
"grid_page_length": 50,
311321
"icon": "fa fa-shopping-cart",
312322
"index_web_pages_for_search": 1,
313323
"is_submittable": 1,
314324
"links": [],
315-
"modified": "2024-03-27 13:10:33.030915",
325+
"modified": "2025-03-03 16:48:39.856779",
316326
"modified_by": "Administrator",
317327
"module": "Buying",
318328
"name": "Request for Quotation",
@@ -377,6 +387,7 @@
377387
"role": "All"
378388
}
379389
],
390+
"row_format": "Dynamic",
380391
"search_fields": "status, transaction_date",
381392
"show_name_in_global_search": 1,
382393
"sort_field": "creation",

erpnext/buying/doctype/request_for_quotation/request_for_quotation.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ class RequestforQuotation(BuyingController):
4242
billing_address_display: DF.TextEditor | None
4343
company: DF.Link
4444
email_template: DF.Link | None
45+
has_unit_price_items: DF.Check
4546
incoterm: DF.Link | None
4647
items: DF.Table[RequestforQuotationItem]
4748
letter_head: DF.Link | None
@@ -61,6 +62,10 @@ class RequestforQuotation(BuyingController):
6162
vendor: DF.Link | None
6263
# end: auto-generated types
6364

65+
def before_validate(self):
66+
self.set_has_unit_price_items()
67+
self.flags.allow_zero_qty = self.has_unit_price_items
68+
6469
def validate(self):
6570
self.validate_duplicate_supplier()
6671
self.validate_supplier_list()
@@ -73,6 +78,17 @@ def validate(self):
7378
# after amend and save, status still shows as cancelled, until submit
7479
self.db_set("status", "Draft")
7580

81+
def set_has_unit_price_items(self):
82+
"""
83+
If permitted in settings and any item has 0 qty, the RFQ has unit price items.
84+
"""
85+
if not frappe.db.get_single_value("Buying Settings", "allow_zero_qty_in_request_for_quotation"):
86+
return
87+
88+
self.has_unit_price_items = any(
89+
not row.qty for row in self.get("items") if (row.item_code and not row.qty)
90+
)
91+
7692
def validate_duplicate_supplier(self):
7793
supplier_list = [d.supplier for d in self.suppliers]
7894
if len(supplier_list) != len(set(supplier_list)):

erpnext/buying/doctype/supplier_quotation/supplier_quotation.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ erpnext.buying.SupplierQuotationController = class SupplierQuotationController e
1111
Quotation: "Quotation",
1212
};
1313

14+
const me = this;
15+
this.frm.set_indicator_formatter("item_code", function (doc) {
16+
return !doc.qty && me.frm.doc.has_unit_price_items ? "yellow" : "";
17+
});
18+
1419
super.setup();
1520
}
1621

@@ -30,6 +35,8 @@ erpnext.buying.SupplierQuotationController = class SupplierQuotationController e
3035
this.frm.page.set_inner_btn_group_as_primary(__("Create"));
3136
this.frm.add_custom_button(__("Quotation"), this.make_quotation.bind(this), __("Create"));
3237
} else if (this.frm.doc.docstatus === 0) {
38+
erpnext.set_unit_price_items_note(this.frm);
39+
3340
this.frm.add_custom_button(
3441
__("Material Request"),
3542
function () {

erpnext/buying/doctype/supplier_quotation/supplier_quotation.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"transaction_date",
2020
"valid_till",
2121
"quotation_number",
22+
"has_unit_price_items",
2223
"amended_from",
2324
"accounting_dimensions_section",
2425
"cost_center",
@@ -921,14 +922,23 @@
921922
"fieldname": "accounting_dimensions_section",
922923
"fieldtype": "Section Break",
923924
"label": "Accounting Dimensions"
925+
},
926+
{
927+
"default": "0",
928+
"fieldname": "has_unit_price_items",
929+
"fieldtype": "Check",
930+
"hidden": 1,
931+
"label": "Has Unit Price Items",
932+
"no_copy": 1
924933
}
925934
],
935+
"grid_page_length": 50,
926936
"icon": "fa fa-shopping-cart",
927937
"idx": 29,
928938
"index_web_pages_for_search": 1,
929939
"is_submittable": 1,
930940
"links": [],
931-
"modified": "2024-03-28 10:20:30.231915",
941+
"modified": "2025-03-03 17:39:38.459977",
932942
"modified_by": "Administrator",
933943
"module": "Buying",
934944
"name": "Supplier Quotation",
@@ -989,6 +999,7 @@
989999
"write": 1
9901000
}
9911001
],
1002+
"row_format": "Dynamic",
9921003
"search_fields": "status, transaction_date, supplier,grand_total",
9931004
"show_name_in_global_search": 1,
9941005
"sort_field": "creation",

erpnext/buying/doctype/supplier_quotation/supplier_quotation.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ class SupplierQuotation(BuyingController):
6060
discount_amount: DF.Currency
6161
grand_total: DF.Currency
6262
group_same_items: DF.Check
63+
has_unit_price_items: DF.Check
6364
ignore_pricing_rule: DF.Check
6465
in_words: DF.Data | None
6566
incoterm: DF.Link | None
@@ -103,6 +104,10 @@ class SupplierQuotation(BuyingController):
103104
valid_till: DF.Date | None
104105
# end: auto-generated types
105106

107+
def before_validate(self):
108+
self.set_has_unit_price_items()
109+
self.flags.allow_zero_qty = self.has_unit_price_items
110+
106111
def validate(self):
107112
super().validate()
108113

@@ -129,6 +134,17 @@ def on_cancel(self):
129134
def on_trash(self):
130135
pass
131136

137+
def set_has_unit_price_items(self):
138+
"""
139+
If permitted in settings and any item has 0 qty, the SQ has unit price items.
140+
"""
141+
if not frappe.db.get_single_value("Buying Settings", "allow_zero_qty_in_supplier_quotation"):
142+
return
143+
144+
self.has_unit_price_items = any(
145+
not row.qty for row in self.get("items") if (row.item_code and not row.qty)
146+
)
147+
132148
def validate_with_previous_doc(self):
133149
super().validate_with_previous_doc(
134150
{

erpnext/public/js/controllers/transaction.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2755,3 +2755,13 @@ erpnext.apply_putaway_rule = (frm, purpose=null) => {
27552755
}
27562756
});
27572757
};
2758+
2759+
erpnext.set_unit_price_items_note = (frm) => {
2760+
if (frm.doc.has_unit_price_items && !frm.is_new()) {
2761+
frm.dashboard.set_headline_alert(
2762+
__("The {0} contains Unit Price Items with 0 Qty.", [__(frm.doc.doctype)]),
2763+
"yellow",
2764+
true
2765+
);
2766+
}
2767+
};

0 commit comments

Comments
 (0)