feat: Unit Price Items in Buying (RFQ, SQ, PO)

- chore: Extract `set_unit_price_items_note` into a util

(cherry picked from commit e403d3f153)

# Conflicts:
#	erpnext/buying/doctype/buying_settings/buying_settings.json
#	erpnext/buying/doctype/purchase_order/purchase_order.json
#	erpnext/buying/doctype/request_for_quotation/request_for_quotation.json
#	erpnext/selling/doctype/quotation/quotation.json
#	erpnext/selling/doctype/selling_settings/selling_settings.json
This commit is contained in:
marination
2025-03-03 17:53:00 +01:00
committed by Mergify
parent 91e167fe72
commit f8fa775af3
17 changed files with 182 additions and 29 deletions

View File

@@ -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

View File

@@ -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[

View File

@@ -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) {

View File

@@ -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",

View File

@@ -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:

View File

@@ -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) {

View File

@@ -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",

View File

@@ -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)):

View File

@@ -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 () {

View File

@@ -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",

View File

@@ -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(
{

View File

@@ -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
);
}
};

View File

@@ -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 {

View File

@@ -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",

View File

@@ -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", {

View File

@@ -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",

View File

@@ -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",