diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index db1926b77cf..ce6af54323f 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -73,9 +73,12 @@ "reports_tab", "remarks_section", "general_ledger_remarks_length", - "ignore_is_opening_check_for_reporting", "column_break_lvjk", - "receivable_payable_remarks_length" + "receivable_payable_remarks_length", + "accounts_receivable_payable_tuning_section", + "receivable_payable_fetch_method", + "legacy_section", + "ignore_is_opening_check_for_reporting" ], "fields": [ { @@ -479,6 +482,23 @@ "fieldname": "ignore_is_opening_check_for_reporting", "fieldtype": "Check", "label": "Ignore Is Opening check for reporting" + }, + { + "default": "Buffered Cursor", + "fieldname": "receivable_payable_fetch_method", + "fieldtype": "Select", + "label": "Data Fetch Method", + "options": "Buffered Cursor\nUnBuffered Cursor" + }, + { + "fieldname": "accounts_receivable_payable_tuning_section", + "fieldtype": "Section Break", + "label": "Accounts Receivable / Payable Tuning" + }, + { + "fieldname": "legacy_section", + "fieldtype": "Section Break", + "label": "Legacy Fields" } ], "icon": "icon-cog", @@ -486,7 +506,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2025-01-23 13:15:44.077853", + "modified": "2025-05-05 12:29:38.302027", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", @@ -515,4 +535,4 @@ "sort_order": "ASC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.js b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.js index 974f037a81d..44aa2eac98d 100644 --- a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.js +++ b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.js @@ -197,7 +197,7 @@ frappe.ui.form.on("Invoice Discounting", { from_date: frm.doc.posting_date, to_date: moment(frm.doc.modified).format("YYYY-MM-DD"), company: frm.doc.company, - group_by: "Group by Voucher (Consolidated)", + categorize_by: "Categorize by Voucher (Consolidated)", show_cancelled_entries: frm.doc.docstatus === 2, }; frappe.set_route("query-report", "General Ledger"); diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index f3785800de0..c4e02ab7eba 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -34,7 +34,7 @@ frappe.ui.form.on("Journal Entry", { to_date: moment(frm.doc.modified).format("YYYY-MM-DD"), company: frm.doc.company, finance_book: frm.doc.finance_book, - group_by: "", + categorize_by: "", show_cancelled_entries: frm.doc.docstatus === 2, }; frappe.set_route("query-report", "General Ledger"); diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index f4819aaea59..6c028eb0a4b 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -319,7 +319,7 @@ frappe.ui.form.on('Payment Entry', { "from_date": frm.doc.posting_date, "to_date": moment(frm.doc.modified).format('YYYY-MM-DD'), "company": frm.doc.company, - "group_by": "", + "categorize_by": "", "show_cancelled_entries": frm.doc.docstatus === 2 }; frappe.set_route("query-report", "General Ledger"); diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 5830246bca9..936b9bcbbd4 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1590,7 +1590,7 @@ class PaymentEntry(AccountsController): # Re allocate amount to those references which have PR set (Higher priority) for ref in self.references: - if not ref.payment_request: + if not (ref.reference_doctype and ref.reference_name and ref.payment_request): continue # fetch outstanding_amount of `Reference` (Payment Term) and `Payment Request` to allocate new amount @@ -1641,7 +1641,7 @@ class PaymentEntry(AccountsController): ) # Re allocate amount to those references which have no PR (Lower priority) for ref in self.references: - if ref.payment_request: + if ref.payment_request or not (ref.reference_doctype and ref.reference_name): continue key = (ref.reference_doctype, ref.reference_name, ref.get("payment_term")) diff --git a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.js b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.js index 82d8cb37fe7..742e4f71576 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.js +++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.js @@ -29,7 +29,7 @@ frappe.ui.form.on("Period Closing Voucher", { from_date: frm.doc.posting_date, to_date: moment(frm.doc.modified).format("YYYY-MM-DD"), company: frm.doc.company, - group_by: "", + categorize_by: "", show_cancelled_entries: frm.doc.docstatus === 2, }; frappe.set_route("query-report", "General Ledger"); diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json index 27f7853410b..372ae008cc3 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json @@ -12,7 +12,7 @@ "posting_date", "company", "account", - "group_by", + "categorize_by", "cost_center", "territory", "ignore_exchange_rate_revaluation_journals", @@ -172,14 +172,6 @@ "fieldtype": "Date", "label": "Start Date" }, - { - "default": "Group by Voucher (Consolidated)", - "depends_on": "eval:(doc.report == 'General Ledger');", - "fieldname": "group_by", - "fieldtype": "Select", - "label": "Group By", - "options": "\nGroup by Voucher\nGroup by Voucher (Consolidated)" - }, { "depends_on": "eval: (doc.report == 'General Ledger');", "fieldname": "currency", @@ -395,10 +387,18 @@ "fieldname": "show_remarks", "fieldtype": "Check", "label": "Show Remarks" + }, + { + "default": "Categorize by Voucher (Consolidated)", + "depends_on": "eval:(doc.report == 'General Ledger');", + "fieldname": "categorize_by", + "fieldtype": "Select", + "label": "Categorize By", + "options": "\nCategorize by Voucher\nCategorize by Voucher (Consolidated)" } ], "links": [], - "modified": "2024-10-18 17:51:39.108481", + "modified": "2025-04-30 14:43:23.643006", "modified_by": "Administrator", "module": "Accounts", "name": "Process Statement Of Accounts", @@ -433,4 +433,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py index 511c89dfc8f..a2e4a79a16d 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -145,7 +145,7 @@ def get_gl_filters(doc, entry, tax_id, presentation_currency): "party": [entry.customer], "party_name": [entry.customer_name] if entry.customer_name else None, "presentation_currency": presentation_currency, - "group_by": doc.group_by, + "categorize_by": doc.categorize_by, "currency": doc.currency, "project": [p.project_name for p in doc.project], "show_opening_entries": 0, diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index ceb3a7f784f..acd21da5a7f 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -597,35 +597,34 @@ def get_due_date_from_template(template_name, posting_date, bill_date): def validate_due_date( - posting_date, due_date, party_type, party, company=None, bill_date=None, template_name=None + posting_date, due_date, party_type, party, company=None, bill_date=None, template_name=None, doctype=None ): if getdate(due_date) < getdate(posting_date): frappe.throw(_("Due Date cannot be before Posting / Supplier Invoice Date")) else: - if not template_name: - return + validate_due_date_with_template(posting_date, due_date, bill_date, template_name, doctype) - default_due_date = get_due_date_from_template(template_name, posting_date, bill_date).strftime( - "%Y-%m-%d" - ) - if not default_due_date: - return +def validate_due_date_with_template(posting_date, due_date, bill_date, template_name, doctype=None): + if not template_name: + return - if default_due_date != posting_date and getdate(due_date) > getdate(default_due_date): - is_credit_controller = ( - frappe.db.get_single_value("Accounts Settings", "credit_controller") in frappe.get_roles() + default_due_date = format(get_due_date_from_template(template_name, posting_date, bill_date)) + + if not default_due_date: + return + + if default_due_date != posting_date and getdate(due_date) > getdate(default_due_date): + if frappe.db.get_single_value("Accounts Settings", "credit_controller") in frappe.get_roles(): + party_type = "supplier" if doctype == "Purchase Invoice" else "customer" + + msgprint( + _("Note: Due Date exceeds allowed {0} credit days by {1} day(s)").format( + party_type, date_diff(due_date, default_due_date) + ) ) - if is_credit_controller: - msgprint( - _("Note: Due / Reference Date exceeds allowed customer credit days by {0} day(s)").format( - date_diff(due_date, default_due_date) - ) - ) - else: - frappe.throw( - _("Due / Reference Date cannot be after {0}").format(formatdate(default_due_date)) - ) + else: + frappe.throw(_("Due Date cannot be after {0}").format(formatdate(default_due_date))) @frappe.whitelist() @@ -903,12 +902,16 @@ def get_party_shipping_address(doctype: str, name: str) -> str | None: ["is_shipping_address", "=", 1], ["address_type", "=", "Shipping"], ], - pluck="name", - limit=1, + fields=["name", "is_shipping_address"], order_by="is_shipping_address DESC", ) - return shipping_addresses[0] if shipping_addresses else None + if shipping_addresses and shipping_addresses[0].is_shipping_address == 1: + return shipping_addresses[0].name + if len(shipping_addresses) == 1: + return shipping_addresses[0].name + else: + return None def get_partywise_advanced_payment_amount( diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index b082d45c520..5e31542fa44 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -49,6 +49,10 @@ class ReceivablePayableReport: self.age_as_on = ( getdate(nowdate()) if self.filters.report_date > getdate(nowdate()) else self.filters.report_date ) + self.ple_fetch_method = ( + frappe.db.get_single_value("Accounts Settings", "receivable_payable_fetch_method") + or "Buffered Cursor" + ) # Fail Safe def run(self, args): self.filters.update(args) @@ -85,10 +89,7 @@ class ReceivablePayableReport: self.skip_total_row = 1 def get_data(self): - self.get_ple_entries() self.get_sales_invoices_or_customers_based_on_sales_person() - self.voucher_balance = OrderedDict() - self.init_voucher_balance() # invoiced, paid, credit_note, outstanding # Build delivery note map against all sales invoices self.build_delivery_note_map() @@ -105,12 +106,40 @@ class ReceivablePayableReport: # Get Exchange Rate Revaluations self.get_exchange_rate_revaluations() + self.prepare_ple_query() self.data = [] + self.voucher_balance = OrderedDict() + if self.ple_fetch_method == "Buffered Cursor": + self.fetch_ple_in_buffered_cursor() + elif self.ple_fetch_method == "UnBuffered Cursor": + self.fetch_ple_in_unbuffered_cursor() + + self.build_data() + + def fetch_ple_in_buffered_cursor(self): + self.ple_entries = frappe.db.sql(self.ple_query.get_sql(), as_dict=True) + + for ple in self.ple_entries: + self.init_voucher_balance(ple) # invoiced, paid, credit_note, outstanding + + # This is unavoidable. Initialization and allocation cannot happen in same loop for ple in self.ple_entries: self.update_voucher_balance(ple) - self.build_data() + delattr(self, "ple_entries") + + def fetch_ple_in_unbuffered_cursor(self): + self.ple_entries = [] + with frappe.db.unbuffered_cursor(): + for ple in frappe.db.sql(self.ple_query.get_sql(), as_dict=True, as_iterator=True): + self.init_voucher_balance(ple) # invoiced, paid, credit_note, outstanding + self.ple_entries.append(ple) + + # This is unavoidable. Initialization and allocation cannot happen in same loop + for ple in self.ple_entries: + self.update_voucher_balance(ple) + delattr(self, "ple_entries") def build_voucher_dict(self, ple): return frappe._dict( @@ -131,26 +160,22 @@ class ReceivablePayableReport: outstanding_in_account_currency=0.0, ) - def init_voucher_balance(self): - # build all keys, since we want to exclude vouchers beyond the report date - for ple in self.ple_entries: - # get the balance object for voucher_type + def init_voucher_balance(self, ple): + if self.filters.get("ignore_accounts"): + key = (ple.voucher_type, ple.voucher_no, ple.party) + else: + key = (ple.account, ple.voucher_type, ple.voucher_no, ple.party) - if self.filters.get("ignore_accounts"): - key = (ple.voucher_type, ple.voucher_no, ple.party) - else: - key = (ple.account, ple.voucher_type, ple.voucher_no, ple.party) + if key not in self.voucher_balance: + self.voucher_balance[key] = self.build_voucher_dict(ple) - if key not in self.voucher_balance: - self.voucher_balance[key] = self.build_voucher_dict(ple) + if ple.voucher_type == ple.against_voucher_type and ple.voucher_no == ple.against_voucher_no: + self.voucher_balance[key].cost_center = ple.cost_center - if ple.voucher_type == ple.against_voucher_type and ple.voucher_no == ple.against_voucher_no: - self.voucher_balance[key].cost_center = ple.cost_center + self.get_invoices(ple) - self.get_invoices(ple) - - if self.filters.get("group_by_party"): - self.init_subtotal_row(ple.party) + if self.filters.get("group_by_party"): + self.init_subtotal_row(ple.party) if self.filters.get("group_by_party") and not self.filters.get("in_party_currency"): self.init_subtotal_row("Total") @@ -764,7 +789,7 @@ class ReceivablePayableReport: index = 4 row["range" + str(index + 1)] = row.outstanding - def get_ple_entries(self): + def prepare_ple_query(self): # get all the GL entries filtered by the given filters self.prepare_conditions() @@ -817,7 +842,7 @@ class ReceivablePayableReport: else: query = query.orderby(self.ple.posting_date, self.ple.party) - self.ple_entries = query.run(as_dict=True) + self.ple_query = query def get_sales_invoices_or_customers_based_on_sales_person(self): if self.filters.get("sales_person"): diff --git a/erpnext/accounts/report/general_ledger/general_ledger.js b/erpnext/accounts/report/general_ledger/general_ledger.js index a0160f33c92..188199292f2 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.js +++ b/erpnext/accounts/report/general_ledger/general_ledger.js @@ -49,7 +49,7 @@ frappe.query_reports["General Ledger"] = { label: __("Voucher No"), fieldtype: "Data", on_change: function () { - frappe.query_report.set_filter_value("group_by", "Group by Voucher (Consolidated)"); + frappe.query_report.set_filter_value("categorize_by", "Categorize by Voucher (Consolidated)"); }, }, { @@ -112,29 +112,29 @@ frappe.query_reports["General Ledger"] = { hidden: 1, }, { - fieldname: "group_by", - label: __("Group by"), + fieldname: "categorize_by", + label: __("Categorize by"), fieldtype: "Select", options: [ "", { - label: __("Group by Voucher"), - value: "Group by Voucher", + label: __("Categorize by Voucher"), + value: "Categorize by Voucher", }, { - label: __("Group by Voucher (Consolidated)"), - value: "Group by Voucher (Consolidated)", + label: __("Categorize by Voucher (Consolidated)"), + value: "Categorize by Voucher (Consolidated)", }, { - label: __("Group by Account"), - value: "Group by Account", + label: __("Categorize by Account"), + value: "Categorize by Account", }, { - label: __("Group by Party"), - value: "Group by Party", + label: __("Categorize by Party"), + value: "Categorize by Party", }, ], - default: "Group by Voucher (Consolidated)", + default: "Categorize by Voucher (Consolidated)", }, { fieldname: "tax_id", diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 62806a957e3..ae48d617f48 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -72,13 +72,17 @@ def validate_filters(filters, account_details): if not account_details.get(account): frappe.throw(_("Account {0} does not exists").format(account)) - if filters.get("account") and filters.get("group_by") == "Group by Account": + if not filters.get("categorize_by") and filters.get("group_by"): + filters["categorize_by"] = filters["group_by"] + filters["categorize_by"] = filters["categorize_by"].replace("Group by", "Categorize by") + + if filters.get("account") and filters.get("categorize_by") == "Categorize by Account": filters.account = frappe.parse_json(filters.get("account")) for account in filters.account: if account_details[account].is_group == 0: frappe.throw(_("Can not filter based on Child Account, if grouped by Account")) - if filters.get("voucher_no") and filters.get("group_by") in ["Group by Voucher"]: + if filters.get("voucher_no") and filters.get("categorize_by") in ["Categorize by Voucher"]: frappe.throw(_("Can not filter based on Voucher No, if grouped by Voucher")) if filters.from_date > filters.to_date: @@ -172,9 +176,9 @@ def get_gl_entries(filters, accounting_dimensions): if filters.get("include_dimensions"): order_by_statement = "order by posting_date, creation" - if filters.get("group_by") == "Group by Voucher": + if filters.get("categorize_by") == "Categorize by Voucher": order_by_statement = "order by posting_date, voucher_type, voucher_no" - if filters.get("group_by") == "Group by Account": + if filters.get("categorize_by") == "Categorize by Account": order_by_statement = "order by account, posting_date, creation" if filters.get("include_default_book_entries"): @@ -261,7 +265,7 @@ def get_conditions(filters): if filters.get("voucher_no_not_in"): conditions.append("voucher_no not in %(voucher_no_not_in)s") - if filters.get("group_by") == "Group by Party" and not filters.get("party_type"): + if filters.get("categorize_by") == "Categorize by Party" and not filters.get("party_type"): conditions.append("party_type in ('Customer', 'Supplier')") if filters.get("party_type"): @@ -273,7 +277,7 @@ def get_conditions(filters): if not ( filters.get("account") or filters.get("party") - or filters.get("group_by") in ["Group by Account", "Group by Party"] + or filters.get("categorize_by") in ["Categorize by Account", "Categorize by Party"] ): if not ignore_is_opening: conditions.append("(posting_date >=%(from_date)s or is_opening = 'Yes')") @@ -368,13 +372,13 @@ def get_data_with_opening_closing(filters, account_details, accounting_dimension # Opening for filtered account data.append(totals.opening) - if filters.get("group_by") != "Group by Voucher (Consolidated)": + if filters.get("categorize_by") != "Categorize by Voucher (Consolidated)": for _acc, acc_dict in gle_map.items(): # acc if acc_dict.entries: # opening data.append({}) - if filters.get("group_by") != "Group by Voucher": + if filters.get("categorize_by") != "Categorize by Voucher": data.append(acc_dict.totals.opening) data += acc_dict.entries @@ -383,7 +387,7 @@ def get_data_with_opening_closing(filters, account_details, accounting_dimension data.append(acc_dict.totals.total) # closing - if filters.get("group_by") != "Group by Voucher": + if filters.get("categorize_by") != "Categorize by Voucher": data.append(acc_dict.totals.closing) data.append({}) else: @@ -416,9 +420,9 @@ def get_totals_dict(): def group_by_field(group_by): - if group_by == "Group by Party": + if group_by == "Categorize by Party": return "party" - elif group_by in ["Group by Voucher (Consolidated)", "Group by Account"]: + elif group_by in ["Categorize by Voucher (Consolidated)", "Categorize by Account"]: return "account" else: return "voucher_no" @@ -426,7 +430,7 @@ def group_by_field(group_by): def initialize_gle_map(gl_entries, filters): gle_map = OrderedDict() - group_by = group_by_field(filters.get("group_by")) + group_by = group_by_field(filters.get("categorize_by")) for gle in gl_entries: gle_map.setdefault(gle.get(group_by), _dict(totals=get_totals_dict(), entries=[])) @@ -437,8 +441,8 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map): totals = get_totals_dict() entries = [] consolidated_gle = OrderedDict() - group_by = group_by_field(filters.get("group_by")) - group_by_voucher_consolidated = filters.get("group_by") == "Group by Voucher (Consolidated)" + group_by = group_by_field(filters.get("categorize_by")) + group_by_voucher_consolidated = filters.get("categorize_by") == "Categorize by Voucher (Consolidated)" if filters.get("show_net_values_in_party_account"): account_type_map = get_account_type_map(filters.get("company")) diff --git a/erpnext/accounts/report/general_ledger/test_general_ledger.py b/erpnext/accounts/report/general_ledger/test_general_ledger.py index ee2a6772892..5e66282c523 100644 --- a/erpnext/accounts/report/general_ledger/test_general_ledger.py +++ b/erpnext/accounts/report/general_ledger/test_general_ledger.py @@ -155,7 +155,7 @@ class TestGeneralLedger(FrappeTestCase): "from_date": today(), "to_date": today(), "account": [account.name], - "group_by": "Group by Voucher (Consolidated)", + "categorize_by": "Categorize by Voucher (Consolidated)", } ) ) @@ -246,7 +246,7 @@ class TestGeneralLedger(FrappeTestCase): "from_date": today(), "to_date": today(), "account": [account.name], - "group_by": "Group by Voucher (Consolidated)", + "categorize_by": "Categorize by Voucher (Consolidated)", "ignore_err": True, } ) @@ -261,7 +261,7 @@ class TestGeneralLedger(FrappeTestCase): "from_date": today(), "to_date": today(), "account": [account.name], - "group_by": "Group by Voucher (Consolidated)", + "categorize_by": "Categorize by Voucher (Consolidated)", "ignore_err": False, } ) @@ -308,7 +308,7 @@ class TestGeneralLedger(FrappeTestCase): "from_date": si.posting_date, "to_date": si.posting_date, "account": [si.debit_to], - "group_by": "Group by Voucher (Consolidated)", + "categorize_by": "Categorize by Voucher (Consolidated)", "ignore_cr_dr_notes": False, } ) @@ -325,7 +325,7 @@ class TestGeneralLedger(FrappeTestCase): "from_date": si.posting_date, "to_date": si.posting_date, "account": [si.debit_to], - "group_by": "Group by Voucher (Consolidated)", + "categorize_by": "Categorize by Voucher (Consolidated)", "ignore_cr_dr_notes": True, } ) diff --git a/erpnext/accounts/test/test_reports.py b/erpnext/accounts/test/test_reports.py index c2e10f8fd47..0cca8db4e77 100644 --- a/erpnext/accounts/test/test_reports.py +++ b/erpnext/accounts/test/test_reports.py @@ -12,8 +12,8 @@ DEFAULT_FILTERS = { REPORT_FILTER_TEST_CASES: list[tuple[ReportName, ReportFilters]] = [ - ("General Ledger", {"group_by": "Group by Voucher (Consolidated)"}), - ("General Ledger", {"group_by": "Group by Voucher (Consolidated)", "include_dimensions": 1}), + ("General Ledger", {"categorize_by": "Categorize by Voucher (Consolidated)"}), + ("General Ledger", {"categorize_by": "Categorize by Voucher (Consolidated)", "include_dimensions": 1}), ("Accounts Payable", {"range1": 30, "range2": 60, "range3": 90, "range4": 120}), ("Accounts Receivable", {"range1": 30, "range2": 60, "range3": 90, "range4": 120}), ("Consolidated Financial Statement", {"report": "Balance Sheet"}), diff --git a/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js index 05cd88633bc..e4a862f329d 100644 --- a/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js +++ b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js @@ -76,14 +76,14 @@ frappe.query_reports["Supplier Quotation Comparison"] = { }, }, { - fieldname: "group_by", - label: __("Group by"), + fieldname: "categorize_by", + label: __("Categorize by"), fieldtype: "Select", options: [ - { label: __("Group by Supplier"), value: "Group by Supplier" }, - { label: __("Group by Item"), value: "Group by Item" }, + { label: __("Categorize by Supplier"), value: "Categorize by Supplier" }, + { label: __("Categorize by Item"), value: "Categorize by Item" }, ], - default: __("Group by Supplier"), + default: __("Categorize by Supplier"), }, { fieldtype: "Check", diff --git a/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py index 684cd3a0f9e..496323785b6 100644 --- a/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py +++ b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py @@ -15,6 +15,8 @@ def execute(filters=None): if not filters: return [], [] + validate_filters(filters) + columns = get_columns(filters) supplier_quotation_data = get_data(filters) @@ -24,6 +26,12 @@ def execute(filters=None): return columns, data, message, chart_data +def validate_filters(filters): + if not filters.get("categorize_by") and filters.get("group_by"): + filters["categorize_by"] = filters["group_by"] + filters["categorize_by"] = filters["categorize_by"].replace("Group by", "Categorize by") + + def get_data(filters): sq = frappe.qb.DocType("Supplier Quotation") sq_item = frappe.qb.DocType("Supplier Quotation Item") @@ -82,7 +90,9 @@ def prepare_data(supplier_quotation_data, filters): group_wise_map = defaultdict(list) supplier_qty_price_map = {} - group_by_field = "supplier_name" if filters.get("group_by") == "Group by Supplier" else "item_code" + group_by_field = ( + "supplier_name" if filters.get("categorize_by") == "Categorize by Supplier" else "item_code" + ) company_currency = frappe.db.get_default("currency") float_precision = cint(frappe.db.get_default("float_precision")) or 2 @@ -274,7 +284,7 @@ def get_columns(filters): }, ] - if filters.get("group_by") == "Group by Item": + if filters.get("categorize_by") == "Categorize by Item": group_by_columns.reverse() columns[0:0] = group_by_columns # add positioned group by columns to the report diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 41067b60252..f6810235d73 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -684,7 +684,9 @@ class AccountsController(TransactionBase): "Customer", self.customer, self.company, + None, self.payment_terms_template, + self.doctype, ) elif self.doctype == "Purchase Invoice": validate_due_date( @@ -695,6 +697,7 @@ class AccountsController(TransactionBase): self.company, self.bill_date, self.payment_terms_template, + self.doctype, ) def set_price_list_currency(self, buying_or_selling): diff --git a/erpnext/patches.txt b/erpnext/patches.txt index cdec75b64ec..be4b89bfc6d 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -372,3 +372,6 @@ erpnext.patches.v14_0.disable_add_row_in_gross_profit execute:frappe.db.set_single_value("Accounts Settings", "exchange_gain_loss_posting_date", "Payment") 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/rename_group_by_to_categorize_by.py b/erpnext/patches/v14_0/rename_group_by_to_categorize_by.py new file mode 100644 index 00000000000..1490ec572f4 --- /dev/null +++ b/erpnext/patches/v14_0/rename_group_by_to_categorize_by.py @@ -0,0 +1,20 @@ +import frappe +from frappe.model.utils.rename_field import rename_field + + +def execute(): + rename_field("Process Statement Of Accounts", "group_by", "categorize_by") + + frappe.db.sql( + """ + UPDATE + `tabProcess Statement Of Accounts` + SET + categorize_by = CASE + WHEN categorize_by = 'Group by Voucher (Consolidated)' THEN 'Categorize by Voucher (Consolidated)' + WHEN categorize_by = 'Group by Voucher' THEN 'Categorize by Voucher' + END + WHERE + categorize_by IN ('Group by Voucher (Consolidated)', 'Group by Voucher') + """ + ) 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/public/js/controllers/stock_controller.js b/erpnext/public/js/controllers/stock_controller.js index 71de690b835..3ca315ccd1c 100644 --- a/erpnext/public/js/controllers/stock_controller.js +++ b/erpnext/public/js/controllers/stock_controller.js @@ -87,7 +87,7 @@ erpnext.stock.StockController = class StockController extends frappe.ui.form.Con from_date: me.frm.doc.posting_date, to_date: moment(me.frm.doc.modified).format('YYYY-MM-DD'), company: me.frm.doc.company, - group_by: "Group by Voucher (Consolidated)", + categorize_by: "Categorize by Voucher (Consolidated)", show_cancelled_entries: me.frm.doc.docstatus === 2, ignore_prepared_report: true }; 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/item/item.js b/erpnext/stock/doctype/item/item.js index a7972790b1a..a84064a363d 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -7,6 +7,35 @@ const SALES_DOCTYPES = ["Quotation", "Sales Order", "Delivery Note", "Sales Invo const PURCHASE_DOCTYPES = ["Purchase Order", "Purchase Receipt", "Purchase Invoice"]; frappe.ui.form.on("Item", { + valuation_method(frm) { + if (!frm.is_new() && frm.doc.valuation_method === "Moving Average") { + let stock_exists = frm.doc.__onload && frm.doc.__onload.stock_exists ? 1 : 0; + let current_valuation_method = frm.doc.__onload.current_valuation_method; + + if (stock_exists && current_valuation_method !== frm.doc.valuation_method) { + let msg = __( + "Changing the valuation method to Moving Average will affect new transactions. If backdated entries are added, earlier FIFO-based entries will be reposted, which may change closing balances." + ); + msg += "
"; + msg += __( + "Also you can't switch back to FIFO after setting the valuation method to Moving Average for this item." + ); + msg += "
"; + msg += __("Do you want to change valuation method?"); + + frappe.confirm( + msg, + () => { + frm.set_value("valuation_method", "Moving Average"); + }, + () => { + frm.set_value("valuation_method", current_valuation_method); + } + ); + } + } + }, + setup: function (frm) { frm.add_fetch("attribute", "numeric_values", "numeric_values"); frm.add_fetch("attribute", "from_range", "from_range"); diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 3728e030542..e71e5b78b6d 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -31,6 +31,7 @@ from erpnext.controllers.item_variant import ( ) from erpnext.setup.doctype.item_group.item_group import invalidate_cache_for from erpnext.stock.doctype.item_default.item_default import ItemDefault +from erpnext.stock.utils import get_valuation_method class DuplicateReorderRows(frappe.ValidationError): @@ -53,6 +54,7 @@ class Item(Document): def onload(self): self.set_onload("stock_exists", self.stock_ledger_created()) self.set_onload("asset_naming_series", get_asset_naming_series()) + self.set_onload("current_valuation_method", get_valuation_method(self.name)) def autoname(self): if frappe.db.get_default("item_naming_by") == "Naming Series": diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 1eefd552c29..81a60b93c70 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -364,6 +364,7 @@ frappe.ui.form.on('Stock Entry', { docstatus: 1, purpose: "Material Transfer", add_to_transit: 1, + per_transferred: ["<", 100], } }) }, __("Get Items From")); diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 3366309f38c..90764180217 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -9,7 +9,17 @@ import frappe from frappe import _ from frappe.model.mapper import get_mapped_doc from frappe.query_builder.functions import Sum -from frappe.utils import cint, comma_or, cstr, flt, format_time, formatdate, getdate, nowdate +from frappe.utils import ( + cint, + comma_or, + cstr, + flt, + format_time, + formatdate, + get_link_to_form, + getdate, + nowdate, +) import erpnext from erpnext.accounts.general_ledger import process_gl_map @@ -430,17 +440,29 @@ class StockEntry(StockController): ).format(frappe.bold(self.company)) ) - elif ( - self.is_opening == "Yes" - and frappe.db.get_value("Account", d.expense_account, "report_type") == "Profit and Loss" - ): + acc_details = frappe.get_cached_value( + "Account", + d.expense_account, + ["account_type", "report_type"], + as_dict=True, + ) + + if self.is_opening == "Yes" and acc_details.report_type == "Profit and Loss": frappe.throw( _( - "Difference Account must be a Asset/Liability type account, since this Stock Entry is an Opening Entry" + "Difference Account must be a Asset/Liability type account (Temporary Opening), since this Stock Entry is an Opening Entry" ), OpeningEntryAccountError, ) + if acc_details.account_type == "Stock": + frappe.throw( + _( + "At row {0}: the Difference Account must not be a Stock type account, please change the Account Type for the account {1} or select a different account" + ).format(d.idx, get_link_to_form("Account", d.expense_account)), + OpeningEntryAccountError, + ) + def validate_warehouse(self): """perform various (sometimes conditional) validations on warehouse""" 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):