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
index 9dd9ef3..50e79d1 100644
--- 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
@@ -2,37 +2,45 @@
// For license information, please see license.txt
frappe.query_reports["Customer Outstanding Balance"] = {
- filters: [
+ "filters": [
{
- fieldname: "from_date",
- label: __("From Date"),
- fieldtype: "Date",
- default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
- reqd: 1
+ "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
+ "fieldname": "to_date",
+ "label": __("To Date"),
+ "fieldtype": "Date",
+ "default": frappe.datetime.get_today(),
+ "reqd": 1
+ },
+ {
+ "fieldname": "company",
+ "label": __("Company"),
+ "fieldtype": "Link",
+ "options": "Company",
+ "reqd": 1,
+ "default": frappe.defaults.get_user_default("Company")
}
],
- formatter: function (value, row, column, data, default_formatter) {
+ "formatter": function (value, row, column, data, default_formatter) {
value = default_formatter(value, row, column, data);
- // Highlight outstanding amount in red if overdue
+ // Highlight outstanding amounts in red if overdue
if (column.fieldname == "outstanding" && data && data.outstanding > 0 && data.age > 30) {
- value = "" + value + "";
+ value = "" + value + "";
}
- // Highlight age in orange if over 60 days
+ // Highlight age in orange if > 60 days
if (column.fieldname == "age" && data && data.age > 60) {
- value = "" + value + "";
+ value = "" + value + "";
}
-
- // Bold formatting for total rows
+
+ // Bold totals
if (data && data.bold) {
value = value.bold();
}
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
index 718a5af..55ad3b5 100644
--- 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
@@ -2,11 +2,19 @@
# For license information, please see license.txt
import frappe
-from frappe import _
-from frappe.utils import flt, getdate, nowdate, date_diff
+from frappe import _, qb
+from frappe.utils import cint, flt, getdate, nowdate
+from collections import OrderedDict
def execute(filters=None):
"""Main function for report execution"""
+ if not filters:
+ filters = {}
+
+ # Set default company if not provided
+ if not filters.get("company"):
+ filters["company"] = frappe.db.get_single_value("Global Defaults", "default_company")
+
columns = get_columns()
data = get_data(filters)
@@ -21,25 +29,12 @@ def get_columns():
"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
+ "options": "Customer",
+ "width": 150
},
{
"label": _("Customer Name"),
@@ -58,7 +53,7 @@ def get_columns():
"fieldname": "voucher_no",
"fieldtype": "Dynamic Link",
"options": "voucher_type",
- "width": 140
+ "width": 150
},
{
"label": _("Due Date"),
@@ -88,7 +83,7 @@ def get_columns():
"label": _("Outstanding Amount"),
"fieldname": "outstanding",
"fieldtype": "Currency",
- "width": 120
+ "width": 130
},
{
"label": _("Age (Days)"),
@@ -115,17 +110,11 @@ def get_columns():
"width": 100
},
{
- "label": _("90-120"),
+ "label": _("90+"),
"fieldname": "range4",
"fieldtype": "Currency",
"width": 100
},
- {
- "label": _("120-Above"),
- "fieldname": "range5",
- "fieldtype": "Currency",
- "width": 100
- },
{
"label": _("Currency"),
"fieldname": "currency",
@@ -137,79 +126,217 @@ def get_columns():
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)
+ ple_entries = get_ple_entries(filters)
- # Calculate age and age ranges
- today = getdate(nowdate())
+ # Build voucher balance
+ voucher_balance = build_voucher_balance(ple_entries)
- 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
+ # Get additional details
+ get_invoice_details(voucher_balance, filters)
+ get_customer_details(voucher_balance)
+
+ # Build final data
+ data = []
+ company_currency = frappe.get_cached_value("Company", filters.get("company"), "default_currency")
+
+ for key, row in voucher_balance.items():
+ # Calculate outstanding
+ row.outstanding = flt(row.invoiced - row.paid - row.credit_note, 2)
- # 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
+ # Only include rows with outstanding amount
+ if abs(row.outstanding) >= 0.01:
+ # Set aging
+ set_ageing(row, filters)
+
+ # Set currency
+ row.currency = company_currency
+
+ data.append(row)
+
+ # Sort by posting date and party
+ data = sorted(data, key=lambda x: (x.get('posting_date'), x.get('party')))
return data
-def get_conditions(filters):
- """Build WHERE conditions based on filters"""
- conditions = ""
+def get_ple_entries(filters):
+ """Get Payment Ledger Entries"""
+ ple = qb.DocType("Payment Ledger Entry")
+ # Build query conditions
+ conditions = [
+ ple.delinked == 0,
+ ple.company == filters.get("company"),
+ ple.party_type == "Customer" # Only customers for receivables
+ ]
+
+ # Date filter
if filters.get("from_date"):
- conditions += " AND ple.posting_date >= %(from_date)s"
-
+ conditions.append(ple.posting_date >= filters.get("from_date"))
if filters.get("to_date"):
- conditions += " AND ple.posting_date <= %(to_date)s"
+ conditions.append(ple.posting_date <= filters.get("to_date"))
- return conditions
\ No newline at end of file
+ # Get receivable accounts
+ receivable_accounts = frappe.db.get_all(
+ "Account",
+ filters={
+ "account_type": "Receivable",
+ "company": filters.get("company")
+ },
+ pluck="name"
+ )
+
+ if receivable_accounts:
+ conditions.append(ple.account.isin(receivable_accounts))
+
+ # Execute query
+ query = qb.from_(ple).select(
+ ple.account,
+ ple.voucher_type,
+ ple.voucher_no,
+ ple.against_voucher_type,
+ ple.against_voucher_no,
+ ple.party_type,
+ ple.party,
+ ple.posting_date,
+ ple.due_date,
+ ple.amount,
+ ple.account_currency
+ ).orderby(ple.posting_date, ple.party)
+
+ # Apply conditions
+ for condition in conditions:
+ query = query.where(condition)
+
+ return query.run(as_dict=True)
+
+def build_voucher_balance(ple_entries):
+ """Build voucher balance from PLE entries"""
+ voucher_balance = OrderedDict()
+
+ for ple in ple_entries:
+ # Create key for grouping - use against_voucher for payments
+ if ple.against_voucher_no and ple.voucher_no != ple.against_voucher_no:
+ # This is a payment against an invoice
+ key = (ple.account, ple.against_voucher_type, ple.against_voucher_no, ple.party)
+ voucher_type = ple.against_voucher_type
+ voucher_no = ple.against_voucher_no
+ else:
+ # This is an invoice or standalone entry
+ key = (ple.account, ple.voucher_type, ple.voucher_no, ple.party)
+ voucher_type = ple.voucher_type
+ voucher_no = ple.voucher_no
+
+ if key not in voucher_balance:
+ voucher_balance[key] = frappe._dict({
+ 'account': ple.account,
+ 'voucher_type': voucher_type,
+ 'voucher_no': voucher_no,
+ 'party': ple.party,
+ 'party_type': ple.party_type,
+ 'posting_date': ple.posting_date,
+ 'due_date': ple.due_date,
+ 'account_currency': ple.account_currency,
+ 'invoiced': 0.0,
+ 'paid': 0.0,
+ 'credit_note': 0.0,
+ 'outstanding': 0.0
+ })
+
+ row = voucher_balance[key]
+ amount = ple.amount
+
+ # Update balances based on transaction type
+ if amount > 0:
+ # Positive amount - invoice or debit entry
+ if ple.voucher_type in ["Journal Entry", "Payment Entry"] and ple.voucher_no != ple.against_voucher_no:
+ row.paid -= amount # Payment received against invoice
+ else:
+ row.invoiced += amount # Invoice amount
+ else:
+ # Negative amount - payment or credit note
+ if ple.voucher_type in ["Sales Invoice"] and ple.voucher_no == ple.against_voucher_no:
+ # Return/credit note against same invoice
+ row.credit_note -= amount
+ else:
+ # Payment made
+ row.paid -= amount
+
+ return voucher_balance
+
+def get_invoice_details(voucher_balance, filters):
+ """Get additional invoice details"""
+ invoice_names = []
+
+ for key, row in voucher_balance.items():
+ if row.voucher_type == "Sales Invoice":
+ invoice_names.append(row.voucher_no)
+
+ if invoice_names:
+ # Get invoice details
+ invoice_details = frappe.db.get_all(
+ "Sales Invoice",
+ filters={
+ "name": ["in", invoice_names],
+ "company": filters.get("company")
+ },
+ fields=["name", "due_date", "po_no"]
+ )
+
+ invoice_dict = {d.name: d for d in invoice_details}
+
+ # Update voucher balance with invoice details
+ for key, row in voucher_balance.items():
+ if row.voucher_type == "Sales Invoice" and row.voucher_no in invoice_dict:
+ details = invoice_dict[row.voucher_no]
+ if not row.due_date and details.due_date:
+ row.due_date = details.due_date
+ row.po_no = details.po_no
+
+def get_customer_details(voucher_balance):
+ """Get customer details"""
+ customers = set()
+
+ for key, row in voucher_balance.items():
+ if row.party_type == "Customer":
+ customers.add(row.party)
+
+ if customers:
+ customer_details = frappe.db.get_all(
+ "Customer",
+ filters={"name": ["in", list(customers)]},
+ fields=["name", "customer_name"]
+ )
+
+ customer_dict = {d.name: d for d in customer_details}
+
+ # Update voucher balance with customer details
+ for key, row in voucher_balance.items():
+ if row.party in customer_dict:
+ row.customer_name = customer_dict[row.party].customer_name
+
+def set_ageing(row, filters):
+ """Set aging buckets"""
+ # Initialize aging fields
+ row.age = 0
+ row.range1 = 0.0 # 0-30
+ row.range2 = 0.0 # 30-60
+ row.range3 = 0.0 # 60-90
+ row.range4 = 0.0 # 90+
+
+ # Use due date if available, otherwise posting date
+ entry_date = row.due_date or row.posting_date
+ age_as_on = getdate(filters.get("to_date") or nowdate())
+
+ if entry_date:
+ row.age = (age_as_on - getdate(entry_date)).days
+
+ # Assign to appropriate aging bucket
+ if row.age <= 30:
+ row.range1 = row.outstanding
+ elif row.age <= 60:
+ row.range2 = row.outstanding
+ elif row.age <= 90:
+ row.range3 = row.outstanding
+ else:
+ row.range4 = row.outstanding
\ No newline at end of file