Tyche Pro

Tyche Pro — Invoice & Fortune Engine. Generate professional PDF-ready invoices, track multi-currency payments, apply tax and late fee calculations, send tiered reminder emails, and export a full revenue analytics dashboard — all from a CSV. The full-power version of Tyche.

Audits

Pending

Install

openclaw skills install tyche-pro

Tyche Pro — Invoice & Revenue Dashboard

Everything in Tyche, plus multi-currency support, revenue goal tracking, project code grouping, aged receivables report, and CSV export of all analytics.

Pro features vs free Tyche

FeatureTyche (Free)Tyche Pro
InvoicesUnlimitedUnlimited
Late fee calc✅✅ + per-invoice override
Reminder tiers3 tiers3 tiers + escalation log
Multi-currency❌✅ Per-invoice currency
Project grouping❌✅ Group by project_code
Revenue goal❌✅ Progress bar
Aged receivables❌✅ 30/60/90 day buckets
Analytics export❌✅ Full CSV dashboard

👉 Get Tyche Pro: openclaw skills install tyche-pro + key at ko-fi.com/occupythemilkyway

🔒 Security

All data stays on your machine. No transmission, no cloud.


Step 1 — Install

pip3 install rich --break-system-packages --quiet

Step 2 — Full invoice & revenue dashboard (Pro)

import os, csv, re, json
from datetime import datetime, timedelta
from collections import defaultdict
from rich.console import Console
from rich.table import Table
from rich.panel import Panel
from rich.progress import Progress, BarColumn, TextColumn
from rich import box

console = Console()

import hashlib as _hashlib
_KEY_HASHES = {
    "6b47cbf0e0ff707aa0dded96233b690b25a0772fa9851046496672d5b5cbd94a",  # Tyche Pro individual
    "33bc286313f89bd8a1cc3f07c470c244c346594f941d33bccddb89b566ca8a3a",  # Bundle (all 5 Pro skills)
}

LICENSE_KEY = os.environ.get("LICENSE_KEY", "").strip()
if _hashlib.sha256(LICENSE_KEY.encode()).hexdigest() not in _KEY_HASHES:
    console.print(Panel(
        "[red bold]🔒 Tyche Pro requires a valid license key.[/red bold]

"
        "Get your key at: [bold cyan]ko-fi.com/occupythemilkyway[/bold cyan]

"
        "Or use the free version: [dim]openclaw skills install tyche[/dim]",
        title="License Required", border_style="red"
    ))
    raise SystemExit(1)

INVOICES_FILE  = os.environ.get("INVOICES_FILE", "").strip()
YOUR_NAME      = os.environ.get("YOUR_NAME", "Your Company")
YOUR_EMAIL     = os.environ.get("YOUR_EMAIL", "")
YOUR_ADDRESS   = os.environ.get("YOUR_ADDRESS", "")
PAYMENT_TERMS  = os.environ.get("PAYMENT_TERMS", "Net 30")
CURRENCY       = os.environ.get("CURRENCY", "USD").upper()
PAYMENT_METHOD = os.environ.get("PAYMENT_METHOD", "")
try:
    TAX_RATE       = float(os.environ.get("TAX_RATE", "0"))
    LATE_FEE_RATE Hݯ]
