Skip to content

Sales Unbilled Report Documentation

Overview

The Sales Unbilled Report module (report.sale.unbill) tracks sales orders that have not yet been fully invoiced. It identifies unbilled revenue, helping businesses manage their billing pipeline and recognize deferred revenue obligations.


Model Information

Model Name: report.sale.unbill Display Name: Sales Unbilled Report Type: Transient Report Model (_transient = True)

Features

  • Tracks orders not yet invoiced
  • Calculates unbilled amounts per order
  • Shows partial billing progress
  • Customer and order filtering
  • Foreign currency support
  • Person-in-charge and category filtering

Key Fields Reference

Report Parameters

Field Type Required Description
order_date_from Date No Filter by order date from (default: Jan 1 of current year)
order_date_to Date No Filter by order date to (default: Dec 31 of current year)
contact_id Many2One No Filter by specific customer
sale_order Char No Filter by order number (partial match)
invoice_number Char No Filter by invoice number (partial match)
person_in_charge Many2One No Filter by salesperson/user
sale_category Many2One No Filter by sales category
show_foreign_currency Boolean No Show foreign currency amounts

Output Fields

Field Description
id Sales order ID
number Order number
date Order date
ref Customer reference
contact_code Customer code
contact_name Customer name
amount_total_cur Order total in company currency
inv_amount_total_cur Total invoiced in company currency
amount_unbill Unbilled amount remaining
amount_unbill_USD Unbilled amount in USD (if foreign currency shown)
person_in_charge Salesperson name
sale_category Sales category name
invoices Array of invoices against this order

Unbilled Calculation Logic

# For each order
order_amount = sale.amount_total_cur

# Sum all invoices
invoice_amount = sum(inv.amount_total_cur for inv in sale.invoices)

# Calculate unbilled
amount_unbill = order_amount - invoice_amount

# Exclude fully invoiced orders (amount_unbill = 0)

Key States: - Only includes non-draft, non-voided orders - Partial invoicing: amount_unbill > 0 - Fully invoiced: amount_unbill = 0 (excluded from report) - Not invoiced: amount_unbill = order_amount (all invoices shown in detail)


API Methods

1. Generate Unbilled Report

Method: get_report_data(ids, context)

Generates unbilled sales tracking report.

Parameters:

ids = [report_id]  # Report configuration ID

context = {
    "no_limit": True  # Optional: disable query limits
}

Returns: dict - Report data:

{
    "company_name": "ABC Company",
    "order_date_from": "2024-01-01",
    "order_date_to": "2024-12-31",
    "show_foreign_currency": False,
    "base_currency_code": "THB",
    "foreign_currency_code": "USD",
    "sale_orders": [
        {
            "id": 123,
            "number": "SO-2024-001",
            "date": "2024-01-15",
            "ref": "CUST-REF-001",
            "contact_code": "C001",
            "contact_name": "ABC Corp",
            "amount_total_cur": 100000.00,
            "inv_amount_total_cur": 60000.00,
            "amount_unbill": 40000.00,
            "amount_unbill_USD": 1200.00,
            "person_in_charge": "John Doe",
            "sale_category": "Retail",
            "invoices": [
                {
                    "id": 456,
                    "number": "INV-2024-001",
                    "date": "2024-01-20",
                    "amount_total_cur": 60000.00
                }
            ]
        }
    ],
    "sale_amount_total": 100000.00,
    "inv_amount_total": 60000.00,
    "unbill_amount_total": 40000.00
}

Example:

# Generate unbilled report for Q1 2024
report_id = get_model("report.sale.unbill").create({
    "order_date_from": "2024-01-01",
    "order_date_to": "2024-03-31"
})

data = get_model("report.sale.unbill").get_report_data([report_id])

print(f"Unbilled Revenue Report:")
print(f"  Total Orders: ${data['sale_amount_total']:,.2f}")
print(f"  Invoiced: ${data['inv_amount_total']:,.2f}")
print(f"  Unbilled: ${data['unbill_amount_total']:,.2f}")
print(f"  Billing %: {(data['inv_amount_total']/data['sale_amount_total'])*100:.1f}%")


