"""Generate the sample data set: one quarter (1 Jul - 30 Sep 2026) for a 4-clinic allied health group.
Writes the inputs a bookkeeper would have (bank statement imports, Odoo exports, remittances, Stripe payout
report) plus an expected-outcome file used only by the regression test in match.py."""
import csv, random, datetime as dt, os
from collections import defaultdict
R = random.Random(20260701)
D = dt.date
OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data")
os.makedirs(OUT, exist_ok=True)
START, END = D(2026, 7, 1), D(2026, 9, 30)
MONTH_ENDS = [D(2026, 7, 31), D(2026, 8, 31), D(2026, 9, 30)]
def days(a, b):
    d = a
    while d <= b:
        yield d; d += dt.timedelta(1)
def bday(d): return d.weekday() < 5
def next_bday(d, n=1):
    while n > 0:
        d += dt.timedelta(1)
        if bday(d): n -= 1
    return d
def c(x): return round(x + 1e-9, 2)

bank, ledger, items, remits, stripe, truth = [], [], [], [], [], []
seq = defaultdict(int)
def nid(p):
    seq[p] += 1; return f"{p}{seq[p]:05d}"
def B(acct, date, desc, amt, tg, reason="MATCH", batch=None):
    if date > END: return None  # statement ends 30 Sep
    lid = nid("BL-")
    bank.append(dict(line_id=lid, account=acct, date=date, description=desc, amount=c(amt), import_batch=batch or f"{acct}-{date:%Y%m%d}"))
    truth.append(dict(kind="bank", id=lid, tg=tg, reason=reason)); return lid
def L(acct, date, partner, ref, label, amt, mtype, tg, batch="", reason="MATCH", june=False):
    eid = nid("JI-")
    if not june: date = min(max(date, START), END)
    ledger.append(dict(entry_id=eid, journal=acct, date=date, partner=partner, ref=ref, label=label, amount=c(amt), move_type=mtype, batch_ref=batch))
    truth.append(dict(kind="ledger", id=eid, tg=tg, reason=reason)); return eid
tgn = [0]
def TG():
    tgn[0] += 1; return f"T{tgn[0]:05d}"

# ---------------- partners
PM = ["Bluegum Plan Management", "Riverbend Plan Partners", "Clearpath NDIS Plans", "Wattle Plan Services", "Banksia Plan Co",
      "Ironbark Plan Management", "Kookaburra Plan Support", "Saltbush Plan Managers", "Redgum Plan Admin", "Southern Cross Plan Mgmt",
      "Lyrebird Plan Management", "Tallowwood Plans", "Acacia Plan Partners", "Mallee Plan Services"]
INS = ["Harbour Mutual Health", "Coastline Health Fund", "Unity Workcover Agents", "Pinnacle Claims Services", "Meridian Health Cover", "Northline Injury Claims"]
CORP = ["Varga & Holt Logistics", "Kestrel Lane Foods", "Brightwater Civil Pty Ltd", "Orchard Street Legal", "Calder Freight Group",
        "Tamsin Ridge Wineries", "Ostrander Manufacturing", "Pellucid Software Pty Ltd", "Greystanes Building Co", "Fernhill Aged Care",
        "Marlowe Print & Pack", "Quillon Engineering", "Hartigan Transport", "Copperleaf Childcare", "Westmere Council Services",
        "Arden Vale Schools", "Sorrento Bay Hotels", "Talisker Mining Services", "Broadmeadow Plastics", "Linfield Dairy Co",
        "Ashgrove Accounting", "Peregrine Security", "Cobblestone Retail Group", "Daintree Packaging", "Elmhurst Property Group",
        "Fairlie Bros Plumbing", "Gilmour Steel Pty Ltd", "Hollis & Fry Architects", "Inglewood Cold Storage", "Jarrah Ridge Estates",
        "Kinross Medical Couriers", "Lockhart Glass", "Mornane Electrical", "Nettlefold Joinery", "Oakbank Recycling",
        "Pendle Hill Bakery", "Quarry Lane Concrete", "Rosedale Nurseries", "Stanhope Trucking", "Thornbury Textiles"]
SUP = ["Physio Supplies Direct", "Allied Med Consumables", "Clinicare Linen Service", "Bayside Cleaning Co", "Metro Waste Removal",
       "Paperline Stationery Pty", "Rehab Equipment Hire", "Southside Couriers", "Kinetic Gym Equipment", "Brightline Signage",
       "Practice Fitout Joinery", "Coolroom HVAC Services", "Northern Pest Control", "Medilab Pathology", "Locum Physio Agency",
       "Clinical Education Hub", "Ergo Furniture Co", "Prime Print Services", "Greenleaf Plants Hire", "CityFix Maintenance",
       "Secure Shred Australia", "Payroll Compliance Advisers", "Strategic HR Partners", "Compass Insurance Brokers", "Ledgerline Tax Advisers",
       "Harbour IT Support", "Fibrewire Networks", "Ward Electrical", "Alpine Water Coolers", "Statewide Uniforms"]
