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", "disable_last_purchase_rate",
"show_pay_button", "show_pay_button",
"use_transaction_date_exchange_rate", "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", "subcontract",
"backflush_raw_materials_of_subcontract_based_on", "backflush_raw_materials_of_subcontract_based_on",
"column_break_11", "column_break_11",
@@ -207,14 +210,37 @@
"fieldtype": "Select", "fieldtype": "Select",
"label": "Update frequency of Project", "label": "Update frequency of Project",
"options": "Each Transaction\nManual" "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", "icon": "fa fa-cog",
"idx": 1, "idx": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
<<<<<<< HEAD
"modified": "2024-01-31 13:34:18.101256", "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", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Buying Settings", "name": "Buying Settings",
@@ -260,7 +286,12 @@
"role": "Purchase User" "role": "Purchase User"
} }
], ],
<<<<<<< HEAD
"sort_field": "modified", "sort_field": "modified",
=======
"row_format": "Dynamic",
"sort_field": "creation",
>>>>>>> e403d3f153 (feat: Unit Price Items in Buying (RFQ, SQ, PO))
"sort_order": "DESC", "sort_order": "DESC",
"states": [], "states": [],
"track_changes": 1 "track_changes": 1

View File

@@ -18,6 +18,9 @@ class BuyingSettings(Document):
from frappe.types import DF from frappe.types import DF
allow_multiple_items: DF.Check 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_purchase_receipt: DF.Check
auto_create_subcontracting_order: DF.Check auto_create_subcontracting_order: DF.Check
backflush_raw_materials_of_subcontract_based_on: DF.Literal[ 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) { 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 () { 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) { supplier: function (frm) {

View File

@@ -24,6 +24,7 @@
"apply_tds", "apply_tds",
"tax_withholding_category", "tax_withholding_category",
"is_subcontracted", "is_subcontracted",
"has_unit_price_items",
"supplier_warehouse", "supplier_warehouse",
"amended_from", "amended_from",
"accounting_dimensions_section", "accounting_dimensions_section",
@@ -1280,11 +1281,20 @@
"print_hide": 1 "print_hide": 1
}, },
{ {
<<<<<<< HEAD
"fieldname": "dispatch_address_display", "fieldname": "dispatch_address_display",
"fieldtype": "Text Editor", "fieldtype": "Text Editor",
"label": "Dispatch Address Details", "label": "Dispatch Address Details",
"print_hide": 1, "print_hide": 1,
"read_only": 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, "grid_page_length": 50,
@@ -1292,7 +1302,11 @@
"idx": 105, "idx": 105,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
<<<<<<< HEAD
"modified": "2025-04-09 16:54:08.836106", "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", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Purchase Order", "name": "Purchase Order",

View File

@@ -97,6 +97,7 @@ class PurchaseOrder(BuyingController):
from_date: DF.Date | None from_date: DF.Date | None
grand_total: DF.Currency grand_total: DF.Currency
group_same_items: DF.Check group_same_items: DF.Check
has_unit_price_items: DF.Check
ignore_pricing_rule: DF.Check ignore_pricing_rule: DF.Check
in_words: DF.Data | None in_words: DF.Data | None
incoterm: DF.Link | None incoterm: DF.Link | None
@@ -191,6 +192,10 @@ class PurchaseOrder(BuyingController):
self.set_onload("supplier_tds", supplier_tds) self.set_onload("supplier_tds", supplier_tds)
self.set_onload("can_update_items", self.can_update_items()) 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): def validate(self):
super().validate() super().validate()
@@ -223,6 +228,17 @@ class PurchaseOrder(BuyingController):
) )
self.reset_default_field_value("set_warehouse", "items", "warehouse") 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): def validate_with_previous_doc(self):
mri_compare_fields = [["project", "="], ["item_code", "="]] mri_compare_fields = [["project", "="], ["item_code", "="]]
if self.is_subcontracted: if self.is_subcontracted:

View File