Ü˙[š\›Û‹™Ù]
“UWёQWԐUH‹ŒKHŠJBˆ‘U‘S•QWÑÓÐSH›Ø]
Ü˙[š\›Û‹™Ù]
”‘U‘S•QWÑÓÐS‹ŒŠJB™^Ù\˜[YQ\œ›ÜŽ‚ˆVԐUHHUWёQWԐUHH‘U‘S•QWÑÓÐSHŒ‚”ÖSHHȕTÑŽˆ‰‹‘UTˆŽˆ¸ «‹‘ДŽˆ°¨È‹ÐQŽˆÐI‹UQŽˆUIŸK™Ù]
ÕT”‘SÖK‰ŠB››ÝÈH]][YK››ÝÊ
B‚™Yˆ›]
[[Ý[Þ[OS›Û™JNˆ™]\›ˆˆžÜÞ[H܈ÖS_^Ø[[Ý[‹Œ™ŸH‚™Yˆ\œÙWØ[[Ý[
˜]ÊN‚ˆÈH™KœÝXŠˆ–׌NK—H‹ˆ‹ÝŠ˜]ÊJH܈Œ‚ˆžNˆ™]\›ˆ›Ø]
ÊHYˆ˘ÛÝ[
‹ˆŠHHH[ÙHŒˆ^Ù\ˆ™]\›ˆŒ™Yˆ\œÙWÙ]J˜]ÊN‚ˆ›Üˆˆ[ˆ
‰VKI[KIY‹‰[KÉYÉVH‹‰YÉ[KÉVH‹‰YI[KIVHŠN‚ˆžNˆ™]\›ˆ]][YKœÝœ[YJ˜]˜Ýš\

KŠBˆ^Ù\ˆ\܈™]\›ˆ›Û™B‚š[›ÚXÙ\ÈH×BšYˆS•“ÒPÑT×ђSH[™ܘ]™^\ÝÊS•“ÒPÑT×ђSJN‚ˆÚ]Ü[ŠS•“ÒPÑT×ђSK[˜ÛÙ[™ÏH]‹N‹\œ›ÜœÏHœ™\XÙHŠH\ÈŽ‚ˆ™XY\ˆHÜ݋‘XÝ™XY\ŠŠBˆ›ÜˆK›ÝÈ[ˆ[[Y\˜]J™XY\‹JN‚ˆšÈHÚ˛ÝÙ\Š
KœÝš\

Nˆ‹œÝš\

H›ÜˆËˆ[ˆ›Ý˚][\Ê
_Bˆ[—ØÝ\ˆHšË™Ù]
˜Ý\œ™[˜ÞH‹ÕT”‘SÖJK\\Š
Bˆ[—ÜÞ[HHȕTÑŽˆ‰‹‘UTˆŽˆ¸ «‹‘ДŽˆ°¨È‹ÐQŽˆÐI‹UQŽˆUIŸK™Ù]
[—ØÝ\‹‰ŠBˆ[›ÚXÙ\˘\[™
ˆš[—۝[X™\ˆŽˆšË™Ù]
š[—۝[X™\ˆ‹ˆ’S•‹^ÚNŒ
HŠKˆ˜ÛY[ۘ[YHŽˆšË™Ù]
˜ÛY[ۘ[YH‹ÛY[ŠKˆ˜ÛY[Ù[XZ[ŽˆšË™Ù]
˜ÛY[Ù[XZ[‹ˆŠKˆ™\ØÜš\[ۈŽˆšË™Ù]
™\ØÜš\[ۈ‹”Ù\šXÙ\È™[™\™YŠKˆ˜[[Ý[Žˆ\œÙWØ[[Ý[
šË™Ù]
˜[[Ý[‹ŒŠJKˆ™YWÙ]HŽˆšË™Ù]
™YWÙ]H‹ˆŠKˆœÝ]\ȎˆšË™Ù]
œÝ]\ȋ[œZYŠK›ÝÙ\Š
Kˆ˜Ý\œ™[˜ÞHŽˆ[—ØÝ\‹ˆœÞ[HŽˆ[—ÜÞ[Kˆœ›Ú™XÝØÛÙHŽˆšË™Ù]
œ›Ú™XÝØÛÙH‹ˆŠK\\Š
H܈‘ÑS‘TS‹ˆJB™[ÙN‚ˆÛۜÛÛKœš[
–ÞY[Ý×x¡.{î#È›ÈS•“ÒPÑT×ђSH8 %\Ú[™È[[È]K–ËÞY[Ý×WˆŠBˆ[›ÚXÙ\ÈHˆȚ[—۝[X™\ˆŽˆ’S•‹LH‹˜ÛY[ۘ[YHŽˆXÛYHÛܜ‹˜ÛY[Ù[XZ[Žˆ˜š[[™ÐXÛYK˜ÛÛH‹™\ØÜš\[ۈŽˆ•ÙXœÚ]H™Y\ÚYۈ‹˜[[Ý[ŽŒL™YWÙ]HŽŠ›ÝË][YY[J^\ÏLMJJKœÝ™[YJ‰VKI[KIYŠKœÝ]\Ȏˆ›Ý™\™YH‹˜Ý\œ™[˜ÞHŽˆ•TÑ‹œÞ[HŽˆ‰‹œ›Ú™XÝØÛÙHŽˆ•ÑPˆŸKˆȚ[—۝[X™\ˆŽˆ’S•‹Lˆ‹˜ÛY[ۘ[YHŽˆ‘Ûؙ^[˜È‹˜ÛY[Ù[XZ[Žˆ˜\Ûؙ^˜ÛÛH‹™\ØÜš\[ۈŽˆÛۜÝ[[™È™]Z[™\ˆ‹˜[[Ý[ŽŒN™YWÙ]HŽŠ›ÝÊÝ[YY[J^\ÏLL
JKœÝ™[YJ‰VKI[KIYŠKœÝ]\Ȏˆ[œZY‹˜Ý\œ™[˜ÞHŽˆ•TÑ‹œÞ[HŽˆ‰‹œ›Ú™XÝØÛÙHŽˆÓӔÕSŸKˆȚ[—۝[X™\ˆŽˆ’S•‹Lȋ˜ÛY[ۘ[YHŽˆ’[š]XÚ‹˜ÛY[Ù[XZ[Žˆœ^P[š]XÚ˜ÛÛH‹™\ØÜš\[ۈŽˆ“ÙÛÈ\ÚYۈ‹˜[[Ý[ŽÍL™YWÙ]HŽŠ›ÝË][YY[J^\ÏMJJKœÝ™[YJ‰VKI[KIYŠKœÝ]\ȎˆœZY‹˜Ý\œ™[˜ÞHŽˆ‘Д‹œÞ[HŽˆ°¨È‹œ›Ú™XÝØÛÙHŽˆ‘TÒQӈŸKˆȚ[—۝[X™\ˆŽˆ’S•‹L
‹˜ÛY[ۘ[YHŽˆ•[Xœ™[HÛȋ˜ÛY[Ù[XZ[Žˆ™š[˜[˜ÙP[Xœ™[K˜Ûȋ™\ØÜš\[ۈŽˆ”ÑSÈ]Y]‹˜[[Ý[ŽŒLŒ™YWÙ]HŽŠ›ÝË][YY[J^\ÏLÍJJKœÝ™[YJ‰VKI[KIYŠKœÝ]\Ȏˆ›Ý™\™YH‹˜Ý\œ™[˜ÞHŽˆ•TÑ‹œÞ[HŽˆ‰‹œ›Ú™XÝØÛÙHŽˆ“PT’ÑUS‘ÈŸKˆȚ[—۝[X™\ˆŽˆ’S•‹L
H‹˜ÛY[ۘ[YHŽˆ”ÛÞ[[Ûܜ‹˜ÛY[Ù[XZ[Žˆ˜\ÛÞ[[˜ÛÛH‹™\ØÜš\[ۈŽˆ\V\ÚYۈ‹˜[[Ý[ŽŒÍ™YWÙ]HŽŠ›ÝÊÝ[YY[J^\ÏLŒ
JKœÝ™[YJ‰VKI[KIYŠKœÝ]\Ȏˆ[œZY‹˜Ý\œ™[˜ÞHŽˆ‘UTˆ‹œÞ[HŽˆ¸ «‹œ›Ú™XÝØÛÙHŽˆ‘TÒQӈŸKˆȚ[—۝[X™\ˆŽˆ’S•‹L
ˆ‹˜ÛY[ۘ[YHŽˆXÛYHÛܜ‹˜ÛY[Ù[XZ[Žˆ˜š[[™ÐXÛYK˜ÛÛH‹™\ØÜš\[ۈŽˆ“[۝HÝ\ܝ‹˜[[Ý[ŽL™YWÙ]HŽŠ›ÝË][YY[J^\ÏMŒŠJKœÝ™[YJ‰VKI[KIYŠKœÝ]\Ȏˆ›Ý™\™YH‹˜Ý\œ™[˜ÞHŽˆ•TÑ‹œÞ[HŽˆ‰‹œ›Ú™XÝØÛÙHŽˆ”ÕTԕŸKˆB‚ˆÈ[œšXÚ™›Üˆ[ˆ[ˆ[›ÚXÙ\΂ˆÝXˆH[–Ș[[Ý[—Bˆ^HÝXˆ
ˆVԐUHÈLˆ[–ÈœÝXÝ[—HHÝX‚ˆ[–ȝ^—HH^ˆ[–ȝÝ[—HHÝXˆ
È^ˆYHH\œÙWÙ]J[–È™YWÙ]H—JBˆ[–È™YWÙ—HHYBˆ[–È™^\×Û]H—HHX^

›ÝÈHYJK™^\ÊHYˆYH[™[–ÈœÝ]\ȗH[ˆ
›Ý™\™YH‹[œZYŠH[ÙHˆ[–È›]WٙYH—HH[–ȝÝ[—H
ˆUWёQWԐUHÈL
ˆ
[–È™^\×Û]H—HÈÌ
HYˆ[–È™^\×Û]H—Hˆ[™UWёQWԐUH[ÙH‚ˆÈš[˜[˜ÚX[È
TÑY\]Z]˜[[Ý[ț܈Ý[[X\žJBÝ[Ú[›ÚXÙYHÝ[JVȝÝ[—H›ÜˆH[ˆ[›ÚXÙ\ÊBÝ[ÜZYHÝ[JVȝÝ[—H›ÜˆH[ˆ[›ÚXÙ\ÈYˆVȜÝ]\ȗH == "paid")
total_outstanding = total_invoiced - total_paid
overdue_invoices  = [i for i in invoices if i["days_late"] > 0]
total_late_fees   = sum(i["late_fee"] for i in invoices)

# Header
console.print()
console.print(Panel.fit(
    f"[bold yellow]⚖️💎 Tyche Pro — Revenue Dashboard[/bold yellow]\n"
    f"Invoiced: [white]{fmt(total_invoiced)}[/white]  Received: [green]{fmt(total_paid)}[/green]  "
    f"Outstanding: [red]{fmt(total_outstanding)}[/red]  Late fees: [orange3]{fmt(total_late_fees)}[/orange3]",
    border_style="yellow"
))

# Revenue goal progress
if REVENUE_GOAL > 0:
    pct = min(total_paid / REVENUE_GOAL * 100, 100)
    bar = "█" * int(pct / 5) + "░" * (20 - int(pct / 5))
    console.print(Panel(
        f"[cyan]{bar}[/cyan]  [yellow]{pct:.1f}%[/yellow]  {fmt(total_paid)} / {fmt(REVENUE_GOAL)} goal",
        title="Revenue Goal", border_style="green"
    ))

# Status table
console.print()
tbl = Table(title="Invoice Status", box=box.ROUNDED, border_style="yellow")
tbl.add_column("Inv #",    width=10, style="dim")
tbl.add_column("Project",  width=10, style="magenta")
tbl.add_column("Client",   width=16, style="cyan")
tbl.add_column("Cur",      width=5)
tbl.add_column("Total",    width=12, justify="right")
tbl.add_column("Due",      width=12, style="dim")
tbl.add_column("Late",     width=8,  justify="right", style="red")
tbl.add_column("Fee",      width=10, justify="right", style="orange3")
tbl.add_column("Status",   width=10)

SC = {"paid":"green","unpaid":"yellow","overdue":"red","partial":"cyan"}
for inv in sorted(invoices, key=lambda x: -(x["days_late"] or 0)):
    sc   = SC.get(inv["status"],"white")
    late = f"{inv['days_late']}d" if inv["days_late"] else "—"
    fee  = fmt(inv["late_fee"], inv["sym"]) if inv["late_fee"] else "—"
    tbl.add_row(
        inv["inv_number"], inv["project_code"], inv["client_name"][:14],
        inv["currency"], fmt(inv["total"], inv["sym"]), inv["due_date"],
        late, fee, f"[{sc}]{inv['status'].title()}[/{sc}]"
    )
console.print(tbl)

# Pro: Aged receivables (30/60/90+ day buckets)
console.print()
buckets = {"Current (0-30d)": [], "31-60 days": [], "61-90 days": [], "90+ days": []}
for inv in overdue_invoices:
    d = inv["days_late"]
    if d <= 30:   buckets["Current (0-30d)"].append(inv)
    elif d <= 60: buckets["31-60 days"].append(inv)
    elif d <= 90: buckets["61-90 days"].append(inv)
    else:         buckets["90+ days"].append(inv)

aged_tbl = Table(title="Aged Receivables", box=box.SIMPLE, border_style="red")
aged_tbl.add_column("Bucket",  style="cyan", width=18)
aged_tbl.add_column("Count",   width=8, justify="right")
aged_tbl.add_column("Amount",  width=14, justify="right", style="red")
for bucket, invs in buckets.items():
    total = sum(i["total"] for i in invs)
    aged_tbl.add_row(bucket, str(len(invs)), fmt(total) if invs else "—")
console.print(aged_tbl)

# Pro: Project grouping
console.print()
proj_totals = defaultdict(lambda: {"invoiced":0,"paid":0,"outstanding":0,"count":0})
for inv in invoices:
    p = inv["project_code"]
    proj_totals[p]["invoiced"]     += inv["total"]
    proj_totals[p]["count"]        += 1
    if inv["status"] == "paid":
        proj_totals[p]["paid"]     += inv["total"]
    else:
        proj_totals[p]["outstanding"] += inv["total"]

proj_tbl = Table(title="Revenue by Project", box=box.SIMPLE, border_style="magenta")
proj_tbl.add_column("Project",     style="magenta", width=14)
proj_tbl.add_column("Invoices",    width=10, justify="right")
proj_tbl.add_column("Invoiced",    width=14, justify="right")
proj_tbl.add_column("Paid",        width=14, justify="right", style="green")
proj_tbl.add_column("Outstanding", width=14, justify="right", style="red")
for proj, data in sorted(proj_totals.items(), key=lambda x: -x[1]["invoiced"]):
    proj_tbl.add_row(proj, str(data["count"]), fmt(data["invoiced"]),
                     fmt(data["paid"]), fmt(data["outstanding"]))
console.print(proj_tbl)

# Reminder emails
overdue_remind = [i for i in invoices if i["days_late"] > 0]
if overdue_remind:
    console.print()
    for inv in overdue_remind:
        late_line = f" A late fee of {fmt(inv['late_fee'], inv['sym'])} has accrued." if inv["late_fee"] else ""
        if inv["days_late"] <= 7:
            tier, colour = "Friendly Reminder", "yellow"
            body = f"Invoice {inv['inv_number']} for {fmt(inv['total'], inv['sym'])} was due {inv['due_date']}.{late_line} If you've sent payment, please disregard."
        elif inv["days_late"] <= 21:
            tier, colour = "Firm Reminder", "orange3"
            body = f"Invoice {inv['inv_number']} for {fmt(inv['total'], inv['sym'])} is now {inv['days_late']} days overdue.{late_line} Please arrange payment or contact me to discuss."
        else:
            tier, colour = "FINAL NOTICE", "red"
            body = f"FINAL NOTICE: Invoice {inv['inv_number']} for {fmt(inv['total'], inv['sym'])} is {inv['days_late']} days overdue.{late_line} Please remit within 48 hours or contact me immediately."
        console.print(Panel(
            f"To: {inv['client_email']}\nSubject: {tier} — {inv['inv_number']} — {inv['client_name']}\n\nDear {inv['client_name']},\n\n{body}\n\nKind regards,\n{YOUR_NAME}",
            title=f"[{colour}]{tier}[/{colour}] — {inv['client_name']} ({inv['days_late']}d overdue)",
            border_style=colour
        ))

# Save outputs
date_str    = now.strftime("%Y-%m-%d")
report_file = f"tyche_pro_report_{date_str}.md"
csv_file    = f"tyche_pro_analytics_{date_str}.csv"

with open(report_file, "w", encoding="utf-8") as f:
    f.write(f"# ⚖️💎 Tyche Pro Report — {date_str}\n\n")
    f.write(f"**Invoiced:** {fmt(total_invoiced)}  **Received:** {fmt(total_paid)}  **Outstanding:** {fmt(total_outstanding)}\n\n")
    f.write("## Invoice Status\n\n| Inv # | Project | Client | Total | Due | Days Late | Status |\n|---|---|---|---|---|---|---|\n")
    for inv in sorted(invoices, key=lambda x: -(x["days_late"] or 0)):
        f.write(f"| {inv['inv_number']} | {inv['project_code']} | {inv['client_name']} | {fmt(inv['total'],inv['sym'])} | {inv['due_date']} | {inv['days_late'] or '—'} | {inv['status'].title()} |\n")

with open(csv_file, "w", newline="", encoding="utf-8") as f:
    writer = csv.writer(f)
    writer.writerow(["inv_number","client","project","currency","total","days_late","late_fee","status"])
    for inv in invoices:
        writer.writerow([inv["inv_number"],inv["client_name"],inv["project_code"],inv["currency"],
                         f"{inv['total']:.2f}",inv["days_late"],f"{inv['late_fee']:.2f}",inv["status"]])

console.print()
console.print(Panel(
    f"[green]✅ Done![/green]  [cyan]{report_file}[/cyan]  |  [cyan]{csv_file}[/cyan]",
    border_style="green"
))