PREF = {"Bluegum Plan Management": ["BLUEGUM PLAN MGMT", "BLUEGUM PLAN MANAGE", "BLUEGUM PM PTY LTD"]}
def variants(name):
    u = name.upper().replace("&", "AND").replace("PTY LTD", "").replace("  ", " ").strip()
    out = [u[:18].strip(), u.replace("PLAN MANAGEMENT", "PLAN MGMT").replace("MANAGEMENT", "MGMT")[:20].strip(),
           (u + " PTY LTD")[:22].strip(), u.replace(" ", "")[:16]]
    return PREF.get(name, out)
def payee(name):
    return R.choice(variants(name))

def bdays_in(a, b): return [d for d in days(a, b) if bday(d)]
inv_seq = [1400]
def inv_no():
    inv_seq[0] += 1; return f"INV/2026/{inv_seq[0]:05d}"
bill_seq = [800]
def bill_no():
    bill_seq[0] += 1; return f"BILL/2026/{bill_seq[0]:04d}"

# ---------------- opening position (30 Jun) and June items still outstanding
LEDGER_OPEN = {"OPS": 412640.18, "PAY": 25000.00, "MER": 18300.55, "USD": 42180.00}
june_out = defaultdict(float)
# unpresented cheques from June: one presents in July, one is never presented (stale)
for no, sup, amt, pres in [("000398", "Northern Pest Control", 418.00, D(2026, 7, 6)), ("000401", "Greenleaf Plants Hire", 286.00, None),
                           ("000402", "Secure Shred Australia", 165.00, D(2026, 7, 9))]:
    tg = TG(); L("OPS", D(2026, 6, 26 if no != "000402" else 29), sup, f"CHQ {no}", f"Cheque {no} {sup}", -amt, "supplier_payment", tg,
                 reason="STALE" if pres is None else "MATCH", june=True)
    june_out["OPS"] += -amt
    if pres: B("OPS", pres, f"CHEQUE {no}", -amt, tg)
# June receipt banked 30 Jun, cleared 1 Jul (deposit in transit)
tg = TG(); L("OPS", D(2026, 6, 30), "Harbour Mutual Health", "INV/2026/01388", "Customer payment INV/2026/01388", 2364.00, "customer_payment", tg, june=True)
june_out["OPS"] += 2364.00; B("OPS", D(2026, 7, 1), f"DIRECT CREDIT {payee('Harbour Mutual Health')} INV 1388", 2364.00, tg)
# June Stripe charges paid out in July (in transit)
# (created below with the Stripe generator, dated 29-30 Jun)

# ---------------- customer invoices (Jun - Sep)
inv_by_partner = defaultdict(list)
def mk_inv(partner, date, amount, gst, kind="NDIS"):
    no = inv_no()
    rec = dict(inv_no=no, move_type="out_invoice", partner=partner, date=date, due=date + dt.timedelta(14 if kind != "CORP" else 30),
               amount_total=c(amount), gst=c(gst), paid_ledger=0.0, kind=kind)
    items.append(rec); inv_by_partner[partner].append(rec); return rec
NDIS_RATES = [193.99, 96.99, 387.98, 290.98, 145.49, 242.49]
for d in bdays_in(D(2026, 6, 1), END):
    for pm in PM:
        for _ in range(R.choice([0, 1, 1, 2, 2, 3])):
            mk_inv(pm, d, R.choice(NDIS_RATES) + R.choice([0, 0, 0, 30.00, 58.20]), 0.0)
    for ins in INS:
        if R.random() < 0.35:
            a = R.choice([88.00, 132.00, 176.00, 264.00, 412.50, 990.00])
            mk_inv(ins, d, a, 0.0, "INS")
for m in [6, 7, 8, 9]:
    for cp in CORP:
        n = R.choice([1, 1, 2])
        for k in range(n):
            ex = R.choice([1200, 1850, 2400, 3150, 4800, 6200, 9750, 14500]) + R.choice([0, 0, 75, 137.5])
            mk_inv(cp, D(2026, m, R.choice([1, 2, 3, 15])), ex * 1.1, ex * 0.1, "CORP")

def fmt_ref(no, style):
    n = no.split("/")[-1]
    return {0: no, 1: f"INV {int(n)}", 2: n, 3: f"INV{n}", 4: f"RE {no.replace('/', ' ')}", 5: ""}[style]

