| """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": |
| |
| |
| |
| 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": |
| |
| 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: |
| |
| 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 |
|
|