"""Post-fix for t24: (1) predicted pay date = date by which half of the open balance is expected to be paid (per invoice: due date + usual lateness; invoices already later than usual are expected within one spread from today); (2) basis says which balances are Binzagr's (grocery) and which are simulated.""" import numpy as np, pandas as pd from common import * P='/mnt/user-data/outputs/Binzagr_Data/11_Dashboard_Data/t24_receivables.csv' AS_OF=pd.Timestamp('2026-10-20') t=pd.read_csv(P); OI=load('fact_open_invoice'); RC=load('fact_receipt') lat=RC[RC.days_late.notna()&(pd.to_datetime(RC.receipt_date)>AS_OF-pd.Timedelta(days=120))].groupby('customer_id').days_late.agg(['mean','std']) gl=RC.days_late.mean() OI['due_in']=(pd.to_datetime(OI.due_date)-AS_OF).dt.days m=OI.customer_id.map(lat['mean']).fillna(gl); sd=OI.customer_id.map(lat['std']).fillna(10).clip(lower=5) d=OI.due_in+m OI['exp_days']=np.where(d>=1,d,np.maximum(1,sd)).round() def wmed(g): g=g.sort_values('exp_days'); c=g.open_sar.cumsum(); return g.exp_days[c>=c.iloc[-1]/2].iloc[0] pd_=OI.groupby('customer_id').apply(wmed) t['predicted_pay_date']=[(AS_OF+pd.Timedelta(days=int(pd_.get(c,1)))).date() for c in t.customer_id] t['basis']='Simulated balance + predicted payment' # fact_open_invoice is simulated for every channel, grocery included; real grocery ageing is on the Receivables screen t.to_csv(P,index=False) print(t.predicted_pay_date.astype(str).value_counts().head(), t.basis.value_counts()) print(t.head(5)[['customer_name','overdue_sar','predicted_pay_date','prob_pay_within_30d']])