def register(tg, inv, amt, date, reason="MATCH", acct="OPS", june=False):
    inv["paid_ledger"] += amt
    return L(acct, date, inv["partner"], inv["inv_no"], f"Customer payment {inv['inv_no']}", amt, "customer_payment", tg, reason=reason, june=june)

# ---- plan manager remittances: pay batches of approved invoices on business days
remit_seq = [44800]
open_pm = {pm: [] for pm in PM}
pm_pays = []
for d in bdays_in(D(2026, 6, 1), END):
    for pm in PM:
        open_pm[pm] += [i for i in inv_by_partner[pm] if i["date"] == d]
    if d < START: continue
    for pm in PM:
        if R.random() > 0.5: continue
        ready = [i for i in open_pm[pm] if (d - i["date"]).days >= R.choice([3, 5, 8])][:R.choice([2, 3, 4, 6, 8, 12])]
        if not ready: continue
        for i in ready: open_pm[pm].remove(i)
        pm_pays.append((d, pm, ready))
short_slots = set(R.sample(range(len(pm_pays)), 9))
unreg_slots = set(R.sample(sorted(set(range(len(pm_pays))) - short_slots), int(len(pm_pays) * 0.09)))
early_slots = set(R.sample(sorted(set(range(len(pm_pays))) - short_slots - unreg_slots), int(len(pm_pays) * 0.08)))
short_err = set(sorted(short_slots)[:3])  # short paid, but the full amount was registered in Odoo
for k, (d, pm, ready) in enumerate(pm_pays):
    remit_seq[0] += 1; rr = f"RA{remit_seq[0]}"
    tg = TG(); lines = []
    for j, i in enumerate(ready):
        paid = i["amount_total"]
        if k in short_slots and j == 0:
            paid = c(paid - R.choice([14.55, 29.10, 30.00, 58.20]))
        lines.append((i, paid)); remits.append(dict(remit_ref=rr, payer=pm, date=d, inv_no=i["inv_no"], invoiced=i["amount_total"], paid=paid))
    tot = c(sum(p for _, p in lines))
    B("OPS", d, f"DIRECT CREDIT {payee(pm)} {rr}", tot, tg, reason="REG" if k in unreg_slots else ("SHORT" if k in short_err else "MATCH"))
    if k in unreg_slots: continue
    ld = d - dt.timedelta(1) if k in early_slots else d
    for j, (i, paid) in enumerate(lines):
        amt = i["amount_total"] if (k in short_err and j == 0) else paid
        register(tg, i, amt, ld, reason="SHORT" if (k in short_err and j == 0) else "MATCH")
# duplicate ledger registration (the same remittance keyed twice for one invoice)
for k in R.sample(sorted(set(range(len(pm_pays))) - unreg_slots - short_slots), 2):
    d, pm, ready = pm_pays[k]; i = ready[-1]
    tg = TG(); register(tg, i, i["amount_total"], d + dt.timedelta(1) if d < END else d, reason="DUPL")

# ---- insurers: pay single invoices 7-20 days after invoice
for ins in INS:
    for i in inv_by_partner[ins]:
        pd = next_bday(i["date"], R.randint(5, 16))
        if pd < START or pd > END: continue
        tg = TG(); st = R.choice([0, 1, 2, 3, 4])
        unreg = R.random() < 0.12
        B("OPS", pd, f"DIRECT CREDIT {payee(ins)} CLM{R.randint(5100000, 5999999)} {fmt_ref(i['inv_no'], st)}".strip(), i["amount_total"], tg,
          reason="REG" if unreg else "MATCH")
        if not unreg:
            register(tg, i, i["amount_total"], pd + dt.timedelta(R.choice([0, 0, 0, 0, 1, 2])))

# ---- corporate: per invoice, sometimes bundled, sometimes late, a few unregistered
special_wrongref = None
for cp in CORP:
    invs = sorted(inv_by_partner[cp], key=lambda x: x["date"])
    k = 0
    while k < len(invs):
        i = invs[k]
        pd = next_bday(i["date"], R.choice([20, 24, 28, 30, 33, 40]))
        bundle = [i]
        if R.random() < 0.10 and k + 1 < len(invs):
            bundle = invs[k:k + R.choice([2, 3])]; pd = next_bday(max(x["date"] for x in bundle), 25)
        k += len(bundle)
        if pd < START or pd > END: continue
        tot = c(sum(x["amount_total"] for x in bundle))
        tg = TG(); r = R.random()
        unreg = r < 0.15
        style = R.choice([0, 1, 2, 3, 5]) if len(bundle) == 1 else 5
        B("OPS", pd, f"DIRECT CREDIT {payee(cp)} {fmt_ref(i['inv_no'], style)}".strip(), tot, tg, reason="REG" if unreg else "MATCH")
        if unreg: continue
        lag = R.choice([0, 0, 0, 0, 1, 2, -1])
        for x in bundle: register(tg, x, x["amount_total"], pd + dt.timedelta(lag))