Model Relationship Description
sale.order Data Source Sales orders with invoicing status
account.invoice One2Many Invoices generated from orders
contact Many2One Customer information
base.user Many2One Person in charge (salesperson)
sale.categ Many2One Sales category
currency.rate Reference For foreign currency conversion

Common Use Cases

Use Case 1: Billing Pipeline Management

# Track what needs to be invoiced this month

from datetime import date, timedelta

# Look back 90 days for pending billing
date_from = (date.today() - timedelta(days=90)).strftime("%Y-%m-%d")
date_to = date.today().strftime("%Y-%m-%d")

report_id = get_model("report.sale.unbill").create({
    "order_date_from": date_from,
    "order_date_to": date_to
})

data = get_model("report.sale.unbill").get_report_data([report_id])

print("Billing Pipeline:")
print(f"Total Unbilled: ${data['unbill_amount_total']:,.2f}\n")

# Sort by unbilled amount
sorted_orders = sorted(data["sale_orders"], key=lambda x: x["amount_unbill"], reverse=True)

print("Top 10 Orders to Invoice:")
for i, order in enumerate(sorted_orders[:10], 1):
    days_old = (date.today() - datetime.strptime(order["date"], "%Y-%m-%d").date()).days
    print(f"{i}. {order['number']} - {order['contact_name']}")
    print(f"   Unbilled: ${order['amount_unbill']:,.2f}")
    print(f"   Age: {days_old} days")
    print(f"   Person: {order['person_in_charge']}")

Use Case 2: Salesperson Billing Accountability

# Track unbilled orders by salesperson

report_id = get_model("report.sale.unbill").create({
    "order_date_from": "2024-01-01",
    "order_date_to": "2024-12-31"
})

data = get_model("report.sale.unbill").get_report_data([report_id])

from collections import defaultdict
by_person = defaultdict(lambda: {"count": 0, "unbilled": 0, "orders": []})

for order in data["sale_orders"]:
    person = order.get("person_in_charge") or "Unassigned"
    by_person[person]["count"] += 1
    by_person[person]["unbilled"] += order["amount_unbill"]
    by_person[person]["orders"].append(order["number"])

print("Unbilled Orders by Salesperson:")
for person, stats in sorted(by_person.items(), key=lambda x: x[1]["unbilled"], reverse=True):
    print(f"\n{person}:")
    print(f"  Orders: {stats['count']}")
    print(f"  Unbilled: ${stats['unbilled']:,.2f}")
    print(f"  Orders: {', '.join(stats['orders'][:5])}")
    if stats['count'] > 5:
        print(f"  ... and {stats['count'] - 5} more")

Use Case 3: Customer Unbilled Balance

# Check unbilled amounts for specific customer

customer_id = 45

report_id = get_model("report.sale.unbill").create({
    "order_date_from": "2024-01-01",
    "order_date_to": "2024-12-31",
    "contact_id": customer_id
})

data = get_model("report.sale.unbill").get_report_data([report_id])

if data["sale_orders"]:
    customer_name = data.get("contact_name", "Customer")
    print(f"Unbilled Balance for {customer_name}:")
    print(f"  Total Unbilled: ${data['unbill_amount_total']:,.2f}\n")

    print("Order Details:")
    for order in data["sale_orders"]:
        billing_pct = ((order["amount_total_cur"] - order["amount_unbill"]) / order["amount_total_cur"]) * 100
        print(f"  {order['number']} ({order['date']}):")
        print(f"    Order Total: ${order['amount_total_cur']:,.2f}")
        print(f"    Invoiced: ${order['inv_amount_total_cur']:,.2f} ({billing_pct:.0f}%)")
        print(f"    Unbilled: ${order['amount_unbill']:,.2f}")

        if order.get("invoices"):
            print(f"    Invoices:")
            for inv in order["invoices"]:
                print(f"      - {inv['number']} ({inv['date']}): ${inv['amount_total_cur']:,.2f}")
else:
    print("No unbilled orders for this customer")

Use Case 4: Aging Unbilled Orders

