Invoice Reports
Track invoices, analyze payment patterns, and optimize accounts receivable
Invoice reports help you monitor outstanding balances, track payment performance, and identify collection issues. These examples demonstrate common invoice analysis patterns from status summaries to aging reports. Use these as templates for managing accounts receivable, forecasting cash flow, and optimizing collection strategies.
Invoice reports aggregate invoice data to reveal payment patterns and collection performance. Always consider the relationship between total, paid, and balance_due amounts. The status field indicates invoice lifecycle, but balance_due is the definitive indicator of outstanding amounts requiring collection.
Prerequisites
Before building invoice reports, understand:
Invoices by Status
Calculate invoice totals and outstanding balances grouped by status.
What this query does
This query groups invoices by their status (paid, open, draft, etc.) and calculates total invoice amounts, outstanding balances, and invoice counts for each status to provide a financial overview.
Query components:
| Component | Description |
|---|---|
| Select | Invoice status, total amounts, balance due, count |
| Group | Results by status field |
| Aggregates | Sum of totals and balance due |
| Output | Financial summary per status |
Query breakdown
results = (
pl.Invoice.select(
pl.attr.status, # Grouping dimension
pl.attr.totals.total.sum(), # Total invoice value
pl.attr.totals.balance_due.sum(), # Outstanding balance
pl.attr.id.count(), # Invoice count
)
.group_by(pl.attr.status) # Group by status
.order_by(pl.attr.status)
.all()
)Result structure:
[
{
"status": "paid",
"sum(totals.total)": 450000.0,
"sum(totals.balance_due)": 0.0,
"count(id)": 245
},
{
"status": "unpaid",
"sum(totals.total)": 125000.0,
"sum(totals.balance_due)": 125000.0,
"count(id)": 68
},
{
"status": "partially_paid",
"sum(totals.total)": 50000.0,
"sum(totals.balance_due)": 15000.0,
"count(id)": 22
}
]Analysis tips:
- Total AR = sum of balance_due for unpaid and partially_paid
- Collection rate = paid total / (paid + unpaid + partially_paid totals)
- Average invoice value = sum(totals.total) / count(id)
- Monitor draft count for billing backlog
Aging Report
Categorize outstanding invoices by how long they've been overdue.
What this query does
This query retrieves all open invoices with outstanding balances and their due dates, then calculates days overdue to categorize invoices into aging buckets (current, 1-30 days, 31-60 days, 60-90 days, 90+ days).
Query components:
| Component | Description |
|---|---|
| Select | Invoice details, balance due, due date |
| Filter | Open invoices with outstanding balances |
| Post-process | Calculate days overdue and categorize |
| Output | Invoices grouped by aging buckets |
Query breakdown
# Get all open invoices
aging_report = (
pl.Invoice.select(
pl.attr.id,
pl.attr.number,
pl.attr.payer.account_id,
pl.attr.totals.balance_due,
pl.attr.due_date,
pl.attr.status,
)
.filter_by(
pl.attr.status == "open|unpaid|partially_paid",
pl.attr.totals.balance_due > 0,
)
.order_by(pl.attr.due_date)
.all()
)
# Categorize by aging (application logic)
today = date.today()
for invoice in aging_report:
due = date.fromisoformat(str(invoice.due_date)[:10])
days_overdue = (today - due).days
# Assign to aging bucket based on days_overdueResult structure:
{
"current": {
"count": 45,
"total": 125000.0
},
"1-30_days": {
"count": 18,
"total": 35000.0
},
"31-60_days": {
"count": 8,
"total": 15000.0
},
"90+_days": {
"count": 5,
"total": 12000.0
}
}Analysis tips:
- Focus collection efforts on 30+ day bucket
- Monitor trends in aging distribution over time
- Calculate Days Sales Outstanding (DSO)
- Set bad debt reserves based on 90+ day bucket
Extended analysis
Calculate aging percentages:
total_ar = sum(invoice["totals.balance_due"] for invoice in aging_report)
for bucket_name, bucket_data in aging_buckets.items():
bucket_total = bucket_data["total"]
percentage = (bucket_total / total_ar) * 100
print(f"{bucket_name}: ${bucket_total:,.2f} ({percentage:.1f}%)")Days Sales Outstanding (DSO):
# DSO = (Accounts Receivable / Total Credit Sales) * Number of Days
dso = total_ar / average_daily_sales
print(f"Days Sales Outstanding: {dso:.1f} days")Monthly Billing Trends
Track invoice volume and values over time.
What this query does
This query groups invoices by year and month, calculating total billed amounts, collected amounts, outstanding balances, and invoice counts to track billing and collection trends over time.
Query components:
| Component | Description |
|---|---|
| Select | Year, month, total/paid/balance aggregates, count |
| Group | Results by year and month |
| Order | Chronologically (newest first) |
| Output | Monthly invoice summary |
Query breakdown
results = (
pl.Invoice.select(
pl.attr.created_at.year(),
pl.attr.created_at.month(),
pl.attr.totals.total.sum(), # Total billed
pl.attr.totals.paid.sum(), # Total collected
pl.attr.totals.balance_due.sum(), # Outstanding
pl.attr.id.count(), # Invoice count
)
.group_by(pl.attr.created_at.year(), pl.attr.created_at.month())
.order_by("desc(year(created_at))", "desc(month(created_at))")
.all()
)Result structure:
[
{
"year(created_at)": 2024,
"month(created_at)": 3,
"sum(totals.total)": 185000.0,
"sum(totals.paid)": 165000.0,
"sum(totals.balance_due)": 20000.0,
"count(id)": 92
},
{
"year(created_at)": 2024,
"month(created_at)": 2,
"sum(totals.total)": 172000.0,
"sum(totals.paid)": 168000.0,
"sum(totals.balance_due)": 4000.0,
"count(id)": 85
}
]Analysis tips:
- Collection rate = paid / total
- Growth rate = (current - previous) / previous
- Average invoice size = total / count
- Outstanding trend indicates collection issues
Overdue Invoices
Identify all invoices past their due date.
What this query does
This query filters invoices to those with open status, outstanding balances, and due dates in the past, ordered by due date to prioritize the oldest overdue invoices for collection efforts.
Query components:
| Component | Description |
|---|---|
| Select | Invoice details and outstanding balance |
| Filter | Open status, positive balance, past due date |
| Order | By due date (oldest first) |
| Output | List of overdue invoices |
Query breakdown
today = datetime.now().date()
overdue = (
pl.Invoice.select(
pl.attr.id,
pl.attr.number,
pl.attr.payer.account_id,
pl.attr.totals.balance_due,
pl.attr.due_date,
pl.attr.created_at,
)
.filter_by(
pl.attr.status == "open|unpaid|partially_paid",
pl.attr.totals.balance_due > 0,
pl.attr.due_date < str(today), # Past due date
)
.order_by(pl.attr.due_date) # Oldest first
.all()
)Result structure:
[
{
"id": "inv_abc123",
"number": "INV-2024-001",
"payer.account_id": "acct_customer1",
"totals.balance_due": 5000.0,
"due_date": "2024-01-15",
"created_at": "2024-01-01"
}
]Analysis tips:
- Calculate days overdue for each invoice
- Group by customer to identify problem accounts
- Track overdue percentage over time
- Compare overdue rates by customer segment
Extended analysis
Calculate overdue metrics:
total_overdue = sum(inv["totals.balance_due"] for inv in overdue)
avg_days_overdue = sum((today - inv["due_date"]).days for inv in overdue) / len(overdue)
print(f"Total overdue: ${total_overdue:,.2f}")
print(f"Average days overdue: {avg_days_overdue:.1f}")Group by customer:
from collections import defaultdict
by_customer = defaultdict(list)
for invoice in overdue:
by_customer[invoice["payer.account_id"]].append(invoice)
# Find customers with most overdue amount
for customer, invoices in sorted(
by_customer.items(),
key=lambda x: sum(i["totals.balance_due"] for i in x[1]),
reverse=True,
):
total = sum(inv["totals.balance_due"] for inv in invoices)
print(f"{customer}: ${total:,.2f} across {len(invoices)} invoices")Collection Performance
Analyze payment collection rates and effectiveness.
What this query does
This query groups invoices from a recent time period by status, calculating total billed amounts, collected amounts, and outstanding balances to measure collection effectiveness and payment velocity.
Query components:
| Component | Description |
|---|---|
| Select | Status, total billed, amount collected, balance |
| Filter | Recent time period (e.g., last 30 days) |
| Group | Results by status |
| Calculate | Collection rate percentage |
Query breakdown
thirty_days_ago = (datetime.now(timezone.utc) - timedelta(days=30)).isoformat()
sum_total = pl.attr.totals.total.sum()
sum_paid = pl.attr.totals.paid.sum()
sum_balance = pl.attr.totals.balance_due.sum()
performance = (
pl.Invoice.select(
pl.attr.status,
sum_total,
sum_paid,
sum_balance,
pl.attr.id.count(),
)
.filter_by(pl.attr.created_at >= thirty_days_ago)
.group_by(pl.attr.status)
.all()
)
# Calculate collection rate
for row in performance:
total = getattr(row, str(sum_total)) or 0
if total > 0:
paid = getattr(row, str(sum_paid)) or 0
rate = (paid / total) * 100
print(f"{row.status}: {rate:.1f}% collected")Result structure:
[
{
"status": "paid",
"sum(totals.total)": 125000.0,
"sum(totals.paid)": 125000.0,
"collection_rate": 100.0
},
{
"status": "partially_paid",
"sum(totals.total)": 50000.0,
"sum(totals.paid)": 35000.0,
"collection_rate": 70.0
}
]Analysis tips:
- Overall collection rate = total paid / total billed
- Track collection rate trends over time
- Compare by customer segment or product
- Set targets based on historical performance
Multi-Dimensional Analysis
Analyze invoices across multiple dimensions simultaneously.
What this query does
This query combines temporal, status, and customer dimensions to provide comprehensive invoice analysis, grouping by year, month, status, and customer to calculate billing, collection, and outstanding balance metrics across all dimensions.
Query components:
| Component | Description |
|---|---|
| Select | Year, month, status, customer, multiple aggregates |
| Group | By time, status, and customer |
| Order | By date and total amount |
| Limit | Top results for performance |
Query breakdown
multi = (
pl.Invoice.select(
pl.attr.created_at.year(),
pl.attr.created_at.month(),
pl.attr.status,
pl.attr.payer.account_id,
pl.attr.totals.total.sum(),
pl.attr.totals.paid.sum(),
pl.attr.totals.balance_due.sum(),
pl.attr.id.count(),
pl.attr.totals.total.avg(),
)
.group_by(
pl.attr.created_at.year(),
pl.attr.created_at.month(),
pl.attr.status,
pl.attr.payer.account_id,
)
.order_by(
"desc(year(created_at))",
"desc(month(created_at))",
"desc(sum(totals[total]))",
)
.limit(100)
.all()
)Result structure:
[
{
"year(created_at)": 2024,
"month(created_at)": 3,
"status": "paid",
"payer.account_id": "acct_customer1",
"sum(totals.total)": 25000.0,
"sum(totals.paid)": 25000.0,
"sum(totals.balance_due)": 0.0,
"count(id)": 5,
"avg(totals.total)": 5000.0
}
]Analysis tips:
- Identify customers with declining payment rates
- Compare customer invoice sizes over time
- Spot customers with growing balances
- Segment customers by payment performance
Common Patterns
Reusable patterns for invoice analysis.
# Comprehensive view of each customer's invoice history
customer_summary = (
pl.Invoice.select(
pl.attr.payer.account_id,
pl.attr.totals.total.sum(),
pl.attr.totals.paid.sum(),
pl.attr.totals.balance_due.sum(),
pl.attr.id.count(),
pl.attr.totals.total.avg(),
)
.group_by(pl.attr.payer.account_id)
.order_by("desc(sum(totals[balance_due]))")
.all()
)# Calculate average time from invoice creation to payment
paid_invoices = (
pl.Invoice.select(pl.attr.created_at, pl.attr.paid_timestamp)
.filter_by(pl.attr.status == "paid", q='paid_timestamp != null')
.all()
)
total_days = 0
for invoice in paid_invoices:
created = datetime.fromisoformat(str(invoice.created_at).replace("Z", "+00:00")).replace(tzinfo=None)
paid = datetime.fromisoformat(str(invoice.paid_timestamp).replace("Z", "+00:00")).replace(tzinfo=None)
days = (paid - created).days
total_days += days
avg_days_to_payment = total_days / len(paid_invoices)
print(f"Average days to payment: {avg_days_to_payment:.1f}")quarterly = (
pl.Invoice.select(
pl.attr.created_at.year(),
pl.attr.created_at.quarter(),
pl.attr.totals.total.sum(),
pl.attr.totals.paid.sum(),
pl.attr.id.count(),
)
.group_by(pl.attr.created_at.year(), pl.attr.created_at.quarter())
.order_by("desc(year(created_at))", "desc(quarter(created_at))")
.all()
)# Compare recurring subscription invoices to one-time invoices
invoice_types = (
pl.Invoice.select(
pl.attr.billing_schedule_id.is_null(), # True = one-time
pl.attr.totals.total.sum(),
pl.attr.id.count(),
pl.attr.totals.total.avg(),
)
.group_by(pl.attr.billing_schedule_id.is_null())
.all()
)Best Practices
Tips for effective invoice reporting.
Status alone doesn't show outstanding amounts:
# Correct: Check balance_due for true outstanding amount
ar_total = (
pl.Invoice.select(pl.attr.totals.balance_due.sum())
.filter_by(
pl.attr.status == "open|unpaid|partially_paid",
pl.attr.totals.balance_due > 0,
)
.all()
)
# Incomplete: Status alone may be misleading
incomplete = (
pl.Invoice.select(pl.attr.totals.total.sum())
.filter_by(
pl.attr.status == "unpaid|partially_paid",
)
.all()
)Track billing backlog:
# Monitor draft invoices not yet sent to customers
draft_backlog = (
pl.Invoice.select(
pl.attr.created_at.date(), pl.attr.totals.total.sum(), pl.attr.id.count()
)
.filter_by(pl.attr.status == "draft")
.group_by(pl.attr.created_at.date())
.all()
)Be clear about which date you're using:
# created_at = when invoice was created
# due_date = when payment is expected
# paid_timestamp = when invoice was fully paid
# For billing volume, use created_at
billing_volume = (
pl.Invoice.select(pl.attr.created_at.month(), pl.attr.id.count())
.group_by(pl.attr.created_at.month())
.all()
)
# For payment analysis, use paid_timestamp
payment_timing = (
pl.Invoice.select(pl.attr.paid_timestamp.month(), pl.attr.id.count())
.filter_by(pl.attr.status == "paid")
.group_by(pl.attr.paid_timestamp.month())
.all()
)Make reporting definitions explicit:
# Document what "accounts receivable" means in your context
# AR = sum of balance_due for invoices where:
# - status is 'open', 'unpaid', or 'partially_paid'
# - balance_due > 0
# - not including closed or draft invoices
# - using due_date for aging calculations
accounts_receivable = (
pl.Invoice.select(pl.attr.totals.balance_due.sum())
.filter_by(
pl.attr.status == "open|unpaid|partially_paid",
pl.attr.totals.balance_due > 0,
)
.all()
)Next Steps
Explore related reporting topics
Revenue Reports
Analyze transaction revenue and payment trends with Revenue Reports documentation, track monthly revenue trends, and understand payment method performance over time.
Customer Analytics
Build customer lifetime value and behavior analysis with Customer Analytics guide, identify high-value customers, and track customer payment patterns for improved retention strategies.
Schema Reference
Key invoice fields commonly used in reporting.
Invoice schema
Common field combinations
Accounts receivable:
-
totals.balance_due,status,due_date - Filter to unpaid/partially_paid with positive balance
- Calculate aging based on due_date
Collection analysis:
-
totals.total,totals.paid,totals.balance_due - Calculate collection rate: paid / total
- Track payment velocity
Customer analysis:
-
payer.account_idfor grouping -
billing_schedule_idto identify recurring vs one-time -
created_atfor customer timeline
Financial reporting:
-
created_atfor billing period -
paid_timestampfor collection period -
statusfor invoice lifecycle tracking