Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
49cc458846 | ||
|
|
6d5b665789 | ||
|
|
602051ab6c | ||
|
|
c1c9055d44 | ||
|
|
a3f8f00fb2 | ||
|
|
2982b95002 | ||
|
|
d4068fc7a0 | ||
|
|
84141ad423 | ||
|
|
96a284295d | ||
|
|
6aa65ef076 | ||
|
|
990275a8fa |
@@ -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.
|
||||
+184
-1
@@ -135,7 +135,9 @@ _All had valid tracking links._
|
||||
|
||||
### Posting Rules & Slack Character Limit
|
||||
|
||||
Slack has a ~4000 character limit per message. If the full table exceeds 25 rows, you **must** split it into multiple messages to prevent the code block formatting from breaking.
|
||||
Slack has a ~4000 character limit per message. If the full table exceeds 25 rows, you **must** split it into multiple messages to prevent the code block formatting from breaking. Don't send the message as json.
|
||||
|
||||
**Important — Slack message formatting:** Always send message text with **actual line breaks**, not `\n` escape characters. If you type `\n` literally in the input, the entire message will appear as one long line in Slack. Write the message across multiple lines in the input field itself
|
||||
|
||||
**Splitting logic:**
|
||||
1. Calculate the number of parts needed: `ceil(total_rows / 25)`
|
||||
@@ -182,6 +184,127 @@ Send each part as a separate call to `mcp__traffic-mcp__Call_slack-feed-traffic-
|
||||
**Confidence:** If ALL criteria match exactly → auto-execute (99%+ confidence). If any field is ambiguous or missing → MANUAL\_REVIEW.
|
||||
|
||||
---
|
||||
---
|
||||
|
||||
## Step 3: Supplier Outreach — "In Production" Past Dispatch
|
||||
|
||||
### Criteria
|
||||
|
||||
- Dispatch date has passed
|
||||
- State = `Your order is in production` OR `Ready for production`
|
||||
- No tracking information
|
||||
|
||||
### Pre-check (if HubSpot integration available)
|
||||
|
||||
Check HubSpot for existing communication on the order item using the **mandatory 3-step search** below. Then categorize each order:
|
||||
|
||||
- **If email found explaining delay WITH reason** → print reason in chat with order, skip outreach email
|
||||
- **If email found but NO reason provided** → print new dispatch date in chat with order_id, add order to markdownfile per supplier.
|
||||
- **If email shows internal issue** (e.g., artwork, stock) → tell which orders are internal reason.
|
||||
- **If no email found** → add order information to markdown file
|
||||
|
||||
**CRITICAL — Skip Aircall tickets:** Tickets with a subject matching `"Aircall new ticket"` or containing `"Aircall"` are auto-generated phone call logs and should be ignored entirely. Do not count them as HubSpot hits and do not include them in any category. If an order's only HubSpot result is an Aircall ticket, treat it as having no HubSpot hit (i.e. it should go to supplier outreach).
|
||||
|
||||
**CRITICAL — Informational-only tickets do NOT count as actionable hits.** Many orders have HubSpot tickets that are purely informational and do NOT indicate that the delay has been communicated to or acknowledged by the supplier. When an order's only tickets are informational, treat it as having no actionable HubSpot hit — it must go to supplier outreach.
|
||||
|
||||
**Informational-only ticket types (ignore for categorization):**
|
||||
- **Supplier order confirmations** — e.g. "Order confirmation Showdown Displays", "Solo midocean Order confirmation", "Bestelling Ontvangen", "Auftrag … zu Bestellung", "Your Bullet Sales Order#", "O- 003-FSSE-…" (PF Concept), "Order … ontvangen voor klant" (Primex). These confirm the order was placed, not that the delay is being handled.
|
||||
- **Auto-generated "in production" / "being printed" emails** — e.g. "Your project is being printed", "Bestelling in Drukproces". These are automated supplier pipeline notifications, not responses to a delay inquiry.
|
||||
- **Shipping date expectation emails sent BEFORE the dispatch date passed** — e.g. "Verwachte verzenddatum", "Shipping date expectations", "BIC Graphic Confirmation: Your Shipment Date". These were sent at order acceptance, not in response to a delay.
|
||||
- **Auto-replies / out-of-office** — e.g. "Automatic reply:", "Out of Office".
|
||||
- **Proof/design sent emails** — e.g. "Bestel ontwerp", "Print proof update". These are part of the normal order flow, not delay communication. (Exception: if the proof is still awaiting approval and is blocking production, that IS actionable — categorize as "other_status".)
|
||||
|
||||
**Actionable ticket types (DO count as real HubSpot hits):**
|
||||
- Delay explanations with reason (e.g. "delayed dispatch", "Vertraging:", "We're sorry, job X is delayed")
|
||||
- Artwork issues blocking production (e.g. "ARTWORK REFUSAL", "artwork query", new file needed)
|
||||
- Stock/production issues (e.g. "STOCK DIFFERENCE", "creation failed", "update failed", "retry attempt")
|
||||
- Internal Helloprint issues (e.g. "AutoCheck Declined", "Speed Up Request")
|
||||
- Complaint tickets (e.g. "Your complaint")
|
||||
- Any ticket where a human wrote about the specific delay or a problem with the order
|
||||
|
||||
**Multi-ticket handling:** When an order has multiple tickets, apply this logic:
|
||||
1. Filter out Aircall tickets entirely.
|
||||
2. Classify each remaining ticket as either "informational" or "actionable" using the lists above.
|
||||
3. If ALL remaining tickets are informational → treat as no HubSpot hit → order goes to supplier outreach.
|
||||
4. If at least one ticket is actionable → use the **most recent actionable ticket** (by `createdate`) to determine the order's category.
|
||||
5. In the PDF report, show the primary (most recent actionable) ticket ID and note the total ticket count (e.g. "413360096501 (+2 more)").
|
||||
|
||||
**CRITICAL — Supplier name sourcing:** When categorizing HubSpot results, ALWAYS use the `supplier_name` from the delay list order data — never derive the supplier name from the HubSpot ticket subject. Ticket subjects often reference sub-brands, parent companies, or intermediary names (e.g. "Oakdene" instead of "Solopress v2") which do not match the actual supplier on the order.
|
||||
|
||||
### HubSpot Search Strategy (MANDATORY — execute ALL 3 steps)
|
||||
|
||||
The `hp_order_line_id` and `hp_order_id` fields in HubSpot frequently contain **multiple comma-separated values** in a single field (e.g., `"9155560, 9162986"`). IN/EQ filters only match when the field contains exactly one value, so they **will miss** multi-value fields. All 3 steps below are required — skipping step 3 consistently causes 10-20% of tickets to be missed.
|
||||
|
||||
**Step 1 — Batch IN-filter on `hp_order_line_id`**
|
||||
Search tickets using `IN` operator on `hp_order_line_id` with batches of max 50 detail IDs. Record which detail IDs got a hit.
|
||||
|
||||
**Step 2 — Batch IN-filter on `hp_order_id`**
|
||||
For all orders where NO hit was found in step 1: search tickets using `IN` operator on `hp_order_id` with batches of max 50 order IDs. Record which orders got a hit.
|
||||
|
||||
**Step 3 — Individual full-text `query` search (DO NOT SKIP — execute for ALL orders)**
|
||||
Execute this step for **every** `id_order_detail` — including those that already had a hit in step 1 or 2. A step 1/2 hit may find a different ticket (e.g. an Aircall ticket or generic auto-created ticket) than the one containing the actual supplier communication. Multi-value `hp_order_line_id` fields (with 50-100+ comma-separated IDs) are common for bundled supplier emails and are only matchable via `query`.
|
||||
|
||||
For each `id_order_detail`: search tickets using the `query` parameter (not a filter) with the individual detail ID as search string. If that returns 0, also try the `id_order`. Merge all results from steps 1, 2, and 3 — do not discard earlier hits, but ensure the full picture of tickets per order is captured.
|
||||
|
||||
> **Why this matters:** In practice, many supplier outreach emails reference dozens of orders in a single ticket with a multi-value `hp_order_line_id`. The IN filter cannot match these. Step 2 may find only tangentially related tickets (Aircall, auto-generated). Without step 3 on ALL orders, you will miss the actual supplier email thread and either miscategorize orders or send duplicate outreach.
|
||||
|
||||
### Supplier-Specific Handling
|
||||
|
||||
Before creating generic outreach emails, check if the supplier has a specific workflow:
|
||||
|
||||
#### Probo
|
||||
- **False delay check (automated):** Probo orders often trigger false delays when multiple order lines are shipped together. Check Presta for linked order lines:
|
||||
1. Gather all linked order lines for the order
|
||||
2. Check the "despatch" tab on all order lines
|
||||
3. If lines were shipped together → remove from delay list (false delay)
|
||||
- **Portal status check (manual):** The Probo portal is not accessible to the AI. List all remaining Probo delayed orders (after false-delay filtering) in a separate "Manual Action Required — Probo" section so the operator can verify actual production status in the Probo portal.
|
||||
- **Probo delay email received in HubSpot** → Flag these orders: update to "not sent on time", note the need to follow up for a new dispatch date.
|
||||
- **Outreach:** Do NOT include Probo orders in the generic supplier email drafts. All Probo follow-up is handled via their portal or based on portal findings.
|
||||
|
||||
#### Flyeralarm
|
||||
- **Any delay** → Do NOT send an outreach email and do NOT include in the supplier email drafts. Flyeralarm delays are handled via their own supplier portal (case creation), which requires manual access.
|
||||
- **Action:** List all Flyeralarm delayed orders in a separate "Manual Action Required — Flyeralarm" section in the output so the operator can handle them in the portal.
|
||||
|
||||
#### Masterprint
|
||||
- **Skip entirely** — Masterprint tracking is already automated. Orders packaged on the Masterprint tracking sheet are auto-removed from the delay list. No outreach needed.
|
||||
|
||||
### Action
|
||||
|
||||
For all **remaining suppliers** (not covered by supplier-specific handling above), create drafts with emails per supplier so we can review them.
|
||||
|
||||
> **CRITICAL RULE — No duplicates in outreach:** Before generating supplier outreach emails, EXCLUDE every order that already appears in a HubSpot category (Section 1) with an actionable ticket. Only orders with zero actionable HubSpot hits (including those with only informational tickets) should appear in the outreach email drafts. An order must never appear in both the HubSpot Analysis section and the Supplier Outreach section.
|
||||
|
||||
> **CRITICAL RULE:** Bundle emails per supplier. Never create multiple emails to the same supplier. Group all delayed order lines for a supplier into ONE email.
|
||||
|
||||
### Email Template
|
||||
|
||||
Always use the following template for supplier outreach emails:
|
||||
|
||||
```
|
||||
Hi Partner,
|
||||
|
||||
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.
|
||||
|
||||
[TABLE]
|
||||
|
||||
Please could you provide an urgent update on the current status of these orders and confirm when they will be ready for dispatch?
|
||||
|
||||
Thank you, and we look forward to your prompt response.
|
||||
```
|
||||
|
||||
The **[TABLE]** must have exactly these 5 columns:
|
||||
|
||||
| Column | Content | Format |
|
||||
|--------|---------|--------|
|
||||
| **Order** | Combined order + detail ID | `{id_order}-{id_order_detail}` (e.g. `5831128-9215018`) |
|
||||
| **Target Dispatch Date** | The agreed dispatch date | `DD Mon YYYY` (e.g. `31 Mar 2026`) |
|
||||
| **Product** | Product type/SKU | As returned by API |
|
||||
| **Supplier Order** | Supplier's own order number | Only if provided, otherwise omit column or show `-` |
|
||||
| **Carrier** | Intended carrier | As returned by API |
|
||||
|
||||
Do not deviate from this template. Do not add extra paragraphs, bullet points, or numbered lists asking for specific information — the template already covers everything needed.
|
||||
|
||||
|
||||
|
||||
## **Logging & Tracking**
|
||||
|
||||
@@ -236,6 +359,66 @@ Generate at end of processing and save as a file:
|
||||
|
||||
---
|
||||
|
||||
## **PDF Report Generation**
|
||||
|
||||
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.
|
||||
|
||||
```bash
|
||||
python generate_report.py <orders_json> <hubspot_tickets_json> <output_pdf> [--date "DD Month YYYY"]
|
||||
```
|
||||
|
||||
**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: "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** — 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 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 (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
|
||||
|
||||
### Implementation Notes
|
||||
|
||||
These notes apply if you ever need to modify `generate_report.py`:
|
||||
|
||||
- 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.
|
||||
|
||||
---
|
||||
|
||||
## **MCP & API Integration Points**
|
||||
|
||||
| Action | Integration | Template/Endpoint |
|
||||
|
||||
@@ -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()
|
||||
@@ -0,0 +1,313 @@
|
||||
---
|
||||
name: delay-list-processor
|
||||
description: "Daily delay list processing skill for the traffic team. Fetches the delay list via API, categorizes orders, auto-snoozes packaged orders from known suppliers, sends bundled supplier emails, notifies customers, logs complaints, and updates tracking. Use this skill whenever the user mentions: delay list, delayed orders, dispatch delays, snooze orders, supplier outreach, delay processing, traffic team workflow, daily delays, or order not shipped. Also trigger when the user asks to process delays or check today's delays."
|
||||
---
|
||||
|
||||
|
||||
**Overview**
|
||||
|
||||
Each day, orders that have surpassed their expected dispatch date appear on the delay list — whether still in production or packaged. This skill processes that list systematically: auto-snoozing where safe, emailing suppliers (bundled per supplier), notifying customers, logging complaints, and flagging edge cases for human review.
|
||||
|
||||
**Scale**: \~4,000 orders/day, delay list can be extensive.
|
||||
|
||||
---
|
||||
|
||||
## **Inputs**
|
||||
|
||||
1. **Delay List** — Fetched via the Delay List API (not a file upload). The API returns the current delay list with the following fields per order:
|
||||
|
||||
* `Order` (order number)
|
||||
* `id_order_detail` (order line ID)
|
||||
* `Assigned` (team member or blank)
|
||||
* `Status` (Open, Customer Informed, Checking, etc.)
|
||||
* `Delay Reason` (No delay reason given, Email Sent to Supplier, etc.)
|
||||
* `New Target Dispatch` (updated date if any)
|
||||
* `Product` (product type/SKU)
|
||||
* `Supplier` (supplier name)
|
||||
* `Sales Price`
|
||||
* `State` (production/packaging state — see values below)
|
||||
* `Delay` (days past target dispatch)
|
||||
* `Carrier` (carrier name if available)
|
||||
* `Tracking Link` (if available)
|
||||
* `Target Dispatch Date`
|
||||
* `Actual Dispatched Date`
|
||||
* `Latest Delivery Date`
|
||||
* `shop_country_code`
|
||||
* `Shop_name`
|
||||
* `Supplier Order number`
|
||||
* `Postcode`
|
||||
2. **Operator context** — Any special notes for the day (supplier outages, holidays, known issues).
|
||||
|
||||
3. **Supplier Complaint Classification Spreadsheet** — Reference for whether a supplier is volume-based (zero cost complaint) or discount-based (10% discount complaint). Read from `references/supplier-classification.md` if available.
|
||||
|
||||
---
|
||||
|
||||
## **Processing Workflow — Execution Order**
|
||||
|
||||
Process in this exact order to ensure data is fresh for downstream decisions:
|
||||
|
||||
### **Step 1: Fetch & Validate the Delay List**
|
||||
|
||||
* Call the Delay List API to retrieve today's current delay list
|
||||
* Validate expected fields are present in the API response
|
||||
* Flag any records with missing critical data (Order, Supplier, State) for MANUAL\_REVIEW
|
||||
* Calculate correct delay days: business days between Target Dispatch Date and today (note: the system's built-in delay count can be incorrect — see Edge Cases)
|
||||
|
||||
### **Step 2: Auto-Snooze Packaged Orders (High Confidence)**
|
||||
|
||||
**Criteria — ALL must match:**
|
||||
|
||||
| Field | Required Value |
|
||||
| ----- | ----- |
|
||||
| **State** | `Your order has been packaged` |
|
||||
| **Target Dispatch Date** | Previous working day |
|
||||
| **Carrier** | One of: `UPS-UPS_STANDARD`, `UPS-UPS_SAVER`, `DPDINT-STANDARD_UK`, `DPDINT-STANDARD`, `CORREOS-STANDARD` |
|
||||
| **Tracking Link** | Must be present (non-empty) |
|
||||
| **Supplier** | One of the approved auto-snooze suppliers (see list below) |
|
||||
|
||||
|
||||
**Attention** Always pass the full raw API response into the filter. Never manually subset the data before filtering. Validate record counts before executing any snoozes.
|
||||
|
||||
**Approved Auto-Snooze Suppliers:**
|
||||
|
||||
* De Groot Drukkerij B.V.
|
||||
* The Postermen
|
||||
* Intervlag
|
||||
* Vila Etiketten
|
||||
* VG Print B.V.
|
||||
* Zalsman B.V.
|
||||
* Oakdene Services
|
||||
* I print 1925
|
||||
* Digiforce
|
||||
* Enson
|
||||
* Probo
|
||||
* Promio
|
||||
* Inpromo
|
||||
* Today's Media
|
||||
* Pressing
|
||||
* Drukkerij Van As
|
||||
* Oakdene Services
|
||||
|
||||
**Action**: If you want to snooze an order call traffic-mcp snooze-order workflow.
|
||||
with a body like this:
|
||||
{
|
||||
orderDetailId: %id_orderdetail%
|
||||
}
|
||||
|
||||
**Action**: Sent information to traffic-mcp with call slack-feed-traffic-delay-list. Use the format below.
|
||||
## Slack Message Format for Snoozed Orders
|
||||
|
||||
When posting snoozed order results to Slack, always use this exact format with Slack mrkdwn syntax.
|
||||
|
||||
### 1. Header
|
||||
A bold line with the total count:
|
||||
```
|
||||
*Here are all {count} snoozed orders:*
|
||||
```
|
||||
|
||||
### 2. Table
|
||||
Wrap the table in a Slack code block (triple backticks) so columns stay aligned:
|
||||
```
|
||||
```
|
||||
Order Detail ID | Supplier | Carrier | Product
|
||||
5768484 | De Groot Drukkerij B.V. | DPFINT-STANDARD | foldedleafletsv2
|
||||
5785996 | Probo | UPS-UPS_STANDARD | smallstickersownsize
|
||||
```
|
||||
```
|
||||
|
||||
- Use `|` as column separator for readability
|
||||
- Include every order — never truncate or summarize with "and X more"
|
||||
- Column order is fixed: Order Detail ID → Supplier → Carrier → Product
|
||||
- The header count must match the actual number of rows
|
||||
|
||||
### 3. Summary footer
|
||||
After the code block, add a summary line in italic:
|
||||
```
|
||||
_All had state "Your order has been packaged", target dispatch date of March 13, and valid tracking links._
|
||||
```
|
||||
|
||||
If orders have different states or dispatch dates, group them:
|
||||
```
|
||||
_• {count} orders with state "{state_a}", target dispatch date of {date_a}_
|
||||
_• {count} orders with state "{state_b}", target dispatch date of {date_b}_
|
||||
_All had valid tracking links._
|
||||
```
|
||||
|
||||
### Posting Rules & Slack Character Limit
|
||||
|
||||
Slack has a ~4000 character limit per message. If the full table exceeds 25 rows, you **must** split it into multiple messages to prevent the code block formatting from breaking. Don't send the message as json.
|
||||
|
||||
**Important — Slack message formatting:** Always send message text with **actual line breaks**, not `\n` escape characters. If you type `\n` literally in the input, the entire message will appear as one long line in Slack. Write the message across multiple lines in the input field itself
|
||||
|
||||
**Splitting logic:**
|
||||
1. Calculate the number of parts needed: `ceil(total_rows / 25)`
|
||||
2. Each message must be a **self-contained code block** (with its own opening and closing triple backticks)
|
||||
3. **First message** starts with the bold header: `*Snoozed orders (part 1/{total_parts}):*`
|
||||
4. **Middle messages** use: `*Snoozed orders (part X/{total_parts}):*`
|
||||
5. **Last message** includes the italic summary footer after the code block
|
||||
6. If all orders fit in a single message (≤25 rows), use the standard header: `*Here are all {count} snoozed orders:*`
|
||||
|
||||
**Example with 56 orders (3 parts):**
|
||||
|
||||
Message 1:
|
||||
```
|
||||
*Snoozed orders (part 1/3):*
|
||||
``` ```
|
||||
Order Detail ID | Supplier | Carrier | Product
|
||||
5768484 | De Groot Drukkerij B.V. | DPFINT-STANDARD | foldedleafletsv2
|
||||
...rows 1-25...
|
||||
``` ```
|
||||
```
|
||||
|
||||
Message 2:
|
||||
```
|
||||
*Snoozed orders (part 2/3):*
|
||||
``` ```
|
||||
Order Detail ID | Supplier | Carrier | Product
|
||||
...rows 26-50...
|
||||
``` ```
|
||||
```
|
||||
|
||||
Message 3:
|
||||
```
|
||||
*Snoozed orders (part 3/3):*
|
||||
``` ```
|
||||
Order Detail ID | Supplier | Carrier | Product
|
||||
...rows 51-56...
|
||||
``` ```
|
||||
_All had state "Your order has been packaged", target dispatch date of March 13, and valid tracking links._
|
||||
```
|
||||
|
||||
Send each part as a separate call to `mcp__traffic-mcp__Call_slack-feed-traffic-delay-list_`.
|
||||
|
||||
|
||||
**Confidence:** If ALL criteria match exactly → auto-execute (99%+ confidence). If any field is ambiguous or missing → MANUAL\_REVIEW.
|
||||
|
||||
---
|
||||
|
||||
## **Logging & Tracking**
|
||||
|
||||
**Every action must be logged. No exceptions.**
|
||||
|
||||
### **Processing Log**
|
||||
|
||||
Maintain a structured log (CSV) saved to the outputs directory with the following fields per order processed:
|
||||
|
||||
| Field | Description |
|
||||
| ----- | ----- |
|
||||
| `timestamp` | ISO 8601 timestamp of action |
|
||||
| `order_id` | Order number |
|
||||
| `id_order_detail` | Order line ID |
|
||||
| `supplier` | Supplier name |
|
||||
| `product` | Product type |
|
||||
| `state` | Order state at time of processing |
|
||||
| `delay_days` | Calculated delay in business days |
|
||||
| `category` | Action category (see list below) |
|
||||
| `action_taken` | Specific action executed |
|
||||
| `confidence` | Confidence percentage |
|
||||
| `auto_or_manual` | AUTO / FLAGGED\_FOR\_REVIEW |
|
||||
| `human_decision` | Operator decision if flagged (blank if auto) |
|
||||
| `batch_number` | Batch ID |
|
||||
| `status` | SUCCESS / FAILED / PENDING\_REVIEW |
|
||||
| `error_details` | Error info if failed |
|
||||
| `email_sent_to` | Supplier/customer email triggered |
|
||||
| `email_bundled_with` | Other order IDs in same supplier email |
|
||||
| `complaint_logged` | true/false |
|
||||
| `complaint_type` | Carrier / Supplier |
|
||||
| `complaint_cost` | Zero / 10% discount |
|
||||
| `snooze_hours` | Hours snoozed |
|
||||
| `notes` | Additional context |
|
||||
|
||||
**Action categories:** `SNOOZE` | `SUPPLIER_EMAIL` | `CUSTOMER_NOTIFY` | `COMPLAINT_CARRIER` | `COMPLAINT_SUPPLIER` | `TRACKING_UPDATE` | `STATUS_UPDATE` | `LABEL_REISSUE` | `MANUAL_REVIEW`
|
||||
|
||||
### **Daily Summary Report**
|
||||
|
||||
Generate at end of processing and save as a file:
|
||||
|
||||
* Total orders on delay list and total processed
|
||||
* Breakdown by action category (counts and percentages)
|
||||
* Auto-processed vs. flagged for review (counts)
|
||||
* Supplier emails sent — count and list of suppliers contacted
|
||||
* Customer notifications sent — count
|
||||
* Complaints logged — carrier vs. supplier, with cost breakdown
|
||||
* Orders snoozed — count and average snooze duration
|
||||
* Failed actions with details
|
||||
* Pending manual review — full list with proposed actions
|
||||
* Top delayed suppliers — ranked by number of delayed orders
|
||||
* Orders delayed 3+ days — highlighted for urgency
|
||||
|
||||
---
|
||||
|
||||
## **MCP & API Integration Points**
|
||||
|
||||
| Action | Integration | Template/Endpoint |
|
||||
| ----- | ----- | ----- |
|
||||
| Fetch delay list | traffic-mcp | get-delay-list workflow |
|
||||
| Snooze order | traffic-mcp | snooze-orders workflow |
|
||||
|
||||
Before processing begins, verify all required MCP servers and API endpoints are accessible. If any integration is unavailable, flag ALL affected orders for MANUAL\_REVIEW rather than failing silently.
|
||||
|
||||
---
|
||||
|
||||
## **Configuration**
|
||||
|
||||
These defaults can be overridden by the operator at the start of each run:
|
||||
|
||||
```
|
||||
snooze_default_hours: 12
|
||||
max_batch_size: 100
|
||||
confidence_threshold: 99 # Percentage — below this → MANUAL_REVIEW
|
||||
customer_notify_after_days: 2 # Notify customer after X business days delayed
|
||||
supplier_email_after_days: 1 # Email supplier after X days with no movement
|
||||
auto_snooze_carriers:
|
||||
- UPS-UPS_STANDARD
|
||||
- UPS-UPS_SAVER
|
||||
- DPDINT-STANDARD_UK
|
||||
- DPDINT-STANDARD
|
||||
supplier_specific_processes:
|
||||
probo: on_hold
|
||||
flyeralarm: on_hold
|
||||
masterprint: completed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## **Edge Cases & Known Issues**
|
||||
|
||||
1. **Incorrect delay day calculation** — The system sometimes shows different delay counts for orders with the same dispatch date. Always recalculate: business days between Target Dispatch Date and today.
|
||||
|
||||
2. **Sales price shows full order, not order-line** — The delay list may show full order price. Be aware when assessing value thresholds.
|
||||
|
||||
3. **False delays from Probo** — Orders shipped together across multiple order lines trigger false delays. When Probo processes activate, always check if lines were shipped together before acting.
|
||||
|
||||
4. **Duplicate supplier emails** — The bundling rule is critical. Always deduplicate and group before sending. Check if a supplier was already emailed today.
|
||||
|
||||
5. **Tracking lag vs. genuine delay** — Packaged orders with tracking from approved carriers/suppliers are most often tracking lag. Auto-snooze handles this, but if an order reappears after multiple snoozes, escalate to MANUAL\_REVIEW.
|
||||
|
||||
6. **Orders assigned to team members** — If an order has an `Assigned` value, it means someone is already working on it. Consider skipping or noting this in the review queue.
|
||||
|
||||
7. **Incomplete order evaluation when filtering**
|
||||
When filtering the delay list, always feed the raw API response directly into the filter logic — never manually reconstruct or subset the data. After filtering, validate that: Every order with statename = "Your order has been packaged" appears in either the match list or the reject list
|
||||
The count of (matches + rejects) equals the total number of packaged orders in the raw response
|
||||
|
||||
8. **The Posterman Exception**
|
||||
When the supplier is **The Postermen**, the carrier field may be any value when validating snooze applicability. All other matching criteria still apply as normal.
|
||||
|
||||
9. Orders from supplier **Oakdene Services** with a carrier of `DPDUK-STANDARD` or `DPDUK-EXPRESSPAK` are eligible for auto-snooze if all other auto-snooze criteria are also met.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
## **Example Run**
|
||||
|
||||
```
|
||||
Operator: "Process today's delay list. No special notes."
|
||||
|
||||
Step 1: Fetched delay list via API → 347 delayed orders. 4 records missing data → MANUAL_REVIEW.
|
||||
Step 2: 89 orders match auto-snooze criteria → Snoozed 12h. Batch 1/1 (89). All succeeded
|
||||
|
||||
SUMMARY: 343 processed | 16 manual review | 0 failures
|
||||
Logs saved: delay_log_2026-02-12.csv, summary_2026-02-12.md
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user