From f853b5bb0155245150990f83a055b14b4ef3eb67 Mon Sep 17 00:00:00 2001 From: Ali <010109ali@gmail.com> Date: Thu, 31 Jul 2025 17:08:00 +0400 Subject: [PATCH] Added customer_outstanding_balance report --- .../customer_outstanding_balance/__init__.py | 0 .../customer_outstanding_balance.js | 42 ++++ .../customer_outstanding_balance.json | 30 +++ .../customer_outstanding_balance.py | 215 ++++++++++++++++++ 4 files changed, 287 insertions(+) create mode 100644 taxes_az/taxes_az/report/customer_outstanding_balance/__init__.py create mode 100644 taxes_az/taxes_az/report/customer_outstanding_balance/customer_outstanding_balance.js create mode 100644 taxes_az/taxes_az/report/customer_outstanding_balance/customer_outstanding_balance.json create mode 100644 taxes_az/taxes_az/report/customer_outstanding_balance/customer_outstanding_balance.py diff --git a/taxes_az/taxes_az/report/customer_outstanding_balance/__init__.py b/taxes_az/taxes_az/report/customer_outstanding_balance/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/taxes_az/taxes_az/report/customer_outstanding_balance/customer_outstanding_balance.js b/taxes_az/taxes_az/report/customer_outstanding_balance/customer_outstanding_balance.js new file mode 100644 index 0000000..9dd9ef3 --- /dev/null +++ b/taxes_az/taxes_az/report/customer_outstanding_balance/customer_outstanding_balance.js @@ -0,0 +1,42 @@ +// Copyright (c) 2024, Company and contributors +// For license information, please see license.txt + +frappe.query_reports["Customer Outstanding Balance"] = { + filters: [ + { + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), + reqd: 1 + }, + { + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", + default: frappe.datetime.get_today(), + reqd: 1 + } + ], + + formatter: function (value, row, column, data, default_formatter) { + value = default_formatter(value, row, column, data); + + // Highlight outstanding amount in red if overdue + if (column.fieldname == "outstanding" && data && data.outstanding > 0 && data.age > 30) { + value = "" + value + ""; + } + + // Highlight age in orange if over 60 days + if (column.fieldname == "age" && data && data.age > 60) { + value = "" + value + ""; + } + + // Bold formatting for total rows + if (data && data.bold) { + value = value.bold(); + } + + return value; + } +}; \ No newline at end of file diff --git a/taxes_az/taxes_az/report/customer_outstanding_balance/customer_outstanding_balance.json b/taxes_az/taxes_az/report/customer_outstanding_balance/customer_outstanding_balance.json new file mode 100644 index 0000000..5ff738c --- /dev/null +++ b/taxes_az/taxes_az/report/customer_outstanding_balance/customer_outstanding_balance.json @@ -0,0 +1,30 @@ +{ + "add_total_row": 1, + "columns": [], + "creation": "2025-07-31 16:21:17.649058", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "letterhead": null, + "modified": "2025-07-31 16:21:17.649058", + "modified_by": "Administrator", + "module": "Taxes Az", + "name": "Customer Outstanding Balance", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Sales Invoice", + "report_name": "Customer Outstanding Balance", + "report_type": "Script Report", + "roles": [ + { + "role": "Accounts User" + }, + { + "role": "Accounts Manager" + } + ], + "timeout": 0 +} \ No newline at end of file diff --git a/taxes_az/taxes_az/report/customer_outstanding_balance/customer_outstanding_balance.py b/taxes_az/taxes_az/report/customer_outstanding_balance/customer_outstanding_balance.py new file mode 100644 index 0000000..718a5af --- /dev/null +++ b/taxes_az/taxes_az/report/customer_outstanding_balance/customer_outstanding_balance.py @@ -0,0 +1,215 @@ +# Copyright (c) 2024, Company and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.utils import flt, getdate, nowdate, date_diff + +def execute(filters=None): + """Main function for report execution""" + columns = get_columns() + data = get_data(filters) + + return columns, data + +def get_columns(): + """Get report columns definition""" + return [ + { + "label": _("Posting Date"), + "fieldname": "posting_date", + "fieldtype": "Date", + "width": 100 + }, + { + "label": _("Party Type"), + "fieldname": "party_type", + "fieldtype": "Data", + "width": 100 + }, + { + "label": _("Party"), + "fieldname": "party", + "fieldtype": "Dynamic Link", + "options": "party_type", + "width": 180 + }, + { + "label": _("Party Account"), + "fieldname": "party_account", + "fieldtype": "Link", + "options": "Account", + "width": 180 + }, + { + "label": _("Customer Name"), + "fieldname": "customer_name", + "fieldtype": "Data", + "width": 150 + }, + { + "label": _("Voucher Type"), + "fieldname": "voucher_type", + "fieldtype": "Data", + "width": 120 + }, + { + "label": _("Voucher No"), + "fieldname": "voucher_no", + "fieldtype": "Dynamic Link", + "options": "voucher_type", + "width": 140 + }, + { + "label": _("Due Date"), + "fieldname": "due_date", + "fieldtype": "Date", + "width": 100 + }, + { + "label": _("Invoiced Amount"), + "fieldname": "invoiced", + "fieldtype": "Currency", + "width": 120 + }, + { + "label": _("Paid Amount"), + "fieldname": "paid", + "fieldtype": "Currency", + "width": 120 + }, + { + "label": _("Credit Note"), + "fieldname": "credit_note", + "fieldtype": "Currency", + "width": 120 + }, + { + "label": _("Outstanding Amount"), + "fieldname": "outstanding", + "fieldtype": "Currency", + "width": 120 + }, + { + "label": _("Age (Days)"), + "fieldname": "age", + "fieldtype": "Int", + "width": 80 + }, + { + "label": _("0-30"), + "fieldname": "range1", + "fieldtype": "Currency", + "width": 100 + }, + { + "label": _("30-60"), + "fieldname": "range2", + "fieldtype": "Currency", + "width": 100 + }, + { + "label": _("60-90"), + "fieldname": "range3", + "fieldtype": "Currency", + "width": 100 + }, + { + "label": _("90-120"), + "fieldname": "range4", + "fieldtype": "Currency", + "width": 100 + }, + { + "label": _("120-Above"), + "fieldname": "range5", + "fieldtype": "Currency", + "width": 100 + }, + { + "label": _("Currency"), + "fieldname": "currency", + "fieldtype": "Link", + "options": "Currency", + "width": 80 + } + ] + +def get_data(filters): + """Get report data""" + conditions = get_conditions(filters) + + # Get Payment Ledger Entries + data = frappe.db.sql(f""" + SELECT + ple.posting_date, + ple.party_type, + ple.party, + ple.account as party_account, + COALESCE(c.customer_name, s.supplier_name, ple.party) as customer_name, + ple.voucher_type, + ple.voucher_no, + ple.due_date, + ple.account_currency as currency, + SUM(CASE WHEN ple.amount > 0 THEN ple.amount ELSE 0 END) as invoiced, + SUM(CASE WHEN ple.amount < 0 THEN ABS(ple.amount) ELSE 0 END) as paid, + 0 as credit_note, + SUM(ple.amount) as outstanding + FROM + `tabPayment Ledger Entry` ple + LEFT JOIN + `tabCustomer` c ON c.name = ple.party AND ple.party_type = 'Customer' + LEFT JOIN + `tabSupplier` s ON s.name = ple.party AND ple.party_type = 'Supplier' + WHERE + ple.delinked = 0 + {conditions} + GROUP BY + ple.account, ple.voucher_type, ple.voucher_no, ple.party + HAVING + ABS(outstanding) >= 0.01 + ORDER BY + ple.posting_date DESC, ple.party + """, filters, as_dict=1) + + # Calculate age and age ranges + today = getdate(nowdate()) + + for row in data: + # Calculate age based on due date or posting date + age_date = row.due_date or row.posting_date + if age_date: + row.age = date_diff(today, age_date) + else: + row.age = 0 + + # Initialize age ranges + row.range1 = row.range2 = row.range3 = row.range4 = row.range5 = 0.0 + + # Allocate outstanding amount to appropriate age range + outstanding = flt(row.outstanding) + if outstanding > 0: + if row.age <= 30: + row.range1 = outstanding + elif row.age <= 60: + row.range2 = outstanding + elif row.age <= 90: + row.range3 = outstanding + elif row.age <= 120: + row.range4 = outstanding + else: + row.range5 = outstanding + + return data + +def get_conditions(filters): + """Build WHERE conditions based on filters""" + conditions = "" + + if filters.get("from_date"): + conditions += " AND ple.posting_date >= %(from_date)s" + + if filters.get("to_date"): + conditions += " AND ple.posting_date <= %(to_date)s" + + return conditions \ No newline at end of file