From e84e49345adf74cc0cb1a41a248c463feb0f0b0d Mon Sep 17 00:00:00 2001 From: ljain112 Date: Fri, 21 Mar 2025 15:18:18 +0530 Subject: [PATCH 1/5] perf: refactored customer ledger summary for performance --- .../customer_ledger_summary.py | 243 ++++++------------ 1 file changed, 83 insertions(+), 160 deletions(-) diff --git a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py index f11c4c24863..666d6049473 100644 --- a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py +++ b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py @@ -4,6 +4,7 @@ import frappe from frappe import _, qb, scrub +from frappe.query_builder import Criterion, Tuple from frappe.query_builder.functions import IfNull from frappe.utils import getdate, nowdate @@ -30,49 +31,82 @@ class PartyLedgerSummaryReport: self.filters.party_type = args.get("party_type") self.party_naming_by = frappe.db.get_single_value(args.get("naming_by")[0], args.get("naming_by")[1]) + self.get_paty_details() + + if not self.parties: + return [], [] + self.get_gl_entries() - self.get_additional_columns() self.get_return_invoices() self.get_party_adjustment_amounts() columns = self.get_columns() data = self.get_data() + return columns, data - def get_additional_columns(self): + def get_additional_fields(self): + additional_fields = [] + + if self.filters.party_type == "Customer": + additional_fields = ["customer_name", "territory", "customer_group", "default_sales_partner"] + else: + additional_fields = ["supplier_name", "supplier_group"] + + return additional_fields + + def prepare_party_conditions(self, doctype): + conditions = [] + group_field = "customer_group" if self.filters.party_type == "Customer" else "supplier_group" + + if self.filters.party: + conditions.append(doctype.name == self.filters.party) + + if self.filters.territory: + conditions.append(doctype.territory == self.filters.territory) + + if self.filters.get(group_field): + conditions.append(doctype.get(group_field) == self.filters.get(group_field)) + + if self.filters.payment_terms_template: + conditions.append(doctype.payment_terms == self.filters.payment_terms_template) + + if self.filters.sales_partner: + conditions.append(doctype.default_sales_partner == self.filters.sales_partner) + + if self.filters.sales_person: + sales_team = qb.DocType("Sales Team") + conditions.append( + (doctype.name).isin( + qb.from_(sales_team) + .select(sales_team.parent) + .where(sales_team.sales_person == self.filters.sales_person) + ) + ) + + return conditions + + def get_paty_details(self): """ Additional Columns for 'User Permission' based access control """ + self.parties = [] + self.party_details = frappe._dict() + party_type = self.filters.party_type + additional_fields = self.get_additional_fields() - if self.filters.party_type == "Customer": - self.territories = frappe._dict({}) - self.customer_group = frappe._dict({}) + doctype = qb.DocType(party_type) + conditions = self.prepare_party_conditions(doctype) + party_details = ( + qb.from_(doctype) + .select(doctype.name.as_("party"), *additional_fields) + .where(Criterion.all(conditions)) + .run(as_dict=True) + ) - customer = qb.DocType("Customer") - result = ( - frappe.qb.from_(customer) - .select( - customer.name, customer.territory, customer.customer_group, customer.default_sales_partner - ) - .where(customer.disabled == 0) - .run(as_dict=True) - ) - - for x in result: - self.territories[x.name] = x.territory - self.customer_group[x.name] = x.customer_group - else: - self.supplier_group = frappe._dict({}) - supplier = qb.DocType("Supplier") - result = ( - frappe.qb.from_(supplier) - .select(supplier.name, supplier.supplier_group) - .where(supplier.disabled == 0) - .run(as_dict=True) - ) - - for x in result: - self.supplier_group[x.name] = x.supplier_group + for row in party_details: + self.parties.append(row.party) + self.party_details[row.party] = row def get_columns(self): columns = [ @@ -195,12 +229,13 @@ class PartyLedgerSummaryReport: self.party_data = frappe._dict({}) for gle in self.gl_entries: + party_details = self.party_details.get(gle.party) self.party_data.setdefault( gle.party, frappe._dict( { - "party": gle.party, - "party_name": gle.party_name, + **party_details, + "party_name": gle.party, "opening_balance": 0, "invoiced_amount": 0, "paid_amount": 0, @@ -211,12 +246,6 @@ class PartyLedgerSummaryReport: ), ) - if self.filters.party_type == "Customer": - self.party_data[gle.party].update({"territory": self.territories.get(gle.party)}) - self.party_data[gle.party].update({"customer_group": self.customer_group.get(gle.party)}) - else: - self.party_data[gle.party].update({"supplier_group": self.supplier_group.get(gle.party)}) - amount = gle.get(invoice_dr_or_cr) - gle.get(reverse_dr_or_cr) self.party_data[gle.party].closing_balance += amount @@ -273,26 +302,12 @@ class PartyLedgerSummaryReport: & (gle.party_type == self.filters.party_type) & (IfNull(gle.party, "") != "") & (gle.posting_date <= self.filters.to_date) + & (gle.party.isin(self.parties)) ) - .orderby(gle.posting_date) ) - if self.filters.party_type == "Customer": - customer = qb.DocType("Customer") - query = ( - query.select(customer.customer_name.as_("party_name")) - .left_join(customer) - .on(customer.name == gle.party) - ) - elif self.filters.party_type == "Supplier": - supplier = qb.DocType("Supplier") - query = ( - query.select(supplier.supplier_name.as_("party_name")) - .left_join(supplier) - .on(supplier.name == gle.party) - ) - query = self.prepare_conditions(query) + self.gl_entries = query.run(as_dict=True) def prepare_conditions(self, query): @@ -303,68 +318,6 @@ class PartyLedgerSummaryReport: if self.filters.finance_book: query = query.where(IfNull(gle.finance_book, "") == self.filters.finance_book) - if self.filters.party: - query = query.where(gle.party == self.filters.party) - - if self.filters.party_type == "Customer": - customer = qb.DocType("Customer") - if self.filters.customer_group: - query = query.where( - (gle.party).isin( - qb.from_(customer) - .select(customer.name) - .where(customer.customer_group == self.filters.customer_group) - ) - ) - - if self.filters.territory: - query = query.where( - (gle.party).isin( - qb.from_(customer) - .select(customer.name) - .where(customer.territory == self.filters.territory) - ) - ) - - if self.filters.payment_terms_template: - query = query.where( - (gle.party).isin( - qb.from_(customer) - .select(customer.name) - .where(customer.payment_terms == self.filters.payment_terms_template) - ) - ) - - if self.filters.sales_partner: - query = query.where( - (gle.party).isin( - qb.from_(customer) - .select(customer.name) - .where(customer.default_sales_partner == self.filters.sales_partner) - ) - ) - - if self.filters.sales_person: - sales_team = qb.DocType("Sales Team") - query = query.where( - (gle.party).isin( - qb.from_(sales_team) - .select(sales_team.parent) - .where(sales_team.sales_person == self.filters.sales_person) - ) - ) - - if self.filters.party_type == "Supplier": - if self.filters.supplier_group: - supplier = qb.DocType("Supplier") - query = query.where( - (gle.party).isin( - qb.from_(supplier) - .select(supplier.name) - .where(supplier.supplier_group == self.filters.supplier_group) - ) - ) - if self.filters.cost_center: self.filters.cost_center = get_cost_centers_with_children(self.filters.cost_center) query = query.where((gle.cost_center).isin(self.filters.cost_center)) @@ -393,6 +346,7 @@ class PartyLedgerSummaryReport: def get_return_invoices(self): doctype = "Sales Invoice" if self.filters.party_type == "Customer" else "Purchase Invoice" + name_field = "customer" if self.filters.party_type == "Customer" else "supplier" self.return_invoices = [ d.name for d in frappe.get_all( @@ -401,37 +355,25 @@ class PartyLedgerSummaryReport: "is_return": 1, "docstatus": 1, "posting_date": ["between", [self.filters.from_date, self.filters.to_date]], + name_field: ["in", self.parties], }, ) ] def get_party_adjustment_amounts(self): account_type = "Expense Account" if self.filters.party_type == "Customer" else "Income Account" - self.income_or_expense_accounts = frappe.db.get_all( - "Account", filters={"account_type": account_type, "company": self.filters.company}, pluck="name" - ) + invoice_dr_or_cr = "debit" if self.filters.party_type == "Customer" else "credit" reverse_dr_or_cr = "credit" if self.filters.party_type == "Customer" else "debit" round_off_account = frappe.get_cached_value("Company", self.filters.company, "round_off_account") - if not self.income_or_expense_accounts: - # prevent empty 'in' condition - self.income_or_expense_accounts.append("") - else: - # escape '%' in account name - # ignoring frappe.db.escape as it replaces single quotes with double quotes - self.income_or_expense_accounts = [x.replace("%", "%%") for x in self.income_or_expense_accounts] + current_period_vouchers = set() + for gle in self.gl_entries: + if gle.posting_date >= self.filters.from_date and gle.posting_date <= self.filters.to_date: + current_period_vouchers.add((gle.voucher_type, gle.voucher_no)) gl = qb.DocType("GL Entry") - accounts_query = self.get_base_accounts_query() - accounts_query_voucher_no = accounts_query.select(gl.voucher_no) - accounts_query_voucher_type = accounts_query.select(gl.voucher_type) - - subquery = self.get_base_subquery() - subquery_voucher_no = subquery.select(gl.voucher_no) - subquery_voucher_type = subquery.select(gl.voucher_type) - - gl_entries = ( + query = ( qb.from_(gl) .select( gl.posting_date, gl.account, gl.party, gl.voucher_type, gl.voucher_no, gl.debit, gl.credit @@ -439,12 +381,13 @@ class PartyLedgerSummaryReport: .where( (gl.docstatus < 2) & (gl.is_cancelled == 0) - & (gl.voucher_no.isin(accounts_query_voucher_no)) - & (gl.voucher_type.isin(accounts_query_voucher_type)) - & (gl.voucher_no.isin(subquery_voucher_no)) - & (gl.voucher_type.isin(subquery_voucher_type)) + & (gl.posting_date.gte(self.filters.from_date)) + & (gl.posting_date.lte(self.filters.to_date)) + & (Tuple((gl.voucher_type, gl.voucher_no)).isin(current_period_vouchers)) ) - ).run(as_dict=True) + ) + query = self.prepare_conditions(query) + gl_entries = query.run(as_dict=True) self.party_adjustment_details = {} self.party_adjustment_accounts = set() @@ -486,26 +429,6 @@ class PartyLedgerSummaryReport: self.party_adjustment_details[party].setdefault(account, 0) self.party_adjustment_details[party][account] += amount - def get_base_accounts_query(self): - gl = qb.DocType("GL Entry") - query = qb.from_(gl).where( - (gl.account.isin(self.income_or_expense_accounts)) - & (gl.posting_date.gte(self.filters.from_date)) - & (gl.posting_date.lte(self.filters.to_date)) - ) - return query - - def get_base_subquery(self): - gl = qb.DocType("GL Entry") - query = qb.from_(gl).where( - (gl.docstatus < 2) - & (gl.party_type == self.filters.party_type) - & (IfNull(gl.party, "") != "") - & (gl.posting_date.between(self.filters.from_date, self.filters.to_date)) - ) - query = self.prepare_conditions(query) - return query - def execute(filters=None): args = { From fca46e0b2d2e1cdf8966b61443fe32fd410007f1 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Tue, 1 Apr 2025 17:18:01 +0530 Subject: [PATCH 2/5] fix: child values for tree doctypes and query refactor --- .../customer_ledger_summary.js | 1 + .../customer_ledger_summary.py | 175 +++++++++++------- 2 files changed, 105 insertions(+), 71 deletions(-) diff --git a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.js b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.js index ae34e338451..736dbed53d3 100644 --- a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.js +++ b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.js @@ -9,6 +9,7 @@ frappe.query_reports["Customer Ledger Summary"] = { fieldtype: "Link", options: "Company", default: frappe.defaults.get_user_default("Company"), + reqd: 1, }, { fieldname: "from_date", diff --git a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py index 666d6049473..56103b73dc2 100644 --- a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py +++ b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py @@ -7,12 +7,16 @@ from frappe import _, qb, scrub from frappe.query_builder import Criterion, Tuple from frappe.query_builder.functions import IfNull from frappe.utils import getdate, nowdate +from frappe.utils.nestedset import get_descendants_of from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_accounting_dimensions, get_dimension_with_children, ) -from erpnext.accounts.report.financial_statements import get_cost_centers_with_children + +TREE_DOCTYPES = frozenset( + ["Customer Group", "Terrirtory", "Supplier Group", "Sales Partner", "Sales Person", "Cost Center"] +) class PartyLedgerSummaryReport: @@ -21,16 +25,10 @@ class PartyLedgerSummaryReport: self.filters.from_date = getdate(self.filters.from_date or nowdate()) self.filters.to_date = getdate(self.filters.to_date or nowdate()) - if not self.filters.get("company"): - self.filters["company"] = frappe.db.get_single_value("Global Defaults", "default_company") - def run(self, args): - if self.filters.from_date > self.filters.to_date: - frappe.throw(_("From Date must be before To Date")) - self.filters.party_type = args.get("party_type") - self.party_naming_by = frappe.db.get_single_value(args.get("naming_by")[0], args.get("naming_by")[1]) + self.validate_filters() self.get_paty_details() if not self.parties: @@ -40,51 +38,26 @@ class PartyLedgerSummaryReport: self.get_return_invoices() self.get_party_adjustment_amounts() + self.party_naming_by = frappe.db.get_single_value(args.get("naming_by")[0], args.get("naming_by")[1]) columns = self.get_columns() data = self.get_data() return columns, data - def get_additional_fields(self): - additional_fields = [] + def validate_filters(self): + if not self.filters.get("company"): + frappe.throw(_("{0} is mandatory").format(_("Company"))) - if self.filters.party_type == "Customer": - additional_fields = ["customer_name", "territory", "customer_group", "default_sales_partner"] - else: - additional_fields = ["supplier_name", "supplier_group"] + if self.filters.from_date > self.filters.to_date: + frappe.throw(_("From Date must be before To Date")) - return additional_fields + self.update_hierarchical_filters() - def prepare_party_conditions(self, doctype): - conditions = [] - group_field = "customer_group" if self.filters.party_type == "Customer" else "supplier_group" - - if self.filters.party: - conditions.append(doctype.name == self.filters.party) - - if self.filters.territory: - conditions.append(doctype.territory == self.filters.territory) - - if self.filters.get(group_field): - conditions.append(doctype.get(group_field) == self.filters.get(group_field)) - - if self.filters.payment_terms_template: - conditions.append(doctype.payment_terms == self.filters.payment_terms_template) - - if self.filters.sales_partner: - conditions.append(doctype.default_sales_partner == self.filters.sales_partner) - - if self.filters.sales_person: - sales_team = qb.DocType("Sales Team") - conditions.append( - (doctype.name).isin( - qb.from_(sales_team) - .select(sales_team.parent) - .where(sales_team.sales_person == self.filters.sales_person) - ) - ) - - return conditions + def update_hierarchical_filters(self): + for doctype in TREE_DOCTYPES: + key = scrub(doctype) + if self.filters.get(key): + self.filters[key] = get_children(doctype, self.filters[key]) def get_paty_details(self): """ @@ -93,21 +66,70 @@ class PartyLedgerSummaryReport: self.parties = [] self.party_details = frappe._dict() party_type = self.filters.party_type - additional_fields = self.get_additional_fields() doctype = qb.DocType(party_type) - conditions = self.prepare_party_conditions(doctype) - party_details = ( + conditions = self.get_party_conditions(doctype) + query = ( qb.from_(doctype) - .select(doctype.name.as_("party"), *additional_fields) + .select(doctype.name.as_("party"), f"{scrub(party_type)}_name") .where(Criterion.all(conditions)) - .run(as_dict=True) ) + from frappe.desk.reportview import build_match_conditions + + query, params = query.walk() + match_conditions = build_match_conditions(party_type) + + if match_conditions: + query += "and" + match_conditions + + party_details = frappe.db.sql(query, params, as_dict=True) + for row in party_details: self.parties.append(row.party) self.party_details[row.party] = row + def get_party_conditions(self, doctype): + conditions = [] + group_field = "customer_group" if self.filters.party_type == "Customer" else "supplier_group" + + if self.filters.party: + conditions.append(doctype.name == self.filters.party) + + if self.filters.territory: + conditions.append(doctype.territory.isin(self.filters.territory)) + + if self.filters.get(group_field): + conditions.append(doctype.get(group_field).isin(self.filters.get(group_field))) + + if self.filters.payment_terms_template: + conditions.append(doctype.payment_terms == self.filters.payment_terms_template) + + if self.filters.sales_partner: + conditions.append(doctype.default_sales_partner.isin(self.filters.sales_partner)) + + if self.filters.sales_person: + sales_team = qb.DocType("Sales Team") + sales_invoice = qb.DocType("Sales Invoice") + + customers = ( + qb.from_(sales_team) + .select(sales_team.parent) + .where(sales_team.sales_person.isin(self.filters.sales_person)) + .where(sales_team.parenttype == "Customer") + ) + ( + qb.from_(sales_team) + .join(sales_invoice) + .on(sales_team.parent == sales_invoice.name) + .select(sales_invoice.customer) + .where(sales_team.sales_person.isin(self.filters.sales_person)) + .where(sales_team.parenttype == "Sales Invoice") + ) + + conditions.append(doctype.name.isin(customers)) + + return conditions + def get_columns(self): columns = [ { @@ -290,8 +312,6 @@ class PartyLedgerSummaryReport: gle.party, gle.voucher_type, gle.voucher_no, - gle.against_voucher_type, - gle.against_voucher, gle.debit, gle.credit, gle.is_opening, @@ -319,7 +339,6 @@ class PartyLedgerSummaryReport: query = query.where(IfNull(gle.finance_book, "") == self.filters.finance_book) if self.filters.cost_center: - self.filters.cost_center = get_cost_centers_with_children(self.filters.cost_center) query = query.where((gle.cost_center).isin(self.filters.cost_center)) if self.filters.project: @@ -346,19 +365,16 @@ class PartyLedgerSummaryReport: def get_return_invoices(self): doctype = "Sales Invoice" if self.filters.party_type == "Customer" else "Purchase Invoice" - name_field = "customer" if self.filters.party_type == "Customer" else "supplier" - self.return_invoices = [ - d.name - for d in frappe.get_all( - doctype, - filters={ - "is_return": 1, - "docstatus": 1, - "posting_date": ["between", [self.filters.from_date, self.filters.to_date]], - name_field: ["in", self.parties], - }, - ) - ] + filters = ( + { + "is_return": 1, + "docstatus": 1, + "posting_date": ["between", [self.filters.from_date, self.filters.to_date]], + f"{scrub(self.filters.party_type)}": ["in", self.parties], + }, + ) + + self.return_invoices = frappe.get_all(doctype, filters=filters, pluck="name") def get_party_adjustment_amounts(self): account_type = "Expense Account" if self.filters.party_type == "Customer" else "Income Account" @@ -368,9 +384,22 @@ class PartyLedgerSummaryReport: round_off_account = frappe.get_cached_value("Company", self.filters.company, "round_off_account") current_period_vouchers = set() + adjustment_voucher_entries = {} + + self.party_adjustment_details = {} + self.party_adjustment_accounts = set() + for gle in self.gl_entries: - if gle.posting_date >= self.filters.from_date and gle.posting_date <= self.filters.to_date: + if ( + gle.is_opening != "Yes" + and gle.posting_date >= self.filters.from_date + and gle.posting_date <= self.filters.to_date + ): current_period_vouchers.add((gle.voucher_type, gle.voucher_no)) + adjustment_voucher_entries.setdefault((gle.voucher_type, gle.voucher_no), []).append(gle) + + if not current_period_vouchers: + return gl = qb.DocType("GL Entry") query = ( @@ -384,16 +413,13 @@ class PartyLedgerSummaryReport: & (gl.posting_date.gte(self.filters.from_date)) & (gl.posting_date.lte(self.filters.to_date)) & (Tuple((gl.voucher_type, gl.voucher_no)).isin(current_period_vouchers)) + & (IfNull(gl.party, "") == "") ) ) query = self.prepare_conditions(query) gl_entries = query.run(as_dict=True) - self.party_adjustment_details = {} - self.party_adjustment_accounts = set() - adjustment_voucher_entries = {} for gle in gl_entries: - adjustment_voucher_entries.setdefault((gle.voucher_type, gle.voucher_no), []) adjustment_voucher_entries[(gle.voucher_type, gle.voucher_no)].append(gle) for voucher_gl_entries in adjustment_voucher_entries.values(): @@ -430,9 +456,16 @@ class PartyLedgerSummaryReport: self.party_adjustment_details[party][account] += amount +def get_children(doctype, value): + children = get_descendants_of(doctype, value) + + return [value, *children] + + def execute(filters=None): args = { "party_type": "Customer", "naming_by": ["Selling Settings", "cust_master_name"], } + return PartyLedgerSummaryReport(filters).run(args) From 038355f87b1950fd40546748d3528440ca461e26 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Tue, 1 Apr 2025 17:43:11 +0530 Subject: [PATCH 3/5] fix: correct function name --- .../report/customer_ledger_summary/customer_ledger_summary.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py index 56103b73dc2..1eabad82064 100644 --- a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py +++ b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py @@ -29,7 +29,7 @@ class PartyLedgerSummaryReport: self.filters.party_type = args.get("party_type") self.validate_filters() - self.get_paty_details() + self.get_party_details() if not self.parties: return [], [] @@ -59,7 +59,7 @@ class PartyLedgerSummaryReport: if self.filters.get(key): self.filters[key] = get_children(doctype, self.filters[key]) - def get_paty_details(self): + def get_party_details(self): """ Additional Columns for 'User Permission' based access control """ From 9a3a80dfd3012eecc5cf20349a5e4d07af6eb229 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 14 Apr 2025 16:47:29 +0530 Subject: [PATCH 4/5] test: basic output of customer ledger summary report --- .../test_customer_ledger_summary.py | 152 ++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 erpnext/accounts/report/customer_ledger_summary/test_customer_ledger_summary.py diff --git a/erpnext/accounts/report/customer_ledger_summary/test_customer_ledger_summary.py b/erpnext/accounts/report/customer_ledger_summary/test_customer_ledger_summary.py new file mode 100644 index 00000000000..66980d821e0 --- /dev/null +++ b/erpnext/accounts/report/customer_ledger_summary/test_customer_ledger_summary.py @@ -0,0 +1,152 @@ +import frappe +from frappe import qb +from frappe.tests import IntegrationTestCase +from frappe.utils import add_days, flt, getdate, today + +from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.accounts.report.customer_ledger_summary.customer_ledger_summary import execute +from erpnext.accounts.test.accounts_mixin import AccountsTestMixin + + +class TestCustomerLedgerSummary(AccountsTestMixin, IntegrationTestCase): + def setUp(self): + self.create_company() + self.create_customer() + self.create_item() + self.clear_old_entries() + + def tearDown(self): + frappe.db.rollback() + + def create_sales_invoice(self, do_not_submit=False, **args): + si = create_sales_invoice( + item=self.item, + company=self.company, + customer=self.customer, + debit_to=self.debit_to, + posting_date=today(), + parent_cost_center=self.cost_center, + cost_center=self.cost_center, + rate=100, + qty=10, + price_list_rate=100, + do_not_save=1, + **args, + ) + si = si.save() + if not do_not_submit: + si = si.submit() + return si + + def create_payment_entry(self, docname, do_not_submit=False): + pe = get_payment_entry("Sales Invoice", docname, bank_account=self.cash, party_amount=40) + pe.paid_from = self.debit_to + pe.insert() + if not do_not_submit: + pe.submit() + return pe + + def create_credit_note(self, docname, do_not_submit=False): + credit_note = create_sales_invoice( + company=self.company, + customer=self.customer, + item=self.item, + qty=-1, + debit_to=self.debit_to, + cost_center=self.cost_center, + is_return=1, + return_against=docname, + do_not_submit=do_not_submit, + ) + + return credit_note + + def test_ledger_summary_basic_output(self): + filters = {"company": self.company, "from_date": today(), "to_date": today()} + + si = self.create_sales_invoice(do_not_submit=True) + si.save().submit() + + expected = { + "party": "_Test Customer", + "party_name": "_Test Customer", + "opening_balance": 0, + "invoiced_amount": 1000.0, + "paid_amount": 0, + "return_amount": 0, + "closing_balance": 1000.0, + "currency": "INR", + "customer_name": "_Test Customer", + } + + report = execute(filters)[1] + self.assertEqual(len(report), 1) + for field in expected: + with self.subTest(field=field): + self.assertEqual(report[0].get(field), expected.get(field)) + + def test_summary_with_return_and_payment(self): + filters = {"company": self.company, "from_date": today(), "to_date": today()} + + si = self.create_sales_invoice(do_not_submit=True) + si.save().submit() + + expected = { + "party": "_Test Customer", + "party_name": "_Test Customer", + "opening_balance": 0, + "invoiced_amount": 1000.0, + "paid_amount": 0, + "return_amount": 0, + "closing_balance": 1000.0, + "currency": "INR", + "customer_name": "_Test Customer", + } + + report = execute(filters)[1] + self.assertEqual(len(report), 1) + for field in expected: + with self.subTest(field=field): + self.assertEqual(report[0].get(field), expected.get(field)) + + cr_note = self.create_credit_note(si.name, True) + cr_note.items[0].qty = -2 + cr_note.save().submit() + + expected_after_cr_note = { + "party": "_Test Customer", + "party_name": "_Test Customer", + "opening_balance": 0, + "invoiced_amount": 1000.0, + "paid_amount": 0, + "return_amount": 200.0, + "closing_balance": 800.0, + "currency": "INR", + } + report = execute(filters)[1] + self.assertEqual(len(report), 1) + for field in expected_after_cr_note: + with self.subTest(field=field): + self.assertEqual(report[0].get(field), expected_after_cr_note.get(field)) + + pe = self.create_payment_entry(si.name, True) + pe.paid_amount = 500 + pe.save().submit() + + expected_after_cr_and_payment = { + "party": "_Test Customer", + "party_name": "_Test Customer", + "opening_balance": 0, + "invoiced_amount": 1000.0, + "paid_amount": 500.0, + "return_amount": 200.0, + "closing_balance": 300.0, + "currency": "INR", + } + + report = execute(filters)[1] + self.assertEqual(len(report), 1) + for field in expected_after_cr_and_payment: + with self.subTest(field=field): + self.assertEqual(report[0].get(field), expected_after_cr_and_payment.get(field)) From 71f0f7a0b5656d271fbb20b2842738708cc208ce Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 14 Apr 2025 16:54:14 +0530 Subject: [PATCH 5/5] test: basic supplier ledger summary --- .../test_supplier_ledger_summary.py | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 erpnext/accounts/report/supplier_ledger_summary/test_supplier_ledger_summary.py diff --git a/erpnext/accounts/report/supplier_ledger_summary/test_supplier_ledger_summary.py b/erpnext/accounts/report/supplier_ledger_summary/test_supplier_ledger_summary.py new file mode 100644 index 00000000000..913f64ab38f --- /dev/null +++ b/erpnext/accounts/report/supplier_ledger_summary/test_supplier_ledger_summary.py @@ -0,0 +1,61 @@ +import frappe +from frappe.tests import IntegrationTestCase +from frappe.utils import today + +from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice +from erpnext.accounts.report.supplier_ledger_summary.supplier_ledger_summary import execute +from erpnext.accounts.test.accounts_mixin import AccountsTestMixin + + +class TestSupplierLedgerSummary(AccountsTestMixin, IntegrationTestCase): + def setUp(self): + self.create_company() + self.create_supplier() + self.create_item() + self.clear_old_entries() + + def tearDown(self): + frappe.db.rollback() + + def create_purchase_invoice(self, do_not_submit=False): + frappe.set_user("Administrator") + pi = make_purchase_invoice( + item=self.item, + company=self.company, + supplier=self.supplier, + is_return=False, + update_stock=False, + posting_date=frappe.utils.datetime.date(2021, 5, 1), + do_not_save=1, + rate=300, + price_list_rate=300, + qty=1, + ) + + pi = pi.save() + if not do_not_submit: + pi = pi.submit() + return pi + + def test_basic_supplier_ledger_summary(self): + self.create_purchase_invoice() + + filters = {"company": self.company, "from_date": today(), "to_date": today()} + + expected = { + "party": "_Test Supplier", + "party_name": "_Test Supplier", + "opening_balance": 0, + "invoiced_amount": 300.0, + "paid_amount": 0, + "return_amount": 0, + "closing_balance": 300.0, + "currency": "INR", + "supplier_name": "_Test Supplier", + } + + report_output = execute(filters)[1] + self.assertEqual(len(report_output), 1) + for field in expected: + with self.subTest(field=field): + self.assertEqual(report_output[0].get(field), expected.get(field))