diff --git a/erpnext/accounts/doctype/account/account_tree.js b/erpnext/accounts/doctype/account/account_tree.js index 8ae90ceb38..d537adfcbf 100644 --- a/erpnext/accounts/doctype/account/account_tree.js +++ b/erpnext/accounts/doctype/account/account_tree.js @@ -56,36 +56,41 @@ frappe.treeview_settings["Account"] = { accounts = nodes; } - const get_balances = frappe.call({ - method: 'erpnext.accounts.utils.get_account_balances', - args: { - accounts: accounts, - company: cur_tree.args.company - }, - }); + frappe.db.get_single_value("Accounts Settings", "show_balance_in_coa").then((value) => { + if(value) { - get_balances.then(r => { - if (!r.message || r.message.length == 0) return; + const get_balances = frappe.call({ + method: 'erpnext.accounts.utils.get_account_balances', + args: { + accounts: accounts, + company: cur_tree.args.company + }, + }); - for (let account of r.message) { + get_balances.then(r => { + if (!r.message || r.message.length == 0) return; - const node = cur_tree.nodes && cur_tree.nodes[account.value]; - if (!node || node.is_root) continue; + for (let account of r.message) { - // show Dr if positive since balance is calculated as debit - credit else show Cr - const balance = account.balance_in_account_currency || account.balance; - const dr_or_cr = balance > 0 ? "Dr": "Cr"; - const format = (value, currency) => format_currency(Math.abs(value), currency); + const node = cur_tree.nodes && cur_tree.nodes[account.value]; + if (!node || node.is_root) continue; - if (account.balance!==undefined) { - node.parent && node.parent.find('.balance-area').remove(); - $('' - + (account.balance_in_account_currency ? - (format(account.balance_in_account_currency, account.account_currency) + " / ") : "") - + format(account.balance, account.company_currency) - + " " + dr_or_cr - + '').insertBefore(node.$ul); - } + // show Dr if positive since balance is calculated as debit - credit else show Cr + const balance = account.balance_in_account_currency || account.balance; + const dr_or_cr = balance > 0 ? "Dr": "Cr"; + const format = (value, currency) => format_currency(Math.abs(value), currency); + + if (account.balance!==undefined) { + node.parent && node.parent.find('.balance-area').remove(); + $('' + + (account.balance_in_account_currency ? + (format(account.balance_in_account_currency, account.account_currency) + " / ") : "") + + format(account.balance, account.company_currency) + + " " + dr_or_cr + + '').insertBefore(node.$ul); + } + } + }); } }); }, diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index 1e2e2acd79..3f985b640b 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -56,7 +56,9 @@ "acc_frozen_upto", "column_break_25", "frozen_accounts_modifier", - "report_settings_sb" + "report_settings_sb", + "tab_break_dpet", + "show_balance_in_coa" ], "fields": [ { @@ -347,6 +349,17 @@ "fieldname": "allow_multi_currency_invoices_against_single_party_account", "fieldtype": "Check", "label": "Allow multi-currency invoices against single party account " + }, + { + "fieldname": "tab_break_dpet", + "fieldtype": "Tab Break", + "label": "Chart Of Accounts" + }, + { + "default": "1", + "fieldname": "show_balance_in_coa", + "fieldtype": "Check", + "label": "Show Balances in Chart Of Accounts" } ], "icon": "icon-cog", @@ -354,7 +367,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2022-11-27 21:49:52.538655", + "modified": "2023-01-02 12:07:42.434214", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", diff --git a/erpnext/accounts/report/gross_profit/gross_profit.js b/erpnext/accounts/report/gross_profit/gross_profit.js index 615804ef62..e89d42977b 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.js +++ b/erpnext/accounts/report/gross_profit/gross_profit.js @@ -50,6 +50,20 @@ frappe.query_reports["Gross Profit"] = { "fieldtype": "Link", "options": "Sales Person" }, + { + "fieldname": "warehouse", + "label": __("Warehouse"), + "fieldtype": "Link", + "options": "Warehouse", + "get_query": function () { + var company = frappe.query_report.get_filter_value('company'); + return { + filters: [ + ["Warehouse", "company", "=", company] + ] + }; + }, + }, ], "tree": true, "name_field": "parent", diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index 25e7891a49..e23265b5e7 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -775,6 +775,13 @@ class GrossProfitGenerator(object): if self.filters.get("item_code"): conditions += " and `tabSales Invoice Item`.item_code = %(item_code)s" + if self.filters.get("warehouse"): + warehouse_details = frappe.db.get_value( + "Warehouse", self.filters.get("warehouse"), ["lft", "rgt"], as_dict=1 + ) + if warehouse_details: + conditions += f" and `tabSales Invoice Item`.warehouse in (select name from `tabWarehouse` wh where wh.lft >= {warehouse_details.lft} and wh.rgt <= {warehouse_details.rgt} and warehouse = wh.name)" + self.si_list = frappe.db.sql( """ select diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 8bd09982bf..9fcb769bc8 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -305,7 +305,7 @@ def get_returned_qty_map_for_row(return_against, party, row_name, doctype): fields += ["sum(abs(`tab{0}`.received_stock_qty)) as received_stock_qty".format(child_doctype)] # Used retrun against and supplier and is_retrun because there is an index added for it - data = frappe.db.get_list( + data = frappe.get_all( doctype, fields=fields, filters=[ diff --git a/erpnext/patches/v14_0/migrate_gl_to_payment_ledger.py b/erpnext/patches/v14_0/migrate_gl_to_payment_ledger.py index e15aa4a1f4..853a99a489 100644 --- a/erpnext/patches/v14_0/migrate_gl_to_payment_ledger.py +++ b/erpnext/patches/v14_0/migrate_gl_to_payment_ledger.py @@ -2,7 +2,8 @@ import frappe from frappe import qb from frappe.query_builder import Case, CustomFunction from frappe.query_builder.custom import ConstantColumn -from frappe.query_builder.functions import IfNull +from frappe.query_builder.functions import Count, IfNull +from frappe.utils import flt from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_dimensions, @@ -17,9 +18,9 @@ def create_accounting_dimension_fields(): make_dimension_in_accounting_doctypes(dimension, ["Payment Ledger Entry"]) -def generate_name_for_payment_ledger_entries(gl_entries): - for index, entry in enumerate(gl_entries, 1): - entry.name = index +def generate_name_for_payment_ledger_entries(gl_entries, start): + for index, entry in enumerate(gl_entries, 0): + entry.name = start + index def get_columns(): @@ -81,6 +82,14 @@ def insert_chunk_into_payment_ledger(insert_query, gl_entries): def execute(): + """ + Description: + Migrate records from `tabGL Entry` to `tabPayment Ledger Entry`. + Patch is non-resumable. if patch failed or is terminatted abnormally, clear 'tabPayment Ledger Entry' table manually before re-running. Re-running is safe only during V13->V14 update. + + Note: Post successful migration to V14, re-running is NOT-SAFE and SHOULD NOT be attempted. + """ + if frappe.reload_doc("accounts", "doctype", "payment_ledger_entry"): # create accounting dimension fields in Payment Ledger create_accounting_dimension_fields() @@ -89,52 +98,90 @@ def execute(): account = qb.DocType("Account") ifelse = CustomFunction("IF", ["condition", "then", "else"]) - gl_entries = ( - qb.from_(gl) - .inner_join(account) - .on((gl.account == account.name) & (account.account_type.isin(["Receivable", "Payable"]))) - .select( - gl.star, - ConstantColumn(1).as_("docstatus"), - account.account_type.as_("account_type"), - IfNull( - ifelse(gl.against_voucher_type == "", None, gl.against_voucher_type), gl.voucher_type - ).as_("against_voucher_type"), - IfNull(ifelse(gl.against_voucher == "", None, gl.against_voucher), gl.voucher_no).as_( - "against_voucher_no" - ), - # convert debit/credit to amount - Case() - .when(account.account_type == "Receivable", gl.debit - gl.credit) - .else_(gl.credit - gl.debit) - .as_("amount"), - # convert debit/credit in account currency to amount in account currency - Case() - .when( - account.account_type == "Receivable", - gl.debit_in_account_currency - gl.credit_in_account_currency, - ) - .else_(gl.credit_in_account_currency - gl.debit_in_account_currency) - .as_("amount_in_account_currency"), - ) - .where(gl.is_cancelled == 0) - .orderby(gl.creation) - .run(as_dict=True) + # Get Records Count + accounts = ( + qb.from_(account) + .select(account.name) + .where((account.account_type == "Receivable") | (account.account_type == "Payable")) + .orderby(account.name) ) + un_processed = ( + qb.from_(gl) + .select(Count(gl.name)) + .where((gl.is_cancelled == 0) & (gl.account.isin(accounts))) + .run() + )[0][0] - # primary key(name) for payment ledger records - generate_name_for_payment_ledger_entries(gl_entries) + if un_processed: + print(f"Migrating {un_processed} GL Entries to Payment Ledger") - # split data into chunks - chunk_size = 1000 - try: - for i in range(0, len(gl_entries), chunk_size): - insert_query = build_insert_query() - insert_chunk_into_payment_ledger(insert_query, gl_entries[i : i + chunk_size]) - frappe.db.commit() - except Exception as err: - frappe.db.rollback() - ple = qb.DocType("Payment Ledger Entry") - qb.from_(ple).delete().where(ple.docstatus >= 0).run() - frappe.db.commit() - raise err + processed = 0 + last_update_percent = 0 + batch_size = 5000 + last_name = None + + while True: + if last_name: + where_clause = gl.name.gt(last_name) & (gl.is_cancelled == 0) + else: + where_clause = gl.is_cancelled == 0 + + gl_entries = ( + qb.from_(gl) + .inner_join(account) + .on((gl.account == account.name) & (account.account_type.isin(["Receivable", "Payable"]))) + .select( + gl.star, + ConstantColumn(1).as_("docstatus"), + account.account_type.as_("account_type"), + IfNull( + ifelse(gl.against_voucher_type == "", None, gl.against_voucher_type), gl.voucher_type + ).as_("against_voucher_type"), + IfNull(ifelse(gl.against_voucher == "", None, gl.against_voucher), gl.voucher_no).as_( + "against_voucher_no" + ), + # convert debit/credit to amount + Case() + .when(account.account_type == "Receivable", gl.debit - gl.credit) + .else_(gl.credit - gl.debit) + .as_("amount"), + # convert debit/credit in account currency to amount in account currency + Case() + .when( + account.account_type == "Receivable", + gl.debit_in_account_currency - gl.credit_in_account_currency, + ) + .else_(gl.credit_in_account_currency - gl.debit_in_account_currency) + .as_("amount_in_account_currency"), + ) + .where(where_clause) + .orderby(gl.name) + .limit(batch_size) + .run(as_dict=True) + ) + + if gl_entries: + last_name = gl_entries[-1].name + + # primary key(name) for payment ledger records + generate_name_for_payment_ledger_entries(gl_entries, processed) + + try: + insert_query = build_insert_query() + insert_chunk_into_payment_ledger(insert_query, gl_entries) + frappe.db.commit() + + processed += len(gl_entries) + + # Progress message + percent = flt((processed / un_processed) * 100, 2) + if percent - last_update_percent > 1: + print(f"{percent}% ({processed}) records processed") + last_update_percent = percent + + except Exception as err: + print("Migration Failed. Clear `tabPayment Ledger Entry` table before re-running") + raise err + else: + break + print(f"{processed} records have been sucessfully migrated") diff --git a/erpnext/patches/v14_0/migrate_remarks_from_gl_to_payment_ledger.py b/erpnext/patches/v14_0/migrate_remarks_from_gl_to_payment_ledger.py index fd2a2a39cc..9d216c4028 100644 --- a/erpnext/patches/v14_0/migrate_remarks_from_gl_to_payment_ledger.py +++ b/erpnext/patches/v14_0/migrate_remarks_from_gl_to_payment_ledger.py @@ -1,81 +1,98 @@ import frappe from frappe import qb -from frappe.utils import create_batch - - -def remove_duplicate_entries(pl_entries): - unique_vouchers = set() - for x in pl_entries: - unique_vouchers.add( - (x.company, x.account, x.party_type, x.party, x.voucher_type, x.voucher_no, x.gle_remarks) - ) - - entries = [] - for x in unique_vouchers: - entries.append( - frappe._dict( - company=x[0], - account=x[1], - party_type=x[2], - party=x[3], - voucher_type=x[4], - voucher_no=x[5], - gle_remarks=x[6], - ) - ) - return entries +from frappe.query_builder import CustomFunction +from frappe.query_builder.functions import Count, IfNull +from frappe.utils import flt def execute(): + """ + Migrate 'remarks' field from 'tabGL Entry' to 'tabPayment Ledger Entry' + """ + if frappe.reload_doc("accounts", "doctype", "payment_ledger_entry"): gle = qb.DocType("GL Entry") ple = qb.DocType("Payment Ledger Entry") - # get ple and their remarks from GL Entry - pl_entries = ( - qb.from_(ple) - .left_join(gle) - .on( - (ple.account == gle.account) - & (ple.party_type == gle.party_type) - & (ple.party == gle.party) - & (ple.voucher_type == gle.voucher_type) - & (ple.voucher_no == gle.voucher_no) - & (ple.company == gle.company) - ) - .select( - ple.company, - ple.account, - ple.party_type, - ple.party, - ple.voucher_type, - ple.voucher_no, - gle.remarks.as_("gle_remarks"), - ) - .where((ple.delinked == 0) & (gle.is_cancelled == 0)) - .run(as_dict=True) - ) + # Get empty PLE records + un_processed = ( + qb.from_(ple).select(Count(ple.name)).where((ple.remarks.isnull()) & (ple.delinked == 0)).run() + )[0][0] - pl_entries = remove_duplicate_entries(pl_entries) + if un_processed: + print(f"Remarks for {un_processed} Payment Ledger records will be updated from GL Entry") - if pl_entries: - # split into multiple batches, update and commit for each batch + ifelse = CustomFunction("IF", ["condition", "then", "else"]) + + processed = 0 + last_percent_update = 0 batch_size = 1000 - for batch in create_batch(pl_entries, batch_size): - for entry in batch: - query = ( - qb.update(ple) - .set(ple.remarks, entry.gle_remarks) - .where( - (ple.company == entry.company) - & (ple.account == entry.account) - & (ple.party_type == entry.party_type) - & (ple.party == entry.party) - & (ple.voucher_type == entry.voucher_type) - & (ple.voucher_no == entry.voucher_no) - ) - ) - query.run() + last_name = None - frappe.db.commit() + while True: + if last_name: + where_clause = (ple.name.gt(last_name)) & (ple.remarks.isnull()) & (ple.delinked == 0) + else: + where_clause = (ple.remarks.isnull()) & (ple.delinked == 0) + + # results are deterministic + names = ( + qb.from_(ple).select(ple.name).where(where_clause).orderby(ple.name).limit(batch_size).run() + ) + + if names: + last_name = names[-1][0] + + pl_entries = ( + qb.from_(ple) + .left_join(gle) + .on( + (ple.account == gle.account) + & (ple.party_type == gle.party_type) + & (ple.party == gle.party) + & (ple.voucher_type == gle.voucher_type) + & (ple.voucher_no == gle.voucher_no) + & ( + ple.against_voucher_type + == IfNull( + ifelse(gle.against_voucher_type == "", None, gle.against_voucher_type), gle.voucher_type + ) + ) + & ( + ple.against_voucher_no + == IfNull(ifelse(gle.against_voucher == "", None, gle.against_voucher), gle.voucher_no) + ) + & (ple.company == gle.company) + & ( + ((ple.account_type == "Receivable") & (ple.amount == (gle.debit - gle.credit))) + | (ple.account_type == "Payable") & (ple.amount == (gle.credit - gle.debit)) + ) + & (gle.remarks.notnull()) + & (gle.is_cancelled == 0) + ) + .select(ple.name) + .distinct() + .select( + gle.remarks.as_("gle_remarks"), + ) + .where(ple.name.isin(names)) + .run(as_dict=True) + ) + + if pl_entries: + for entry in pl_entries: + query = qb.update(ple).set(ple.remarks, entry.gle_remarks).where((ple.name == entry.name)) + query.run() + + frappe.db.commit() + + processed += len(pl_entries) + percentage = flt((processed / un_processed) * 100, 2) + if percentage - last_percent_update > 1: + print(f"{percentage}% ({processed}) PLE records updated") + last_percent_update = percentage + + else: + break + print("Remarks succesfully migrated") diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py index 9f409d4b96..72654e6f81 100644 --- a/erpnext/stock/doctype/bin/bin.py +++ b/erpnext/stock/doctype/bin/bin.py @@ -159,13 +159,18 @@ def update_qty(bin_name, args): last_sle_qty = ( frappe.qb.from_(sle) .select(sle.qty_after_transaction) - .where((sle.item_code == args.get("item_code")) & (sle.warehouse == args.get("warehouse"))) + .where( + (sle.item_code == args.get("item_code")) + & (sle.warehouse == args.get("warehouse")) + & (sle.is_cancelled == 0) + ) .orderby(CombineDatetime(sle.posting_date, sle.posting_time), order=Order.desc) .orderby(sle.creation, order=Order.desc) .limit(1) .run() ) + actual_qty = 0.0 if last_sle_qty: actual_qty = last_sle_qty[0][0] diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 3739cb8c9d..af0d148325 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -888,7 +888,7 @@ def update_billing_percentage(pr_doc, update_modified=True): # Update Billing % based on pending accepted qty total_amount, total_billed_amount = 0, 0 for item in pr_doc.items: - return_data = frappe.db.get_list( + return_data = frappe.get_all( "Purchase Receipt", fields=["sum(abs(`tabPurchase Receipt Item`.qty)) as qty"], filters=[ diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 5d75bfd05a..d8b12ed5b9 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -1179,7 +1179,7 @@ def get_stock_ledger_entries( def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None): return frappe.db.get_value( "Stock Ledger Entry", - {"voucher_detail_no": voucher_detail_no, "name": ["!=", excluded_sle]}, + {"voucher_detail_no": voucher_detail_no, "name": ["!=", excluded_sle], "is_cancelled": 0}, [ "item_code", "warehouse",