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