Fixed customer_outstanding_balance report
This commit is contained in:
parent
f853b5bb01
commit
32fb26e379
|
|
@ -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 = "<span style='color: #d73527; font-weight: bold;'>" + value + "</span>";
|
||||
value = "<span style='color: #d32f2f; font-weight: bold;'>" + value + "</span>";
|
||||
}
|
||||
|
||||
// Highlight age in orange if over 60 days
|
||||
// Highlight age in orange if > 60 days
|
||||
if (column.fieldname == "age" && data && data.age > 60) {
|
||||
value = "<span style='color: #ff6600; font-weight: bold;'>" + value + "</span>";
|
||||
value = "<span style='color: #f57c00; font-weight: bold;'>" + value + "</span>";
|
||||
}
|
||||
|
||||
// Bold formatting for total rows
|
||||
// Bold totals
|
||||
if (data && data.bold) {
|
||||
value = value.bold();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
# Initialize age ranges
|
||||
row.range1 = row.range2 = row.range3 = row.range4 = row.range5 = 0.0
|
||||
# Build final data
|
||||
data = []
|
||||
company_currency = frappe.get_cached_value("Company", filters.get("company"), "default_currency")
|
||||
|
||||
# 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
|
||||
for key, row in voucher_balance.items():
|
||||
# Calculate outstanding
|
||||
row.outstanding = flt(row.invoiced - row.paid - row.credit_note, 2)
|
||||
|
||||
# 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
|
||||
# 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
|
||||
Loading…
Reference in New Issue