Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,188 +2,197 @@
# For license information, please see license.txt

import frappe
from frappe.utils import getdate, nowdate

def execute(filters: dict | None = None):
if not filters:
filters = {}

columns = get_columns()
data = get_data(filters)

return columns, data

def get_columns() -> list[dict]:
return [
{"label": "Posting Date", "fieldname": "posting_date", "fieldtype": "Date", "width": 100},
{"label": "Customer", "fieldname": "customer", "fieldtype": "Link", "options": "Customer", "width": 200},
{"label": "Customer Group", "fieldname": "customer_group", "fieldtype": "Link", "options": "Customer Group", "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": 200},
{"label": "Due Date", "fieldname": "due_date", "fieldtype": "Date", "width": 100},
{"label": "Amount", "fieldname": "grand_total", "fieldtype": "Currency", "width": 150},
{"label": "Paid Amount", "fieldname": "paid_amount", "fieldtype": "Currency", "width": 150},
{"label": "Outstanding Amount", "fieldname": "outstanding_amount", "fieldtype": "Currency", "width": 180},
]

def get_conditions(filters: dict) -> tuple[str, list]:
conditions = []
vals = []

if filters.get("company"):
conditions.append("so.company = %s")
vals.append(filters.get("company"))

if filters.get("customer"):
conditions.append("so.customer = %s")
vals.append(filters.get("customer"))

if filters.get("customer_group"):
conditions.append("cust.customer_group = %s")
vals.append(filters.get("customer_group"))

return " AND ".join(conditions), vals

def get_data(filters: dict) -> list[dict]:
date_cond, date_vals = "", []
fd, td = filters.get("from_date"), filters.get("to_date")

if fd and td:
date_cond = "AND so.transaction_date BETWEEN %s AND %s"
date_vals.extend([fd, td])

conditions, vals = get_conditions(filters)

w_states = ["Proforma Invoice", "Invoiced"]
wf_cond = "AND so.workflow_state IN ({})".format(", ".join(["%s"] * len(w_states)))

bind_vals = w_states + date_vals + vals

sales_orders = frappe.db.sql(f"""
SELECT
so.transaction_date AS posting_date,
so.customer,
ps.due_date AS due_date,
so.name AS sales_order,
so.rounded_total AS so_rounded_total,
cust.customer_group,
MAX(CASE WHEN si.name IS NOT NULL THEN si.name ELSE NULL END) AS sales_invoice,
MAX(si.rounded_total) AS si_rounded_total,
MAX((
SELECT SUM(per.allocated_amount)
FROM `tabPayment Entry Reference` per
JOIN `tabPayment Entry` pe ON pe.name = per.parent
WHERE per.reference_doctype = 'Sales Invoice'
AND per.reference_name = si.name
AND pe.docstatus = 1
)) AS paid_amount

FROM `tabSales Order` so
LEFT JOIN `tabCustomer` cust ON cust.name = so.customer
LEFT JOIN `tabPayment Schedule` ps ON ps.parent = so.name
LEFT JOIN `tabSales Invoice Item` sii ON sii.sales_order = so.name
LEFT JOIN `tabSales Invoice` si ON si.name = sii.parent AND si.docstatus = 1

WHERE so.docstatus = 1
{wf_cond}
{date_cond}
{"AND " + conditions if conditions else ""}
GROUP BY so.name
""", bind_vals, as_dict=True)

result = []
for row in sales_orders:
if row.sales_invoice:
row.voucher_type = "Sales Invoice"
row.voucher_no = row.sales_invoice
row.grand_total = row.si_rounded_total or 0
else:
row.voucher_type = "Sales Order"
row.voucher_no = row.sales_order
row.grand_total = row.so_rounded_total or 0

row.paid_amount = row.paid_amount or 0
row.outstanding_amount = row.grand_total - row.paid_amount

result.append(row)
journal_entries = get_journal_entries(filters)
result.extend(journal_entries)
# Sort by posting date descending
result.sort(key=lambda x: x.posting_date, reverse=True)
return result
from frappe.utils import flt

