From da1dcb8b21726baaa836642ee33d0c2d47d71652 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 2 May 2025 13:46:46 +0530 Subject: [PATCH 01/13] refactor: using sql procedures for AR report - dynamic filters are passed (cherry picked from commit e5920c57aaf7d14e54b2d0fd36cf8796ef200a4c) # Conflicts: # erpnext/accounts/report/accounts_receivable/accounts_receivable.py --- .../accounts_receivable.py | 181 ++++++++++++++++++ 1 file changed, 181 insertions(+) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index a87f359f564..db3e1656e5b 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -115,6 +115,14 @@ class ReceivablePayableReport: elif self.ple_fetch_method == "UnBuffered Cursor": self.fetch_ple_in_unbuffered_cursor() +<<<<<<< HEAD +======= + self.init_and_run_sql_procedures() + + # Build delivery note map against all sales invoices + self.build_delivery_note_map() + +>>>>>>> e5920c57aa (refactor: using sql procedures for AR report) self.build_data() def fetch_ple_in_buffered_cursor(self): @@ -299,6 +307,179 @@ class ReceivablePayableReport: row.paid -= amount row.paid_in_account_currency -= amount_in_account_currency + def init_and_run_sql_procedures(self): + # create in-memory temporary table for performance + frappe.db.sql("drop table if exists voucher_balance;") + # Memory storage engine doesn't support remarks - BLOB, TEXT types. + # Alternative? + frappe.db.sql( + """ + create temporary table voucher_balance( + name varchar(224), + voucher_type varchar(140), + voucher_no varchar(140), + party varchar(140), + party_account varchar(140), + posting_date date, + account_currency varchar(140), + invoiced decimal(21,9), + paid decimal(21,9), + credit_note decimal(21,9), + outstanding decimal(21,9), + invoiced_in_account_currency decimal(21,9), + paid_in_account_currency decimal(21,9), + credit_note_in_account_currency decimal(21,9), + outstanding_in_account_currency decimal(21,9)) engine=memory; + """ + ) + + # Only used for passing definitions to 'row type of' + frappe.db.sql("drop table if exists ple_row;") + frappe.db.sql( + """ + create temporary table ple_row( + name varchar(224), + account varchar(140), + voucher_type varchar(140), + voucher_no varchar(140), + against_voucher_type varchar(140), + against_voucher_no varchar(140), + party_type varchar(140), + cost_center varchar(140), + party varchar(140), + posting_date date, + due_date date, + account_currency varchar(140), + amount decimal(21,9), + amount_in_account_currency decimal(21,9)) engine=memory; + """ + ) + + # Generate hash from key + frappe.db.sql("drop function if exists genkey;") + frappe.db.sql( + """ + create function genkey(rec row type of ple_row, allocate bool) returns char(224) + begin + if allocate then + return sha2(concat_ws(',', rec.account, rec.against_voucher_type, rec.against_voucher_no, rec.party), 224); + else + return sha2(concat_ws(',', rec.account, rec.voucher_type, rec.voucher_no, rec.party), 224); + end if; + end + """ + ) + + # Init + frappe.db.sql("drop procedure if exists init;") + init_procedure = """ + create procedure init(in ple row type of `ple_row`) + begin + if not exists (select name from `voucher_balance` where name = genkey(ple, false)) + then + insert into `voucher_balance` values (genkey(ple, false), ple.voucher_type, ple.voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency, 0, 0, 0, 0, 0, 0, 0, 0); + end if; + end; + """ + frappe.db.sql(init_procedure) + + # Allocate + frappe.db.sql("drop procedure if exists allocate;") + allocate_procedure = """ + create procedure allocate(in ple row type of `ple_row`) + begin + declare invoiced decimal(21,9) default 0; + declare invoiced_in_account_currency decimal(21,9) default 0; + declare paid decimal(21,9) default 0; + declare paid_in_account_currency decimal(21,9) default 0; + declare credit_note decimal(21,9) default 0; + declare credit_note_in_account_currency decimal(21,9) default 0; + + + if ple.amount > 0 then + if (ple.voucher_type in ("Journal Entry", "Payment Entry") and (ple.voucher_no != ple.against_voucher_no)) then + set paid = -1 * ple.amount; + set paid_in_account_currency = -1 * ple.amount_in_account_currency; + else + set invoiced = ple.amount; + set invoiced_in_account_currency = ple.amount_in_account_currency; + end if; + else + + if ple.voucher_type in ("Sales Invoice", "Purchase Invoice") then + if (ple.voucher_no = ple.against_voucher_no) then + set paid = -1 * ple.amount; + set paid_in_account_currency = -1 * ple.amount_in_account_currency; + else + set credit_note = -1 * ple.amount; + set credit_note_in_account_currency = -1 * ple.amount_in_account_currency; + end if; + else + set paid = -1 * ple.amount; + set paid_in_account_currency = -1 * ple.amount_in_account_currency; + end if; + + end if; + + insert into `voucher_balance` values (genkey(ple, true), ple.against_voucher_type, ple.against_voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency, invoiced, paid, 0, 0, invoiced_in_account_currency, paid_in_account_currency, 0, 0); + end; + """ + frappe.db.sql(allocate_procedure) + + frappe.db.sql("drop procedure if exists build;") + build_balance = f""" + begin not atomic + declare done boolean default false; + declare rec1 row type of ple_row; + declare ple cursor for {self.ple_query.get_sql()}; + declare continue handler for not found set done = true; + + open ple; + fetch ple into rec1; + while not done do + call init(rec1); + fetch ple into rec1; + end while; + close ple; + + set done = false; + open ple; + fetch ple into rec1; + while not done do + call allocate(rec1); + fetch ple into rec1; + end while; + close ple; + end; + """ + frappe.db.sql(build_balance) + + res = frappe.db.sql( + """select + name, + voucher_type, + voucher_no, + party, + party_account, + posting_date, + account_currency, + sum(invoiced), + sum(paid), + sum(credit_note), + sum(invoiced) - sum(paid) - sum(credit_note), + sum(invoiced_in_account_currency), + sum(paid_in_account_currency), + sum(credit_note_in_account_currency), + sum(invoiced_in_account_currency) - sum(paid_in_account_currency) - sum(credit_note_in_account_currency) + from `voucher_balance` group by name;""" + ) + self.printv(res) + + def printv(self, res): + for x in res: + # if x[3] == "ACC-SINV-2025-00035": + print(x) + def update_sub_total_row(self, row, party): total_row = self.total_row_map.get(party) From 12eaa64572a247d9509acc973c0e9d047ccedabf Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 4 Jun 2025 11:56:09 +0530 Subject: [PATCH 02/13] refactor: better readability (cherry picked from commit 097e74979f68b2361335e5d348085946841fa0be) --- .../accounts_receivable.py | 264 ++++++++++-------- 1 file changed, 140 insertions(+), 124 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index db3e1656e5b..46424fcae11 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -308,136 +308,19 @@ class ReceivablePayableReport: row.paid_in_account_currency -= amount_in_account_currency def init_and_run_sql_procedures(self): - # create in-memory temporary table for performance - frappe.db.sql("drop table if exists voucher_balance;") - # Memory storage engine doesn't support remarks - BLOB, TEXT types. - # Alternative? - frappe.db.sql( - """ - create temporary table voucher_balance( - name varchar(224), - voucher_type varchar(140), - voucher_no varchar(140), - party varchar(140), - party_account varchar(140), - posting_date date, - account_currency varchar(140), - invoiced decimal(21,9), - paid decimal(21,9), - credit_note decimal(21,9), - outstanding decimal(21,9), - invoiced_in_account_currency decimal(21,9), - paid_in_account_currency decimal(21,9), - credit_note_in_account_currency decimal(21,9), - outstanding_in_account_currency decimal(21,9)) engine=memory; - """ - ) + self.proc = InitSQLProceduresForAR() - # Only used for passing definitions to 'row type of' - frappe.db.sql("drop table if exists ple_row;") - frappe.db.sql( - """ - create temporary table ple_row( - name varchar(224), - account varchar(140), - voucher_type varchar(140), - voucher_no varchar(140), - against_voucher_type varchar(140), - against_voucher_no varchar(140), - party_type varchar(140), - cost_center varchar(140), - party varchar(140), - posting_date date, - due_date date, - account_currency varchar(140), - amount decimal(21,9), - amount_in_account_currency decimal(21,9)) engine=memory; - """ - ) - - # Generate hash from key - frappe.db.sql("drop function if exists genkey;") - frappe.db.sql( - """ - create function genkey(rec row type of ple_row, allocate bool) returns char(224) - begin - if allocate then - return sha2(concat_ws(',', rec.account, rec.against_voucher_type, rec.against_voucher_no, rec.party), 224); - else - return sha2(concat_ws(',', rec.account, rec.voucher_type, rec.voucher_no, rec.party), 224); - end if; - end - """ - ) - - # Init - frappe.db.sql("drop procedure if exists init;") - init_procedure = """ - create procedure init(in ple row type of `ple_row`) - begin - if not exists (select name from `voucher_balance` where name = genkey(ple, false)) - then - insert into `voucher_balance` values (genkey(ple, false), ple.voucher_type, ple.voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency, 0, 0, 0, 0, 0, 0, 0, 0); - end if; - end; - """ - frappe.db.sql(init_procedure) - - # Allocate - frappe.db.sql("drop procedure if exists allocate;") - allocate_procedure = """ - create procedure allocate(in ple row type of `ple_row`) - begin - declare invoiced decimal(21,9) default 0; - declare invoiced_in_account_currency decimal(21,9) default 0; - declare paid decimal(21,9) default 0; - declare paid_in_account_currency decimal(21,9) default 0; - declare credit_note decimal(21,9) default 0; - declare credit_note_in_account_currency decimal(21,9) default 0; - - - if ple.amount > 0 then - if (ple.voucher_type in ("Journal Entry", "Payment Entry") and (ple.voucher_no != ple.against_voucher_no)) then - set paid = -1 * ple.amount; - set paid_in_account_currency = -1 * ple.amount_in_account_currency; - else - set invoiced = ple.amount; - set invoiced_in_account_currency = ple.amount_in_account_currency; - end if; - else - - if ple.voucher_type in ("Sales Invoice", "Purchase Invoice") then - if (ple.voucher_no = ple.against_voucher_no) then - set paid = -1 * ple.amount; - set paid_in_account_currency = -1 * ple.amount_in_account_currency; - else - set credit_note = -1 * ple.amount; - set credit_note_in_account_currency = -1 * ple.amount_in_account_currency; - end if; - else - set paid = -1 * ple.amount; - set paid_in_account_currency = -1 * ple.amount_in_account_currency; - end if; - - end if; - - insert into `voucher_balance` values (genkey(ple, true), ple.against_voucher_type, ple.against_voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency, invoiced, paid, 0, 0, invoiced_in_account_currency, paid_in_account_currency, 0, 0); - end; - """ - frappe.db.sql(allocate_procedure) - - frappe.db.sql("drop procedure if exists build;") build_balance = f""" begin not atomic declare done boolean default false; - declare rec1 row type of ple_row; + declare rec1 row type of `{self.proc._row_def_table_name}`; declare ple cursor for {self.ple_query.get_sql()}; declare continue handler for not found set done = true; open ple; fetch ple into rec1; while not done do - call init(rec1); + call {self.proc.init_procedure_name}(rec1); fetch ple into rec1; end while; close ple; @@ -446,7 +329,7 @@ class ReceivablePayableReport: open ple; fetch ple into rec1; while not done do - call allocate(rec1); + call {self.proc.allocate_procedure_name}(rec1); fetch ple into rec1; end while; close ple; @@ -455,7 +338,7 @@ class ReceivablePayableReport: frappe.db.sql(build_balance) res = frappe.db.sql( - """select + f"""select name, voucher_type, voucher_no, @@ -471,13 +354,12 @@ class ReceivablePayableReport: sum(paid_in_account_currency), sum(credit_note_in_account_currency), sum(invoiced_in_account_currency) - sum(paid_in_account_currency) - sum(credit_note_in_account_currency) - from `voucher_balance` group by name;""" + from `{self.proc._voucher_balance_name}` group by name;""" ) self.printv(res) def printv(self, res): for x in res: - # if x[3] == "ACC-SINV-2025-00035": print(x) def update_sub_total_row(self, row, party): @@ -1412,3 +1294,137 @@ def get_customer_group_with_children(customer_groups): frappe.throw(_("Customer Group: {0} does not exist").format(d)) return list(set(all_customer_groups)) + + +class InitSQLProceduresForAR: + """ + Initialize SQL Procedures, Functions and Temporary tables to build Receivable / Payable report + """ + + # Temporary Tables + _voucher_balance_name = "_voucher_balance" + _voucher_balance_definition = f""" + create temporary table `{_voucher_balance_name}`( + name varchar(224), + voucher_type varchar(140), + voucher_no varchar(140), + party varchar(140), + party_account varchar(140), + posting_date date, + account_currency varchar(140), + invoiced decimal(21,9), + paid decimal(21,9), + credit_note decimal(21,9), + outstanding decimal(21,9), + invoiced_in_account_currency decimal(21,9), + paid_in_account_currency decimal(21,9), + credit_note_in_account_currency decimal(21,9), + outstanding_in_account_currency decimal(21,9)) engine=memory; + """ + _row_def_table_name = "_ple_row" + _row_def_table_definition = f""" + create temporary table `{_row_def_table_name}`( + name varchar(224), + account varchar(140), + voucher_type varchar(140), + voucher_no varchar(140), + against_voucher_type varchar(140), + against_voucher_no varchar(140), + party_type varchar(140), + cost_center varchar(140), + party varchar(140), + posting_date date, + due_date date, + account_currency varchar(140), + amount decimal(21,9), + amount_in_account_currency decimal(21,9)) engine=memory; + """ + + # Function + genkey_function_name = "genkey" + genkey_function_sql = f""" + create function `{genkey_function_name}`(rec row type of `{_row_def_table_name}`, allocate bool) returns char(224) + begin + if allocate then + return sha2(concat_ws(',', rec.account, rec.against_voucher_type, rec.against_voucher_no, rec.party), 224); + else + return sha2(concat_ws(',', rec.account, rec.voucher_type, rec.voucher_no, rec.party), 224); + end if; + end + """ + + # Procedures + init_procedure_name = "ar_init_tmp_table" + init_procedure_sql = f""" + create procedure ar_init_tmp_table(in ple row type of `{_row_def_table_name}`) + begin + if not exists (select name from `{_voucher_balance_name}` where name = genkey(ple, false)) + then + insert into `{_voucher_balance_name}` values (genkey(ple, false), ple.voucher_type, ple.voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency, 0, 0, 0, 0, 0, 0, 0, 0); + end if; + end; + """ + + allocate_procedure_name = "ar_allocate_to_tmp_table" + allocate_procedure_sql = f""" + create procedure ar_allocate_to_tmp_table(in ple row type of `{_row_def_table_name}`) + begin + declare invoiced decimal(21,9) default 0; + declare invoiced_in_account_currency decimal(21,9) default 0; + declare paid decimal(21,9) default 0; + declare paid_in_account_currency decimal(21,9) default 0; + declare credit_note decimal(21,9) default 0; + declare credit_note_in_account_currency decimal(21,9) default 0; + + + if ple.amount > 0 then + if (ple.voucher_type in ("Journal Entry", "Payment Entry") and (ple.voucher_no != ple.against_voucher_no)) then + set paid = -1 * ple.amount; + set paid_in_account_currency = -1 * ple.amount_in_account_currency; + else + set invoiced = ple.amount; + set invoiced_in_account_currency = ple.amount_in_account_currency; + end if; + else + + if ple.voucher_type in ("Sales Invoice", "Purchase Invoice") then + if (ple.voucher_no = ple.against_voucher_no) then + set paid = -1 * ple.amount; + set paid_in_account_currency = -1 * ple.amount_in_account_currency; + else + set credit_note = -1 * ple.amount; + set credit_note_in_account_currency = -1 * ple.amount_in_account_currency; + end if; + else + set paid = -1 * ple.amount; + set paid_in_account_currency = -1 * ple.amount_in_account_currency; + end if; + + end if; + + insert into `{_voucher_balance_name}` values (`{genkey_function_name}`(ple, true), ple.against_voucher_type, ple.against_voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency, invoiced, paid, 0, 0, invoiced_in_account_currency, paid_in_account_currency, 0, 0); + end; + """ + + def __init__(self): + existing_procedures = frappe.db.sql( + f"select routine_name from information_schema.routines where routine_type in ('FUNCTION','PROCEDURE') and routine_schema='{frappe.conf.db_name}';" + ) + if existing_procedures: + # normalize + existing_procedures = [x[0] for x in existing_procedures] + + if self.genkey_function_name not in existing_procedures: + frappe.db.sql(self.genkey_function_sql) + + if self.init_procedure_name not in existing_procedures: + frappe.db.sql(self.init_procedure_sql) + + if self.allocate_procedure_name not in existing_procedures: + frappe.db.sql(self.allocate_procedure_sql) + + frappe.db.sql(f"drop table if exists `{self._voucher_balance_name}`") + frappe.db.sql(self._voucher_balance_definition) + + frappe.db.sql(f"drop table if exists `{self._row_def_table_name}`") + frappe.db.sql(self._row_def_table_definition) From de2f80811f4b77494628d34a52af2986b6a3b18b Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 4 Jun 2025 14:12:18 +0530 Subject: [PATCH 03/13] refactor: introduce sql option for data fetch (cherry picked from commit 8cf8f6abadc53132cc35ee00454994740aad0981) # Conflicts: # erpnext/accounts/doctype/accounts_settings/accounts_settings.py --- .../accounts_settings/accounts_settings.json | 2 +- .../accounts_settings/accounts_settings.py | 64 +++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index ce6af54323f..5c20f47749f 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -488,7 +488,7 @@ "fieldname": "receivable_payable_fetch_method", "fieldtype": "Select", "label": "Data Fetch Method", - "options": "Buffered Cursor\nUnBuffered Cursor" + "options": "Buffered Cursor\nUnBuffered Cursor\nRaw SQL" }, { "fieldname": "accounts_receivable_payable_tuning_section", diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py index ac3d44bb5e7..1244e19c96d 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py @@ -14,6 +14,70 @@ from erpnext.stock.utils import check_pending_reposting class AccountsSettings(Document): +<<<<<<< HEAD +======= + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + acc_frozen_upto: DF.Date | None + add_taxes_from_item_tax_template: DF.Check + add_taxes_from_taxes_and_charges_template: DF.Check + allow_multi_currency_invoices_against_single_party_account: DF.Check + allow_pegged_currencies_exchange_rates: DF.Check + allow_stale: DF.Check + auto_reconcile_payments: DF.Check + auto_reconciliation_job_trigger: DF.Int + automatically_fetch_payment_terms: DF.Check + automatically_process_deferred_accounting_entry: DF.Check + book_asset_depreciation_entry_automatically: DF.Check + book_deferred_entries_based_on: DF.Literal["Days", "Months"] + book_deferred_entries_via_journal_entry: DF.Check + book_tax_discount_loss: DF.Check + calculate_depr_using_total_days: DF.Check + check_supplier_invoice_uniqueness: DF.Check + confirm_before_resetting_posting_date: DF.Check + create_pr_in_draft_status: DF.Check + credit_controller: DF.Link | None + delete_linked_ledger_entries: DF.Check + determine_address_tax_category_from: DF.Literal["Billing Address", "Shipping Address"] + enable_common_party_accounting: DF.Check + enable_fuzzy_matching: DF.Check + enable_immutable_ledger: DF.Check + enable_party_matching: DF.Check + exchange_gain_loss_posting_date: DF.Literal["Invoice", "Payment", "Reconciliation Date"] + frozen_accounts_modifier: DF.Link | None + general_ledger_remarks_length: DF.Int + ignore_account_closing_balance: DF.Check + ignore_is_opening_check_for_reporting: DF.Check + maintain_same_internal_transaction_rate: DF.Check + maintain_same_rate_action: DF.Literal["Stop", "Warn"] + make_payment_via_journal_entry: DF.Check + merge_similar_account_heads: DF.Check + over_billing_allowance: DF.Currency + post_change_gl_entries: DF.Check + receivable_payable_fetch_method: DF.Literal["Buffered Cursor", "UnBuffered Cursor", "Raw SQL"] + receivable_payable_remarks_length: DF.Int + reconciliation_queue_size: DF.Int + role_allowed_to_over_bill: DF.Link | None + role_to_override_stop_action: DF.Link | None + round_row_wise_tax: DF.Check + show_balance_in_coa: DF.Check + show_inclusive_tax_in_print: DF.Check + show_payment_schedule_in_print: DF.Check + show_taxes_as_table_in_print: DF.Check + stale_days: DF.Int + submit_journal_entries: DF.Check + unlink_advance_payment_on_cancelation_of_order: DF.Check + unlink_payment_on_cancellation_of_invoice: DF.Check + use_new_budget_controller: DF.Check + # end: auto-generated types + +>>>>>>> 8cf8f6abad (refactor: introduce sql option for data fetch) def validate(self): old_doc = self.get_doc_before_save() clear_cache = False From dd1020ccfbc3fd432bec8fff95cc19348b3c5b57 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 4 Jun 2025 14:26:15 +0530 Subject: [PATCH 04/13] refactor: call procedures based on config (cherry picked from commit e90c6a33bd81227e610f051c3558bf29b6d979d9) # Conflicts: # erpnext/accounts/report/accounts_receivable/accounts_receivable.py --- .../accounts_receivable.py | 49 +++++++++++++------ 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 46424fcae11..eb5f480f5fe 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -114,10 +114,15 @@ class ReceivablePayableReport: self.fetch_ple_in_buffered_cursor() elif self.ple_fetch_method == "UnBuffered Cursor": self.fetch_ple_in_unbuffered_cursor() +<<<<<<< HEAD <<<<<<< HEAD ======= self.init_and_run_sql_procedures() +======= + elif self.ple_fetch_method == "Raw SQL": + self.init_and_run_sql_procedures() +>>>>>>> e90c6a33bd (refactor: call procedures based on config) # Build delivery note map against all sales invoices self.build_delivery_note_map() @@ -343,24 +348,40 @@ class ReceivablePayableReport: voucher_type, voucher_no, party, - party_account, + party_account `account`, posting_date, account_currency, - sum(invoiced), - sum(paid), - sum(credit_note), - sum(invoiced) - sum(paid) - sum(credit_note), - sum(invoiced_in_account_currency), - sum(paid_in_account_currency), - sum(credit_note_in_account_currency), - sum(invoiced_in_account_currency) - sum(paid_in_account_currency) - sum(credit_note_in_account_currency) - from `{self.proc._voucher_balance_name}` group by name;""" + sum(invoiced) `invoiced`, + sum(paid) `paid`, + sum(credit_note) `credit_note`, + sum(invoiced) - sum(paid) - sum(credit_note) `outstanding`, + sum(invoiced_in_account_currency) `invoiced_in_account_currency`, + sum(paid_in_account_currency) `paid_in_account_currency`, + sum(credit_note_in_account_currency) `credit_note_in_account_currency`, + sum(invoiced_in_account_currency) - sum(paid_in_account_currency) - sum(credit_note_in_account_currency) `outstanding_in_account_currency` + from `{self.proc._voucher_balance_name}` group by name;""", + as_dict=True, ) - self.printv(res) - - def printv(self, res): for x in res: - print(x) + if self.filters.get("ignore_accounts"): + key = (x.voucher_type, x.voucher_no, x.party) + else: + key = (x.account, x.voucher_type, x.voucher_no, x.party) + + _d = self.build_voucher_dict(x) + for field in [ + "invoiced", + "paid", + "credit_note", + "outstanding", + "invoiced_in_account_currency", + "paid_in_account_currency", + "credit_note_in_account_currency", + "outstanding_in_account_currency", + ]: + _d[field] = x.get(field) + + self.voucher_balance[key] = _d def update_sub_total_row(self, row, party): total_row = self.total_row_map.get(party) From ba25d334b1139914e8bc43e0ed5d20fd302a451c Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 4 Jun 2025 14:30:31 +0530 Subject: [PATCH 05/13] refactor: order by posting date (cherry picked from commit 7b7440d44a74a0d9aa035a2aed9f32a49f5ef5da) --- .../accounts/report/accounts_receivable/accounts_receivable.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index eb5f480f5fe..99608e58d25 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -359,7 +359,7 @@ class ReceivablePayableReport: sum(paid_in_account_currency) `paid_in_account_currency`, sum(credit_note_in_account_currency) `credit_note_in_account_currency`, sum(invoiced_in_account_currency) - sum(paid_in_account_currency) - sum(credit_note_in_account_currency) `outstanding_in_account_currency` - from `{self.proc._voucher_balance_name}` group by name;""", + from `{self.proc._voucher_balance_name}` group by name order by posting_date;""", as_dict=True, ) for x in res: From 006a8c71fd02969511f6124f5558306d97583502 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 4 Jun 2025 14:47:01 +0530 Subject: [PATCH 06/13] refactor: utility to drop existing procedures and include cost center (cherry picked from commit da32bb5f514783a9b8e350375d958ede39ca6b72) # Conflicts: # erpnext/accounts/doctype/accounts_settings/accounts_settings.js # erpnext/accounts/doctype/accounts_settings/accounts_settings.json # erpnext/accounts/doctype/accounts_settings/accounts_settings.py --- .../accounts_settings/accounts_settings.js | 40 ++++++++++ .../accounts_settings/accounts_settings.json | 80 +++++++++++++++++++ .../accounts_settings/accounts_settings.py | 35 ++++++++ .../accounts_receivable.py | 23 +++--- 4 files changed, 169 insertions(+), 9 deletions(-) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.js b/erpnext/accounts/doctype/accounts_settings/accounts_settings.js index 5b9a52e8f8b..74c623c8acb 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.js +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.js @@ -3,4 +3,44 @@ frappe.ui.form.on("Accounts Settings", { refresh: function (frm) {}, +<<<<<<< HEAD +======= + enable_immutable_ledger: function (frm) { + if (!frm.doc.enable_immutable_ledger) { + return; + } + + let msg = __("Enabling this will change the way how cancelled transactions are handled."); + msg += " "; + msg += __("Please enable only if the understand the effects of enabling this."); + msg += "
"; + msg += __("Do you still want to enable immutable ledger?"); + + frappe.confirm( + msg, + () => {}, + () => { + frm.set_value("enable_immutable_ledger", 0); + } + ); + }, + + add_taxes_from_taxes_and_charges_template(frm) { + toggle_tax_settings(frm, "add_taxes_from_taxes_and_charges_template"); + }, + + add_taxes_from_item_tax_template(frm) { + toggle_tax_settings(frm, "add_taxes_from_item_tax_template"); + }, + + drop_ar_procedures: function (frm) { + frm.call({ + doc: frm.doc, + method: "drop_ar_sql_procedures", + callback: function (r) { + frappe.show_alert(__("Procedures dropped"), 5); + }, + }); + }, +>>>>>>> da32bb5f51 (refactor: utility to drop existing procedures and include cost center) }); diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index 5c20f47749f..a2b75df6b5e 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -77,6 +77,8 @@ "receivable_payable_remarks_length", "accounts_receivable_payable_tuning_section", "receivable_payable_fetch_method", + "column_break_ntmi", + "drop_ar_procedures", "legacy_section", "ignore_is_opening_check_for_reporting" ], @@ -499,6 +501,84 @@ "fieldname": "legacy_section", "fieldtype": "Section Break", "label": "Legacy Fields" +<<<<<<< HEAD +======= + }, + { + "default": "0", + "fieldname": "maintain_same_internal_transaction_rate", + "fieldtype": "Check", + "label": "Maintain Same Rate Throughout Internal Transaction" + }, + { + "default": "Stop", + "depends_on": "maintain_same_internal_transaction_rate", + "fieldname": "maintain_same_rate_action", + "fieldtype": "Select", + "label": "Action if Same Rate is Not Maintained Throughout Internal Transaction", + "mandatory_depends_on": "maintain_same_internal_transaction_rate", + "options": "Stop\nWarn" + }, + { + "depends_on": "eval: doc.maintain_same_internal_transaction_rate && doc.maintain_same_rate_action == 'Stop'", + "fieldname": "role_to_override_stop_action", + "fieldtype": "Link", + "label": "Role Allowed to Override Stop Action", + "options": "Role" + }, + { + "fieldname": "budget_settings", + "fieldtype": "Tab Break", + "label": "Budget" + }, + { + "default": "1", + "fieldname": "use_new_budget_controller", + "fieldtype": "Check", + "label": "Use New Budget Controller" + }, + { + "default": "1", + "description": "If enabled, user will be alerted before resetting posting date to current date in relevant transactions", + "fieldname": "confirm_before_resetting_posting_date", + "fieldtype": "Check", + "label": "Confirm before resetting posting date" + }, + { + "fieldname": "item_price_settings_section", + "fieldtype": "Section Break", + "label": "Item Price Settings" + }, + { + "fieldname": "column_break_feyo", + "fieldtype": "Column Break" + }, + { + "default": "0", + "description": "System will do an implicit conversion using the pegged currency.
\nEx: Instead of AED -> INR, system will do AED -> USD -> INR using the pegged exchange rate of AED against USD.", + "documentation_url": "/app/pegged-currencies/Pegged Currencies", + "fieldname": "allow_pegged_currencies_exchange_rates", + "fieldtype": "Check", + "label": "Allow Implicit Pegged Currency Conversion" + }, + { + "default": "0", + "description": "If no taxes are set, and Taxes and Charges Template is selected, the system will automatically apply the taxes from the chosen template.", + "fieldname": "add_taxes_from_taxes_and_charges_template", + "fieldtype": "Check", + "label": "Automatically Add Taxes from Taxes and Charges Template" + }, + { + "fieldname": "column_break_ntmi", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval:doc.receivable_payable_fetch_method == \"Raw SQL\"", + "description": "Drops existing SQL Procedures and Function setup by Accounts Receivable report", + "fieldname": "drop_ar_procedures", + "fieldtype": "Button", + "label": "Drop Procedures" +>>>>>>> da32bb5f51 (refactor: utility to drop existing procedures and include cost center) } ], "icon": "icon-cog", diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py index 1244e19c96d..abfa0abc92f 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py @@ -129,3 +129,38 @@ class AccountsSettings(Document): def validate_pending_reposts(self): if self.acc_frozen_upto: check_pending_reposting(self.acc_frozen_upto) +<<<<<<< HEAD +======= + + def validate_and_sync_auto_reconcile_config(self): + if self.has_value_changed("auto_reconciliation_job_trigger"): + if ( + cint(self.auto_reconciliation_job_trigger) > 0 + and cint(self.auto_reconciliation_job_trigger) < 60 + ): + sync_auto_reconcile_config(self.auto_reconciliation_job_trigger) + else: + frappe.throw(_("Cron Interval should be between 1 and 59 Min")) + + if self.has_value_changed("reconciliation_queue_size"): + if cint(self.reconciliation_queue_size) < 5 or cint(self.reconciliation_queue_size) > 100: + frappe.throw(_("Queue Size should be between 5 and 100")) + + def validate_auto_tax_settings(self): + if self.add_taxes_from_item_tax_template and self.add_taxes_from_taxes_and_charges_template: + frappe.throw( + _("You cannot enable both the settings '{0}' and '{1}'.").format( + frappe.bold(_(self.meta.get_label("add_taxes_from_item_tax_template"))), + frappe.bold(_(self.meta.get_label("add_taxes_from_taxes_and_charges_template"))), + ), + title=_("Auto Tax Settings Error"), + ) + + @frappe.whitelist() + def drop_ar_sql_procedures(self): + from erpnext.accounts.report.accounts_receivable.accounts_receivable import InitSQLProceduresForAR + + frappe.db.sql(f"drop function if exists {InitSQLProceduresForAR.genkey_function_name}") + frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.init_procedure_name}") + frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.allocate_procedure_name}") +>>>>>>> da32bb5f51 (refactor: utility to drop existing procedures and include cost center) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 99608e58d25..b8f88f4a4d8 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -351,6 +351,7 @@ class ReceivablePayableReport: party_account `account`, posting_date, account_currency, + cost_center, sum(invoiced) `invoiced`, sum(paid) `paid`, sum(credit_note) `credit_note`, @@ -378,6 +379,7 @@ class ReceivablePayableReport: "paid_in_account_currency", "credit_note_in_account_currency", "outstanding_in_account_currency", + "cost_center", ]: _d[field] = x.get(field) @@ -1333,14 +1335,13 @@ class InitSQLProceduresForAR: party_account varchar(140), posting_date date, account_currency varchar(140), + cost_center varchar(140), invoiced decimal(21,9), paid decimal(21,9), credit_note decimal(21,9), - outstanding decimal(21,9), invoiced_in_account_currency decimal(21,9), paid_in_account_currency decimal(21,9), - credit_note_in_account_currency decimal(21,9), - outstanding_in_account_currency decimal(21,9)) engine=memory; + credit_note_in_account_currency decimal(21,9)) engine=memory; """ _row_def_table_name = "_ple_row" _row_def_table_definition = f""" @@ -1381,7 +1382,7 @@ class InitSQLProceduresForAR: begin if not exists (select name from `{_voucher_balance_name}` where name = genkey(ple, false)) then - insert into `{_voucher_balance_name}` values (genkey(ple, false), ple.voucher_type, ple.voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency, 0, 0, 0, 0, 0, 0, 0, 0); + insert into `{_voucher_balance_name}` values (genkey(ple, false), ple.voucher_type, ple.voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency, ple.cost_center, 0, 0, 0, 0, 0, 0); end if; end; """ @@ -1423,17 +1424,21 @@ class InitSQLProceduresForAR: end if; - insert into `{_voucher_balance_name}` values (`{genkey_function_name}`(ple, true), ple.against_voucher_type, ple.against_voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency, invoiced, paid, 0, 0, invoiced_in_account_currency, paid_in_account_currency, 0, 0); + insert into `{_voucher_balance_name}` values (`{genkey_function_name}`(ple, true), ple.against_voucher_type, ple.against_voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency,'', invoiced, paid, 0, invoiced_in_account_currency, paid_in_account_currency, 0); end; """ - def __init__(self): - existing_procedures = frappe.db.sql( + def get_existing_procedures(self): + procedures = frappe.db.sql( f"select routine_name from information_schema.routines where routine_type in ('FUNCTION','PROCEDURE') and routine_schema='{frappe.conf.db_name}';" ) - if existing_procedures: + if procedures: # normalize - existing_procedures = [x[0] for x in existing_procedures] + procedures = [x[0] for x in procedures] + return procedures + + def __init__(self): + existing_procedures = self.get_existing_procedures() if self.genkey_function_name not in existing_procedures: frappe.db.sql(self.genkey_function_sql) From 5188745c53ca4c47de7689370fdb5094ace14c12 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 5 Jun 2025 11:24:28 +0530 Subject: [PATCH 07/13] refactor: prefix-ed names for easy distinction (cherry picked from commit c5e35cc330b18534a32b5452e77e6adec67ff3e4) --- .../report/accounts_receivable/accounts_receivable.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index b8f88f4a4d8..3bd9891ce75 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -1325,7 +1325,7 @@ class InitSQLProceduresForAR: """ # Temporary Tables - _voucher_balance_name = "_voucher_balance" + _voucher_balance_name = "_ar_voucher_balance" _voucher_balance_definition = f""" create temporary table `{_voucher_balance_name}`( name varchar(224), @@ -1343,7 +1343,7 @@ class InitSQLProceduresForAR: paid_in_account_currency decimal(21,9), credit_note_in_account_currency decimal(21,9)) engine=memory; """ - _row_def_table_name = "_ple_row" + _row_def_table_name = "_ar_ple_row" _row_def_table_definition = f""" create temporary table `{_row_def_table_name}`( name varchar(224), @@ -1363,7 +1363,7 @@ class InitSQLProceduresForAR: """ # Function - genkey_function_name = "genkey" + genkey_function_name = "ar_genkey" genkey_function_sql = f""" create function `{genkey_function_name}`(rec row type of `{_row_def_table_name}`, allocate bool) returns char(224) begin @@ -1380,9 +1380,9 @@ class InitSQLProceduresForAR: init_procedure_sql = f""" create procedure ar_init_tmp_table(in ple row type of `{_row_def_table_name}`) begin - if not exists (select name from `{_voucher_balance_name}` where name = genkey(ple, false)) + if not exists (select name from `{_voucher_balance_name}` where name = `{genkey_function_name}`(ple, false)) then - insert into `{_voucher_balance_name}` values (genkey(ple, false), ple.voucher_type, ple.voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency, ple.cost_center, 0, 0, 0, 0, 0, 0); + insert into `{_voucher_balance_name}` values (`{genkey_function_name}`(ple, false), ple.voucher_type, ple.voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency, ple.cost_center, 0, 0, 0, 0, 0, 0); end if; end; """ From d7822f493b337910b5e546cec839488d62207acd Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 5 Jun 2025 11:29:48 +0530 Subject: [PATCH 08/13] refactor: better variable name (cherry picked from commit 1a90c0d031b01d462c87d8448e73af9e89df29a6) --- .../report/accounts_receivable/accounts_receivable.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 3bd9891ce75..ad512d69e1d 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -342,7 +342,7 @@ class ReceivablePayableReport: """ frappe.db.sql(build_balance) - res = frappe.db.sql( + balances = frappe.db.sql( f"""select name, voucher_type, @@ -363,7 +363,7 @@ class ReceivablePayableReport: from `{self.proc._voucher_balance_name}` group by name order by posting_date;""", as_dict=True, ) - for x in res: + for x in balances: if self.filters.get("ignore_accounts"): key = (x.voucher_type, x.voucher_no, x.party) else: From 5bcf642dff6d524ed13ff31e54af7c660c702b95 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 10 Jun 2025 14:49:03 +0530 Subject: [PATCH 09/13] refactor: dynamic DB field types (cherry picked from commit 9d0ebe3427b0a0bf7af686c02177e20649cbbbc5) # Conflicts: # erpnext/accounts/report/accounts_receivable/accounts_receivable.py --- .../accounts_receivable.py | 76 ++++++++++--------- 1 file changed, 42 insertions(+), 34 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index ad512d69e1d..f81efd2ca7d 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -6,6 +6,11 @@ from collections import OrderedDict import frappe from frappe import _, qb, query_builder, scrub +<<<<<<< HEAD +======= +from frappe.database.schema import get_definition +from frappe.desk.reportview import build_match_conditions +>>>>>>> 9d0ebe3427 (refactor: dynamic DB field types) from frappe.query_builder import Criterion from frappe.query_builder.functions import Date, Substring, Sum from frappe.utils import cint, cstr, flt, getdate, nowdate @@ -1324,53 +1329,56 @@ class InitSQLProceduresForAR: Initialize SQL Procedures, Functions and Temporary tables to build Receivable / Payable report """ + _varchar_type = get_definition("Data") + _currency_type = get_definition("Currency") # Temporary Tables _voucher_balance_name = "_ar_voucher_balance" _voucher_balance_definition = f""" create temporary table `{_voucher_balance_name}`( - name varchar(224), - voucher_type varchar(140), - voucher_no varchar(140), - party varchar(140), - party_account varchar(140), + name {_varchar_type}, + voucher_type {_varchar_type}, + voucher_no {_varchar_type}, + party {_varchar_type}, + party_account {_varchar_type}, posting_date date, - account_currency varchar(140), - cost_center varchar(140), - invoiced decimal(21,9), - paid decimal(21,9), - credit_note decimal(21,9), - invoiced_in_account_currency decimal(21,9), - paid_in_account_currency decimal(21,9), - credit_note_in_account_currency decimal(21,9)) engine=memory; + account_currency {_varchar_type}, + cost_center {_varchar_type}, + invoiced {_currency_type}, + paid {_currency_type}, + credit_note {_currency_type}, + invoiced_in_account_currency {_currency_type}, + paid_in_account_currency {_currency_type}, + credit_note_in_account_currency {_currency_type}) engine=memory; """ + _row_def_table_name = "_ar_ple_row" _row_def_table_definition = f""" create temporary table `{_row_def_table_name}`( - name varchar(224), - account varchar(140), - voucher_type varchar(140), - voucher_no varchar(140), - against_voucher_type varchar(140), - against_voucher_no varchar(140), - party_type varchar(140), - cost_center varchar(140), - party varchar(140), + name {_varchar_type}, + account {_varchar_type}, + voucher_type {_varchar_type}, + voucher_no {_varchar_type}, + against_voucher_type {_varchar_type}, + against_voucher_no {_varchar_type}, + party_type {_varchar_type}, + cost_center {_varchar_type}, + party {_varchar_type}, posting_date date, due_date date, - account_currency varchar(140), - amount decimal(21,9), - amount_in_account_currency decimal(21,9)) engine=memory; + account_currency {_varchar_type}, + amount {_currency_type}, + amount_in_account_currency {_currency_type}) engine=memory; """ # Function genkey_function_name = "ar_genkey" genkey_function_sql = f""" - create function `{genkey_function_name}`(rec row type of `{_row_def_table_name}`, allocate bool) returns char(224) + create function `{genkey_function_name}`(rec row type of `{_row_def_table_name}`, allocate bool) returns char(40) begin if allocate then - return sha2(concat_ws(',', rec.account, rec.against_voucher_type, rec.against_voucher_no, rec.party), 224); + return sha1(concat_ws(',', rec.account, rec.against_voucher_type, rec.against_voucher_no, rec.party)); else - return sha2(concat_ws(',', rec.account, rec.voucher_type, rec.voucher_no, rec.party), 224); + return sha1(concat_ws(',', rec.account, rec.voucher_type, rec.voucher_no, rec.party)); end if; end """ @@ -1391,12 +1399,12 @@ class InitSQLProceduresForAR: allocate_procedure_sql = f""" create procedure ar_allocate_to_tmp_table(in ple row type of `{_row_def_table_name}`) begin - declare invoiced decimal(21,9) default 0; - declare invoiced_in_account_currency decimal(21,9) default 0; - declare paid decimal(21,9) default 0; - declare paid_in_account_currency decimal(21,9) default 0; - declare credit_note decimal(21,9) default 0; - declare credit_note_in_account_currency decimal(21,9) default 0; + declare invoiced {_currency_type} default 0; + declare invoiced_in_account_currency {_currency_type} default 0; + declare paid {_currency_type} default 0; + declare paid_in_account_currency {_currency_type} default 0; + declare credit_note {_currency_type} default 0; + declare credit_note_in_account_currency {_currency_type} default 0; if ple.amount > 0 then From e533158a1c499949399d1072006a4ad51f415edd Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 3 Jul 2025 15:18:53 +0530 Subject: [PATCH 10/13] chore: drop unused utility method (cherry picked from commit 52c0df24e3caf58ac81e87dc91930a394983c041) --- .../report/accounts_receivable/accounts_receivable.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index f81efd2ca7d..b982b7f21d6 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -1436,17 +1436,8 @@ class InitSQLProceduresForAR: end; """ - def get_existing_procedures(self): - procedures = frappe.db.sql( - f"select routine_name from information_schema.routines where routine_type in ('FUNCTION','PROCEDURE') and routine_schema='{frappe.conf.db_name}';" - ) - if procedures: - # normalize - procedures = [x[0] for x in procedures] - return procedures - def __init__(self): - existing_procedures = self.get_existing_procedures() + existing_procedures = frappe.db.get_routines() if self.genkey_function_name not in existing_procedures: frappe.db.sql(self.genkey_function_sql) From 804edad233d9e18b3a73a13572692e06c457b778 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 3 Jul 2025 15:41:14 +0530 Subject: [PATCH 11/13] refactor: build and pass match conditions as qb criterion (cherry picked from commit 7efeed54de47a0b52b1a8081ea2aee7c2831277d) # Conflicts: # erpnext/accounts/report/accounts_receivable/accounts_receivable.py # erpnext/accounts/utils.py --- .../accounts_receivable.py | 27 ++++++++- erpnext/accounts/utils.py | 55 +++++++++++++++++++ 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index b982b7f21d6..f016ef848d9 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -9,8 +9,11 @@ from frappe import _, qb, query_builder, scrub <<<<<<< HEAD ======= from frappe.database.schema import get_definition +<<<<<<< HEAD from frappe.desk.reportview import build_match_conditions >>>>>>> 9d0ebe3427 (refactor: dynamic DB field types) +======= +>>>>>>> 7efeed54de (refactor: build and pass match conditions as qb criterion) from frappe.query_builder import Criterion from frappe.query_builder.functions import Date, Substring, Sum from frappe.utils import cint, cstr, flt, getdate, nowdate @@ -19,7 +22,16 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_accounting_dimensions, get_dimension_with_children, ) +<<<<<<< HEAD from erpnext.accounts.utils import get_currency_precision +======= +from erpnext.accounts.utils import ( + build_qb_match_conditions, + get_advance_payment_doctypes, + get_currency_precision, + get_party_types_from_account_type, +) +>>>>>>> 7efeed54de (refactor: build and pass match conditions as qb criterion) # This report gives a summary of all Outstanding Invoices considering the following @@ -136,8 +148,12 @@ class ReceivablePayableReport: self.build_data() def fetch_ple_in_buffered_cursor(self): +<<<<<<< HEAD query, param = self.ple_query.walk() self.ple_entries = frappe.db.sql(query, param, as_dict=True) +======= + self.ple_entries = self.ple_query.run(as_dict=True) +>>>>>>> 7efeed54de (refactor: build and pass match conditions as qb criterion) for ple in self.ple_entries: self.init_voucher_balance(ple) # invoiced, paid, credit_note, outstanding @@ -150,9 +166,12 @@ class ReceivablePayableReport: def fetch_ple_in_unbuffered_cursor(self): self.ple_entries = [] +<<<<<<< HEAD query, param = self.ple_query.walk() +======= +>>>>>>> 7efeed54de (refactor: build and pass match conditions as qb criterion) with frappe.db.unbuffered_cursor(): - for ple in frappe.db.sql(query, param, as_dict=True, as_iterator=True): + for ple in self.ple_query.run(as_dict=True, as_iterator=True): self.init_voucher_balance(ple) # invoiced, paid, credit_note, outstanding self.ple_entries.append(ple) @@ -930,6 +949,12 @@ class ReceivablePayableReport: else: query = query.select(ple.remarks) +<<<<<<< HEAD +======= + if match_conditions := build_qb_match_conditions("Payment Ledger Entry"): + query = query.where(Criterion.all(match_conditions)) + +>>>>>>> 7efeed54de (refactor: build and pass match conditions as qb criterion) if self.filters.get("group_by_party"): query = query.orderby(self.ple.party, self.ple.posting_date) else: diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index af760519054..9c0d0636525 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -8,6 +8,7 @@ from typing import TYPE_CHECKING, Optional import frappe import frappe.defaults from frappe import _, qb, throw +from frappe.desk.reportview import build_match_conditions from frappe.model.meta import get_field_precision from frappe.query_builder import AliasedQuery, Criterion, Table from frappe.query_builder.functions import Round, Sum @@ -2191,3 +2192,57 @@ def run_ledger_health_checks(): doc.general_and_payment_ledger_mismatch = True doc.checked_on = run_date doc.save() +<<<<<<< HEAD +======= + + +def sync_auto_reconcile_config(auto_reconciliation_job_trigger: int = 15): + auto_reconciliation_job_trigger = auto_reconciliation_job_trigger or frappe.get_single_value( + "Accounts Settings", "auto_reconciliation_job_trigger" + ) + method = "erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.trigger_reconciliation_for_queued_docs" + + sch_event = frappe.get_doc( + "Scheduler Event", {"scheduled_against": "Process Payment Reconciliation", "method": method} + ) + if frappe.db.get_value("Scheduled Job Type", {"method": method}): + frappe.get_doc( + "Scheduled Job Type", + { + "method": method, + }, + ).update( + { + "cron_format": f"0/{auto_reconciliation_job_trigger} * * * *", + "scheduler_event": sch_event.name, + } + ).save() + else: + frappe.get_doc( + { + "doctype": "Scheduled Job Type", + "method": method, + "scheduler_event": sch_event.name, + "cron_format": f"0/{auto_reconciliation_job_trigger} * * * *", + "create_log": True, + "stopped": False, + "frequency": "Cron", + } + ).save() + + +def build_qb_match_conditions(doctype, user=None) -> list: + match_filters = build_match_conditions(doctype, user, False) + criterion = [] + if match_filters: + from frappe import qb + + _dt = qb.DocType(doctype) + + for filter in match_filters: + for d, names in filter.items(): + fieldname = d.lower().replace(" ", "_") + criterion.append(_dt[fieldname].isin(names)) + + return criterion +>>>>>>> 7efeed54de (refactor: build and pass match conditions as qb criterion) From f364a4149057ddd23894584c8637aa3e98ab7386 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 9 Jul 2025 10:45:58 +0530 Subject: [PATCH 12/13] chore: rename method (cherry picked from commit fc8ca7d82cbc1bd68d9574fba20f8a608907804c) # Conflicts: # erpnext/accounts/report/accounts_receivable/accounts_receivable.py --- .../report/accounts_receivable/accounts_receivable.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index f016ef848d9..7bc44a2dd2a 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -138,8 +138,12 @@ class ReceivablePayableReport: self.init_and_run_sql_procedures() ======= elif self.ple_fetch_method == "Raw SQL": +<<<<<<< HEAD self.init_and_run_sql_procedures() >>>>>>> e90c6a33bd (refactor: call procedures based on config) +======= + self.fetch_ple_in_sql_procedures() +>>>>>>> fc8ca7d82c (chore: rename method) # Build delivery note map against all sales invoices self.build_delivery_note_map() @@ -336,7 +340,7 @@ class ReceivablePayableReport: row.paid -= amount row.paid_in_account_currency -= amount_in_account_currency - def init_and_run_sql_procedures(self): + def fetch_ple_in_sql_procedures(self): self.proc = InitSQLProceduresForAR() build_balance = f""" From ff998572e75b3e3043d0718f56375459b45ce7e2 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 15 Jul 2025 07:31:50 +0530 Subject: [PATCH 13/13] chore: resolve conflicts --- .../accounts_settings/accounts_settings.js | 30 ------ .../accounts_settings/accounts_settings.json | 67 -------------- .../accounts_settings/accounts_settings.py | 91 ------------------- .../accounts_receivable.py | 40 -------- erpnext/accounts/utils.py | 38 -------- 5 files changed, 266 deletions(-) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.js b/erpnext/accounts/doctype/accounts_settings/accounts_settings.js index 74c623c8acb..eaae180bdcd 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.js +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.js @@ -3,35 +3,6 @@ frappe.ui.form.on("Accounts Settings", { refresh: function (frm) {}, -<<<<<<< HEAD -======= - enable_immutable_ledger: function (frm) { - if (!frm.doc.enable_immutable_ledger) { - return; - } - - let msg = __("Enabling this will change the way how cancelled transactions are handled."); - msg += " "; - msg += __("Please enable only if the understand the effects of enabling this."); - msg += "
"; - msg += __("Do you still want to enable immutable ledger?"); - - frappe.confirm( - msg, - () => {}, - () => { - frm.set_value("enable_immutable_ledger", 0); - } - ); - }, - - add_taxes_from_taxes_and_charges_template(frm) { - toggle_tax_settings(frm, "add_taxes_from_taxes_and_charges_template"); - }, - - add_taxes_from_item_tax_template(frm) { - toggle_tax_settings(frm, "add_taxes_from_item_tax_template"); - }, drop_ar_procedures: function (frm) { frm.call({ @@ -42,5 +13,4 @@ frappe.ui.form.on("Accounts Settings", { }, }); }, ->>>>>>> da32bb5f51 (refactor: utility to drop existing procedures and include cost center) }); diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index a2b75df6b5e..6d9c56406d9 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -501,72 +501,6 @@ "fieldname": "legacy_section", "fieldtype": "Section Break", "label": "Legacy Fields" -<<<<<<< HEAD -======= - }, - { - "default": "0", - "fieldname": "maintain_same_internal_transaction_rate", - "fieldtype": "Check", - "label": "Maintain Same Rate Throughout Internal Transaction" - }, - { - "default": "Stop", - "depends_on": "maintain_same_internal_transaction_rate", - "fieldname": "maintain_same_rate_action", - "fieldtype": "Select", - "label": "Action if Same Rate is Not Maintained Throughout Internal Transaction", - "mandatory_depends_on": "maintain_same_internal_transaction_rate", - "options": "Stop\nWarn" - }, - { - "depends_on": "eval: doc.maintain_same_internal_transaction_rate && doc.maintain_same_rate_action == 'Stop'", - "fieldname": "role_to_override_stop_action", - "fieldtype": "Link", - "label": "Role Allowed to Override Stop Action", - "options": "Role" - }, - { - "fieldname": "budget_settings", - "fieldtype": "Tab Break", - "label": "Budget" - }, - { - "default": "1", - "fieldname": "use_new_budget_controller", - "fieldtype": "Check", - "label": "Use New Budget Controller" - }, - { - "default": "1", - "description": "If enabled, user will be alerted before resetting posting date to current date in relevant transactions", - "fieldname": "confirm_before_resetting_posting_date", - "fieldtype": "Check", - "label": "Confirm before resetting posting date" - }, - { - "fieldname": "item_price_settings_section", - "fieldtype": "Section Break", - "label": "Item Price Settings" - }, - { - "fieldname": "column_break_feyo", - "fieldtype": "Column Break" - }, - { - "default": "0", - "description": "System will do an implicit conversion using the pegged currency.
\nEx: Instead of AED -> INR, system will do AED -> USD -> INR using the pegged exchange rate of AED against USD.", - "documentation_url": "/app/pegged-currencies/Pegged Currencies", - "fieldname": "allow_pegged_currencies_exchange_rates", - "fieldtype": "Check", - "label": "Allow Implicit Pegged Currency Conversion" - }, - { - "default": "0", - "description": "If no taxes are set, and Taxes and Charges Template is selected, the system will automatically apply the taxes from the chosen template.", - "fieldname": "add_taxes_from_taxes_and_charges_template", - "fieldtype": "Check", - "label": "Automatically Add Taxes from Taxes and Charges Template" }, { "fieldname": "column_break_ntmi", @@ -578,7 +512,6 @@ "fieldname": "drop_ar_procedures", "fieldtype": "Button", "label": "Drop Procedures" ->>>>>>> da32bb5f51 (refactor: utility to drop existing procedures and include cost center) } ], "icon": "icon-cog", diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py index abfa0abc92f..326b0a32df5 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py @@ -14,70 +14,6 @@ from erpnext.stock.utils import check_pending_reposting class AccountsSettings(Document): -<<<<<<< HEAD -======= - # begin: auto-generated types - # This code is auto-generated. Do not modify anything in this block. - - from typing import TYPE_CHECKING - - if TYPE_CHECKING: - from frappe.types import DF - - acc_frozen_upto: DF.Date | None - add_taxes_from_item_tax_template: DF.Check - add_taxes_from_taxes_and_charges_template: DF.Check - allow_multi_currency_invoices_against_single_party_account: DF.Check - allow_pegged_currencies_exchange_rates: DF.Check - allow_stale: DF.Check - auto_reconcile_payments: DF.Check - auto_reconciliation_job_trigger: DF.Int - automatically_fetch_payment_terms: DF.Check - automatically_process_deferred_accounting_entry: DF.Check - book_asset_depreciation_entry_automatically: DF.Check - book_deferred_entries_based_on: DF.Literal["Days", "Months"] - book_deferred_entries_via_journal_entry: DF.Check - book_tax_discount_loss: DF.Check - calculate_depr_using_total_days: DF.Check - check_supplier_invoice_uniqueness: DF.Check - confirm_before_resetting_posting_date: DF.Check - create_pr_in_draft_status: DF.Check - credit_controller: DF.Link | None - delete_linked_ledger_entries: DF.Check - determine_address_tax_category_from: DF.Literal["Billing Address", "Shipping Address"] - enable_common_party_accounting: DF.Check - enable_fuzzy_matching: DF.Check - enable_immutable_ledger: DF.Check - enable_party_matching: DF.Check - exchange_gain_loss_posting_date: DF.Literal["Invoice", "Payment", "Reconciliation Date"] - frozen_accounts_modifier: DF.Link | None - general_ledger_remarks_length: DF.Int - ignore_account_closing_balance: DF.Check - ignore_is_opening_check_for_reporting: DF.Check - maintain_same_internal_transaction_rate: DF.Check - maintain_same_rate_action: DF.Literal["Stop", "Warn"] - make_payment_via_journal_entry: DF.Check - merge_similar_account_heads: DF.Check - over_billing_allowance: DF.Currency - post_change_gl_entries: DF.Check - receivable_payable_fetch_method: DF.Literal["Buffered Cursor", "UnBuffered Cursor", "Raw SQL"] - receivable_payable_remarks_length: DF.Int - reconciliation_queue_size: DF.Int - role_allowed_to_over_bill: DF.Link | None - role_to_override_stop_action: DF.Link | None - round_row_wise_tax: DF.Check - show_balance_in_coa: DF.Check - show_inclusive_tax_in_print: DF.Check - show_payment_schedule_in_print: DF.Check - show_taxes_as_table_in_print: DF.Check - stale_days: DF.Int - submit_journal_entries: DF.Check - unlink_advance_payment_on_cancelation_of_order: DF.Check - unlink_payment_on_cancellation_of_invoice: DF.Check - use_new_budget_controller: DF.Check - # end: auto-generated types - ->>>>>>> 8cf8f6abad (refactor: introduce sql option for data fetch) def validate(self): old_doc = self.get_doc_before_save() clear_cache = False @@ -129,32 +65,6 @@ class AccountsSettings(Document): def validate_pending_reposts(self): if self.acc_frozen_upto: check_pending_reposting(self.acc_frozen_upto) -<<<<<<< HEAD -======= - - def validate_and_sync_auto_reconcile_config(self): - if self.has_value_changed("auto_reconciliation_job_trigger"): - if ( - cint(self.auto_reconciliation_job_trigger) > 0 - and cint(self.auto_reconciliation_job_trigger) < 60 - ): - sync_auto_reconcile_config(self.auto_reconciliation_job_trigger) - else: - frappe.throw(_("Cron Interval should be between 1 and 59 Min")) - - if self.has_value_changed("reconciliation_queue_size"): - if cint(self.reconciliation_queue_size) < 5 or cint(self.reconciliation_queue_size) > 100: - frappe.throw(_("Queue Size should be between 5 and 100")) - - def validate_auto_tax_settings(self): - if self.add_taxes_from_item_tax_template and self.add_taxes_from_taxes_and_charges_template: - frappe.throw( - _("You cannot enable both the settings '{0}' and '{1}'.").format( - frappe.bold(_(self.meta.get_label("add_taxes_from_item_tax_template"))), - frappe.bold(_(self.meta.get_label("add_taxes_from_taxes_and_charges_template"))), - ), - title=_("Auto Tax Settings Error"), - ) @frappe.whitelist() def drop_ar_sql_procedures(self): @@ -163,4 +73,3 @@ class AccountsSettings(Document): frappe.db.sql(f"drop function if exists {InitSQLProceduresForAR.genkey_function_name}") frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.init_procedure_name}") frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.allocate_procedure_name}") ->>>>>>> da32bb5f51 (refactor: utility to drop existing procedures and include cost center) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 7bc44a2dd2a..b98923345c7 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -6,14 +6,7 @@ from collections import OrderedDict import frappe from frappe import _, qb, query_builder, scrub -<<<<<<< HEAD -======= from frappe.database.schema import get_definition -<<<<<<< HEAD -from frappe.desk.reportview import build_match_conditions ->>>>>>> 9d0ebe3427 (refactor: dynamic DB field types) -======= ->>>>>>> 7efeed54de (refactor: build and pass match conditions as qb criterion) from frappe.query_builder import Criterion from frappe.query_builder.functions import Date, Substring, Sum from frappe.utils import cint, cstr, flt, getdate, nowdate @@ -22,16 +15,10 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_accounting_dimensions, get_dimension_with_children, ) -<<<<<<< HEAD -from erpnext.accounts.utils import get_currency_precision -======= from erpnext.accounts.utils import ( build_qb_match_conditions, - get_advance_payment_doctypes, get_currency_precision, - get_party_types_from_account_type, ) ->>>>>>> 7efeed54de (refactor: build and pass match conditions as qb criterion) # This report gives a summary of all Outstanding Invoices considering the following @@ -108,9 +95,6 @@ class ReceivablePayableReport: def get_data(self): self.get_sales_invoices_or_customers_based_on_sales_person() - # Build delivery note map against all sales invoices - self.build_delivery_note_map() - # Get invoice details like bill_no, due_date etc for all invoices self.get_invoice_details() @@ -131,33 +115,16 @@ class ReceivablePayableReport: self.fetch_ple_in_buffered_cursor() elif self.ple_fetch_method == "UnBuffered Cursor": self.fetch_ple_in_unbuffered_cursor() -<<<<<<< HEAD - -<<<<<<< HEAD -======= - self.init_and_run_sql_procedures() -======= elif self.ple_fetch_method == "Raw SQL": -<<<<<<< HEAD - self.init_and_run_sql_procedures() ->>>>>>> e90c6a33bd (refactor: call procedures based on config) -======= self.fetch_ple_in_sql_procedures() ->>>>>>> fc8ca7d82c (chore: rename method) # Build delivery note map against all sales invoices self.build_delivery_note_map() ->>>>>>> e5920c57aa (refactor: using sql procedures for AR report) self.build_data() def fetch_ple_in_buffered_cursor(self): -<<<<<<< HEAD - query, param = self.ple_query.walk() - self.ple_entries = frappe.db.sql(query, param, as_dict=True) -======= self.ple_entries = self.ple_query.run(as_dict=True) ->>>>>>> 7efeed54de (refactor: build and pass match conditions as qb criterion) for ple in self.ple_entries: self.init_voucher_balance(ple) # invoiced, paid, credit_note, outstanding @@ -170,10 +137,6 @@ class ReceivablePayableReport: def fetch_ple_in_unbuffered_cursor(self): self.ple_entries = [] -<<<<<<< HEAD - query, param = self.ple_query.walk() -======= ->>>>>>> 7efeed54de (refactor: build and pass match conditions as qb criterion) with frappe.db.unbuffered_cursor(): for ple in self.ple_query.run(as_dict=True, as_iterator=True): self.init_voucher_balance(ple) # invoiced, paid, credit_note, outstanding @@ -953,12 +916,9 @@ class ReceivablePayableReport: else: query = query.select(ple.remarks) -<<<<<<< HEAD -======= if match_conditions := build_qb_match_conditions("Payment Ledger Entry"): query = query.where(Criterion.all(match_conditions)) ->>>>>>> 7efeed54de (refactor: build and pass match conditions as qb criterion) if self.filters.get("group_by_party"): query = query.orderby(self.ple.party, self.ple.posting_date) else: diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 9c0d0636525..04c95e7e376 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -2192,43 +2192,6 @@ def run_ledger_health_checks(): doc.general_and_payment_ledger_mismatch = True doc.checked_on = run_date doc.save() -<<<<<<< HEAD -======= - - -def sync_auto_reconcile_config(auto_reconciliation_job_trigger: int = 15): - auto_reconciliation_job_trigger = auto_reconciliation_job_trigger or frappe.get_single_value( - "Accounts Settings", "auto_reconciliation_job_trigger" - ) - method = "erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.trigger_reconciliation_for_queued_docs" - - sch_event = frappe.get_doc( - "Scheduler Event", {"scheduled_against": "Process Payment Reconciliation", "method": method} - ) - if frappe.db.get_value("Scheduled Job Type", {"method": method}): - frappe.get_doc( - "Scheduled Job Type", - { - "method": method, - }, - ).update( - { - "cron_format": f"0/{auto_reconciliation_job_trigger} * * * *", - "scheduler_event": sch_event.name, - } - ).save() - else: - frappe.get_doc( - { - "doctype": "Scheduled Job Type", - "method": method, - "scheduler_event": sch_event.name, - "cron_format": f"0/{auto_reconciliation_job_trigger} * * * *", - "create_log": True, - "stopped": False, - "frequency": "Cron", - } - ).save() def build_qb_match_conditions(doctype, user=None) -> list: @@ -2245,4 +2208,3 @@ def build_qb_match_conditions(doctype, user=None) -> list: criterion.append(_dt[fieldname].isin(names)) return criterion ->>>>>>> 7efeed54de (refactor: build and pass match conditions as qb criterion)