added multiple fixes to report generating
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
# Delay List PDF Report — Styling Reference
|
||||
|
||||
This file defines the visual design for all Step 3 PDF reports generated by the delay-list-processor skill. The design follows the Helloprint brand identity.
|
||||
|
||||
---
|
||||
|
||||
## Brand Colours
|
||||
|
||||
### Primary Palette
|
||||
| Name | Hex | Usage |
|
||||
|----------------|-----------|----------------------------------------------------------|
|
||||
| Grochimaru | `#D9D909` | Accent highlight (sparingly) |
|
||||
| Snowflake | `#F0F0F0` | Alternating table row background, light backgrounds |
|
||||
| Charmed Green | `#008539` | Primary brand green — cover title block background, section headers, HRFlowable dividers |
|
||||
| Link Green | `#049E46` | Links, secondary green accents |
|
||||
| Theme Black | `#022B07` | Primary text colour, dark table header backgrounds |
|
||||
|
||||
### Secondary Palette
|
||||
| Name | Hex | Usage |
|
||||
|----------------|-----------|----------------------------------------------------------|
|
||||
| Heroic Red | `#D64545` | Internal Issues section header, error/alert indicators |
|
||||
| Honey Teriyaki | `#F36D13` | Review Needed section header, warning indicators |
|
||||
| Ripe Mango | `#F0C225` | Flyeralarm/Probo manual action section headers |
|
||||
| Hampton Beach | `#9D6639` | Reserved (future use) |
|
||||
| Seaside | `#67A5B1` | Other Statuses / Shipped section header, info indicators |
|
||||
|
||||
### Derived Colours
|
||||
| Purpose | Hex | Notes |
|
||||
|----------------------------|-----------|------------------------------------------|
|
||||
| In Production header | `#008539` | Uses Charmed Green (on track = green) |
|
||||
| Table grid lines | `#CCCCCC` | Neutral thin grid |
|
||||
| Table alt row | `#F0F0F0` | Snowflake |
|
||||
| Table header text | `#FFFFFF` | White on dark backgrounds |
|
||||
| Body text | `#022B07` | Theme Black |
|
||||
| Page header/footer text | `#666666` | Muted grey |
|
||||
| Header rule line | `#CCCCCC` | Below page header |
|
||||
|
||||
---
|
||||
|
||||
## Typography
|
||||
|
||||
**Font family:** Inter (Regular and Bold)
|
||||
|
||||
Since `reportlab` does not include Inter by default, download and register `Inter-Regular.ttf` and `Inter-Bold.ttf` before generating the PDF. Fall back to Helvetica only if Inter cannot be loaded.
|
||||
|
||||
```python
|
||||
# Font registration example
|
||||
from reportlab.pdfbase import pdfmetrics
|
||||
from reportlab.pdfbase.ttfonts import TTFont
|
||||
|
||||
try:
|
||||
pdfmetrics.registerFont(TTFont('Inter', 'Inter-Regular.ttf'))
|
||||
pdfmetrics.registerFont(TTFont('Inter-Bold', 'Inter-Bold.ttf'))
|
||||
FONT = 'Inter'
|
||||
FONT_BOLD = 'Inter-Bold'
|
||||
except:
|
||||
FONT = 'Helvetica'
|
||||
FONT_BOLD = 'Helvetica-Bold'
|
||||
```
|
||||
|
||||
### Font Sizes
|
||||
| Element | Font | Size | Style |
|
||||
|----------------------------|---------|------|-------|
|
||||
| Cover title | Bold | 28pt | White on Charmed Green |
|
||||
| Cover subtitle | Regular | 14pt | White on Charmed Green |
|
||||
| Cover date | Regular | 12pt | White on Charmed Green |
|
||||
| Section heading (e.g. "Section 1 — HubSpot Analysis") | Bold | 16pt | Theme Black |
|
||||
| Subsection heading (e.g. "1.1 Internal Issues") | Bold | 13pt | Theme Black |
|
||||
| Supplier heading (e.g. "Oakdene Services — 15 order(s)") | Bold | 13pt | Theme Black |
|
||||
| Table header cells | Bold | 8pt | White |
|
||||
| Table body cells | Regular | 7.5pt | Theme Black |
|
||||
| Email template text | Regular | 9pt | Theme Black |
|
||||
| Page header | Regular | 8pt | #666666 |
|
||||
| Page footer | Regular | 8pt | #666666 |
|
||||
| Stat card number | Bold | 28pt | Theme Black |
|
||||
| Stat card label | Regular | 9pt | #666666 |
|
||||
| Disclaimer / caveat text | Regular | 8pt | Italic, #666666 |
|
||||
|
||||
---
|
||||
|
||||
## Page Layout
|
||||
|
||||
- **Page size:** A4 (595.27 x 841.89 pt)
|
||||
- **Margins:** 18mm left/right, 20mm top (to accommodate header), 18mm bottom
|
||||
- **Header:** "Helloprint | Delay List Step 3 Report | DD Mon YYYY" — 8pt, #666666, with a thin rule line (#CCCCCC) below
|
||||
- **Footer:** Left: "Confidential - Helloprint Operations" — Right: "Page X" — 8pt, #666666
|
||||
|
||||
---
|
||||
|
||||
## Cover Page (Page 1)
|
||||
|
||||
1. **Title block** — Full-width rectangle with `#008539` (Charmed Green) background
|
||||
- "Delay List Report" — 28pt Bold, white
|
||||
- "Step 3: Supplier Outreach Analysis" — 14pt Regular, white
|
||||
- "DD Mon YYYY" — 12pt Regular, white
|
||||
- Padding: 20pt top/bottom, 18mm left
|
||||
|
||||
2. **Summary stat cards** — 2×4 table grid below the title block
|
||||
- Each cell: large number (28pt Bold, Theme Black) + label below (9pt Regular, #666666)
|
||||
- Cell borders: thin #CCCCCC
|
||||
- Cards: Total Orders | HubSpot Hits | Need Outreach | Internal Issues | Masterprint (Skip) | Flyeralarm (Manual) | Probo (Manual) | Suppliers to Contact
|
||||
|
||||
3. **Caveat note** — Italic 8pt disclaimer about HubSpot search scope
|
||||
|
||||
---
|
||||
|
||||
## Section 1 — HubSpot Analysis
|
||||
|
||||
Section heading: "Section 1 — HubSpot Analysis" (16pt Bold, Theme Black)
|
||||
|
||||
### Subsections with colour-coded table headers:
|
||||
|
||||
| Subsection | Header background | Header text |
|
||||
|------------|-------------------|-------------|
|
||||
| 1.1 Internal Issues | `#D64545` (Heroic Red) | White |
|
||||
| 1.2 In Production / On Track | `#008539` (Charmed Green) | White |
|
||||
| 1.3 Shipped | `#67A5B1` (Seaside) | White |
|
||||
| 1.4 Other Statuses | `#67A5B1` (Seaside) | White |
|
||||
| 1.5 Review Needed | `#F36D13` (Honey Teriyaki) | White |
|
||||
|
||||
### Table columns:
|
||||
| Column | Width % | Content |
|
||||
|--------|---------|---------|
|
||||
| Ticket ID | 18% | Real `hs_ticket_id` |
|
||||
| Order Detail ID | 15% | `id_order_detail` |
|
||||
| Order ID | 12% | `id_order` |
|
||||
| Details | 55% | Ticket subject / summary (use Paragraph flowable for wrapping) |
|
||||
|
||||
- Alternating rows: white / `#F0F0F0`
|
||||
- Grid: 0.5pt `#CCCCCC`
|
||||
- Cell padding: 4pt
|
||||
|
||||
---
|
||||
|
||||
## Section 2 — Manual Action Required
|
||||
|
||||
Section heading: "Section 2 — Manual Action Required" (16pt Bold, Theme Black)
|
||||
|
||||
### Flyeralarm
|
||||
- Subsection heading: "Flyeralarm (N orders)" — 13pt Bold
|
||||
- Instruction text (italic, 9pt): "These orders must be handled via the Flyeralarm supplier portal (case creation). Do NOT send outreach emails."
|
||||
- Table with `#F0C225` (Ripe Mango) header background, white text
|
||||
- Columns: Order Detail ID | Order ID | Supplier | Product | Target Dispatch
|
||||
|
||||
### Probo
|
||||
- Subsection heading: "Probo (N orders)" — 13pt Bold
|
||||
- Instruction text (italic, 9pt): "Verify actual production status in the Probo portal. Probo delay emails received for some orders — follow up for new dispatch date."
|
||||
- Table with `#F0C225` (Ripe Mango) header background, white text
|
||||
- Columns: Order Detail ID | Order ID | Product | Target Dispatch | **Delay Email?** (Yes/No — whether a Probo delay email was found in HubSpot)
|
||||
|
||||
### Masterprint
|
||||
- Subsection heading: "Masterprint (N orders)" — 13pt Bold
|
||||
- Text only (italic, 9pt): "Masterprint tracking is automated. No outreach needed."
|
||||
|
||||
---
|
||||
|
||||
## Section 3 — Supplier Outreach Emails
|
||||
|
||||
Section heading: "Section 3 — Supplier Outreach Emails" (16pt Bold, Theme Black)
|
||||
Subtitle: "N orders across M suppliers require outreach." (9pt Regular, #666666)
|
||||
|
||||
### Per-supplier block:
|
||||
|
||||
1. **Divider:** HRFlowable, 1pt, `#008539` (Charmed Green)
|
||||
2. **Supplier heading:** "Supplier Name — N order(s)" (13pt Bold, Theme Black)
|
||||
3. **Email opening:** "Hi Partner," (9pt Regular)
|
||||
4. **Email body:** Template text (9pt Regular)
|
||||
5. **Order table:**
|
||||
- Header: `#022B07` (Theme Black) background, white text, 8pt Bold
|
||||
- Columns and widths:
|
||||
|
||||
| Column | Width % | Content | Format |
|
||||
|--------|---------|---------|--------|
|
||||
| Order | 20% | `{id_order}-{id_order_detail}` | e.g. "5831128-9215018" |
|
||||
| Target Dispatch Date | 15% | Agreed dispatch date | DD Mon YYYY |
|
||||
| Product | 25% | Product type/SKU | As from API |
|
||||
| Supplier Order | 15% | Supplier's order number | Value or "-" |
|
||||
| Carrier | 25% | Intended carrier | As from API |
|
||||
|
||||
6. **Email closing:** "Please could you provide..." + "Thank you..." (9pt Regular)
|
||||
7. **Spacer:** 12pt before next supplier
|
||||
|
||||
### Important implementation notes:
|
||||
- Do NOT wrap supplier email blocks in a single-cell Table — use individual Paragraph + Table + Spacer flowables so reportlab can split across page breaks
|
||||
- Use `KeepTogether` only for supplier blocks with ≤5 orders
|
||||
- Alternating rows: white / `#F0F0F0`
|
||||
- Grid: 0.5pt `#CCCCCC`
|
||||
|
||||
---
|
||||
|
||||
## Inter Font Download
|
||||
|
||||
To use Inter, download from Google Fonts before generating:
|
||||
|
||||
```python
|
||||
import urllib.request, os
|
||||
|
||||
FONT_DIR = '/tmp/fonts'
|
||||
os.makedirs(FONT_DIR, exist_ok=True)
|
||||
|
||||
INTER_REGULAR = f'{FONT_DIR}/Inter-Regular.ttf'
|
||||
INTER_BOLD = f'{FONT_DIR}/Inter-Bold.ttf'
|
||||
|
||||
if not os.path.exists(INTER_REGULAR):
|
||||
urllib.request.urlretrieve(
|
||||
'https://github.com/google/fonts/raw/main/ofl/inter/Inter%5Bopsz%2Cwght%5D.ttf',
|
||||
INTER_REGULAR
|
||||
)
|
||||
# Note: Inter is a variable font. For reportlab, you may need the static builds:
|
||||
# https://github.com/rsms/inter/releases — use Inter-Regular.ttf and Inter-Bold.ttf
|
||||
```
|
||||
|
||||
If the download fails (network restrictions), fall back to Helvetica silently.
|
||||
+34
-23
@@ -361,50 +361,61 @@ Generate at end of processing and save as a file:
|
||||
|
||||
## **PDF Report Generation**
|
||||
|
||||
After completing Step 3, generate a professional PDF report using `reportlab`. The PDF serves as the primary deliverable for the traffic team to review and act on. Install with `pip install reportlab --break-system-packages` if not available.
|
||||
After completing Step 3, generate the PDF report by running **`generate_report.py`** (in this same skill folder). This script contains all layout, branding, classification logic, and table formatting. Do NOT write PDF generation code from scratch — always use this script.
|
||||
|
||||
### PDF Structure
|
||||
```bash
|
||||
python generate_report.py <orders_json> <hubspot_tickets_json> <output_pdf> [--date "DD Month YYYY"]
|
||||
```
|
||||
|
||||
The report has three main sections, each starting on a new page:
|
||||
**Example:**
|
||||
```bash
|
||||
python generate_report.py s3_today.json hs_tickets.json delay-list-step3-report-2026-04-10.pdf --date "10 April 2026"
|
||||
```
|
||||
|
||||
The script requires two input JSON files:
|
||||
- **orders_json** — the Step 3 filtered orders (output of the delay list API filtering in Step 3 above). List of order objects with keys: `id_order_detail`, `id_order`, `supplier_name`, `product_tid`, `despatch_date_target`, `supplier_order_number`, `carriername`.
|
||||
- **hubspot_tickets_json** — merged HubSpot ticket search results from the mandatory 3-step search. List of ticket objects with nested `properties` containing: `hs_ticket_id`, `hp_order_line_id`, `subject`, `content`, `createdate`.
|
||||
|
||||
The script handles all classification (HubSpot 5-category split, Flyeralarm/Probo/Masterprint separation, Probo delay email detection, outreach grouping) and generates the branded PDF automatically.
|
||||
|
||||
> **Styling reference:** All colours, fonts, layout specs, table column widths, and section structure are also documented in **`REPORT-STYLE.md`** (in this same skill folder) for reference. The script implements these specs. If you need to modify the visual design, update both the script and REPORT-STYLE.md.
|
||||
|
||||
### PDF Structure & Content
|
||||
|
||||
The report has three main sections, each starting on a new page. The exact visual design (colours, fonts, column widths, spacing) is specified in `REPORT-STYLE.md`. Below is the content specification:
|
||||
|
||||
**Page 1 — Cover & Summary**
|
||||
- Title block with dark background: "Delay List Report — Step 3: Supplier Outreach Analysis" + date
|
||||
- Summary stat cards in a grid layout showing: Total Orders, HubSpot Hits, Need Outreach, Internal Issues, Masterprint (Skipped), Flyeralarm (Manual), Probo (Manual), Suppliers to Contact
|
||||
- Title block: "Delay List Report" + "Step 3: Supplier Outreach Analysis" + date
|
||||
- Summary stat cards (2×4 grid): Total Orders, HubSpot Hits, Need Outreach, Internal Issues, Masterprint (Skipped), Flyeralarm (Manual), Probo (Manual), Suppliers to Contact
|
||||
- Caveat note about HubSpot search scope limitations
|
||||
|
||||
**Section 1 — HubSpot Analysis**
|
||||
Colour-coded tables categorising orders with existing communication:
|
||||
- **1.1 Internal Issues** (red header) — orders blocked by artwork errors, API failures, retry limits, price disputes. These need Helloprint action, not supplier outreach.
|
||||
- **1.2 In Production / On Track / Reprints** (green header) — orders confirmed in production or with reprints underway. No outreach needed.
|
||||
- **1.3 Other Statuses** (blue header) — shipped, awaiting approval, customer issues, unclear status.
|
||||
- **1.4 HubSpot Tickets Found — Review Needed** (orange header) — tickets exist but need manual review to determine action.
|
||||
- **1.1 Internal Issues** — orders blocked by artwork errors, API failures, retry limits, price disputes. These need Helloprint action, not supplier outreach.
|
||||
- **1.2 In Production / On Track** — orders confirmed in production with delay reason, reprints underway. No outreach needed.
|
||||
- **1.3 Shipped** — orders where tracking/shipping confirmation was found.
|
||||
- **1.4 Other Statuses** — awaiting approval, customer issues, unclear status.
|
||||
- **1.5 HubSpot Tickets Found — Review Needed** — tickets exist but need manual review to determine action.
|
||||
|
||||
Each table has 4 columns: **Ticket ID** (the real HubSpot `hs_ticket_id`), **Order Detail ID** (`id_order_detail`), **Order ID** (`id_order`), **Details** (summary of the issue/status). The Ticket ID must be the actual HubSpot ticket identifier — do NOT use the `id_order_detail` as Ticket ID. When performing the HubSpot search, always store the returned `hs_ticket_id` alongside the matched `hp_order_line_id` / `hp_order_id` so they can be rendered correctly in the report. Use Paragraph flowables for the Details column to allow text wrapping.
|
||||
|
||||
**Section 2 — Manual Action Required**
|
||||
- **Flyeralarm** — order table with note that portal case creation is required
|
||||
- **Probo** — order table with note to verify in Probo portal
|
||||
- **Flyeralarm** — order table with instruction text, columns: Order Detail ID, Order ID, Supplier, Product, Target Dispatch
|
||||
- **Probo** — order table with instruction text, columns: Order Detail ID, Order ID, Product, Target Dispatch, **Delay Email?** (Yes/No — whether a Probo delay email was found in HubSpot)
|
||||
- **Masterprint** — count + note that tracking is automated
|
||||
|
||||
**Section 3 — Supplier Outreach Emails**
|
||||
For each supplier (sorted by order count descending):
|
||||
- Supplier heading with order count
|
||||
- Supplier heading with order count (format: "Supplier Name — N order(s)")
|
||||
- Draft email using the standard email template (see Email Template section above)
|
||||
- Embedded order table within the email block
|
||||
|
||||
### PDF Styling
|
||||
### Implementation Notes
|
||||
|
||||
Use these design principles for a clean, professional look:
|
||||
These notes apply if you ever need to modify `generate_report.py`:
|
||||
|
||||
- **Colours**: Dark navy (#1a1a2e) for title block, blue (#0f3460) for section headers, red (#e94560) for internal issues, green (#27ae60) for on-track, orange (#f39c12) for review-needed, blue (#2980b9) for other statuses
|
||||
- **Fonts**: Helvetica family (Bold for headers, Regular for body)
|
||||
- **Tables**: Dark header row with white text, alternating row backgrounds, thin grid lines (#bdc3c7), compact padding (2-3pt)
|
||||
- **Page**: A4, 18mm left/right margins, header line with "Helloprint | Delay List Step 3 Report | [date]", footer with "Confidential - Helloprint Operations" and page number
|
||||
- **Email blocks**: Separated by horizontal rules (blue #2980b9), not wrapped in a single table cell (large supplier email blocks with many orders must be able to split across pages)
|
||||
|
||||
### Implementation Note
|
||||
|
||||
Do NOT wrap email blocks in a single-cell Table flowable — suppliers with many orders (e.g. Yurchak with 40+ orders) will overflow a single page and cause a LayoutError. Instead, use individual flowables (Paragraph, Table, Spacer) separated by HRFlowable dividers. This allows reportlab to split content naturally across page breaks.
|
||||
- Do NOT wrap email blocks in a single-cell Table flowable — suppliers with many orders will overflow a single page and cause a LayoutError. Use individual flowables separated by HRFlowable dividers. `KeepTogether` only for supplier blocks with ≤5 orders.
|
||||
- All table cell values must be wrapped in `Paragraph` flowables (via the `w()` helper) to enable automatic line-breaking. Plain strings will overflow their columns.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,572 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
generate_report.py — Step 3 Delay List PDF Report Generator
|
||||
Helloprint branded, following REPORT-STYLE.md specs.
|
||||
|
||||
Usage:
|
||||
python generate_report.py <orders_json> <hubspot_tickets_json> <output_pdf> [--date "DD Month YYYY"]
|
||||
|
||||
Arguments:
|
||||
orders_json Path to Step 3 filtered orders JSON (output of delay list API filtering)
|
||||
hubspot_tickets_json Path to HubSpot ticket search results JSON (batch 0 / merged results)
|
||||
output_pdf Path for the generated PDF report
|
||||
--date Report date string (default: today)
|
||||
|
||||
Example:
|
||||
python generate_report.py s3_today.json hs_tickets.json report.pdf --date "10 April 2026"
|
||||
|
||||
The input JSON files must have the following structure:
|
||||
- orders_json: list of order objects with keys: id_order_detail, id_order, supplier_name,
|
||||
product_tid, despatch_date_target, supplier_order_number, carriername
|
||||
- hubspot_tickets_json: list of HubSpot ticket objects with nested 'properties' containing:
|
||||
hs_ticket_id, hp_order_line_id, subject, content, createdate, hs_pipeline_stage
|
||||
"""
|
||||
import json, sys, os, argparse, urllib.request
|
||||
from datetime import datetime
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from reportlab.lib.pagesizes import A4
|
||||
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
||||
from reportlab.lib.units import mm
|
||||
from reportlab.lib import colors
|
||||
from reportlab.platypus import (
|
||||
SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer,
|
||||
KeepTogether, PageBreak, HRFlowable
|
||||
)
|
||||
from reportlab.lib.enums import TA_LEFT, TA_CENTER
|
||||
from reportlab.pdfbase import pdfmetrics
|
||||
from reportlab.pdfbase.ttfonts import TTFont
|
||||
except ImportError:
|
||||
import subprocess
|
||||
subprocess.check_call([sys.executable, "-m", "pip", "install", "reportlab", "--break-system-packages"])
|
||||
from reportlab.lib.pagesizes import A4
|
||||
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
||||
from reportlab.lib.units import mm
|
||||
from reportlab.lib import colors
|
||||
from reportlab.platypus import (
|
||||
SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer,
|
||||
KeepTogether, PageBreak, HRFlowable
|
||||
)
|
||||
from reportlab.lib.enums import TA_LEFT, TA_CENTER
|
||||
from reportlab.pdfbase import pdfmetrics
|
||||
from reportlab.pdfbase.ttfonts import TTFont
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
# Font setup — try Inter (Helloprint brand), fall back to Helvetica
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
FONT_DIR = '/tmp/fonts'
|
||||
os.makedirs(FONT_DIR, exist_ok=True)
|
||||
try:
|
||||
for variant, url in [
|
||||
('Inter-Regular', 'https://github.com/rsms/inter/raw/master/docs/font-files/Inter-Regular.ttf'),
|
||||
('Inter-Bold', 'https://github.com/rsms/inter/raw/master/docs/font-files/Inter-Bold.ttf'),
|
||||
]:
|
||||
fp = f'{FONT_DIR}/{variant}.ttf'
|
||||
if not os.path.exists(fp):
|
||||
urllib.request.urlretrieve(url, fp)
|
||||
pdfmetrics.registerFont(TTFont(variant, fp))
|
||||
FONT = 'Inter-Regular'
|
||||
FONT_B = 'Inter-Bold'
|
||||
except Exception:
|
||||
FONT = 'Helvetica'
|
||||
FONT_B = 'Helvetica-Bold'
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
# Brand colours — from REPORT-STYLE.md
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
CHARMED_GREEN = colors.HexColor("#008539")
|
||||
LINK_GREEN = colors.HexColor("#049E46")
|
||||
THEME_BLACK = colors.HexColor("#022B07")
|
||||
SNOWFLAKE = colors.HexColor("#F0F0F0")
|
||||
HEROIC_RED = colors.HexColor("#D64545")
|
||||
HONEY_TERIYAKI = colors.HexColor("#F36D13")
|
||||
RIPE_MANGO = colors.HexColor("#F0C225")
|
||||
SEASIDE = colors.HexColor("#67A5B1")
|
||||
GRID_GRAY = colors.HexColor("#CCCCCC")
|
||||
MUTED_GRAY = colors.HexColor("#666666")
|
||||
WHITE = colors.white
|
||||
|
||||
PAGE_W, PAGE_H = A4
|
||||
CONTENT_W = PAGE_W - 36 * mm # 18 mm margins each side
|
||||
|
||||
# Supplier sets
|
||||
FLYERALARM_NAMES = {"Flyeralarm", "Flyeralarm BE", "Flyeralarm FR", "Flyeralarm DE"}
|
||||
PROBO_NAMES = {"Probo"}
|
||||
MASTERPRINT_NAMES = {"Masterprint XL"}
|
||||
|
||||
# Informational ticket subject patterns (case-insensitive)
|
||||
INFO_PATTERNS = [
|
||||
'order confirmation', 'bestelling ontvangen', 'auftrag',
|
||||
'being printed', 'ready to print', 'thank you for your order',
|
||||
'print proof update', 'bestel ontwerp', 'drukproef', 'reminder drukproef',
|
||||
'confirmation de projet', 'solo midocean order confirmation',
|
||||
'artwork received', 'quotation', 'o- 003-fsse', '[01840023]',
|
||||
'good news! your',
|
||||
]
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
# Helpers
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
def fmt_date(d):
|
||||
"""Format a date string to 'DD Mon YYYY'."""
|
||||
if not d:
|
||||
return "N/A"
|
||||
for f in ["%Y-%m-%d %H:%M:%S", "%Y-%m-%d"]:
|
||||
try:
|
||||
return datetime.strptime(d[:19], f).strftime("%d %b %Y")
|
||||
except ValueError:
|
||||
pass
|
||||
return d[:10]
|
||||
|
||||
|
||||
def load_json(fp):
|
||||
with open(fp, encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def is_informational(subj: str) -> bool:
|
||||
sl = subj.lower()
|
||||
return any(pat in sl for pat in INFO_PATTERNS)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
# Paragraph styles
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
def _style(name, font=None, size=9, color=THEME_BLACK, align=TA_LEFT,
|
||||
space_before=0, space_after=0, leading=None, bold=False):
|
||||
fn = (FONT_B if bold else font) or FONT
|
||||
return ParagraphStyle(name, fontName=fn, fontSize=size, textColor=color,
|
||||
alignment=align, spaceBefore=space_before,
|
||||
spaceAfter=space_after, leading=leading or size * 1.3)
|
||||
|
||||
|
||||
S_BODY = _style('Body', size=9)
|
||||
S_SECTION = _style('Section', size=16, bold=True, space_before=12, space_after=8)
|
||||
S_SUBSEC = _style('SubSec', size=13, bold=True, space_before=10, space_after=6)
|
||||
S_SUPPLIER = _style('Supplier', size=13, bold=True, space_before=6, space_after=4)
|
||||
S_EMAIL = _style('Email', size=9, leading=13)
|
||||
S_CAVEAT = _style('Caveat', size=8, color=MUTED_GRAY)
|
||||
S_STAT_NUM = _style('StatNum', size=28, bold=True, align=TA_CENTER)
|
||||
S_STAT_LBL = _style('StatLbl', size=9, color=MUTED_GRAY, align=TA_CENTER)
|
||||
S_COVER_TITLE = _style('CoverTitle', size=28, bold=True, color=WHITE)
|
||||
S_COVER_SUB = _style('CoverSub', size=14, color=WHITE)
|
||||
S_COVER_DATE = _style('CoverDate', size=12, color=WHITE)
|
||||
S_SUBTITLE = _style('Subtitle', size=9, color=MUTED_GRAY, space_after=8)
|
||||
S_ITALIC = _style('Italic', size=9, color=MUTED_GRAY)
|
||||
S_TABLE_WRAP = _style('TableWrap', size=7.5, leading=10)
|
||||
S_HDR_CELL = _style('HdrCell', size=8, bold=True, color=WHITE)
|
||||
|
||||
|
||||
def p(text, style):
|
||||
"""Shortcut: create a Paragraph."""
|
||||
return Paragraph(text, style)
|
||||
|
||||
|
||||
def w(text, style=None):
|
||||
"""Wrap any cell value in a Paragraph for automatic line-breaking."""
|
||||
if isinstance(text, Paragraph):
|
||||
return text
|
||||
return Paragraph(str(text), style or S_TABLE_WRAP)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
# Table builder
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
def section_table(header_bg, col_widths_pct, header_labels, rows):
|
||||
"""Colour-coded table with alternating rows and Paragraph-wrapped headers."""
|
||||
cw = [CONTENT_W * pct / 100 for pct in col_widths_pct]
|
||||
hdr = [Paragraph(lbl, S_HDR_CELL) for lbl in header_labels]
|
||||
data = [hdr] + rows
|
||||
t = Table(data, colWidths=cw, repeatRows=1)
|
||||
cmds = [
|
||||
('BACKGROUND', (0, 0), (-1, 0), header_bg),
|
||||
('TEXTCOLOR', (0, 0), (-1, 0), WHITE),
|
||||
('FONTNAME', (0, 0), (-1, 0), FONT_B),
|
||||
('FONTSIZE', (0, 0), (-1, 0), 8),
|
||||
('FONTNAME', (0, 1), (-1, -1), FONT),
|
||||
('FONTSIZE', (0, 1), (-1, -1), 7.5),
|
||||
('TEXTCOLOR', (0, 1), (-1, -1), THEME_BLACK),
|
||||
('GRID', (0, 0), (-1, -1), 0.5, GRID_GRAY),
|
||||
('TOPPADDING', (0, 0), (-1, -1), 4),
|
||||
('BOTTOMPADDING', (0, 0), (-1, -1), 4),
|
||||
('LEFTPADDING', (0, 0), (-1, -1), 4),
|
||||
('VALIGN', (0, 0), (-1, -1), 'TOP'),
|
||||
('ALIGN', (0, 0), (-1, 0), 'LEFT'),
|
||||
]
|
||||
for i in range(1, len(data)):
|
||||
if i % 2 == 0:
|
||||
cmds.append(('BACKGROUND', (0, i), (-1, i), SNOWFLAKE))
|
||||
t.setStyle(TableStyle(cmds))
|
||||
return t
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
# Data classification
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
def classify_orders(orders, tickets_raw):
|
||||
"""
|
||||
Classify orders into HubSpot categories + supplier groups.
|
||||
Returns a dict with all classified lists.
|
||||
"""
|
||||
detail_map = {str(o['id_order_detail']): o for o in orders}
|
||||
|
||||
# Map tickets to detail IDs
|
||||
detail_tickets = defaultdict(list)
|
||||
for t in tickets_raw:
|
||||
pr = t.get('properties', {})
|
||||
lid = str(pr.get('hp_order_line_id', ''))
|
||||
detail_tickets[lid].append(pr)
|
||||
|
||||
# HubSpot categories
|
||||
internal, in_prod, shipped, other_status, review_needed = [], [], [], [], []
|
||||
actionable_dids = set()
|
||||
|
||||
for did, tix_list in detail_tickets.items():
|
||||
if did not in detail_map:
|
||||
continue
|
||||
order = detail_map[did]
|
||||
|
||||
# Filter out Aircall + informational tickets
|
||||
act_tix = [t for t in tix_list
|
||||
if 'aircall' not in (t.get('subject', '') or '').lower()
|
||||
and not is_informational(t.get('subject', '') or '')]
|
||||
if not act_tix:
|
||||
continue
|
||||
|
||||
best = max(act_tix, key=lambda x: x.get('createdate', ''))
|
||||
bs = (best.get('subject', '') or '').lower()
|
||||
bc = (best.get('content', '') or '').lower()
|
||||
entry = (order, best, len(tix_list))
|
||||
actionable_dids.add(did)
|
||||
|
||||
if any(k in bs for k in ['creation failed', 'artwork rejected', 'artwork query',
|
||||
'error in api', 'artwork refusal']):
|
||||
internal.append(entry)
|
||||
elif any(k in bs for k in ['delay order', 'delayed delivery', 'vertraging',
|
||||
'update to your expected shipping date', 'not sent', 'urgent']):
|
||||
in_prod.append(entry)
|
||||
elif any(k in bs for k in ['on the way', 'shipped', 'tracking']):
|
||||
shipped.append(entry)
|
||||
elif 'cancel' in bc[:200]:
|
||||
other_status.append(entry)
|
||||
elif 'problème technique' in bc[:200] or 'technical' in bc[:200]:
|
||||
internal.append(entry)
|
||||
else:
|
||||
review_needed.append(entry)
|
||||
|
||||
# Supplier-specific groups
|
||||
fly_orders = [o for o in orders if o.get('supplier_name', '') in FLYERALARM_NAMES]
|
||||
probo_orders = [o for o in orders if o.get('supplier_name', '') in PROBO_NAMES]
|
||||
master_orders = [o for o in orders if o.get('supplier_name', '') in MASTERPRINT_NAMES]
|
||||
|
||||
# Probo delay email detection
|
||||
probo_delay_dids = set()
|
||||
for o in probo_orders:
|
||||
did = str(o['id_order_detail'])
|
||||
for t in detail_tickets.get(did, []):
|
||||
s = (t.get('subject', '') or '').lower()
|
||||
if 'vertraging' in s or 'delay' in s:
|
||||
probo_delay_dids.add(did)
|
||||
|
||||
# Remove Flyeralarm orders that are already in actionable HubSpot categories
|
||||
fly_orders = [o for o in fly_orders if str(o['id_order_detail']) not in actionable_dids]
|
||||
|
||||
# Outreach: not special supplier + not actionable HubSpot hit
|
||||
special_dids = set(str(o['id_order_detail']) for o in fly_orders + probo_orders + master_orders)
|
||||
outreach = [o for o in orders
|
||||
if str(o['id_order_detail']) not in actionable_dids
|
||||
and str(o['id_order_detail']) not in special_dids]
|
||||
|
||||
outreach_by_sup = defaultdict(list)
|
||||
for o in outreach:
|
||||
outreach_by_sup[o.get('supplier_name', 'Unknown')].append(o)
|
||||
sorted_suppliers = sorted(outreach_by_sup.items(), key=lambda x: -len(x[1]))
|
||||
|
||||
return {
|
||||
'internal': internal,
|
||||
'in_prod': in_prod,
|
||||
'shipped': shipped,
|
||||
'other_status': other_status,
|
||||
'review_needed': review_needed,
|
||||
'actionable_dids': actionable_dids,
|
||||
'fly_orders': fly_orders,
|
||||
'probo_orders': probo_orders,
|
||||
'master_orders': master_orders,
|
||||
'probo_delay_dids': probo_delay_dids,
|
||||
'outreach': outreach,
|
||||
'sorted_suppliers': sorted_suppliers,
|
||||
}
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
# PDF generation
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
def generate_pdf(orders, cls, report_date, output_path):
|
||||
"""
|
||||
Generate the branded PDF report.
|
||||
|
||||
Args:
|
||||
orders: list of order dicts
|
||||
cls: classification dict (from classify_orders)
|
||||
report_date: date string for the report (e.g. "10 April 2026")
|
||||
output_path: file path for the output PDF
|
||||
"""
|
||||
internal = cls['internal']
|
||||
in_prod = cls['in_prod']
|
||||
shipped = cls['shipped']
|
||||
other_status = cls['other_status']
|
||||
review_needed = cls['review_needed']
|
||||
actionable_dids = cls['actionable_dids']
|
||||
fly_orders = cls['fly_orders']
|
||||
probo_orders = cls['probo_orders']
|
||||
master_orders = cls['master_orders']
|
||||
probo_delay_dids = cls['probo_delay_dids']
|
||||
outreach = cls['outreach']
|
||||
sorted_suppliers = cls['sorted_suppliers']
|
||||
|
||||
# ── Page template ──
|
||||
def page_template(canvas, doc):
|
||||
canvas.saveState()
|
||||
canvas.setFont(FONT, 8)
|
||||
canvas.setFillColor(MUTED_GRAY)
|
||||
canvas.drawString(18 * mm, PAGE_H - 14 * mm,
|
||||
f"Helloprint | Delay List Step 3 Report | {report_date}")
|
||||
canvas.setStrokeColor(GRID_GRAY)
|
||||
canvas.setLineWidth(0.5)
|
||||
canvas.line(18 * mm, PAGE_H - 15 * mm, PAGE_W - 18 * mm, PAGE_H - 15 * mm)
|
||||
canvas.drawString(18 * mm, 10 * mm, "Confidential – Helloprint Operations")
|
||||
canvas.drawRightString(PAGE_W - 18 * mm, 10 * mm, f"Page {doc.page}")
|
||||
canvas.restoreState()
|
||||
|
||||
# ── HubSpot row builder ──
|
||||
def hs_rows(items):
|
||||
rows = []
|
||||
for order, ticket, total_cnt in items:
|
||||
tid = ticket.get('hs_ticket_id', 'N/A')
|
||||
did = order.get('id_order_detail', 'N/A')
|
||||
oid = order.get('id_order', 'N/A')
|
||||
subj = ticket.get('subject', '')[:80]
|
||||
sup = order.get('supplier_name', '')
|
||||
extra = f" (+{total_cnt - 1} more)" if total_cnt > 1 else ""
|
||||
detail_text = f"<b>{sup}</b><br/>{subj}{extra}"
|
||||
rows.append([w(tid), w(did), w(oid), p(detail_text, S_TABLE_WRAP)])
|
||||
return rows
|
||||
|
||||
story = []
|
||||
|
||||
# ━━ COVER PAGE ━━
|
||||
title_data = [
|
||||
[p("Delay List Report", S_COVER_TITLE)],
|
||||
[p("Step 3: Supplier Outreach Analysis", S_COVER_SUB)],
|
||||
[p(report_date, S_COVER_DATE)],
|
||||
]
|
||||
title_table = Table(title_data, colWidths=[CONTENT_W])
|
||||
title_table.setStyle(TableStyle([
|
||||
('BACKGROUND', (0, 0), (-1, -1), CHARMED_GREEN),
|
||||
('TOPPADDING', (0, 0), (0, 0), 20),
|
||||
('BOTTOMPADDING', (-1, -1), (-1, -1), 20),
|
||||
('TOPPADDING', (0, 1), (0, 1), 0),
|
||||
('BOTTOMPADDING', (0, 1), (0, 1), 4),
|
||||
('TOPPADDING', (0, 2), (0, 2), 0),
|
||||
('LEFTPADDING', (0, 0), (-1, -1), 18),
|
||||
]))
|
||||
story.append(title_table)
|
||||
story.append(Spacer(1, 20))
|
||||
|
||||
# Stat cards — 2×4 grid
|
||||
hs_hits = len(actionable_dids)
|
||||
stats = [
|
||||
("Total Orders", len(orders)),
|
||||
("HubSpot Hits", hs_hits),
|
||||
("Need Outreach", len(outreach)),
|
||||
("Internal Issues", len(internal)),
|
||||
("Masterprint (Skip)", len(master_orders)),
|
||||
("Flyeralarm (Manual)", len(fly_orders)),
|
||||
("Probo (Manual)", len(probo_orders)),
|
||||
("Suppliers to Contact", len(sorted_suppliers)),
|
||||
]
|
||||
for i in range(0, 8, 2):
|
||||
row = []
|
||||
for j in range(2):
|
||||
lbl, val = stats[i + j]
|
||||
cell = [p(f"<b>{val}</b>", S_STAT_NUM), p(lbl, S_STAT_LBL)]
|
||||
row.append(cell)
|
||||
card = Table([row], colWidths=[CONTENT_W / 2, CONTENT_W / 2])
|
||||
card.setStyle(TableStyle([
|
||||
('BOX', (0, 0), (-1, -1), 0.5, GRID_GRAY),
|
||||
('INNERGRID', (0, 0), (-1, -1), 0.5, GRID_GRAY),
|
||||
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
|
||||
('TOPPADDING', (0, 0), (-1, -1), 10),
|
||||
('BOTTOMPADDING', (0, 0), (-1, -1), 10),
|
||||
]))
|
||||
story.append(card)
|
||||
|
||||
story.append(Spacer(1, 12))
|
||||
story.append(p("<i>Note: HubSpot search covers batch IN-filter on hp_order_line_id (step 1). "
|
||||
"Steps 2 and 3 (hp_order_id IN + individual query) may reveal additional "
|
||||
"tickets not shown here.</i>", S_CAVEAT))
|
||||
|
||||
# ━━ SECTION 1 — HUBSPOT ANALYSIS ━━
|
||||
story.append(PageBreak())
|
||||
story.append(p("Section 1 — HubSpot Analysis", S_SECTION))
|
||||
|
||||
HS_COLS = [18, 15, 12, 55]
|
||||
HS_HDR = ["Ticket ID", "Order Detail ID", "Order ID", "Details"]
|
||||
|
||||
for title, items, color in [
|
||||
("1.1 Internal Issues", internal, HEROIC_RED),
|
||||
("1.2 In Production / On Track", in_prod, CHARMED_GREEN),
|
||||
("1.3 Shipped", shipped, SEASIDE),
|
||||
("1.4 Other Statuses", other_status, SEASIDE),
|
||||
("1.5 Review Needed", review_needed, HONEY_TERIYAKI),
|
||||
]:
|
||||
story.append(p(title, S_SUBSEC))
|
||||
if items:
|
||||
story.append(section_table(color, HS_COLS, HS_HDR, hs_rows(items)))
|
||||
else:
|
||||
story.append(p(f"<i>No orders in this category.</i>", S_ITALIC))
|
||||
story.append(Spacer(1, 10))
|
||||
|
||||
# ━━ SECTION 2 — MANUAL ACTION REQUIRED ━━
|
||||
story.append(PageBreak())
|
||||
story.append(p("Section 2 — Manual Action Required", S_SECTION))
|
||||
|
||||
# Flyeralarm
|
||||
story.append(p(f"Flyeralarm ({len(fly_orders)} orders)", S_SUBSEC))
|
||||
story.append(p("<i>These orders must be handled via the Flyeralarm supplier portal "
|
||||
"(case creation). Do NOT send outreach emails.</i>", S_ITALIC))
|
||||
story.append(Spacer(1, 4))
|
||||
if fly_orders:
|
||||
fly_rows = [[w(o.get('id_order_detail', '')), w(o.get('id_order', '')),
|
||||
w(o.get('supplier_name', '')),
|
||||
w(o.get('product_tid', '')[:30]),
|
||||
w(fmt_date(o.get('despatch_date_target', '')))]
|
||||
for o in fly_orders]
|
||||
story.append(section_table(RIPE_MANGO, [15, 15, 20, 30, 20],
|
||||
["Order Detail ID", "Order ID", "Supplier", "Product", "Target Dispatch"], fly_rows))
|
||||
story.append(Spacer(1, 12))
|
||||
|
||||
# Probo
|
||||
story.append(p(f"Probo ({len(probo_orders)} orders)", S_SUBSEC))
|
||||
story.append(p("<i>Verify actual production status in the Probo portal. Probo delay emails "
|
||||
"received for some orders — follow up for new dispatch date.</i>", S_ITALIC))
|
||||
story.append(Spacer(1, 4))
|
||||
if probo_orders:
|
||||
probo_rows = []
|
||||
for o in probo_orders:
|
||||
did = str(o.get('id_order_detail', ''))
|
||||
probo_rows.append([
|
||||
w(did), w(o.get('id_order', '')),
|
||||
w(o.get('product_tid', '')[:30]),
|
||||
w(fmt_date(o.get('despatch_date_target', ''))),
|
||||
w("Yes" if did in probo_delay_dids else "No")
|
||||
])
|
||||
story.append(section_table(RIPE_MANGO, [15, 15, 30, 22, 18],
|
||||
["Order Detail ID", "Order ID", "Product", "Target Dispatch", "Delay Email?"],
|
||||
probo_rows))
|
||||
story.append(Spacer(1, 12))
|
||||
|
||||
# Masterprint
|
||||
story.append(p(f"Masterprint ({len(master_orders)} orders)", S_SUBSEC))
|
||||
story.append(p("<i>Masterprint tracking is automated. No outreach needed.</i>", S_ITALIC))
|
||||
|
||||
# ━━ SECTION 3 — SUPPLIER OUTREACH EMAILS ━━
|
||||
story.append(PageBreak())
|
||||
story.append(p("Section 3 — Supplier Outreach Emails", S_SECTION))
|
||||
story.append(p(f"{len(outreach)} orders across {len(sorted_suppliers)} suppliers "
|
||||
"require outreach.", S_SUBTITLE))
|
||||
|
||||
EMAIL_OPEN = "Hi Partner,"
|
||||
EMAIL_BODY = ("The orders listed below were due to be dispatched on the agreed target "
|
||||
"date, however they appear to not have been completed or sent yet.")
|
||||
EMAIL_CLOSE_1 = ("Please could you provide an urgent update on the current status of these "
|
||||
"orders and confirm when they will be ready for dispatch?")
|
||||
EMAIL_CLOSE_2 = "Thank you, and we look forward to your prompt response."
|
||||
|
||||
OUT_COLS = [20, 15, 25, 15, 25]
|
||||
OUT_HDR = ["Order", "Target Dispatch", "Product", "Supplier Order", "Carrier"]
|
||||
|
||||
for supplier, sup_orders in sorted_suppliers:
|
||||
story.append(HRFlowable(width="100%", thickness=1, color=CHARMED_GREEN,
|
||||
spaceBefore=6, spaceAfter=6))
|
||||
story.append(p(f"{supplier} — {len(sup_orders)} order(s)", S_SUPPLIER))
|
||||
story.append(p(EMAIL_OPEN, S_EMAIL))
|
||||
story.append(Spacer(1, 4))
|
||||
story.append(p(EMAIL_BODY, S_EMAIL))
|
||||
story.append(Spacer(1, 6))
|
||||
|
||||
rows = [[w(f"{o.get('id_order', '')}-{o.get('id_order_detail', '')}"),
|
||||
w(fmt_date(o.get('despatch_date_target', ''))),
|
||||
w(o.get('product_tid', '')[:35]),
|
||||
w(o.get('supplier_order_number', '') or '-'),
|
||||
w(o.get('carriername', '') or '-')]
|
||||
for o in sup_orders]
|
||||
|
||||
tbl = section_table(THEME_BLACK, OUT_COLS, OUT_HDR, rows)
|
||||
|
||||
if len(sup_orders) <= 5:
|
||||
story.append(KeepTogether([
|
||||
tbl, Spacer(1, 6),
|
||||
p(EMAIL_CLOSE_1, S_EMAIL), Spacer(1, 4),
|
||||
p(EMAIL_CLOSE_2, S_EMAIL)
|
||||
]))
|
||||
else:
|
||||
story.append(tbl)
|
||||
story.append(Spacer(1, 6))
|
||||
story.append(p(EMAIL_CLOSE_1, S_EMAIL))
|
||||
story.append(Spacer(1, 4))
|
||||
story.append(p(EMAIL_CLOSE_2, S_EMAIL))
|
||||
|
||||
story.append(Spacer(1, 12))
|
||||
|
||||
# ── Build PDF ──
|
||||
Path(output_path).parent.mkdir(parents=True, exist_ok=True)
|
||||
doc = SimpleDocTemplate(str(output_path), pagesize=A4,
|
||||
leftMargin=18 * mm, rightMargin=18 * mm,
|
||||
topMargin=20 * mm, bottomMargin=18 * mm)
|
||||
doc.build(story, onFirstPage=page_template, onLaterPages=page_template)
|
||||
|
||||
sz = Path(output_path).stat().st_size / 1024
|
||||
print(f"PDF generated: {output_path} ({sz:.0f} KB)")
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
# Main
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Generate Step 3 Delay List PDF Report")
|
||||
parser.add_argument("orders_json", help="Path to Step 3 filtered orders JSON")
|
||||
parser.add_argument("hubspot_json", help="Path to HubSpot ticket results JSON")
|
||||
parser.add_argument("output_pdf", help="Output PDF file path")
|
||||
parser.add_argument("--date", default=datetime.now().strftime("%d %B %Y"),
|
||||
help="Report date (default: today)")
|
||||
args = parser.parse_args()
|
||||
|
||||
print(f"Loading orders from {args.orders_json}...")
|
||||
orders = load_json(args.orders_json)
|
||||
print(f" → {len(orders)} orders")
|
||||
|
||||
print(f"Loading HubSpot tickets from {args.hubspot_json}...")
|
||||
tickets = load_json(args.hubspot_json)
|
||||
print(f" → {len(tickets)} tickets")
|
||||
|
||||
print("Classifying orders...")
|
||||
cls = classify_orders(orders, tickets)
|
||||
print(f" internal={len(cls['internal'])} in_prod={len(cls['in_prod'])} "
|
||||
f"shipped={len(cls['shipped'])} other={len(cls['other_status'])} "
|
||||
f"review={len(cls['review_needed'])}")
|
||||
print(f" fly={len(cls['fly_orders'])} probo={len(cls['probo_orders'])} "
|
||||
f"master={len(cls['master_orders'])} outreach={len(cls['outreach'])} "
|
||||
f"({len(cls['sorted_suppliers'])} suppliers)")
|
||||
|
||||
print(f"Generating PDF for {args.date}...")
|
||||
generate_pdf(orders, cls, args.date, args.output_pdf)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user