# a payment quoting another customer's invoice number (escalate)
victim = [i for i in inv_by_partner["Ostrander Manufacturing"] if i["date"].month == 8][0]
tg = TG(); B("OPS", D(2026, 9, 18), f"DIRECT CREDIT QUILLON ENGINEERIN {fmt_ref(victim['inv_no'], 1)}", victim["amount_total"], tg, reason="UNEXP")

# ---- US client pays to USD account
USD_CLIENT = "Cascadia Health Analytics Inc"
for m in [6, 7, 8]:
    i = mk_inv(USD_CLIENT, D(2026, m, 5), 6800.00, 0.0, "USD")
    pd = next_bday(i["date"], 30)
    if START <= pd <= END:
        tg = TG(); B("USD", pd, f"INWARD TT CASCADIA HEALTH ANALY {fmt_ref(i['inv_no'], 0)}", 6800.00 - 18.00, tg, reason="FEE-DIFF")
        register(tg, i, 6800.00, pd, acct="USD")

# ---------------- Stripe (online booking payments): charges -> daily payouts, 2 business days
stripe_charges = []
ch_seq = [0]
for d in days(D(2026, 6, 29), END):
    n = R.randint(18, 34) if bday(d) else (R.randint(6, 12) if d.weekday() == 5 else 0)
    for _ in range(n):
        ch_seq[0] += 1
        amt = R.choice([65.00, 85.00, 95.00, 110.00, 120.00, 135.00, 150.00, 180.00, 220.00])
        i = mk_inv("Stripe patient " + f"{R.randint(10000, 99999)}", d, amt, 0.0, "STRIPE")
        fee = c(amt * 0.0175 + 0.30); stripe_charges.append(dict(id=f"ch_3Q{ch_seq[0]:06d}", created=d, gross=amt, fee=fee, inv=i))
pay_of = defaultdict(list)
for ch in stripe_charges:
    pay_of[next_bday(ch["created"], 2)].append(ch)
refund_days = set(R.sample(sorted(pay_of), 6)); dispute_day = sorted(pay_of)[40]
po_seq = [0]
for arr in sorted(pay_of):
    chs = pay_of[arr]; po_seq[0] += 1; po = f"po_1Q{po_seq[0]:04d}KX"
    tg = TG(); net = 0.0
    for ch in chs:
        # Odoo: payment created per charge by the booking integration, gross, on the charge date
        register(tg, ch["inv"], ch["gross"], ch["created"], reason="STRIPE-FEE", june=ch["created"] < START)
        stripe.append(dict(payout_id=po, arrival=arr, txn=ch["id"], type="charge", created=ch["created"], gross=ch["gross"], fee=ch["fee"],
                           net=c(ch["gross"] - ch["fee"]), invoice=ch["inv"]["inv_no"])); net += ch["gross"] - ch["fee"]
    if arr in refund_days:
        ch = R.choice(chs); stripe.append(dict(payout_id=po, arrival=arr, txn="re_" + ch["id"][3:], type="refund", created=arr, gross=-ch["gross"],
                                               fee=0.0, net=-ch["gross"], invoice=ch["inv"]["inv_no"])); net -= ch["gross"]
        L("OPS", arr, ch["inv"]["partner"], ch["inv"]["inv_no"], f"Refund {ch['inv']['inv_no']}", -ch["gross"], "customer_refund", tg, reason="STRIPE-FEE")
    if arr == dispute_day:
        ch = chs[3]; stripe.append(dict(payout_id=po, arrival=arr, txn="dp_" + ch["id"][3:], type="dispute", created=arr, gross=-ch["gross"], fee=25.00,
                                         net=-ch["gross"] - 25.00, invoice=ch["inv"]["inv_no"])); net -= ch["gross"] + 25.00
    if arr <= END:
        B("OPS", arr, f"STRIPE PAYOUT STRIPE {po}", c(net), tg, reason="STRIPE-FEE")
for ch in stripe_charges:
    if ch["created"] < START: june_out["OPS"] += ch["gross"]
for ch in [x for x in stripe_charges if x["created"] < START]:
    pass

# ---------------- supplier bills and payments
bills_by_sup = defaultdict(list)
def mk_bill(sup, date, amt, gst=True, due_days=14):
    no = bill_no()
    rec = dict(inv_no=no, move_type="in_invoice", partner=sup, date=date, due=date + dt.timedelta(due_days), amount_total=c(amt),
               gst=c(amt / 11) if gst else 0.0, paid_ledger=0.0, kind="BILL")
    items.append(rec); bills_by_sup[sup].append(rec); return rec
