"""Period helpers — floral wholesale is highly seasonal, so YoY / same-period comparisons are first-class. Everything returns ISO date strings (yyyy-mm-dd). `today` is injectable for testing/reproducibility. """ import datetime as dt def _d(x): return x.isoformat() def today(): return dt.date.today() def ytd(t=None): """(start, end) for year-to-date through `t`.""" t = t or today() return _d(dt.date(t.year, 1, 1)), _d(t) def ytd_last_year(t=None): """Same calendar window one year earlier (Jan 1 LY → same month/day LY). Clamps Feb 29 → Feb 28 in non-leap years.""" t = t or today() try: end = dt.date(t.year - 1, t.month, t.day) except ValueError: end = dt.date(t.year - 1, t.month, t.day - 1) return _d(dt.date(t.year - 1, 1, 1)), end if isinstance(end, str) else _d(end) def ltm(t=None): """Trailing 12 months ending `t`.""" t = t or today() return _d(t - dt.timedelta(days=365)), _d(t) def prior_ltm(t=None): t = t or today() return _d(t - dt.timedelta(days=730)), _d(t - dt.timedelta(days=365)) def mtd(t=None): t = t or today() return _d(dt.date(t.year, t.month, 1)), _d(t) def wtd(t=None): """(Monday of this ISO week, t) — week-to-date.""" t = t or today() return _d(t - dt.timedelta(days=t.weekday())), _d(t) def qtd(t=None): """(first day of this calendar quarter, t) — quarter-to-date.""" t = t or today() q_start_month = ((t.month - 1) // 3) * 3 + 1 return _d(dt.date(t.year, q_start_month, 1)), _d(t) def quarter_range(t=None): """(first day, last day) of the calendar quarter containing t.""" t = t or today() qs = ((t.month - 1) // 3) * 3 + 1 first = dt.date(t.year, qs, 1) nxt = dt.date(t.year + (1 if qs + 3 > 12 else 0), ((qs + 2) % 12) + 1, 1) return _d(first), _d(nxt - dt.timedelta(days=1)) def shift_year(date_from, date_to, weeks=False): """Same window one year earlier. For week-aligned comparisons (weeks=True) shift by exactly 52 weeks (364 days) so the weekday lines up; otherwise shift by calendar year (clamping Feb 29).""" f = dt.date.fromisoformat(date_from) t = dt.date.fromisoformat(date_to) if weeks: return _d(f - dt.timedelta(days=364)), _d(t - dt.timedelta(days=364)) def back(d): try: return dt.date(d.year - 1, d.month, d.day) except ValueError: return dt.date(d.year - 1, d.month, d.day - 1) return _d(back(f)), _d(back(t)) def last_n_days(n, t=None): t = t or today() return _d(t - dt.timedelta(days=n)), _d(t) def year_range(year): return f'{year}-01-01', f'{year}-12-31' def month_range(year, month): """(first day, last day) of a given calendar month, ISO strings.""" first = dt.date(year, month, 1) nxt = dt.date(year + (1 if month == 12 else 0), 1 if month == 12 else month + 1, 1) return _d(first), _d(nxt - dt.timedelta(days=1)) def period_options(t=None, n_months=15): """Ordered {label: (date_from, date_to)} for the close/reconciliation period picker.""" t = t or today() opts = {} opts['This month (to date)'] = (_d(dt.date(t.year, t.month, 1)), _d(t)) lm_year, lm_month = (t.year, t.month - 1) if t.month > 1 else (t.year - 1, 12) lf, lt = month_range(lm_year, lm_month) opts[f'Last month ({lf[:7]})'] = (lf, lt) opts['Quarter to date'] = (_d(dt.date(t.year, ((t.month - 1) // 3) * 3 + 1, 1)), _d(t)) opts['Year to date'] = (_d(dt.date(t.year, 1, 1)), _d(t)) for label, mf, mt in reversed(month_starts(n_months, t)): if label == f'{t.year}-{t.month:02d}' or label == lf[:7]: continue opts[label] = (mf, mt) return opts def month_starts(n_back=13, t=None): """List of (yyyy-mm, start, end) for the last n_back months including current.""" t = t or today() out = [] y, m = t.year, t.month for _ in range(n_back): start = dt.date(y, m, 1) end = (dt.date(y + (m // 12), (m % 12) + 1, 1) - dt.timedelta(days=1)) out.append((f'{y}-{m:02d}', _d(start), _d(end))) m -= 1 if m == 0: m = 12 y -= 1 return list(reversed(out)) def week_starts(n_back=12, t=None): """List of (label, start, end) for the last n_back ISO weeks (Mon–Sun) including the current (partial) week. label = the Monday as 'MM-DD' (compact, chart-friendly); end is clamped to t for the current week so weekly totals never include future days.""" t = t or today() this_mon = t - dt.timedelta(days=t.weekday()) out = [] for i in range(n_back): mon = this_mon - dt.timedelta(weeks=i) sun = mon + dt.timedelta(days=6) end = min(sun, t) out.append((f'{mon.month:02d}-{mon.day:02d}', _d(mon), _d(end))) return list(reversed(out)) def yoy_pct(this_v, last_v): if last_v: return (this_v - last_v) / last_v * 100.0 return None