feat: Group by AR/AP report (#20573)
* feat: Group by AR/AP report
* fix: Do not consider total row in charts
* fix: Subtotal row for last party
diff --git a/erpnext/accounts/report/accounts_payable/accounts_payable.js b/erpnext/accounts/report/accounts_payable/accounts_payable.js
index b1f427c..df700ec 100644
--- a/erpnext/accounts/report/accounts_payable/accounts_payable.js
+++ b/erpnext/accounts/report/accounts_payable/accounts_payable.js
@@ -101,6 +101,11 @@
"options": "Supplier Group"
},
{
+ "fieldname": "group_by_party",
+ "label": __("Group By Supplier"),
+ "fieldtype": "Check"
+ },
+ {
"fieldname":"based_on_payment_terms",
"label": __("Based On Payment Terms"),
"fieldtype": "Check",
@@ -112,6 +117,16 @@
"hidden": 1
}
],
+
+ "formatter": function(value, row, column, data, default_formatter) {
+ value = default_formatter(value, row, column, data);
+ if (data && data.bold) {
+ value = value.bold();
+
+ }
+ return value;
+ },
+
onload: function(report) {
report.page.add_inner_button(__("Accounts Payable Summary"), function() {
var filters = report.get_values();
diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js
index 9b4dda2..5d0154f 100644
--- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js
+++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js
@@ -87,7 +87,7 @@
frappe.query_report.set_filter_value('payment_terms', value["payment_terms"]);
});
- frappe.db.get_value('Customer Credit Limit', {'parent': customer, 'company': company},
+ frappe.db.get_value('Customer Credit Limit', {'parent': customer, 'company': company},
["credit_limit"], function(value) {
if (value) {
frappe.query_report.set_filter_value('credit_limit', value["credit_limit"]);
@@ -132,6 +132,11 @@
"options": "Sales Person"
},
{
+ "fieldname": "group_by_party",
+ "label": __("Group By Customer"),
+ "fieldtype": "Check"
+ },
+ {
"fieldname":"based_on_payment_terms",
"label": __("Based On Payment Terms"),
"fieldtype": "Check",
@@ -177,6 +182,15 @@
}
],
+ "formatter": function(value, row, column, data, default_formatter) {
+ value = default_formatter(value, row, column, data);
+ if (data && data.bold) {
+ value = value.bold();
+
+ }
+ return value;
+ },
+
onload: function(report) {
report.page.add_inner_button(__("Accounts Receivable Summary"), function() {
var filters = report.get_values();
diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
index f82146a..0438f6d 100755
--- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
+++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
@@ -46,7 +46,7 @@
self.get_columns()
self.get_data()
self.get_chart_data()
- return self.columns, self.data, None, self.chart
+ return self.columns, self.data, None, self.chart, None, self.skip_total_row
def set_defaults(self):
if not self.filters.get("company"):
@@ -57,6 +57,12 @@
self.party_type = self.filters.party_type
self.party_details = {}
self.invoices = set()
+ self.skip_total_row = 0
+
+ if self.filters.get('group_by_party'):
+ self.previous_party=''
+ self.total_row_map = {}
+ self.skip_total_row = 1
def get_data(self):
self.get_gl_entries()
@@ -102,6 +108,12 @@
)
self.get_invoices(gle)
+ if self.filters.get('group_by_party'):
+ self.init_subtotal_row(gle.party)
+
+ if self.filters.get('group_by_party'):
+ self.init_subtotal_row('Total')
+
def get_invoices(self, gle):
if gle.voucher_type in ('Sales Invoice', 'Purchase Invoice'):
if self.filters.get("sales_person"):
@@ -111,6 +123,20 @@
else:
self.invoices.add(gle.voucher_no)
+ def init_subtotal_row(self, party):
+ if not self.total_row_map.get(party):
+ self.total_row_map.setdefault(party, {
+ 'party': party,
+ 'bold': 1
+ })
+
+ for field in self.get_currency_fields():
+ self.total_row_map[party][field] = 0.0
+
+ def get_currency_fields(self):
+ return ['invoiced', 'paid', 'credit_note', 'outstanding', 'range1',
+ 'range2', 'range3', 'range4', 'range5']
+
def update_voucher_balance(self, gle):
# get the row where this balance needs to be updated
# if its a payment, it will return the linked invoice or will be considered as advance
@@ -135,6 +161,18 @@
# advance / unlinked payment or other adjustment
row.paid -= gle_balance
+ def update_sub_total_row(self, row, party):
+ total_row = self.total_row_map.get(party)
+
+ for field in self.get_currency_fields():
+ total_row[field] += row.get(field, 0.0)
+
+ def append_subtotal_row(self, party):
+ sub_total_row = self.total_row_map.get(party)
+ self.data.append(sub_total_row)
+ self.data.append({})
+ self.update_sub_total_row(sub_total_row, 'Total')
+
def get_voucher_balance(self, gle):
if self.filters.get("sales_person"):
against_voucher = gle.against_voucher or gle.voucher_no
@@ -192,11 +230,22 @@
else:
self.append_row(row)
+ if self.filters.get('group_by_party'):
+ self.append_subtotal_row(self.previous_party)
+ self.data.append(self.total_row_map.get('Total'))
+
def append_row(self, row):
self.allocate_future_payments(row)
self.set_invoice_details(row)
self.set_party_details(row)
self.set_ageing(row)
+
+ if self.filters.get('group_by_party'):
+ self.update_sub_total_row(row, row.party)
+ if self.previous_party and (self.previous_party != row.party):
+ self.append_subtotal_row(self.previous_party)
+ self.previous_party = row.party
+
self.data.append(row)
def set_invoice_details(self, row):
@@ -503,6 +552,7 @@
# get all the GL entries filtered by the given filters
conditions, values = self.prepare_conditions()
+ order_by = self.get_order_by_condition()
if self.filters.get(scrub(self.party_type)):
select_fields = "debit_in_account_currency as debit, credit_in_account_currency as credit"
@@ -520,9 +570,8 @@
and party_type=%s
and (party is not null and party != '')
and posting_date <= %s
- {1}
- order by posting_date, party"""
- .format(select_fields, conditions), values, as_dict=True)
+ {1} {2}"""
+ .format(select_fields, conditions, order_by), values, as_dict=True)
def get_sales_invoices_or_customers_based_on_sales_person(self):
if self.filters.get("sales_person"):
@@ -557,6 +606,12 @@
return " and ".join(conditions), values
+ def get_order_by_condition(self):
+ if self.filters.get('group_by_party'):
+ return "order by party, posting_date"
+ else:
+ return "order by posting_date, party"
+
def add_common_filters(self, conditions, values, party_type_field):
if self.filters.company:
conditions.append("company=%s")
@@ -736,11 +791,13 @@
def get_chart_data(self):
rows = []
for row in self.data:
- values = [row.range1, row.range2, row.range3, row.range4, row.range5]
- precision = cint(frappe.db.get_default("float_precision")) or 2
- rows.append({
- 'values': [flt(val, precision) for val in values]
- })
+ row = frappe._dict(row)
+ if not cint(row.bold):
+ values = [row.range1, row.range2, row.range3, row.range4, row.range5]
+ precision = cint(frappe.db.get_default("float_precision")) or 2
+ rows.append({
+ 'values': [flt(val, precision) for val in values]
+ })
self.chart = {
"data": {