@@ -28,6 +28,10 @@ frappe.ui.form.on("Request for Quotation", {
is_group: 0, is_group: 0,
}, },
})); }));
frm.set_indicator_formatter("item_code", function (doc) {
return !doc.qty && frm.doc.has_unit_price_items ? "yellow" : "";
});
}, },
onload: function (frm) { onload: function (frm) {
@@ -163,6 +167,10 @@ frappe.ui.form.on("Request for Quotation", {
__("View") __("View")
); );
} }
if (frm.doc.docstatus === 0) {
erpnext.set_unit_price_items_note(frm);
}
}, },
show_supplier_quotation_comparison(frm) { show_supplier_quotation_comparison(frm) {

View File

@@ -16,6 +16,7 @@
"transaction_date", "transaction_date",
"schedule_date", "schedule_date",
"status", "status",
"has_unit_price_items",
"amended_from", "amended_from",
"suppliers_section", "suppliers_section",
"suppliers", "suppliers",
@@ -306,13 +307,26 @@
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Billing Address Details", "label": "Billing Address Details",
"read_only": 1 "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", "icon": "fa fa-shopping-cart",
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
<<<<<<< HEAD
"modified": "2023-11-06 12:45:28.898706", "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", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Request for Quotation", "name": "Request for Quotation",
@@ -377,6 +391,7 @@
"role": "All" "role": "All"
} }
], ],
"row_format": "Dynamic",
"search_fields": "status, transaction_date", "search_fields": "status, transaction_date",
"show_name_in_global_search": 1, "show_name_in_global_search": 1,
"sort_field": "modified", "sort_field": "modified",

View File

@@ -42,6 +42,7 @@ class RequestforQuotation(BuyingController):
billing_address_display: DF.SmallText | None billing_address_display: DF.SmallText | None
company: DF.Link company: DF.Link
email_template: DF.Link | None email_template: DF.Link | None
has_unit_price_items: DF.Check
incoterm: DF.Link | None incoterm: DF.Link | None
items: DF.Table[RequestforQuotationItem] items: DF.Table[RequestforQuotationItem]
letter_head: DF.Link | None letter_head: DF.Link | None
@@ -61,6 +62,10 @@ class RequestforQuotation(BuyingController):
vendor: DF.Link | None vendor: DF.Link | None
# end: auto-generated types # 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): def validate(self):
self.validate_duplicate_supplier() self.validate_duplicate_supplier()
self.validate_supplier_list() self.validate_supplier_list()
@@ -72,6 +77,17 @@ class RequestforQuotation(BuyingController):
# after amend and save, status still shows as cancelled, until submit # after amend and save, status still shows as cancelled, until submit
self.db_set("status", "Draft") 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): def validate_duplicate_supplier(self):
supplier_list = [d.supplier for d in self.suppliers] supplier_list = [d.supplier for d in self.suppliers]
if len(supplier_list) != len(set(supplier_list)): if len(supplier_list) != len(set(supplier_list)):

View File

@@ -11,6 +11,11 @@ erpnext.buying.SupplierQuotationController = class SupplierQuotationController e
Quotation: "Quotation", 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(); super.setup();
} }
@@ -26,6 +31,8 @@ erpnext.buying.SupplierQuotationController = class SupplierQuotationController e
cur_frm.page.set_inner_btn_group_as_primary(__("Create")); cur_frm.page.set_inner_btn_group_as_primary(__("Create"));
cur_frm.add_custom_button(__("Quotation"), this.make_quotation, __("Create")); cur_frm.add_custom_button(__("Quotation"), this.make_quotation, __("Create"));
} else if (this.frm.doc.docstatus === 0) { } else if (this.frm.doc.docstatus === 0) {
erpnext.set_unit_price_items_note(this.frm);
this.frm.add_custom_button( this.frm.add_custom_button(
__("Material Request"), __("Material Request"),
function () { function () {

View File

@@ -19,6 +19,7 @@
"transaction_date", "transaction_date",
"valid_till", "valid_till",
"quotation_number", "quotation_number",
"has_unit_price_items",
"amended_from", "amended_from",
"accounting_dimensions_section", "accounting_dimensions_section",
"cost_center", "cost_center",
@@ -921,14 +922,23 @@
"fieldname": "accounting_dimensions_section", "fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Accounting Dimensions" "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", "icon": "fa fa-shopping-cart",
"idx": 29, "idx": 29,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2024-03-28 10:20:30.231915", "modified": "2025-03-03 17:39:38.459977",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Supplier Quotation", "name": "Supplier Quotation",
@@ -989,6 +999,7 @@
"write": 1 "write": 1
} }
], ],
"row_format": "Dynamic",
"search_fields": "status, transaction_date, supplier,grand_total", "search_fields": "status, transaction_date, supplier,grand_total",
"show_name_in_global_search": 1, "show_name_in_global_search": 1,
"sort_field": "modified", "sort_field": "modified",

View File

@@ -60,6 +60,7 @@ class SupplierQuotation(BuyingController):
discount_amount: DF.Currency discount_amount: DF.Currency
grand_total: DF.Currency grand_total: DF.Currency
group_same_items: DF.Check group_same_items: DF.Check
has_unit_price_items: DF.Check
ignore_pricing_rule: DF.Check ignore_pricing_rule: DF.Check
in_words: DF.Data | None in_words: DF.Data | None
incoterm: DF.Link | None incoterm: DF.Link | None
@@ -103,6 +104,10 @@ class SupplierQuotation(BuyingController):
valid_till: DF.Date | None valid_till: DF.Date | None
# end: auto-generated types # 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): def validate(self):
super().validate() super().validate()
@@ -129,6 +134,17 @@ class SupplierQuotation(BuyingController):
def on_trash(self): def on_trash(self):
pass 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): def validate_with_previous_doc(self):
super().validate_with_previous_doc( 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"); frm.trigger("set_dynamic_field_label");
if (frm.doc.docstatus === 0) { 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"); 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) { set_label: function (frm) {
frm.fields_dict.customer_address.set_label(__(frm.doc.quotation_to + " Address")); 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 { erpnext.selling.QuotationController = class QuotationController extends erpnext.selling.SellingController {

View File

@@ -1091,18 +1091,23 @@
"fieldname": "has_unit_price_items", "fieldname": "has_unit_price_items",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 1, "hidden": 1,
"label": "Has Unit Price Items" "label": "Has Unit Price Items",
"no_copy": 1
} }
], ],
"icon": "fa fa-shopping-cart", "icon": "fa fa-shopping-cart",
"idx": 82, "idx": 82,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
<<<<<<< HEAD
<<<<<<< HEAD <<<<<<< HEAD
"modified": "2024-11-26 12:43:29.293637", "modified": "2024-11-26 12:43:29.293637",
======= =======
"modified": "2025-02-28 18:52:44.063265", "modified": "2025-02-28 18:52:44.063265",
>>>>>>> c1e4e7af28 (feat: Unit Price Contract) >>>>>>> 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", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Quotation", "name": "Quotation",

