Spaces:
Running
Running
| """Build CLEAN PR-style line rows from form values — deterministic pipe format.""" | |
| from __future__ import annotations | |
| import calendar | |
| from datetime import date | |
| from typing import Any | |
| INTERVAL_ORDER = [ | |
| "Daily", | |
| "Weekly", | |
| "Monthly", | |
| "Bi-Monthly", | |
| "Quarterly", | |
| "Bi-Annual", | |
| "Annual", | |
| ] | |
| INTERVAL_SHORT: dict[str, str] = { | |
| "Daily": "D", | |
| "Weekly": "W", | |
| "Monthly": "M", | |
| "Bi-Monthly": "BM", | |
| "Quarterly": "Q", | |
| "Bi-Annual": "BA", | |
| "Annual": "A", | |
| } | |
| def _month_end_dates(year: int, months: list[int]) -> list[date]: | |
| out: list[date] = [] | |
| for m in months: | |
| last = calendar.monthrange(year, m)[1] | |
| out.append(date(year, m, last)) | |
| return out | |
| def delivery_dates(count: int, interval: str, year: int) -> list[str]: | |
| """Return formatted dates like '31 Mar 26' for each delivery line.""" | |
| count = max(1, min(int(count), 52)) | |
| y = int(year) | |
| dates: list[date] = [] | |
| if interval == "Quarterly": | |
| # Anchor at March of `year`, then Mar → Jun → Sep → Mar (next year), … | |
| # Skips December quarter-ends so multi-year schedules reach Q1 of the next year | |
| # (e.g. 4 deliveries in 2026: Mar, Jun, Sep 2026, then Mar 2027). | |
| yy = y | |
| trip = (3, 6, 9) | |
| i = 0 | |
| while len(dates) < count: | |
| if i > 0 and i % 3 == 0: | |
| yy += 1 | |
| m = trip[i % 3] | |
| last = calendar.monthrange(yy, m)[1] | |
| dates.append(date(yy, m, last)) | |
| i += 1 | |
| elif interval == "Monthly": | |
| if count == 1: | |
| dates.append(date(y, 6, 30)) | |
| elif count <= 12: | |
| for i in range(count): | |
| month = min(12, max(1, round(1 + i * 11 / max(count - 1, 1)))) | |
| last = calendar.monthrange(y, month)[1] | |
| dates.append(date(y, month, last)) | |
| else: | |
| mm = 1 | |
| yy = y | |
| while len(dates) < count: | |
| last = calendar.monthrange(yy, mm)[1] | |
| dates.append(date(yy, mm, last)) | |
| mm += 1 | |
| if mm > 12: | |
| mm = 1 | |
| yy += 1 | |
| elif interval == "Annual": | |
| for i in range(count): | |
| yy = y + i | |
| dates.append(date(yy, 12, 31)) | |
| elif interval == "Weekly": | |
| # Approximate: spread week-ending Fridays across the year | |
| from datetime import timedelta | |
| start = date(y, 1, 1) | |
| step = max(1, 364 // max(count, 1)) | |
| for i in range(count): | |
| d = start + timedelta(days=min(364, i * step)) | |
| dates.append(d) | |
| else: | |
| # Bi-Monthly, Daily, Bi-Annual — month-end ladder across the year | |
| for i in range(count): | |
| month = min(12, max(1, round((i + 1) * 12 / max(count, 1)))) | |
| last = calendar.monthrange(y, month)[1] | |
| dates.append(date(y, month, last)) | |
| out: list[str] = [] | |
| for d in dates[:count]: | |
| out.append(d.strftime("%d %b %y")) | |
| return out | |
| def _abbrev_dynamic(field_id: str, label: str, value: str | int | float) -> str: | |
| s = str(value).strip() | |
| if not s: | |
| return "—" | |
| lid = field_id.lower() | |
| if "sample" in lid or lid.endswith("_size") or "size" in lid: | |
| nums = "".join(c for c in s if c.isdigit()) | |
| if nums: | |
| return f"N{nums}" | |
| if len(s) <= 6 and s.isupper(): | |
| return s | |
| tok = s.split()[0] if s.split() else s | |
| return tok[:12].upper() | |
| def _long_part(field_id: str, label: str, value: str | int | float) -> str: | |
| s = str(value).strip() | |
| lid = field_id.lower() | |
| if "sample" in lid or lid.endswith("_size") or "size" in lid: | |
| nums = "".join(c for c in s if c.isdigit()) | |
| if nums: | |
| return f"Sample Size {nums}" | |
| return s | |
| def build_pr_rows( | |
| *, | |
| mat_grp: int, | |
| dynamic_fields_ordered: list[dict[str, Any]], | |
| dynamic_values: dict[str, Any], | |
| deliveries: int, | |
| interval: str, | |
| other_spec: str, | |
| year: int, | |
| ) -> list[dict[str, Any]]: | |
| """One row per delivery; SHORT/LONG pipe strings deterministic.""" | |
| iv_short = INTERVAL_SHORT.get(interval, interval[:2].upper()) | |
| iv_long = interval | |
| dyn_parts_short: list[str] = [] | |
| dyn_parts_long: list[str] = [] | |
| for f in dynamic_fields_ordered: | |
| fid = f["id"] | |
| label = f.get("label", fid) | |
| val = dynamic_values.get(fid, "") | |
| if val == "" or val is None: | |
| continue | |
| dyn_parts_short.append(_abbrev_dynamic(fid, label, val)) | |
| dyn_parts_long.append(_long_part(fid, label, val)) | |
| other_clean = (other_spec or "").strip() or "—" | |
| dates = delivery_dates(deliveries, interval, year) | |
| rows: list[dict[str, Any]] = [] | |
| for i in range(deliveries): | |
| di = i + 1 | |
| d_label = f"D{di}" | |
| short_core = " | ".join( | |
| [d_label, iv_short, *dyn_parts_short, other_clean] | |
| ) | |
| long_core = " | ".join( | |
| [d_label, iv_long, *dyn_parts_long, other_clean] | |
| ) | |
| line_no = 10 * di | |
| date_s = dates[i] if i < len(dates) else dates[-1] if dates else "" | |
| rows.append( | |
| { | |
| "pr_line_item": line_no, | |
| "mat_grp": mat_grp, | |
| "pr_short_text": short_core, | |
| "pr_long_text": long_core, | |
| "pr_quantity": 1, | |
| "delivery_date": date_s, | |
| } | |
| ) | |
| return rows | |