From 3993525bf6902bc3762a3f902a6e65c74e0b9748 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 6 May 2025 18:43:27 +0530 Subject: [PATCH] feat!: configure which rate is used to auto-update price list (backport #47417) (#47433) * feat!: configure which rate is used to auto-update price list (cherry picked from commit 3ebde4526aa2da63462923731c1fd05f3f600792) # Conflicts: # erpnext/selling/doctype/sales_order/test_sales_order.py # erpnext/setup/setup_wizard/operations/defaults_setup.py # erpnext/setup/setup_wizard/operations/install_fixtures.py # erpnext/stock/doctype/stock_settings/stock_settings.json # erpnext/stock/doctype/stock_settings/stock_settings.py # erpnext/stock/get_item_details.py * fix: merge conflicts --------- Co-authored-by: Sagar Vora <16315650+sagarvora@users.noreply.github.com> --- erpnext/patches.txt | 1 + .../v14_0/set_update_price_list_based_on.py | 14 +++ .../doctype/sales_order/test_sales_order.py | 113 ++++++++++++++++-- .../selling_settings/selling_settings.js | 4 +- .../setup_wizard/operations/defaults_setup.py | 1 + .../operations/install_fixtures.py | 1 + .../doctype/stock_settings/stock_settings.js | 26 ++++ .../stock_settings/stock_settings.json | 12 +- erpnext/stock/get_item_details.py | 110 ++++++++++------- 9 files changed, 225 insertions(+), 57 deletions(-) create mode 100644 erpnext/patches/v14_0/set_update_price_list_based_on.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index bee1aaa07f5..be4b89bfc6d 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -374,3 +374,4 @@ erpnext.patches.v14_0.update_posting_datetime erpnext.stock.doctype.stock_ledger_entry.patches.ensure_sle_indexes erpnext.patches.v14_0.rename_group_by_to_categorize_by execute:frappe.db.set_single_value("Accounts Settings", "receivable_payable_fetch_method", "Buffered Cursor") +erpnext.patches.v14_0.set_update_price_list_based_on diff --git a/erpnext/patches/v14_0/set_update_price_list_based_on.py b/erpnext/patches/v14_0/set_update_price_list_based_on.py new file mode 100644 index 00000000000..4ddef4b0c25 --- /dev/null +++ b/erpnext/patches/v14_0/set_update_price_list_based_on.py @@ -0,0 +1,14 @@ +import frappe +from frappe.utils import cint + + +def execute(): + frappe.db.set_single_value( + "Stock Settings", + "update_price_list_based_on", + ( + "Price List Rate" + if cint(frappe.db.get_single_value("Selling Settings", "editable_price_list_rate")) + else "Rate" + ), + ) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index b1207ee97a1..2f795cae5c4 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -853,7 +853,13 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): def test_auto_insert_price(self): make_item("_Test Item for Auto Price List", {"is_stock_item": 0}) make_item("_Test Item for Auto Price List with Discount Percentage", {"is_stock_item": 0}) - frappe.db.set_value("Stock Settings", None, "auto_insert_price_list_rate_if_missing", 1) + frappe.db.set_single_value( + "Stock Settings", + { + "auto_insert_price_list_rate_if_missing": 1, + "update_price_list_based_on": "Price List Rate", + }, + ) item_price = frappe.db.get_value( "Item Price", {"price_list": "_Test Price List", "item_code": "_Test Item for Auto Price List"} @@ -865,6 +871,7 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): item_code="_Test Item for Auto Price List", selling_price_list="_Test Price List", rate=100 ) + # ensure price gets inserted based on rate if price list rate is not defined by user self.assertEqual( frappe.db.get_value( "Item Price", @@ -874,6 +881,8 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): 100, ) + # ensure price gets insterted based on user-defined *Price List Rate* + # if update_price_list_based_on is set to Price List Rate make_sales_order( item_code="_Test Item for Auto Price List with Discount Percentage", selling_price_list="_Test Price List", @@ -881,18 +890,43 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): discount_percentage=20, ) - self.assertEqual( - frappe.db.get_value( - "Item Price", - { - "price_list": "_Test Price List", - "item_code": "_Test Item for Auto Price List with Discount Percentage", - }, - "price_list_rate", - ), - 200, + item_price = frappe.db.get_value( + "Item Price", + { + "price_list": "_Test Price List", + "item_code": "_Test Item for Auto Price List with Discount Percentage", + }, + ("name", "price_list_rate"), + as_dict=True, ) + self.assertEqual(item_price.price_list_rate, 200) + frappe.delete_doc("Item Price", item_price.name) + + frappe.db.set_single_value("Stock Settings", "update_price_list_based_on", "Rate") + + # ensure price gets insterted based on user-defined *Rate* + # if update_price_list_based_on is set to Rate + make_sales_order( + item_code="_Test Item for Auto Price List with Discount Percentage", + selling_price_list="_Test Price List", + price_list_rate=200, + discount_percentage=20, + ) + + item_price = frappe.db.get_value( + "Item Price", + { + "price_list": "_Test Price List", + "item_code": "_Test Item for Auto Price List with Discount Percentage", + }, + ("name", "price_list_rate"), + as_dict=True, + ) + + self.assertEqual(item_price.price_list_rate, 160) + frappe.delete_doc("Item Price", item_price.name) + # do not update price list frappe.db.set_value("Stock Settings", None, "auto_insert_price_list_rate_if_missing", 0) @@ -917,6 +951,63 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): frappe.db.set_value("Stock Settings", None, "auto_insert_price_list_rate_if_missing", 1) + def test_update_existing_item_price(self): + item_code = "_Test Item for Price List Updation" + price_list = "_Test Price List" + + make_item(item_code, {"is_stock_item": 0}) + + frappe.db.set_single_value( + "Stock Settings", + { + "auto_insert_price_list_rate_if_missing": 1, + "update_existing_price_list_rate": 1, + "update_price_list_based_on": "Rate", + }, + ) + + # setup: price creation + make_sales_order(item_code=item_code, selling_price_list=price_list, rate=100) + + # test price updation based on Rate + make_sales_order(item_code=item_code, selling_price_list=price_list, rate=90) + + self.assertEqual( + frappe.db.get_value( + "Item Price", + {"price_list": price_list, "item_code": item_code}, + "price_list_rate", + ), + 90, + ) + + frappe.db.set_single_value( + "Stock Settings", + { + "update_price_list_based_on": "Price List Rate", + }, + ) + + # test price updation based on Price List Rate + make_sales_order( + item_code=item_code, + selling_price_list=price_list, + price_list_rate=200, + discount_percentage=20, + ) + + self.assertEqual( + frappe.db.get_value( + "Item Price", + {"price_list": price_list, "item_code": item_code}, + "price_list_rate", + ), + 200, + ) + + # reset `update_existing_price_list_rate` to 0 + frappe.db.set_single_value("Stock Settings", "update_existing_price_list_rate", 0) + def test_drop_shipping(self): from erpnext.buying.doctype.purchase_order.purchase_order import update_status from erpnext.selling.doctype.sales_order.sales_order import ( diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.js b/erpnext/selling/doctype/selling_settings/selling_settings.js index 4471458fb10..f7670e69d47 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.js +++ b/erpnext/selling/doctype/selling_settings/selling_settings.js @@ -2,5 +2,7 @@ // For license information, please see license.txt frappe.ui.form.on("Selling Settings", { - refresh: function (frm) {}, + after_save(frm) { + frappe.boot.user.defaults.editable_price_list_rate = frm.doc.editable_price_list_rate; + }, }); diff --git a/erpnext/setup/setup_wizard/operations/defaults_setup.py b/erpnext/setup/setup_wizard/operations/defaults_setup.py index daceafa94b6..e6645f11a20 100644 --- a/erpnext/setup/setup_wizard/operations/defaults_setup.py +++ b/erpnext/setup/setup_wizard/operations/defaults_setup.py @@ -34,6 +34,7 @@ def set_default_settings(args): stock_settings.stock_uom = _("Nos") stock_settings.auto_indent = 1 stock_settings.auto_insert_price_list_rate_if_missing = 1 + stock_settings.update_price_list_based_on = "Rate" stock_settings.automatically_set_serial_nos_based_on_fifo = 1 stock_settings.set_qty_in_transactions_based_on_serial_no_input = 1 stock_settings.save() diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index 87152cae42f..d611357bc8d 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -478,6 +478,7 @@ def update_stock_settings(): stock_settings.stock_uom = _("Nos") stock_settings.auto_indent = 1 stock_settings.auto_insert_price_list_rate_if_missing = 1 + stock_settings.update_price_list_based_on = "Rate" stock_settings.automatically_set_serial_nos_based_on_fifo = 1 stock_settings.set_qty_in_transactions_based_on_serial_no_input = 1 stock_settings.save() diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.js b/erpnext/stock/doctype/stock_settings/stock_settings.js index 1972b193732..a819307902d 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.js +++ b/erpnext/stock/doctype/stock_settings/stock_settings.js @@ -35,4 +35,30 @@ frappe.ui.form.on("Stock Settings", { } ); }, + auto_insert_price_list_rate_if_missing(frm) { + if (!frm.doc.auto_insert_price_list_rate_if_missing) return; + + frm.set_value( + "update_price_list_based_on", + cint(frappe.defaults.get_default("editable_price_list_rate")) ? "Price List Rate" : "Rate" + ); + }, + update_price_list_based_on(frm) { + if ( + frm.doc.update_price_list_based_on === "Price List Rate" && + !cint(frappe.defaults.get_default("editable_price_list_rate")) + ) { + const dialog = frappe.warn( + __("Incompatible Setting Detected"), + __( + "

Price List Rate has not been set as editable in Selling Settings. In this scenario, setting Update Price List Based On to Price List Rate will prevent auto-updation of Item Price.

Are you sure you want to continue?" + ) + ); + dialog.set_secondary_action(() => { + frm.set_value("update_price_list_based_on", "Rate"); + dialog.hide(); + }); + return; + } + }, }); diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json index af68e63f339..c7aefcca7a6 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.json +++ b/erpnext/stock/doctype/stock_settings/stock_settings.json @@ -16,6 +16,7 @@ "stock_uom", "price_list_defaults_section", "auto_insert_price_list_rate_if_missing", + "update_price_list_based_on", "column_break_12", "update_existing_price_list_rate", "stock_validations_tab", @@ -347,6 +348,15 @@ "fieldname": "allow_existing_serial_no", "fieldtype": "Check", "label": "Allow existing Serial No to be Manufactured/Received again" + }, + { + "default": "Rate", + "depends_on": "eval: doc.auto_insert_price_list_rate_if_missing", + "fieldname": "update_price_list_based_on", + "fieldtype": "Select", + "label": "Update Price List Based On", + "mandatory_depends_on": "eval: doc.auto_insert_price_list_rate_if_missing", + "options": "Rate\nPrice List Rate" } ], "icon": "icon-cog", @@ -354,7 +364,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-05-31 14:15:14.145048", + "modified": "2025-05-06 02:39:24.284587", "modified_by": "Administrator", "module": "Stock", "name": "Stock Settings", diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 79bfc480d6a..bdc442e07a1 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -855,8 +855,8 @@ def get_price_list_rate(args, item_doc, out=None): price_list_rate = get_price_list_rate_for(args, item_doc.variant_of) # insert in database - if price_list_rate is None or frappe.db.get_single_value( - "Stock Settings", "update_existing_price_list_rate" + if price_list_rate is None or frappe.get_cached_value( + "Stock Settings", "Stock Settings", "update_existing_price_list_rate" ): insert_item_price(args) @@ -890,49 +890,71 @@ def insert_item_price(args): ): return - if frappe.db.get_value("Price List", args.price_list, "currency", cache=True) == args.currency and cint( - frappe.db.get_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing") - ): - if frappe.has_permission("Item Price", "write"): - price_list_rate = ( - (flt(args.rate) + flt(args.discount_amount)) / args.get("conversion_factor") - if args.get("conversion_factor") - else (flt(args.rate) + flt(args.discount_amount)) - ) + stock_settings = frappe.get_cached_doc("Stock Settings") - item_price = frappe.db.get_value( - "Item Price", - {"item_code": args.item_code, "price_list": args.price_list, "currency": args.currency}, - ["name", "price_list_rate"], - as_dict=1, - ) - if item_price and item_price.name: - if item_price.price_list_rate != price_list_rate and frappe.db.get_single_value( - "Stock Settings", "update_existing_price_list_rate" - ): - frappe.db.set_value("Item Price", item_price.name, "price_list_rate", price_list_rate) - frappe.msgprint( - _("Item Price updated for {0} in Price List {1}").format( - args.item_code, args.price_list - ), - alert=True, - ) - else: - item_price = frappe.get_doc( - { - "doctype": "Item Price", - "price_list": args.price_list, - "item_code": args.item_code, - "currency": args.currency, - "price_list_rate": price_list_rate, - "uom": args.stock_uom, - } - ) - item_price.insert() - frappe.msgprint( - _("Item Price added for {0} in Price List {1}").format(args.item_code, args.price_list), - alert=True, - ) + if ( + not frappe.db.get_value("Price List", args.price_list, "currency", cache=True) == args.currency + or not stock_settings.auto_insert_price_list_rate_if_missing + or not frappe.has_permission("Item Price", "write") + ): + return + + item_price = frappe.db.get_value( + "Item Price", + { + "item_code": args.item_code, + "price_list": args.price_list, + "currency": args.currency, + "uom": args.stock_uom, + }, + ["name", "price_list_rate"], + as_dict=1, + ) + + update_based_on_price_list_rate = stock_settings.update_price_list_based_on == "Price List Rate" + + if item_price and item_price.name: + if not stock_settings.update_existing_price_list_rate: + return + + rate_to_consider = flt(args.price_list_rate) if update_based_on_price_list_rate else flt(args.rate) + price_list_rate = _get_stock_uom_rate(rate_to_consider, args) + + if not price_list_rate or item_price.price_list_rate == price_list_rate: + return + + frappe.db.set_value("Item Price", item_price.name, "price_list_rate", price_list_rate) + frappe.msgprint( + _("Item Price updated for {0} in Price List {1}").format(args.item_code, args.price_list), + alert=True, + ) + else: + rate_to_consider = ( + (flt(args.price_list_rate) or flt(args.rate)) + if update_based_on_price_list_rate + else flt(args.rate) + ) + price_list_rate = _get_stock_uom_rate(rate_to_consider, args) + + item_price = frappe.get_doc( + { + "doctype": "Item Price", + "price_list": args.price_list, + "item_code": args.item_code, + "currency": args.currency, + "price_list_rate": price_list_rate, + "uom": args.stock_uom, + } + ) + item_price.insert() + frappe.msgprint( + _("Item Price added for {0} in Price List {1}").format(args.item_code, args.price_list), + alert=True, + ) + + +def _get_stock_uom_rate(rate, args): + return rate / args.conversion_factor if args.conversion_factor else rate def get_item_price(args, item_code, ignore_party=False):