for d in bdays_in(D(2026, 6, 1), END):
    for _ in range(R.choice([1, 2, 2, 3, 4])):
        s = R.choice(SUP); mk_bill(s, d, R.choice([165, 242, 385, 495, 660, 880, 1210, 1650, 2420, 3850, 5940]) + R.choice([0, 0, 12.10, 36.30]))
batch_seq = [410]
returned_done = False
for d in [x for x in bdays_in(START, END) if x.weekday() == 2]:
    due = [b for s in SUP for b in bills_by_sup[s] if b["due"] <= d + dt.timedelta(7) and b["paid_ledger"] == 0 and b["date"] < d]
    due = due[:R.randint(12, 22)]
    if not due: continue
    batch_seq[0] += 1; br = f"BATCH/2026/{batch_seq[0]:04d}"
    tg = TG(); tot = 0.0
    for b in due:
        b["paid_ledger"] = -b["amount_total"]
        L("OPS", d, b["partner"], b["inv_no"], f"Vendor payment {b['inv_no']} ({br})", -b["amount_total"], "supplier_payment", tg, batch=br); tot += b["amount_total"]
    B("OPS", d, f"ABA PAYMENT FILE {batch_seq[0]:04d} {len(due)} PAYMENTS", -c(tot), tg)
    if not returned_done and d.month == 8:
        returned_done = True; b = due[2]
        B("OPS", next_bday(d, 2), f"RETURNED PAYMENT ABA{batch_seq[0]:04d} {payee(b['partner'])} INVALID ACCT", b["amount_total"], TG(), reason="RETPAY")
# direct debits
DDS = [("Collins St Property Trust", 14850.00, 1, True, "RENT"), ("Northcote Retail Holdings", 9680.00, 1, True, "RENT"),
       ("Bayview Commercial Leasing", 8250.00, 1, True, "RENT"), ("Moorabbin Office Park", 7425.00, 1, True, "RENT"),
       ("Fleetwise Vehicle Leasing", 3184.62, 15, True, "LEASE"), ("Medisure Professional Indemnity", 2215.40, 20, True, "INS"),
       ("Telwave Business Mobile", 1189.30, 12, True, "TEL"), ("Fibrewire Networks", 649.00, 8, True, "TEL"),
       ("Powerco Energy Retail", 2634.18, 22, True, "UTIL"), ("Clinic Practice Software AU", 1870.00, 3, True, "SAAS"),
       ("Equipment Finance Co", 4416.75, 28, False, "LOAN"), ("Cloud Payroll Pty Ltd", 495.00, 5, True, "SAAS")]
nobill = {("Telwave Business Mobile", 8), ("Clinic Practice Software AU", 9)}
for m in [7, 8, 9]:
    for sup, amt, day, gst, kind in DDS:
        d = D(2026, m, day)
        while not bday(d): d += dt.timedelta(1)
        a = c(amt * (1 + R.choice([0, 0, 0, 0.013, -0.021]))) if kind in ("UTIL", "TEL") else amt
        tg = TG()
        if (sup, m) in nobill:
            B("OPS", d, f"DIRECT DEBIT {payee(sup)}", -a, tg, reason="NOBILL"); continue
        b = mk_bill(sup, d - dt.timedelta(R.randint(3, 9)), a, gst)
        r = R.random()
        if kind == "LOAN":
            B("OPS", d, f"DIRECT DEBIT EQUIPMENT FINANCE LOAN {m:02d}", -a, tg)
            L("OPS", d, sup, f"LOAN-{m:02d}", "Equipment loan repayment", -a, "entry", tg); b["paid_ledger"] = -a; continue
        if r < 0.2:
            B("OPS", d, f"DIRECT DEBIT {payee(sup)} {R.randint(100000, 999999)}", -a, tg, reason="REGBILL"); continue
        B("OPS", d, f"DIRECT DEBIT {payee(sup)} {R.randint(100000, 999999)}", -a, tg)
        b["paid_ledger"] = -a
        L("OPS", d + dt.timedelta(R.choice([0, 0, 0, 1, -1, 2])), sup, b["inv_no"], f"Vendor payment {b['inv_no']}", -a, "supplier_payment", tg)
# cheques
chq = [402]
for m in [7, 8, 9]:
    for _ in range(3):
        chq[0] += 1; s = R.choice(SUP); d = D(2026, m, R.choice([4, 11, 18, 24, 27, 29]))
        while not bday(d): d -= dt.timedelta(1)
        b = mk_bill(s, d - dt.timedelta(10), R.choice([132, 286, 418, 550, 924])); b["paid_ledger"] = -b["amount_total"]
        tg = TG(); L("OPS", d, s, f"CHQ {chq[0]:06d}", f"Cheque {chq[0]:06d} {s}", -b["amount_total"], "supplier_payment", tg)
        pres = next_bday(d, R.choice([3, 5, 8, 12, 18]))
        if pres <= END: B("OPS", pres, f"CHEQUE {chq[0]:06d}", -b["amount_total"], tg)