def execute(filters=None):

if not filters:
filters = {}
columns = get_columns()
data = get_data(filters)

return columns, data

def get_columns():
return [
{"label": "Posting Date", "fieldname": "posting_date", "fieldtype": "Date", "width": 100},
{"label": "Customer", "fieldname": "customer", "fieldtype": "Link", "options": "Customer", "width": 200},
{"label": "Customer Group", "fieldname": "customer_group", "fieldtype": "Link", "options": "Customer Group", "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": 200},
{"label": "Due Date", "fieldname": "due_date", "fieldtype": "Date", "width": 100},
{"label": "Amount", "fieldname": "grand_total", "fieldtype": "Currency", "width": 150},
{"label": "Paid Amount", "fieldname": "paid_amount", "fieldtype": "Currency", "width": 150},
{"label": "Outstanding Amount", "fieldname": "outstanding_amount", "fieldtype": "Currency", "width": 180},
]

def get_conditions(filters):
conditions = []
vals = []

if filters.get("company"):
conditions.append("so.company = %s")
vals.append(filters.get("company"))

if filters.get("customer"):
conditions.append("so.customer = %s")
vals.append(filters.get("customer"))

if filters.get("customer_group"):
conditions.append("cust.customer_group = %s")
vals.append(filters.get("customer_group"))

return " AND ".join(conditions), vals


def get_data(filters):
date_cond, date_vals = "", []
if filters.get("from_date") and filters.get("to_date"):
date_cond = "AND so.transaction_date BETWEEN %s AND %s"
date_vals = [filters.get("from_date"), filters.get("to_date")]

conditions, vals = get_conditions(filters)

w_states = ["Proforma Invoice", "Invoiced"]
wf_cond = "AND so.workflow_state IN ({})".format(", ".join(["%s"] * len(w_states)))

bind_vals = w_states + date_vals + vals
sales_orders = frappe.db.sql(f"""
SELECT
so.transaction_date AS posting_date,
so.customer,
cust.customer_group,
ps.due_date,
so.name AS sales_order,
so.rounded_total AS so_total,
si.name AS sales_invoice,
si.rounded_total AS si_total

FROM `tabSales Order` so
LEFT JOIN `tabCustomer` cust ON cust.name = so.customer
LEFT JOIN `tabPayment Schedule` ps ON ps.parent = so.name
LEFT JOIN `tabSales Invoice Item` sii ON sii.sales_order = so.name
LEFT JOIN `tabSales Invoice` si
ON si.name = sii.parent AND si.docstatus = 1

WHERE so.docstatus = 1
{wf_cond}
{date_cond}
{"AND " + conditions if conditions else ""}
""", bind_vals, as_dict=True)

result = []

for row in sales_orders:

if row.sales_invoice:
voucher_type = "Sales Invoice"
voucher_no = row.sales_invoice
amount = flt(row.si_total)
else:
voucher_type = "Sales Order"
voucher_no = row.sales_order
amount = flt(row.so_total)

paid_amount = frappe.db.sql("""
SELECT SUM(ABS(amount))
FROM `tabPayment Ledger Entry`
WHERE against_voucher_no = %s
AND party = %s
AND amount < 0
""", (voucher_no, row.customer))[0][0] or 0

outstanding = amount - flt(paid_amount)

result.append({
"posting_date": row.posting_date,
"customer": row.customer,
"customer_group": row.customer_group,
"voucher_type": voucher_type,
"voucher_no": voucher_no,
"due_date": row.due_date,
"grand_total": amount,
"paid_amount": paid_amount,
"outstanding_amount": outstanding
})
result.extend(get_journal_entries(filters))
result.sort(key=lambda x: x.get("posting_date"), reverse=True)
return result