View File

@@ -106,7 +106,7 @@ frappe.ui.form.on("Sales Order", {
} }
if (frm.doc.docstatus === 0) { 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) { if (frm.doc.is_internal_customer) {
frm.events.get_items_from_internal_purchase_order(frm); 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"); 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", { frappe.ui.form.on("Sales Order Item", {

View File

@@ -1656,14 +1656,15 @@
"fieldname": "has_unit_price_items", "fieldname": "has_unit_price_items",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 1, "hidden": 1,
"label": "Has Unit Price Items" "label": "Has Unit Price Items",
"no_copy": 1
} }
], ],
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
"idx": 105, "idx": 105,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2025-02-28 18:52:01.932669", "modified": "2025-03-03 16:49:00.676927",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Sales Order", "name": "Sales Order",

View File

@@ -37,8 +37,8 @@
======= =======
"enable_discount_accounting", "enable_discount_accounting",
"enable_cutoff_date_on_bulk_delivery_note_creation", "enable_cutoff_date_on_bulk_delivery_note_creation",
"allow_zero_qty_in_sales_order",
"allow_zero_qty_in_quotation", "allow_zero_qty_in_quotation",
"allow_zero_qty_in_sales_order",
"experimental_section", "experimental_section",
"use_server_side_reactivity" "use_server_side_reactivity"
>>>>>>> c1e4e7af28 (feat: Unit Price Contract) >>>>>>> c1e4e7af28 (feat: Unit Price Contract)
@@ -233,14 +233,18 @@
"default": "0", "default": "0",
"fieldname": "allow_zero_qty_in_sales_order", "fieldname": "allow_zero_qty_in_sales_order",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Allow 0 Qty in Sales Order (Unit Price Contract)" "label": "Allow 0 Qty in Sales Order (Unit Price Items)"
}, },
{ {
"default": "0", "default": "0",
"fieldname": "allow_zero_qty_in_quotation", "fieldname": "allow_zero_qty_in_quotation",
"fieldtype": "Check", "fieldtype": "Check",
<<<<<<< HEAD
"label": "Allow 0 Qty in Quotation (Unit Price Contract)" "label": "Allow 0 Qty in Quotation (Unit Price Contract)"
>>>>>>> c1e4e7af28 (feat: 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, "grid_page_length": 50,
@@ -249,11 +253,15 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
<<<<<<< HEAD
<<<<<<< HEAD <<<<<<< HEAD
"modified": "2023-10-25 14:03:03.966701", "modified": "2023-10-25 14:03:03.966701",
======= =======
"modified": "2025-02-28 18:19:46.436595", "modified": "2025-02-28 18:19:46.436595",
>>>>>>> c1e4e7af28 (feat: Unit Price Contract) >>>>>>> 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", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Selling Settings", "name": "Selling Settings",