# ATO BAS (Q4 FY26) from operating
tg = TG(); B("OPS", D(2026, 7, 28), "BPAY ATO 551004219987 BAS", -38412.00, tg); L("OPS", D(2026, 7, 28), "Australian Taxation Office", "BAS Q4 FY26", "BAS payment Apr-Jun", -38412.00, "entry", tg)
# bank fees / interest
for m, me in zip([7, 8, 9], MONTH_ENDS):
    B("OPS", me, "ACCOUNT KEEPING FEE", -30.00, TG(), reason="FEE")
    B("OPS", me, "TRANSACTION FEES", -c(R.uniform(38, 64)), TG(), reason="FEE")
    B("OPS", me, "INTEREST CREDIT", c(R.uniform(280, 420)), TG(), reason="INT")
# duplicate statement import (14 Aug lines imported twice)
# unexplained
B("OPS", D(2026, 8, 21), "DEPOSIT BRANCH 3021 CASH", 1850.00, TG(), reason="UNEXP")
B("OPS", D(2026, 9, 9), "VISA DEBIT DIGITALSVC*ONLINE 8841", -49.95, TG(), reason="UNEXP")

# ---------------- payroll clearing account
EMP = [f"{R.choice('ABCDEFGHJKLMNPRSTW')}. {s}" for s in
       ["Nguyen", "Okafor", "Brennan", "Kowalski", "Haddad", "Fitzgerald", "Tran", "Ramos", "Lindqvist", "Mehta", "O'Rourke", "Papadopoulos",
        "Sutherland", "Yamamoto", "Abernathy", "Castellano", "Dempsey", "Eriksen", "Ferraro", "Gallagher", "Hosseini", "Ivanova", "Jovanovic",
        "Kaur", "Lombardi", "Macintyre", "Novak", "Oyelaran", "Pellegrino", "Quach", "Rasmussen", "Sandoval", "Teague", "Upton", "Vasquez",
        "Whitlock", "Xu", "Yilmaz", "Zielinski", "Ashby", "Barossa", "Coombe", "Delacroix", "Esposito", "Fairweather", "Grieve", "Halloran",
        "Iqbal", "Jessop", "Kerrigan", "Lachlan", "Moreau", "Nakamura", "Ormsby", "Prentice", "Redfern"]]
base = {e: R.choice([1680, 1920, 2150, 2380, 2610, 2840, 3070, 3390]) for e in EMP}
runs = [D(2026, 7, 2) + dt.timedelta(14 * k) for k in range(7)]
for k, rd in enumerate(runs):
    pr = f"PR{rd:%y%m%d}"
    pays = {e: c(base[e] * R.uniform(0.92, 1.12)) for e in EMP}
    net = c(sum(pays.values())); payg = c(net * 0.24); sup = c(net * 0.14)
    need = c(net + payg * (1 if k % 2 == 1 else 0) + sup)
    tg = TG(); fd = rd - dt.timedelta(1)
    B("OPS", fd, f"TFR TO PAYROLL CLEARING {pr}", -need, tg); L("OPS", fd, "Internal transfer", f"TFR {pr}", f"Transfer to payroll clearing {pr}", -need, "transfer", tg)
    tg = TG(); B("PAY", fd, f"TFR FROM OPERATING {pr}", need, tg)
    L("PAY", fd if k != 4 else fd + dt.timedelta(1), "Internal transfer", f"TFR {pr}", f"Transfer from operating {pr}", need, "transfer", tg)
    tg = TG(); L("PAY", rd, "Payroll", pr, f"Net wages pay run {pr}", -net, "entry", tg)
    bad = EMP[17] if k == 3 else None
    for e in EMP:
        B("PAY", rd, f"SALARY {e.upper()} {pr}", -pays[e], tg)
    if bad:
        B("PAY", next_bday(rd, 1), f"RETURN SALARY {bad.upper()} {pr} INVALID BSB", pays[bad], TG(), reason="RETPAY-PAY")
        B("PAY", next_bday(rd, 2), f"SALARY {bad.upper()} {pr} REISSUE", -pays[bad], TG(), reason="RETPAY-PAY")
    tg = TG(); sd = next_bday(rd, R.choice([2, 3, 5]))
    B("PAY", sd, f"SUPERCHOICE CLEARING {pr}", -sup, tg); L("PAY", sd if k != 6 else rd, "Super clearing house", f"SUPER {pr}", f"Super contributions {pr}", -sup, "entry", tg)
    if k % 2 == 1:
        tg = TG(); ad = D(2026, rd.month, 21) if rd.month < 10 else rd
        while not bday(ad): ad += dt.timedelta(1)
        B("PAY", ad, "BPAY ATO PAYG W1", -payg, tg); L("PAY", ad, "Australian Taxation Office", f"PAYG {pr}", f"PAYG withholding {pr}", -payg, "entry", tg)