# Identify old unbilled orders needing attention

from datetime import datetime, date

report_id = get_model("report.sale.unbill").create({
    "order_date_from": "2023-01-01",  # Look back further
    "order_date_to": date.today().strftime("%Y-%m-%d")
})

data = get_model("report.sale.unbill").get_report_data([report_id])

# Categorize by age
aging_buckets = {
    "0-30 days": {"orders": [], "amount": 0},
    "31-60 days": {"orders": [], "amount": 0},
    "61-90 days": {"orders": [], "amount": 0},
    "90+ days": {"orders": [], "amount": 0}
}

today = date.today()
for order in data["sale_orders"]:
    order_date = datetime.strptime(order["date"], "%Y-%m-%d").date()
    age = (today - order_date).days

    if age <= 30:
        bucket = "0-30 days"
    elif age <= 60:
        bucket = "31-60 days"
    elif age <= 90:
        bucket = "61-90 days"
    else:
        bucket = "90+ days"

    aging_buckets[bucket]["orders"].append(order)
    aging_buckets[bucket]["amount"] += order["amount_unbill"]

print("Unbilled Orders Aging Analysis:")
for bucket, data in aging_buckets.items():
    if data["orders"]:
        print(f"\n{bucket}: {len(data['orders'])} orders, ${data['amount']:,.2f}")
        if bucket == "90+ days":  # Show details for old orders
            for order in data["orders"]:
                print(f"  - {order['number']} ({order['date']}): ${order['amount_unbill']:,.2f}")

Use Case 5: Revenue Recognition Tracking

# Track deferred revenue for accounting

report_id = get_model("report.sale.unbill").create({
    "order_date_from": "2024-01-01",
    "order_date_to": "2024-12-31"
})

data = get_model("report.sale.unbill").get_report_data([report_id])

print("Revenue Recognition Report:")
print(f"Total Order Value: ${data['sale_amount_total']:,.2f}")
print(f"Recognized (Invoiced): ${data['inv_amount_total']:,.2f}")
print(f"Deferred (Unbilled): ${data['unbill_amount_total']:,.2f}")

recognition_rate = (data['inv_amount_total'] / data['sale_amount_total']) * 100 if data['sale_amount_total'] else 0
print(f"Recognition Rate: {recognition_rate:.1f}%")

# Monthly breakdown
from datetime import datetime
from dateutil.relativedelta import relativedelta

monthly_deferred = {}
start_date = datetime(2024, 1, 1)

for i in range(12):
    month = (start_date + relativedelta(months=i)).strftime("%Y-%m")
    month_orders = [
        o for o in data["sale_orders"]
        if o["date"].startswith(month)
    ]

    if month_orders:
        month_unbilled = sum(o["amount_unbill"] for o in month_orders)
        monthly_deferred[month] = month_unbilled

print("\nMonthly Deferred Revenue:")
for month, amount in sorted(monthly_deferred.items()):
    print(f"  {month}: ${amount:,.2f}")

Performance Tips

1. Date Range Filtering

  • Default range is full year (may be slow for large datasets)
  • Narrow date range for faster queries
  • Use no_limit context only when necessary

2. Customer Filtering

  • Filter by specific customer for detailed analysis
  • Faster than loading all orders then filtering

3. Foreign Currency

  • show_foreign_currency adds USD conversion overhead
  • Only enable when needed for multi-currency analysis

Troubleshooting

"No unbilled orders found"

Cause: All orders fully invoiced, or outside date range Solution: - Expand date range - Check order states (draft/voided excluded) - Verify orders have invoices linked

"Foreign currency amounts missing"

Cause: Currency rate not available for USD Solution: - Ensure USD currency rate exists in system - Check currency.rate model for USD entries - Foreign currency only shown if show_foreign_currency enabled


Version History

Last Updated: 2026-01-05 Model Version: report_sale_unbill.py (168 lines) Framework: Netforce


Additional Resources

  • Sales Order Documentation: sale.order
  • Invoice Model Documentation: account.invoice

This documentation is generated for developer onboarding and reference purposes.