def get_journal_entries(filters):
"""
Returns filtered Journal Entry records linked to customers.
Includes Draft Journal Entries if checkbox is enabled.
Excludes fully paid Journal Entries.
Shows paid amount if partially paid.
"""
conditions = []
vals = []
if filters.get("include_draft_journal_entries"):
docstatus_condition = "je.docstatus IN (0, 1)"
else:
docstatus_condition = "je.docstatus = 1"

if filters.get("company"):
conditions.append("je.company = %s")
vals.append(filters["company"])

if filters.get("customer"):
conditions.append("jel.party = %s")
vals.append(filters["customer"])

if filters.get("from_date") and filters.get("to_date"):
conditions.append("je.posting_date BETWEEN %s AND %s")
vals.append(filters["from_date"])
vals.append(filters["to_date"])

where = " AND ".join(conditions)
if where:
where = "AND " + where

return frappe.db.sql(f"""
SELECT
je.posting_date,
jel.party AS customer,
cust.customer_group,
'Journal Entry' AS voucher_type,
je.name AS voucher_no,

(jel.debit - jel.credit) AS grand_total,

COALESCE((
SELECT SUM(per.allocated_amount)
FROM `tabPayment Entry Reference` per
JOIN `tabPayment Entry` pe ON pe.name = per.parent
WHERE per.reference_doctype = 'Journal Entry'
AND per.reference_name = je.name
AND pe.docstatus = 1
), 0) AS paid_amount,

(jel.debit - jel.credit) -
COALESCE((
SELECT SUM(per.allocated_amount)
FROM `tabPayment Entry Reference` per
JOIN `tabPayment Entry` pe ON pe.name = per.parent
WHERE per.reference_doctype = 'Journal Entry'
AND per.reference_name = je.name
AND pe.docstatus = 1
), 0) AS outstanding_amount,

NULL AS due_date

FROM `tabJournal Entry` je
JOIN `tabJournal Entry Account` jel
ON jel.parent = je.name
LEFT JOIN `tabCustomer` cust
ON cust.name = jel.party

WHERE {docstatus_condition}
AND jel.party_type = 'Customer'
{where}

GROUP BY je.name
HAVING outstanding_amount != 0
""", vals, as_dict=True)

conditions = []
vals = []

if filters.get("include_draft_journal_entries"):
docstatus_condition = "je.docstatus IN (0,1)"
else:
docstatus_condition = "je.docstatus = 1"

if filters.get("company"):
conditions.append("je.company = %s")
vals.append(filters["company"])

if filters.get("customer"):
conditions.append("jel.party = %s")
vals.append(filters["customer"])

if filters.get("from_date") and filters.get("to_date"):
conditions.append("je.posting_date BETWEEN %s AND %s")
vals.append(filters["from_date"])
vals.append(filters["to_date"])

where = " AND ".join(conditions)
if where:
where = "AND " + where

data = frappe.db.sql(f"""
SELECT
je.posting_date,
jel.party AS customer,
cust.customer_group,
'Journal Entry' AS voucher_type,
je.name AS voucher_no,
SUM(jel.debit - jel.credit) AS grand_total

FROM `tabJournal Entry` je
JOIN `tabJournal Entry Account` jel ON jel.parent = je.name
LEFT JOIN `tabCustomer` cust ON cust.name = jel.party

WHERE {docstatus_condition}
AND jel.party_type = 'Customer'
{where}

GROUP BY je.name
""", vals, as_dict=True)

result = []

for row in data:

paid_amount = frappe.db.sql("""
SELECT SUM(ABS(amount))
FROM `tabPayment Ledger Entry`
WHERE against_voucher_no = %s
AND party = %s
AND amount < 0
""", (row.voucher_no, row.customer))[0][0] or 0

outstanding = flt(row.grand_total) - flt(paid_amount)

if abs(outstanding) < 0.01:
continue

result.append({
"posting_date": row.posting_date,
"customer": row.customer,
"customer_group": row.customer_group,
"voucher_type": "Journal Entry",
"voucher_no": row.voucher_no,
"due_date": None,
"grand_total": row.grand_total,
"paid_amount": paid_amount,
"outstanding_amount": outstanding
})

return result