for me in MONTH_ENDS: B("PAY", me, "ACCOUNT KEEPING FEE", -10.00, TG(), reason="FEE")

# ---------------- merchant settlement account (EFTPOS terminals at 4 clinics)
CLIN = {"T401": "Carlton", "T402": "Footscray", "T403": "Box Hill", "T404": "Frankston"}
settle_tot = defaultdict(float)
for d in days(START - dt.timedelta(1), END):
    if d.weekday() == 6: continue
    for t, cl in CLIN.items():
        amt = c(R.uniform(900, 3600) if bday(d) else R.uniform(400, 1300))
        sd = next_bday(d, 1)
        tg = TG()
        refund_gap = (t == "T403" and d in (D(2026, 8, 6), D(2026, 9, 15)))
        posted = c(amt - 120.00) if refund_gap else amt
        if d >= START:
            L("MER", d, f"{cl} clinic takings", f"POS {t} {d:%d%m%y}", f"Daily card takings {cl} {d:%d/%m}", amt, "entry", tg, reason="REFUND-GAP" if refund_gap else "MATCH")
        else:
            june_out["MER"] += amt
            L_flag = False
        if sd <= END:
            B("MER", sd, f"SETTLEMENT {t} {d:%d%m%y}", posted, tg, reason="REFUND-GAP" if refund_gap else "MATCH")
        if d < START:
            ledger.append(dict(entry_id=nid("JI-"), journal="MER", date=d, partner=f"{cl} clinic takings", ref=f"POS {t} {d:%d%m%y}",
                               label=f"Daily card takings {cl} {d:%d/%m}", amount=amt, move_type="entry", batch_ref=""))
            truth.append(dict(kind="ledger", id=ledger[-1]["entry_id"], tg=tg, reason="MATCH"))
        settle_tot[d.month] += amt
# weekly sweep MER -> OPS on Mondays
for d in [x for x in days(START, END) if x.weekday() == 0]:
    amt = c(R.choice([22000, 26000, 30000, 34000]))
    tg = TG(); B("MER", d, "SWEEP TO OPERATING", -amt, tg); L("MER", d, "Internal transfer", f"SWEEP {d:%d%m}", "Sweep to operating", -amt, "transfer", tg)
    tg = TG(); B("OPS", d, "SWEEP FROM MERCHANT ACCT", amt, tg); L("OPS", d if d != D(2026, 8, 31) else D(2026, 9, 1), "Internal transfer", f"SWEEP {d:%d%m}", "Sweep from merchant account", amt, "transfer", tg)
for m, me in zip([7, 8, 9], MONTH_ENDS):
    fee = c(settle_tot[m] * 0.0098)
    B("MER", next_bday(me, 0) if bday(me) else me, f"MERCHANT SERVICE FEE {me:%b%y}".upper(), -fee, TG(), reason="MERCH")
B("MER", D(2026, 8, 19), "CHARGEBACK T402 REF 88104 CARD ****4417", -340.00, TG(), reason="CHGBK")
B("MER", D(2026, 9, 3), "ADJUSTMENT CR", 64.90, TG(), reason="UNEXP")

# ---------------- USD account and FX conversions
FXR = {7: 0.6584, 8: 0.6531, 9: 0.6612}
for m in [7, 8, 9]:
    usd = 9000.00; d = D(2026, m, 10)
    while not bday(d): d += dt.timedelta(1)
    bank_aud = c(usd / (FXR[m] + R.choice([-0.0007, 0.0004, -0.0003])))
    book_aud = c(usd / FXR[m])
    tg = TG(); B("OPS", d, f"FX CONVERSION AUD TO USD FX{m:02d}{d.day:02d}", -bank_aud, tg, reason="FXR")
    L("OPS", d, "Internal transfer", f"FX{m:02d}{d.day:02d}", f"Transfer to USD account (USD {usd:,.2f} @ {FXR[m]})", -book_aud, "transfer", tg, reason="FXR")
    tg = TG(); B("USD", d, f"FX CONVERSION FROM AUD FX{m:02d}{d.day:02d}", usd, tg); L("USD", d, "Internal transfer", f"FX{m:02d}{d.day:02d}", "Transfer from operating", usd, "transfer", tg)
# Sep 30 conversion booked in Odoo, funds land 1 Oct (in transit)
tg = TG(); L("USD", D(2026, 9, 30), "Internal transfer", "FX0930", "Transfer from operating (in transit)", 5000.00, "transfer", tg)
tg = TG(); aud = c(5000 / 0.6612); B("OPS", D(2026, 9, 30), "FX CONVERSION AUD TO USD FX0930", -aud, tg); L("OPS", D(2026, 9, 30), "Internal transfer", "FX0930", "Transfer to USD account (USD 5,000.00 @ 0.6612)", -aud, "transfer", tg)
USV = [("Telehealth Platform Inc", 2400.00, 6), ("Clinical Notes Cloud LLC", 1180.00, 14), ("Outcome Measures Co", 640.00, 20), ("Video Visit Systems", 960.00, 25)]
for m in [7, 8, 9]:
    for v, amt, day in USV:
        d = D(2026, m, day)
        while not bday(d): d += dt.timedelta(1)
        b = mk_bill(v, d - dt.timedelta(5), amt, False); b["paid_ledger"] = -amt
        tg = TG(); B("USD", d, f"OUTWARD TT {payee(v)} {b['inv_no'].replace('/', '')}", -amt, tg); L("USD", d, v, b["inv_no"], f"Vendor payment {b['inv_no']}", -amt, "supplier_payment", tg)
        B("USD", d, "INTL TRANSFER FEE", -15.00, TG(), reason="FEE")
B("USD", D(2026, 9, 22), "INWARD TT REF 7781 ORIGINATOR UNKNOWN", 1200.00, TG(), reason="UNEXP")

# ---------------- duplicate import: OPS 14 Aug lines re-imported in a second file
dup_day = D(2026, 8, 14)
for b in [x for x in bank if x["account"] == "OPS" and x["date"] == dup_day]:
    lid = nid("BL-")
    bank.append(dict(b, line_id=lid, import_batch=f"OPS-{dup_day:%Y%m%d}-R"))
    truth.append(dict(kind="bank", id=lid, tg="DUP", reason="DUPIMP"))

# ---------------- statement balances (official) and Odoo trial balance
bank.sort(key=lambda x: (x["account"], x["date"], x["import_batch"], x["line_id"]))
ledger.sort(key=lambda x: (x["journal"], x["date"], x["entry_id"]))
bal_rows, tb_rows = [], []
for a in ["OPS", "PAY", "MER", "USD"]:
    bopen = c(LEDGER_OPEN[a] - june_out[a])
    prev = D(2026, 6, 30); o = bopen
    for me in MONTH_ENDS:
        mv = sum(x["amount"] for x in bank if x["account"] == a and prev < x["date"] <= me and not x["import_batch"].endswith("-R"))
        cl = c(o + mv); bal_rows.append(dict(account=a, period_start=prev + dt.timedelta(1), period_end=me, opening=o, closing=cl)); o = cl; prev = me
    lb = LEDGER_OPEN[a]; tb_rows.append(dict(account=a, date=D(2026, 6, 30), balance=c(lb)))
    for me in MONTH_ENDS:
        lb = c(LEDGER_OPEN[a] + sum(x["amount"] for x in ledger if x["journal"] == a and D(2026, 6, 30) < x["date"] <= me))
        tb_rows.append(dict(account=a, date=me, balance=lb))

# Odoo open items at export (30 Sep): residual = total - registered payments; fully paid items left out
oi = []
for it in items:
    if it["kind"] == "STRIPE": continue
    res = c(it["amount_total"] - it["paid_ledger"]) if it["move_type"] == "out_invoice" else c(it["amount_total"] + it["paid_ledger"])
    if abs(res) < 0.005 or it["date"] > END: continue
    oi.append(dict(inv_no=it["inv_no"], move_type=it["move_type"], partner=it["partner"], date=it["date"], due=it["due"],
                   currency="USD" if it["kind"] == "USD" or it["partner"] in [v[0] for v in USV] else "AUD", amount_total=it["amount_total"], gst=it["gst"], residual=res))
def w(name, rows):
    with open(os.path.join(OUT, name), "w", newline="") as f:
        wr = csv.DictWriter(f, fieldnames=list(rows[0].keys())); wr.writeheader(); wr.writerows(rows)
w("bank_statement_lines.csv", bank); w("statement_balances.csv", bal_rows); w("odoo_bank_journal_items.csv", ledger)
w("odoo_trial_balance_bank.csv", tb_rows); w("odoo_open_items.csv", oi); w("remittance_advices.csv", remits)
w("stripe_payout_report.csv", stripe); w("expected_outcomes.csv", truth)
w("fx_rates_odoo.csv", [dict(month=f"2026-{m:02d}", usd_per_aud=r) for m, r in FXR.items()])
from collections import Counter
print("bank", len(bank), Counter(x["account"] for x in bank)); print("ledger", len(ledger), "open items", len(oi), "remits", len(remits), "stripe", len(stripe))
