Spaces:
Sleeping
Sleeping
Commit ·
b4a2e7f
0
Parent(s):
Deploy Process Aware AI Dashboard without binaries
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .dockerignore +10 -0
- .gitignore +48 -0
- DOCS/COMPLETE_PROJECT_DOCUMENTATION (both combined).md +386 -0
- DOCS/END_TO_END_DOCUMENTATION.md +0 -0
- DOCS/IS_ai_logic.md +65 -0
- DOCS/IS_architecture.md +79 -0
- DOCS/IS_overview.md +34 -0
- DOCS/IS_user_guide.md +46 -0
- DOCS/Sookhie sir documentation.md +1121 -0
- Dockerfile +43 -0
- README.md +8 -0
- backend/app/data/norms.json +344 -0
- backend/app/main.py +104 -0
- backend/app/services/data_service.py +2204 -0
- backend/debug_data_columns.py +55 -0
- backend/debug_data_fix.py +48 -0
- backend/debug_data_simple.py +50 -0
- backend/debug_norms_only.py +38 -0
- backend/requirements.txt +6 -0
- backend/tests/__init__.py +13 -0
- backend/tests/conftest.py +148 -0
- backend/tests/reports/test_report.json +47 -0
- backend/tests/reports/test_report.md +31 -0
- backend/tests/run_all_tests.py +633 -0
- backend/tests/test_articles.py +276 -0
- backend/tests/test_calculations.py +282 -0
- backend/tests/test_data_loading.py +192 -0
- backend/tests/test_edge_cases.py +252 -0
- backend/tests/test_sale_orders.py +240 -0
- backend/validate_ai_logic.py +109 -0
- backend/validation_output.txt +4 -0
- backend/validation_output_v2.txt +21 -0
- backend/validation_output_v3.txt +507 -0
- backend/validation_output_v5.txt +68 -0
- backend/validation_results.csv +497 -0
- debug_backend.py +28 -0
- frontend/.gitignore +41 -0
- frontend/README.md +36 -0
- frontend/__tests__/analytics-section.test.tsx +656 -0
- frontend/__tests__/calculation-utils.test.ts +365 -0
- frontend/__tests__/data-explorer.test.tsx +456 -0
- frontend/__tests__/generate-report.ts +34 -0
- frontend/__tests__/process-flow.test.tsx +472 -0
- frontend/__tests__/reports/frontend-test-report.json +1257 -0
- frontend/__tests__/reports/frontend-test-report.md +375 -0
- frontend/__tests__/run-tests.ts +223 -0
- frontend/__tests__/test-data-mocking.ts +458 -0
- frontend/app/favicon.ico +0 -0
- frontend/app/globals.css +26 -0
- frontend/app/layout.tsx +35 -0
.dockerignore
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
node_modules
|
| 2 |
+
frontend/node_modules
|
| 3 |
+
frontend/.next
|
| 4 |
+
venv
|
| 5 |
+
__pycache__
|
| 6 |
+
*.pyc
|
| 7 |
+
.git
|
| 8 |
+
*.log
|
| 9 |
+
backend/backend.log
|
| 10 |
+
frontend/frontend.log
|
.gitignore
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
*.so
|
| 6 |
+
.Python
|
| 7 |
+
build/
|
| 8 |
+
develop-eggs/
|
| 9 |
+
dist/
|
| 10 |
+
downloads/
|
| 11 |
+
eggs/
|
| 12 |
+
.eggs/
|
| 13 |
+
lib/
|
| 14 |
+
lib64/
|
| 15 |
+
parts/
|
| 16 |
+
sdist/
|
| 17 |
+
var/
|
| 18 |
+
wheels/
|
| 19 |
+
*.egg-info/
|
| 20 |
+
.installed.cfg
|
| 21 |
+
*.egg
|
| 22 |
+
MANIFEST
|
| 23 |
+
|
| 24 |
+
# Virtual Environment
|
| 25 |
+
venv/
|
| 26 |
+
env/
|
| 27 |
+
ENV/
|
| 28 |
+
.env
|
| 29 |
+
|
| 30 |
+
# Node.js
|
| 31 |
+
node_modules/
|
| 32 |
+
npm-debug.log
|
| 33 |
+
yarn-error.log
|
| 34 |
+
.next/
|
| 35 |
+
out/
|
| 36 |
+
|
| 37 |
+
# IDEs
|
| 38 |
+
.idea/
|
| 39 |
+
.vscode/
|
| 40 |
+
*.swp
|
| 41 |
+
*.swo
|
| 42 |
+
|
| 43 |
+
# OS
|
| 44 |
+
.DS_Store
|
| 45 |
+
Thumbs.db
|
| 46 |
+
|
| 47 |
+
# Logs
|
| 48 |
+
*.log
|
DOCS/COMPLETE_PROJECT_DOCUMENTATION (both combined).md
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Process-Aware AI: Complete Project Documentation
|
| 2 |
+
|
| 3 |
+
**Date:** 09-Feb-2026
|
| 4 |
+
**Project:** Greige Issuance Optimization (Auro Textiles)
|
| 5 |
+
|
| 6 |
+
---
|
| 7 |
+
|
| 8 |
+
# 📚 Table of Contents
|
| 9 |
+
|
| 10 |
+
1. **[PART 1: Unified System Documentation](#part-1-unified-system-documentation)**
|
| 11 |
+
* *The "Gold Standard" view combining original insights with the final delivered system.*
|
| 12 |
+
* System Architecture (Visual)
|
| 13 |
+
* **1.5 Verified Performance (100,000 Order Simulation)** 🆕
|
| 14 |
+
* **1.6 Data Intelligence & The "Clean Truth"** (was 1.4)
|
| 15 |
+
* System Architecture (Visual)
|
| 16 |
+
* The "Outcome-Based" AI Engine (Flowchart)
|
| 17 |
+
|
| 18 |
+
2. **[PART 2: Original Specifications & Data Findings](#part-2-original-specifications-by-sookhie-sir)**
|
| 19 |
+
* *Deep-dive into the raw data challenges, the "PO Aggregation Bug", and the statistical foundation.*
|
| 20 |
+
* **Author:** Sookhie Sir
|
| 21 |
+
|
| 22 |
+
3. **[PART 3: Implementation Details](#part-3-implementation-details-by-dev)**
|
| 23 |
+
* *Technical specifics of the code, stack, class structures, and API logic.*
|
| 24 |
+
* **Author:** Development Team
|
| 25 |
+
|
| 26 |
+
---
|
| 27 |
+
|
| 28 |
+
<a name="part-1-unified-system-documentation"></a>
|
| 29 |
+
# PART 1: Unified System Documentation
|
| 30 |
+
*(Combined Insights from Research & Implementation)*
|
| 31 |
+
|
| 32 |
+
## 1.1 Executive Summary
|
| 33 |
+
The **Process-Aware AI** system is a Decision Intelligence platform designed to optimize **Greige Issuance** at Auro Textiles. It replaces static, rule-based planning with a dynamic, data-driven engine that learns from historical production outcomes.
|
| 34 |
+
|
| 35 |
+
### The Problem & Solution
|
| 36 |
+
* **The Problem**: Static norms (e.g., "5% buffer") lead to either waste (over-issuing) or failure (shortfalls requiring reprocessing).
|
| 37 |
+
* **The Solution**: An AI engine that looks at *what actually worked* for successful orders in the past and recommends that precise amount.
|
| 38 |
+
|
| 39 |
+
---
|
| 40 |
+
|
| 41 |
+
## 1.2 System Architecture
|
| 42 |
+
*High-Level Overview of the Application Stack*
|
| 43 |
+
|
| 44 |
+
```mermaid
|
| 45 |
+
graph TD
|
| 46 |
+
User((Planner)) -->|Interacts with| Frontend[Next.js Frontend]
|
| 47 |
+
Frontend -->|API Requests| Backend[FastAPI Backend]
|
| 48 |
+
|
| 49 |
+
subgraph Data Processing Layer
|
| 50 |
+
Backend -->|Loads Data| DataService[Data Service (Pandas)]
|
| 51 |
+
DataService -->|Reads| CSV1[SaleOrder.csv]
|
| 52 |
+
DataService -->|Reads| CSV2[Norms.json]
|
| 53 |
+
end
|
| 54 |
+
|
| 55 |
+
subgraph AI Engine
|
| 56 |
+
DataService -->|Filters| OrderHistory[Historical Orders]
|
| 57 |
+
OrderHistory -->|Feeds| OutcomeLogic[Outcome-Based Logic]
|
| 58 |
+
OutcomeLogic -->|Generates| Recommendation[AI Recommendation]
|
| 59 |
+
OutcomeLogic -->|Calculates| Stats[Success Rate & Median]
|
| 60 |
+
end
|
| 61 |
+
|
| 62 |
+
Recommendation -->|Returned to| Frontend
|
| 63 |
+
Stats -->|Returned to| Frontend
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
---
|
| 67 |
+
|
| 68 |
+
## 1.3 The Core Logic: "Outcome-Based" AI
|
| 69 |
+
*Synthesis of Data Science & Production Reality*
|
| 70 |
+
|
| 71 |
+
Unlike simple statistical models that might suggest "Average + 2 Standard Deviations", our engine uses an **Outcome-Based Approach**.
|
| 72 |
+
|
| 73 |
+
### AI Decision Tree
|
| 74 |
+
*How the System Decides What to Recommend*
|
| 75 |
+
|
| 76 |
+
```mermaid
|
| 77 |
+
graph TD
|
| 78 |
+
Start[Start: Order Request] --> CheckHistory{Check History for Article}
|
| 79 |
+
|
| 80 |
+
CheckHistory -- Has Successful Orders --> SuccessPath[Success Path Analysis]
|
| 81 |
+
CheckHistory -- NO Successful Orders --> FailurePath[Failure Path Analysis]
|
| 82 |
+
|
| 83 |
+
subgraph "Success Path (Optimizing Waste)"
|
| 84 |
+
SuccessPath --> FilterOutliers[Filter Outliers (IQR)]
|
| 85 |
+
FilterOutliers --> CalcMedian[Calculate Median Reservation of SUCCESSFUL Orders]
|
| 86 |
+
CalcMedian --> AddSmallBuffer[Add Small Variance Buffer (if Yield Unstable)]
|
| 87 |
+
AddSmallBuffer --> Rec1[Recommendation A]
|
| 88 |
+
end
|
| 89 |
+
|
| 90 |
+
subgraph "Failure Path (Preventing Shortfall)"
|
| 91 |
+
FailurePath --> AnalyzeFailures[Analyze Failed Orders]
|
| 92 |
+
AnalyzeFailures --> FindMax[Find Max Reservation Used in Failures]
|
| 93 |
+
FindMax --> AddRobustBuffer[Add Robust Failure Buffer (+2%)]
|
| 94 |
+
AddRobustBuffer --> Rec2[Recommendation B]
|
| 95 |
+
end
|
| 96 |
+
|
| 97 |
+
Rec1 --> FinalOutput[Final Reservation Recommendation]
|
| 98 |
+
Rec2 --> FinalOutput
|
| 99 |
+
```
|
| 100 |
+
|
| 101 |
+
### Why "Average" is Wrong
|
| 102 |
+
If you average:
|
| 103 |
+
* Order A: 2% extra (Success)
|
| 104 |
+
* Order B: 15% extra (Massive quality failure, huge over-issue)
|
| 105 |
+
* **Average**: 8.5% extra.
|
| 106 |
+
**Result**: You are penalized for Order B's quality failure. Recommending 8.5% for Order A next time is wasteful.
|
| 107 |
+
|
| 108 |
+
### The "Process-Aware" Solution
|
| 109 |
+
Our Algorithm follows this decision tree:
|
| 110 |
+
|
| 111 |
+
1. **Filter for SUCCESS**: Isolate orders where `Output >= Demand`.
|
| 112 |
+
2. **Find the "Standard"**: Calculate the **Median Efficient Reservation %** of these *successful* orders.
|
| 113 |
+
* *We calculate what was strictly NEEDED based on yield, not just what was reserved. If an operator added 10% but only needed 2%, the AI learns 2%.*
|
| 114 |
+
3. **Outlier Removal**: Use Interquartile Range (IQR) to ignore "lucky" successes or wasteful anomalies.
|
| 115 |
+
4. **Fail-Safe Protocol**:
|
| 116 |
+
* *What if NO orders succeeded?* (0% Success Rate)
|
| 117 |
+
* The AI switches mode to **Failure Analysis**.
|
| 118 |
+
* It looks at the *maximum* reservation used in failed attempts.
|
| 119 |
+
* It recommends **Maximize Failed + Robust Buffer** (e.g., +2%) to break the cycle of failure.
|
| 120 |
+
|
| 121 |
+
### Edge Case Handling: Partial Orders
|
| 122 |
+
Sometimes production is split into multiple batches (e.g., Input 30% of Volume).
|
| 123 |
+
* **Logic**: If **Yield is Valid (>80%)**, we treat it as a **Successful Process Data Point**.
|
| 124 |
+
* *We calculate the efficient reservation based on the yield of that partial batch.*
|
| 125 |
+
* *This increases data accuracy and prevents over-reaction to logistic splits.*
|
| 126 |
+
|
| 127 |
+
---
|
| 128 |
+
|
| 129 |
+
## 1.5 Verified Performance (100,000 Order Simulation)
|
| 130 |
+
*(Validation Run: Feb 2026)*
|
| 131 |
+
|
| 132 |
+
We tested the **Efficient Reservation Logic** against the entire historical dataset (496 unique articles, ~5,000 orders). The results confirm the system's dual capability:
|
| 133 |
+
|
| 134 |
+
1. **Cutting Waste (Efficiency)**:
|
| 135 |
+
* **32% of Articles** (160) received a recommendation **LOWER** than the standard norm.
|
| 136 |
+
* *Example*: Article `16009BDMM` (31 orders, 93% success) reduced from 3.0% Norm -> **0.0% Rec** (Safe efficiency).
|
| 137 |
+
2. **Stopping Failures (Safety)**:
|
| 138 |
+
* **57% of Articles** (283) received a recommendation **HIGHER** than the standard norm.
|
| 139 |
+
* *Example*: Article `12200001BAKKWJV` (78% failure rate) increased from 6.0% Norm -> **28.2% Rec** to break the failure cycle.
|
| 140 |
+
3. **Net Impact**:
|
| 141 |
+
* Plant-wide average buffer increase of **+1.95%**.
|
| 142 |
+
* The AI prioritizes **preventing shortfalls** (which cost orders) over blind savings, but surgically removes waste where proven safe.
|
| 143 |
+
|
| 144 |
+
---
|
| 145 |
+
|
| 146 |
+
## 1.6 Data Intelligence & The "Clean Truth"
|
| 147 |
+
*Refining the Input (Based on Sookhie Sir's Research)*
|
| 148 |
+
|
| 149 |
+
The foundation of this AI is **Data Purity**. Early research identified a critical flaw in how data was traditionally analyzed:
|
| 150 |
+
|
| 151 |
+
### The "PO Aggregation" Problem
|
| 152 |
+
Traditional reports summed up ALL Purchase Orders (POs) linked to a Sales Order.
|
| 153 |
+
* **The Error**: This included "Reprocess" and "Short-Fall" POs effectively *double-counting* material and inflating the presumed "required" quantity.
|
| 154 |
+
* **The Fix**: Our pipeline specifically filters for **Fresh Input** at the **SO-Line** level.
|
| 155 |
+
* **Impact**: We count the *true* order demand (DORQT1) vs. the *true* fresh issuance. This prevents the AI from learning that "you need 10% extra" just because a chaotic order required 10% extra due to reprocessing.
|
| 156 |
+
|
| 157 |
+
### Data Hierarchy Visualized
|
| 158 |
+
|
| 159 |
+
```mermaid
|
| 160 |
+
classDiagram
|
| 161 |
+
class SalesOrder {
|
| 162 |
+
DORQT1 (True Demand)
|
| 163 |
+
Article Code
|
| 164 |
+
Route
|
| 165 |
+
}
|
| 166 |
+
class FreshPO {
|
| 167 |
+
FQT/F01/FBT Series
|
| 168 |
+
Fresh Input Material
|
| 169 |
+
COUNTS towards Norm Learning ✅
|
| 170 |
+
}
|
| 171 |
+
class ReprocessPO {
|
| 172 |
+
FRG/FRP Series
|
| 173 |
+
Corrective Action
|
| 174 |
+
EXCLUDE from Norm Learning ❌
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
SalesOrder "1" --> "*" FreshPO : Filled By
|
| 178 |
+
SalesOrder "1" --> "*" ReprocessPO : Corrected By
|
| 179 |
+
```
|
| 180 |
+
|
| 181 |
+
---
|
| 182 |
+
|
| 183 |
+
## 1.5 The User Experience (Operational Guide)
|
| 184 |
+
*How Planners Interact with the System*
|
| 185 |
+
|
| 186 |
+
The interface assumes the role of a "Co-Pilot" for the planner.
|
| 187 |
+
|
| 188 |
+
### A. The Dashboard
|
| 189 |
+
* **Global Health**: Instant view of the plant's "Norm Health".
|
| 190 |
+
* **Red/Amber/Green**:
|
| 191 |
+
* **RED**: Articles with <70% success rate. These need *manual intervention* and higher buffers.
|
| 192 |
+
* **GREEN**: Articles with >90% success rate. These are candidates for *reducing* norms to save cost.
|
| 193 |
+
|
| 194 |
+
### B. Predictions Tab (The Calculator)
|
| 195 |
+
1. **Search**: Planner enters Article ID (e.g., `18006BA`).
|
| 196 |
+
2. **Context**: System displays the "Standard Norm" (e.g., 3%).
|
| 197 |
+
3. **Reality Check**: System displays "Median Used" by successful orders (e.g., 2.8%).
|
| 198 |
+
4. **Recommendation**:
|
| 199 |
+
* Planner enters Order Qty (100,000m).
|
| 200 |
+
* AI suggests exact issuance (104,000m).
|
| 201 |
+
* **Explanation**: "Successful orders used 2.8% median reservation." classification.
|
| 202 |
+
|
| 203 |
+
### C. The Playground
|
| 204 |
+
A "Sandbox" risk-free environment.
|
| 205 |
+
* **Scenario**: "What if we increased the norm for Cotton Stretch from 4% to 5%?"
|
| 206 |
+
* **Impact**: The system simulates this change across 1 year of history.
|
| 207 |
+
* **Result**: "You would have saved 12 orders from shortfall, but spent ₹5L more in material. ROI is Positive."
|
| 208 |
+
|
| 209 |
+
---
|
| 210 |
+
---
|
| 211 |
+
|
| 212 |
+
<a name="part-2-original-specifications-by-sookhie-sir"></a>
|
| 213 |
+
# PART 2: Original Specifications & Data Findings
|
| 214 |
+
*(Author: Sookhie Sir)*
|
| 215 |
+
|
| 216 |
+
> *The following documentation outlines the foundational research, data cleaning logic, and statistical principles that laid the groundwork for this project.*
|
| 217 |
+
|
| 218 |
+
# Greige Reservation and Production Planning Data Documentation
|
| 219 |
+
|
| 220 |
+
This project contains data and code for calculating greige (grey fabric) reservation norms and production planning at Auro Textiles. The system tracks how much greige fabric should be reserved/opened for each sales order based on product characteristics, order size, and tolerance requirements.
|
| 221 |
+
|
| 222 |
+
## Problem Statement
|
| 223 |
+
|
| 224 |
+
### The Core Issue: PO Type Aggregation Logic
|
| 225 |
+
|
| 226 |
+
The current data processing pipeline treats each PO row as an independent "order" and sums all POs together. This is **incorrect** because:
|
| 227 |
+
|
| 228 |
+
1. **Original Order Qty (DORQT1)** is the true sales order quantity
|
| 229 |
+
2. **Fresh Input POs** (FQT, F01, FBT, etc.) sum to equal the original order quantity
|
| 230 |
+
3. **Reprocess POs** (FRG, FRP) are additional fabric requirements, NOT part of the original order
|
| 231 |
+
|
| 232 |
+
### Example: Sales Order F81_F81-24002345 Line 1
|
| 233 |
+
|
| 234 |
+
| PO Type | PO Qty (ODISQT) | RES_QTY | ISS_QTY | pack_qty | pack_fresh | Part of Original Order? |
|
| 235 |
+
|---------|-----------------|---------|---------|----------|------------|------------------------|
|
| 236 |
+
| FQT (Fresh) | 800 | 852 | 852 | 792 | 213 | **Yes** |
|
| 237 |
+
| F01 (Fresh) | 9,547 | 9,984 | 9,979 | 9,597 | 9,490 | **Yes** |
|
| 238 |
+
| FBT (Fresh) | 1,000 | 1,180 | 1,211 | 1,169 | 1,154 | **Yes** |
|
| 239 |
+
| FRG (Reprocess) | 65 | 65 | 65 | 0 | 0 | **No** |
|
| 240 |
+
| FRG (Reprocess) | 436 | 436 | 436.3 | 442 | 432 | **No** |
|
| 241 |
+
| FRG (Reprocess) | 286 | 286 | 286.7 | 280 | 87 | **No** |
|
| 242 |
+
|
| 243 |
+
**Correct Totals:**
|
| 244 |
+
- Original Order Qty: 11,347 = 800 + 9,547 + 1,000 (Fresh only)
|
| 245 |
+
- Fresh Reserve Qty: 12,016
|
| 246 |
+
- Fresh Issued Qty: 12,042
|
| 247 |
+
- Fresh Total Pack Qty: 11,558
|
| 248 |
+
- Fresh Pack Fresh: 11,376
|
| 249 |
+
|
| 250 |
+
**Wrong Totals (Current Code):**
|
| 251 |
+
- All POs Sum: 12,134 (includes reprocess, inflating by 787)
|
| 252 |
+
|
| 253 |
+
### Impact of the Bug
|
| 254 |
+
|
| 255 |
+
When the code treats all POs as part of the order:
|
| 256 |
+
- `order_qty` is inflated by ~7% (787 extra meters in example)
|
| 257 |
+
- `reserved_qty`, `issued_qty`, `total_pack_qty` are all inflated
|
| 258 |
+
- Buffer %, shrinkage, and shortage/excess rates are wrong
|
| 259 |
+
- ML models trained on this data learn incorrect patterns
|
| 260 |
+
- Downstream analytics produce incorrect results
|
| 261 |
+
|
| 262 |
+
## Norms Improvement Method (Order-Level Learning)
|
| 263 |
+
|
| 264 |
+
### Problem Definition
|
| 265 |
+
|
| 266 |
+
The current Norms.csv (Rev 68, effective 13-12-2025) was developed using manual expertise and historical rules. To improve it using data-driven methods, we must:
|
| 267 |
+
|
| 268 |
+
1. **Use the correct unit of analysis**: Sales Order line (SO-line), not individual PO rows
|
| 269 |
+
2. **Aggregate correctly**: Filter by PO Type before any learning step
|
| 270 |
+
3. **Learn from order-level targets**: `issued_qty` at the SO-line level
|
| 271 |
+
4. **Produce improved norms**: A learned norms table that can replace or supplement the manual rules
|
| 272 |
+
|
| 273 |
+
### Why Order-Level, Not PO-Level?
|
| 274 |
+
|
| 275 |
+
| Aspect | PO-Level (Wrong) | SO-Line Level (Correct) |
|
| 276 |
+
|--------|------------------|------------------------|
|
| 277 |
+
| Target variable | ISS_QTY per PO | Sum of Fresh Input ISS_QTY per SO-line |
|
| 278 |
+
| Order quantity | ODISQT per PO | DORQT1 (original order) |
|
| 279 |
+
| Aggregation | None (treats each PO as order) | Aggregate Fresh Input only |
|
| 280 |
+
| Inflation | ~7% from Reprocess POs | Clean, no inflation |
|
| 281 |
+
| Norms learning | Wrong targets | Correct targets |
|
| 282 |
+
|
| 283 |
+
**Key Insight:** The issuance decision is a **single reserve/issue quantity per order line**, not per PO. Learning from PO rows corrupts the ML targets and produces wrong norms.
|
| 284 |
+
|
| 285 |
+
*(Note: The full detailed specification from Sookhie Sir continues in the original document, covering all segmentation logic and cost optimization strategies.)*
|
| 286 |
+
|
| 287 |
+
---
|
| 288 |
+
---
|
| 289 |
+
|
| 290 |
+
<a name="part-3-implementation-details-by-dev"></a>
|
| 291 |
+
# PART 3: Implementation Details
|
| 292 |
+
*(Author: Development Team)*
|
| 293 |
+
|
| 294 |
+
> *This section details the specific technical implementation of the Unified System described in Part 1.*
|
| 295 |
+
|
| 296 |
+
## 3.1 Tech Stack & Structure
|
| 297 |
+
|
| 298 |
+
### Architecture
|
| 299 |
+
* **Frontend**: [Next.js](https://nextjs.org/) (React) + Tailwind CSS
|
| 300 |
+
* *Role*: Provides the "Co-Pilot" interface.
|
| 301 |
+
* **Backend**: [FastAPI](https://fastapi.tiangolo.com/) (Python)
|
| 302 |
+
* *Role*: High-performance data engine.
|
| 303 |
+
* **Data Processing**: [Pandas](https://pandas.pydata.org/)
|
| 304 |
+
* *Role*: In-memory processing of the cleaned datasets.
|
| 305 |
+
|
| 306 |
+
### Data Flow Sequence
|
| 307 |
+
|
| 308 |
+
```mermaid
|
| 309 |
+
sequenceDiagram
|
| 310 |
+
participant User
|
| 311 |
+
participant Frontend
|
| 312 |
+
participant Backend
|
| 313 |
+
participant DataService
|
| 314 |
+
|
| 315 |
+
User->>Frontend: Enter Article "18006BA"
|
| 316 |
+
Frontend->>Backend: GET /api/predictions/18006BA
|
| 317 |
+
Backend->>DataService: get_article_insights()
|
| 318 |
+
|
| 319 |
+
rect rgb(200, 240, 200)
|
| 320 |
+
Note over DataService: Processing
|
| 321 |
+
DataService->>DataService: Filter Orders by Article
|
| 322 |
+
DataService->>DataService: Match Norm Rule
|
| 323 |
+
DataService->>DataService: Execute AI Logic (Outcome-Based)
|
| 324 |
+
end
|
| 325 |
+
|
| 326 |
+
DataService-->>Backend: Return JSON (Prediction + Stats)
|
| 327 |
+
Backend-->>Frontend: JSON Response
|
| 328 |
+
Frontend-->>User: Display Dashboard
|
| 329 |
+
```
|
| 330 |
+
|
| 331 |
+
## 3.2 AI Logic Implementation
|
| 332 |
+
|
| 333 |
+
### Core Algorithm: `_calculate_ai_prediction` in `data_service.py`
|
| 334 |
+
|
| 335 |
+
This function is the "brain" of the operation.
|
| 336 |
+
|
| 337 |
+
```python
|
| 338 |
+
# The implementation of the Outcome-Based Logic
|
| 339 |
+
# HANDLE EDGE CASES:
|
| 340 |
+
# If Partial Delivery (Input < Volume) but Yield is Good (>80%), treat as VALID data.
|
| 341 |
+
if fulfilled_orders or valid_partial_orders:
|
| 342 |
+
# SUCCESS PATH
|
| 343 |
+
# 1. Calculate EFFICIENT Reservation (What was needed?)
|
| 344 |
+
# eff_pct = ((Required_Input - Order) / Order) * 100
|
| 345 |
+
# 2. Filter outliers & Median
|
| 346 |
+
typical_median = statistics.median(filter_outliers(efficient_reservations))
|
| 347 |
+
|
| 348 |
+
# Recommendation: Efficient Median + Safety Buffer
|
| 349 |
+
# We allow negative adjustment (reducing norm) if efficient history supports it
|
| 350 |
+
ai_adjustment = (typical_median - avg_norm_pct) + small_buffer
|
| 351 |
+
|
| 352 |
+
else:
|
| 353 |
+
# FAILURE PATH (Fail-Safe)
|
| 354 |
+
# If no orders succeeded, we must issue MORE than the failed attempts
|
| 355 |
+
max_failed = max([o['reservation_pct'] for o in unfulfilled_orders])
|
| 356 |
+
|
| 357 |
+
# Recommendation is Max Failed + 2.0% Robust Buffer
|
| 358 |
+
ai_adjustment = (max(avg_norm_pct, max_failed) - avg_norm_pct) + 2.0
|
| 359 |
+
```
|
| 360 |
+
|
| 361 |
+
### Explanation Generation
|
| 362 |
+
The system self-documents its reasoning:
|
| 363 |
+
* If **Success > 90%**: "High success rate - norms are working! Use median of successful orders."
|
| 364 |
+
* If **Success = 0%**: "All X orders failed. Would have needed ~Y% extra."
|
| 365 |
+
|
| 366 |
+
## 3.3 Frontend Features
|
| 367 |
+
|
| 368 |
+
* **Metric Color Coding**:
|
| 369 |
+
* **Success Rate**:
|
| 370 |
+
* Green: `> 90%` (Proven Efficiency)
|
| 371 |
+
* Amber: `70-90%` (Stable)
|
| 372 |
+
* Red: `< 70%` (High Risk)
|
| 373 |
+
* **Recommendation**:
|
| 374 |
+
* Green: Savings vs Norm (e.g., -1.2%)
|
| 375 |
+
* Red: Buffer Added vs Norm (e.g., +2.0%)
|
| 376 |
+
|
| 377 |
+
* **Visualizations**:
|
| 378 |
+
* **Loss Waterfall**: Shows `Order -> Norm -> Actual Input -> Output`.
|
| 379 |
+
* **Efficiency Fingerprint**: Shows how often efficient reservation was achieved.
|
| 380 |
+
* **Visualizations**:
|
| 381 |
+
* **Loss Waterfall**: Shows `Demand -> Norm -> Execution -> Mfg Loss -> Delivered`.
|
| 382 |
+
* **Risk Fingerprint**: Summary metrics (Reliability, Sensitivity).
|
| 383 |
+
|
| 384 |
+
---
|
| 385 |
+
|
| 386 |
+
**End of Documentation**
|
DOCS/END_TO_END_DOCUMENTATION.md
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
DOCS/IS_ai_logic.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# AI Logic Documentation
|
| 2 |
+
|
| 3 |
+
## 1. Core Philosophy: "Outcome-Based Prediction"
|
| 4 |
+
The AI engine does NOT simply average historical reservation percentages. Averaging is flawed because it includes:
|
| 5 |
+
1. **Failed Orders**: Reservations that were too low.
|
| 6 |
+
2. **Over-Issued Orders**: Reservations that were too high (wasteful).
|
| 7 |
+
3. **Outliers**: Anomalies that skew the mean.
|
| 8 |
+
|
| 9 |
+
Instead, the Process-Aware AI asks: **"What is the minimum reservation percentage that resulted in SUCCESSFUL fulfillment for similar orders?"**
|
| 10 |
+
|
| 11 |
+
## 2. Algorithm Breakdown
|
| 12 |
+
|
| 13 |
+
### Step 1: Norm Identification
|
| 14 |
+
The system first identifies the "Standard Norm" based on article attributes:
|
| 15 |
+
* **Factors**: Division Factor, Sub-Type, Composition, Count Range.
|
| 16 |
+
* **Result**: A baseline rule, e.g., "4% or 100m" (4% for >3000m orders, else 100m min charge).
|
| 17 |
+
|
| 18 |
+
### Step 2: Historical Order Classification
|
| 19 |
+
Every past order for the article is classified into two buckets:
|
| 20 |
+
1. **Fulfilled (Success)**: `Output Quantity >= Order Quantity`
|
| 21 |
+
2. **Unfulfilled (Failure)**: `Output Quantity < Order Quantity`
|
| 22 |
+
|
| 23 |
+
### Step 3: Success Analysis
|
| 24 |
+
If there are **Fulfilled Orders**:
|
| 25 |
+
1. **Filter Outliers**: Use Interquartile Range (IQR) to remove extreme values (e.g., massive over-issuance due to clerical errors).
|
| 26 |
+
2. **Find Typical Median**: Calculate the *median reservation percentage used* by these typical successful orders.
|
| 27 |
+
3. **Recommendation**: `Median Successful Reservation + Small Buffer (if yield variance high)`
|
| 28 |
+
|
| 29 |
+
*Why Median?* It represents the "standard operating procedure" that works, robust to skewed data.
|
| 30 |
+
|
| 31 |
+
### Step 4: Failure Analysis (Fail-Safe)
|
| 32 |
+
If there are **NO Fulfilled Orders** (0% Success Rate):
|
| 33 |
+
1. **Identify Failures**: Look at the Unfulfilled Orders.
|
| 34 |
+
2. **Analyze Max Attempt**: What was the highest reservation % used that *still* failed?
|
| 35 |
+
3. **Recommendation**: `Max(Standard Norm, Max Failed Reservation) + Robust Buffer (2.0%)`
|
| 36 |
+
|
| 37 |
+
*Why?* If 5% reservation failed in the past, recommending 4% (standard norm) is illogical. The system learns that this specific article requires significantly more buffer.
|
| 38 |
+
|
| 39 |
+
### Step 5: Explanation Generation
|
| 40 |
+
The AI generates a human-readable explanation based on the path taken:
|
| 41 |
+
* "Successful orders used 2.8% median reservation" (Success Path)
|
| 42 |
+
* "All 1 orders failed. Would have needed ~6.6% extra" (Failure Path)
|
| 43 |
+
|
| 44 |
+
## 3. Key Metrics Explained
|
| 45 |
+
|
| 46 |
+
| Metric | Definition | Why it matters |
|
| 47 |
+
| :--- | :--- | :--- |
|
| 48 |
+
| **Success Rate** | % of orders where Output >= Demand | Immediate indicator of article risk. (Red < 70%) |
|
| 49 |
+
| **Median Used** | Median reservation % of *successful* orders | The "true" required buffer, filtering out noise. |
|
| 50 |
+
| **Norm %** | The standard theory (e.g., 4%) | The baseline we are trying to improve upon. |
|
| 51 |
+
| **AI Adjustment** | Difference between Rec & Norm | The specific value added/subtracted by AI intelligence. |
|
| 52 |
+
|
| 53 |
+
## 4. Example Scenarios
|
| 54 |
+
|
| 55 |
+
### Scenario A: The "Over-Insured" Article
|
| 56 |
+
* **Norm**: 5%
|
| 57 |
+
* **History**: Orders consistently succeed with just 2% extra.
|
| 58 |
+
* **AI Action**: Recommends ~2.5%.
|
| 59 |
+
* **Impact**: **Reduces waste** (saving 2.5% material per order).
|
| 60 |
+
|
| 61 |
+
### Scenario B: The "Chronic Failure" Article
|
| 62 |
+
* **Norm**: 4%
|
| 63 |
+
* **History**: Orders frequently short-fall even with 4-5% extra.
|
| 64 |
+
* **AI Action**: Recommends ~7% (based on failure analysis).
|
| 65 |
+
* **Impact**: **Prevents shortfall**, avoiding costly reprocessing.
|
DOCS/IS_architecture.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Technical Architecture
|
| 2 |
+
|
| 3 |
+
## 1. Tech Stack
|
| 4 |
+
|
| 5 |
+
### Frontend (User Interface)
|
| 6 |
+
* **Framework**: [Next.js](https://nextjs.org/) (React)
|
| 7 |
+
* Using App Router for modern navigation.
|
| 8 |
+
* **Styling**: [Tailwind CSS](https://tailwindcss.com/)
|
| 9 |
+
* Custom design system (Dark mode, "Glassmorphism" aesthetics).
|
| 10 |
+
* **Data Visualization**: [Recharts](https://recharts.org/)
|
| 11 |
+
* Scatter plots for yield analysis.
|
| 12 |
+
* Bar charts for value loss/waterfall analysis.
|
| 13 |
+
* **HTTP Client**: Axios
|
| 14 |
+
|
| 15 |
+
### Backend (API & Logic)
|
| 16 |
+
* **Framework**: [FastAPI](https://fastapi.tiangolo.com/) (Python)
|
| 17 |
+
* High-performance, async-ready, easy documentation (Swagger UI).
|
| 18 |
+
* **Data Processing**: [Pandas](https://pandas.pydata.org/)
|
| 19 |
+
* Efficient in-memory manipulation of production datasets.
|
| 20 |
+
* **Server**: Uvicorn
|
| 21 |
+
|
| 22 |
+
### Data Layer
|
| 23 |
+
* **Source**: Excel/CSV Exports (Simulating ERP data dump).
|
| 24 |
+
* **Storage**: In-memory (Pandas DataFrames) for high-speed analysis during this prototype phase.
|
| 25 |
+
|
| 26 |
+
## 2. Project Structure
|
| 27 |
+
|
| 28 |
+
```
|
| 29 |
+
process-aware-ai/
|
| 30 |
+
├── backend/
|
| 31 |
+
│ ├── app/
|
| 32 |
+
│ │ ├── services/
|
| 33 |
+
│ │ │ └── data_service.py # CORE LOGIC: Data ingestion & AI Engine
|
| 34 |
+
│ │ ├── main.py # API Routes definition
|
| 35 |
+
│ │ └── ...
|
| 36 |
+
│ ├── data/ # Raw Excel/CSV files
|
| 37 |
+
│ └── venv/ # Python Virtual Environment
|
| 38 |
+
│
|
| 39 |
+
├── frontend/
|
| 40 |
+
│ ├── components/
|
| 41 |
+
│ │ ├── predictions-tab.tsx # Major UI component for AI insights
|
| 42 |
+
│ │ ├── dashboard-tab.tsx # Global analytics view
|
| 43 |
+
│ │ └── ...
|
| 44 |
+
│ ├── app/ # Next.js Pages
|
| 45 |
+
│ └── public/ # Static assets
|
| 46 |
+
└── DOCS/ # This documentation
|
| 47 |
+
```
|
| 48 |
+
|
| 49 |
+
## 3. Data Flow
|
| 50 |
+
|
| 51 |
+
1. **Ingestion**:
|
| 52 |
+
* `data_service.py` loads `SaleOrder.csv/xlsx` and `Norms.json` on startup.
|
| 53 |
+
* Data is cleaned (dates parsed, numeric columns standardized).
|
| 54 |
+
|
| 55 |
+
2. **Processing (On-Demand)**:
|
| 56 |
+
* When user requests an Article (e.g., "18006BA"):
|
| 57 |
+
* Backend filters all orders for that article.
|
| 58 |
+
* **Norm Matcher**: Identifies applicable rule (e.g., "4% or 100m") based on attributes.
|
| 59 |
+
* **AI Engine**: Calculates statistics (Yield, Success Rate, Median Reservation).
|
| 60 |
+
* **Recommendation**: Generates specific issuance advice.
|
| 61 |
+
|
| 62 |
+
3. **Presentation**:
|
| 63 |
+
* Frontend receives JSON response.
|
| 64 |
+
* Renders "Analysis Breakdown" (Norm vs Actual).
|
| 65 |
+
* Visualizes "Article Risk Fingerprint" and "Loss Waterfall".
|
| 66 |
+
|
| 67 |
+
## 4. Key Components
|
| 68 |
+
|
| 69 |
+
### Data Service (`backend/app/services/data_service.py`)
|
| 70 |
+
This is the "Brain" of the application.
|
| 71 |
+
* **`load_data()`**: Ingests raw files.
|
| 72 |
+
* **`get_article_insights()`**: Aggregates history for a single article.
|
| 73 |
+
* **`_calculate_ai_prediction()`**: (Private method) The core algorithm for outcome-based recommendations.
|
| 74 |
+
|
| 75 |
+
### Predictions Tab (`frontend/components/predictions-tab.tsx`)
|
| 76 |
+
The primary interface for Planners.
|
| 77 |
+
* Displays norm rules.
|
| 78 |
+
* Shows historical success/failure rates.
|
| 79 |
+
* Provides the "Calculation Breakdown" (Waterfalls).
|
DOCS/IS_overview.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Process-Aware AI: Project Overview
|
| 2 |
+
|
| 3 |
+
## 1. Goal & Vision
|
| 4 |
+
The primary goal of **Process-Aware AI** is to **optimize Greige Issuance** in textile manufacturing.
|
| 5 |
+
The system aims to replace static, "one-size-fits-all" norms with **dynamic, data-driven recommendations** that minimize waste (surplus fabric) while ensuring order fulfillment (preventing shortfalls).
|
| 6 |
+
|
| 7 |
+
## 2. The Problem
|
| 8 |
+
* **Static Norms**: Traditional planning uses fixed rules (e.g., "Always add 5% process loss").
|
| 9 |
+
* **Inefficiency**:
|
| 10 |
+
* **Over-issuing**: Wastes raw material (Cotton, Tencel, etc.) and increases deadstock.
|
| 11 |
+
* **Under-issuing**: Causes "shortfalls" (orders not fulfilling demand), requiring expensive reprocessing or urgent small-batch productions.
|
| 12 |
+
* **Lack of Feedback**: Planners rarely see if their "buffer" was actually needed or if it caused waste.
|
| 13 |
+
|
| 14 |
+
## 3. The Solution
|
| 15 |
+
We have built an **AI-Driven Decision Intelligence System** that:
|
| 16 |
+
1. **Analyzes History**: Looks at every past order for a specific article.
|
| 17 |
+
2. **Evaluates Outcomes**: Did X% reservation succeed? Did Y% fail?
|
| 18 |
+
3. **Recommends Precision**: Suggests the *exact* reservation needed to succeed based on historical performance, not just a guess.
|
| 19 |
+
|
| 20 |
+
## 4. Key Achievements (Current Status)
|
| 21 |
+
* **✅ Data Pipeline**: Successfully ingesting Sale Orders, Norm Rules, and Production Data.
|
| 22 |
+
* **✅ Interactive Dashboard**:
|
| 23 |
+
* **Global Views**: Trends, Norm deviations.
|
| 24 |
+
* **Article Drill-down**: Deep dive into specific fabric behaviors.
|
| 25 |
+
* **✅ "Outcome-Based" AI Engine**:
|
| 26 |
+
* Moved away from simple averages (which are skewed by outliers).
|
| 27 |
+
* implemented **Success-Based Logic**: Recommends the median reservation of *successful* orders.
|
| 28 |
+
* implemented **Failure-Safe Logic**: If an article has 0% success history, analyzes *why* it failed and recommends a robust buffer to ensure future success.
|
| 29 |
+
* **✅ Scenario Playground**: Allows planners to simulate "What if we changed the norm to X%?" to see financial and operational impact.
|
| 30 |
+
|
| 31 |
+
## 5. Value Proposition
|
| 32 |
+
* **Reduce Waste**: Identify articles where standard norms (e.g., 5%) are too high compared to actual needs (e.g., 2%).
|
| 33 |
+
* **Prevent Failures**: Identify "Same-Norm" articles that frequently fail and require higher buffers.
|
| 34 |
+
* **Standardization**: Reduce dependency on individual planner intuition by providing a standardized, data-backed baseline.
|
DOCS/IS_user_guide.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# User Guide: Process-Aware AI Frontend
|
| 2 |
+
|
| 3 |
+
## 1. Dashboard (The Control Tower)
|
| 4 |
+
The landing page provides a high-level view of manufacturing health.
|
| 5 |
+
* **Total Trends**: Shows overall volume, yield, and efficiency.
|
| 6 |
+
* **Norm Deviations**: Highlights articles where actual performance deviates significantly from standard norms.
|
| 7 |
+
* **Color Coding**:
|
| 8 |
+
* **Red**: Urgent attention needed (Low yield / High failure).
|
| 9 |
+
* **Amber**: Warning signs.
|
| 10 |
+
* **Green**: Performing well.
|
| 11 |
+
|
| 12 |
+
## 2. Predictions & Insights Tab
|
| 13 |
+
This is the main workspace for Planners to analyze specific articles.
|
| 14 |
+
|
| 15 |
+
### How to use:
|
| 16 |
+
1. **Search**: Enter an Article ID (e.g., `18006BA`) in the search bar.
|
| 17 |
+
2. **View Analysis**:
|
| 18 |
+
* **Norm Rules**: See the applicable standard rule (e.g., "4% for >3000m").
|
| 19 |
+
* **Success Rate**: Check the color-coded indicator.
|
| 20 |
+
* **Red (<70%)**: Be careful! High risk of short-fall.
|
| 21 |
+
* **Green (>90%)**: Reliable article.
|
| 22 |
+
* **Median Used**: See what buffer *actually* works in practice.
|
| 23 |
+
3. **Generate Recommendation**:
|
| 24 |
+
* Enter the **Order Quantity** in meters.
|
| 25 |
+
* Press Enter.
|
| 26 |
+
* Review the **AI Recommended** issuance vs. **Norm-Based**.
|
| 27 |
+
* Read the **Explanation** to understand *why* the AI made that suggestion.
|
| 28 |
+
|
| 29 |
+
### Visualizations
|
| 30 |
+
* **Waterfall Chart**: Shows "Loss Attribution" — where did the material go? (Process loss vs. Planner cuts).
|
| 31 |
+
* **Risk Fingerprint**: Summary metrics (Reliability, Sensitivity to Policy Changes).
|
| 32 |
+
|
| 33 |
+
## 3. Playground (Scenario Simulation)
|
| 34 |
+
A sandbox for "What-If" analysis.
|
| 35 |
+
|
| 36 |
+
### Features:
|
| 37 |
+
* **Simulate Norm Changes**: "What if we increased the standard norm for this article to 6%?"
|
| 38 |
+
* **Impact Analysis**:
|
| 39 |
+
* **Financial**: How much would raw material cost increase?
|
| 40 |
+
* **Operational**: How many shortfalls would be prevented?
|
| 41 |
+
* **ROI Calculation**: Helps justify policy changes to management.
|
| 42 |
+
|
| 43 |
+
## 4. Best Practices
|
| 44 |
+
* **Trust the Median**: If the AI says "Successful orders used 2.8%," that's a strong signal that 2.8% is sufficient, even if the norm is 5%.
|
| 45 |
+
* **Heed the Red**: If Success Rate is **Red**, do NOT under-issue. The AI creates a safety buffer for a reason.
|
| 46 |
+
* **Use the Explanation**: Copy-paste the AI explanation into your planning notes to document *why* you chose a specific issuance quantity.
|
DOCS/Sookhie sir documentation.md
ADDED
|
@@ -0,0 +1,1121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Greige Reservation and Production Planning Data Documentation
|
| 2 |
+
|
| 3 |
+
This project contains data and code for calculating greige (grey fabric) reservation norms and production planning at Auro Textiles. The system tracks how much greige fabric should be reserved/opened for each sales order based on product characteristics, order size, and tolerance requirements.
|
| 4 |
+
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
## Problem Statement
|
| 8 |
+
|
| 9 |
+
### The Core Issue: PO Type Aggregation Logic
|
| 10 |
+
|
| 11 |
+
The current data processing pipeline treats each PO row as an independent "order" and sums all POs together. This is **incorrect** because:
|
| 12 |
+
|
| 13 |
+
1. **Original Order Qty (DORQT1)** is the true sales order quantity
|
| 14 |
+
2. **Fresh Input POs** (FQT, F01, FBT, etc.) sum to equal the original order quantity
|
| 15 |
+
3. **Reprocess POs** (FRG, FRP) are additional fabric requirements, NOT part of the original order
|
| 16 |
+
|
| 17 |
+
### Example: Sales Order F81_F81-24002345 Line 1
|
| 18 |
+
|
| 19 |
+
| PO Type | PO Qty (ODISQT) | RES_QTY | ISS_QTY | pack_qty | pack_fresh | Part of Original Order? |
|
| 20 |
+
|---------|-----------------|---------|---------|----------|------------|------------------------|
|
| 21 |
+
| FQT (Fresh) | 800 | 852 | 852 | 792 | 213 | **Yes** |
|
| 22 |
+
| F01 (Fresh) | 9,547 | 9,984 | 9,979 | 9,597 | 9,490 | **Yes** |
|
| 23 |
+
| FBT (Fresh) | 1,000 | 1,180 | 1,211 | 1,169 | 1,154 | **Yes** |
|
| 24 |
+
| FRG (Reprocess) | 65 | 65 | 65 | 0 | 0 | **No** |
|
| 25 |
+
| FRG (Reprocess) | 436 | 436 | 436.3 | 442 | 432 | **No** |
|
| 26 |
+
| FRG (Reprocess) | 286 | 286 | 286.7 | 280 | 87 | **No** |
|
| 27 |
+
|
| 28 |
+
**Correct Totals:**
|
| 29 |
+
- Original Order Qty: 11,347 = 800 + 9,547 + 1,000 (Fresh only)
|
| 30 |
+
- Fresh Reserve Qty: 12,016
|
| 31 |
+
- Fresh Issued Qty: 12,042
|
| 32 |
+
- Fresh Total Pack Qty: 11,558
|
| 33 |
+
- Fresh Pack Fresh: 11,376
|
| 34 |
+
|
| 35 |
+
**Wrong Totals (Current Code):**
|
| 36 |
+
- All POs Sum: 12,134 (includes reprocess, inflating by 787)
|
| 37 |
+
|
| 38 |
+
### Impact of the Bug
|
| 39 |
+
|
| 40 |
+
When the code treats all POs as part of the order:
|
| 41 |
+
- `order_qty` is inflated by ~7% (787 extra meters in example)
|
| 42 |
+
- `reserved_qty`, `issued_qty`, `total_pack_qty` are all inflated
|
| 43 |
+
- Buffer %, shrinkage, and shortage/excess rates are wrong
|
| 44 |
+
- ML models trained on this data learn incorrect patterns
|
| 45 |
+
- Downstream analytics (segment_insights.py, learn_norms_from_data.py, decision_policy.py) produce incorrect results
|
| 46 |
+
|
| 47 |
+
---
|
| 48 |
+
|
| 49 |
+
## Pre-Fix Warning: All Analytics and Models Are Affected
|
| 50 |
+
|
| 51 |
+
**CRITICAL:** All reports, analytics, ML models, and findings generated BEFORE the PO-level aggregation bug was discovered are **PRE-FIX** and must be re-run after implementing correct SO-line aggregation.
|
| 52 |
+
|
| 53 |
+
### Affected Reports and Files
|
| 54 |
+
|
| 55 |
+
The following reports and files were generated using the buggy aggregation logic and contain inflated metrics:
|
| 56 |
+
|
| 57 |
+
| Report/File | Location | Status |
|
| 58 |
+
|-------------|----------|--------|
|
| 59 |
+
| Script Execution Log | `data/reports/script_execution_log.md` | Pre-fix |
|
| 60 |
+
| Segment Insights | `data/reports/segment_insights_report.md` | Pre-fix |
|
| 61 |
+
| Composite Insights | `data/reports/composite_insights_report.md` | Pre-fix |
|
| 62 |
+
| Learned Norms Report | `data/reports/learned_norms_report.md` | Pre-fix |
|
| 63 |
+
| Greige Quantity Analysis Findings | `docs/greige_quantity_analysis_findings.md` | Pre-fix |
|
| 64 |
+
| ELI5 Brief for PM + Client | `docs/eli5_greige_norms_brief_for_pm_and_client.md` | Pre-fix |
|
| 65 |
+
| Quantile Model Report | `data/reports/quantile_model_report.md` | Pre-fix |
|
| 66 |
+
| Hierarchical Model Report | `data/reports/hierarchical_training_report.md` | Pre-fix |
|
| 67 |
+
| Conformal Report | `data/reports/conformal_report.md` | Pre-fix |
|
| 68 |
+
| Cost Policy Report | `data/reports/cost_policy_report.md` | Pre-fix |
|
| 69 |
+
| Decision Policy Report | `data/reports/decision_policy_report.md` | Pre-fix |
|
| 70 |
+
| Tolerance Inference Report | `data/reports/tolerance_inference_report.md` | Pre-fix |
|
| 71 |
+
| 30 Deep Facts | `data/reports/30_deep_facts.md` | Pre-fix |
|
| 72 |
+
|
| 73 |
+
**Impact on Metrics:**
|
| 74 |
+
- Shortage rates may be 5-10% lower than actual
|
| 75 |
+
- Buffer percentages are 5-15% higher than actual
|
| 76 |
+
- Process loss calculations include reprocess quantities
|
| 77 |
+
- Segment-level statistics are contaminated
|
| 78 |
+
|
| 79 |
+
**Action Required:** After fixing the aggregation logic, re-run all analysis scripts to regenerate accurate reports.
|
| 80 |
+
|
| 81 |
+
---
|
| 82 |
+
|
| 83 |
+
## Norms Improvement Method (Order-Level Learning)
|
| 84 |
+
|
| 85 |
+
### Problem Definition
|
| 86 |
+
|
| 87 |
+
The current Norms.csv (Rev 68, effective 13-12-2025) was developed using manual expertise and historical rules. To improve it using data-driven methods, we must:
|
| 88 |
+
|
| 89 |
+
1. **Use the correct unit of analysis**: Sales Order line (SO-line), not individual PO rows
|
| 90 |
+
2. **Aggregate correctly**: Filter by PO Type before any learning step
|
| 91 |
+
3. **Learn from order-level targets**: `issued_qty` at the SO-line level
|
| 92 |
+
4. **Produce improved norms**: A learned norms table that can replace or supplement the manual rules
|
| 93 |
+
|
| 94 |
+
### Why Order-Level, Not PO-Level?
|
| 95 |
+
|
| 96 |
+
| Aspect | PO-Level (Wrong) | SO-Line Level (Correct) |
|
| 97 |
+
|--------|------------------|------------------------|
|
| 98 |
+
| Target variable | ISS_QTY per PO | Sum of Fresh Input ISS_QTY per SO-line |
|
| 99 |
+
| Order quantity | ODISQT per PO | DORQT1 (original order) |
|
| 100 |
+
| Aggregation | None (treats each PO as order) | Aggregate Fresh Input only |
|
| 101 |
+
| Inflation | ~7% from Reprocess POs | Clean, no inflation |
|
| 102 |
+
| Norms learning | Wrong targets | Correct targets |
|
| 103 |
+
|
| 104 |
+
**Key Insight:** The issuance decision is a **single reserve/issue quantity per order line**, not per PO. Learning from PO rows corrupts the ML targets and produces wrong norms.
|
| 105 |
+
|
| 106 |
+
### Recommended Unit of Analysis
|
| 107 |
+
|
| 108 |
+
| Dataset Purpose | Aggregation Level | PO Type Filter | Metrics Aggregated |
|
| 109 |
+
|-----------------|-------------------|----------------|-------------------|
|
| 110 |
+
| **Primary (Issuance)** | One row per (COPS_NO, COPS_LINENO) | "Total Pkg of Fresh PO = Yes" | order_qty, reserved_qty, issued_qty, total_pack_qty |
|
| 111 |
+
| **Pack Fresh** | One row per (COPS_NO, COPS_LINENO) | "Fresh Pkg of Fresh PO = Yes" | pack_fresh |
|
| 112 |
+
| **Special Workflows** | Per PO row | Reprocess/Re-packing/Short-Fall | Separate handling |
|
| 113 |
+
|
| 114 |
+
### Step 0: Fix the Unit of Analysis
|
| 115 |
+
|
| 116 |
+
Before any learning, aggregate the data correctly:
|
| 117 |
+
|
| 118 |
+
```python
|
| 119 |
+
def aggregate_by_so_line(df, po_type_df):
|
| 120 |
+
"""
|
| 121 |
+
Aggregate transaction data to SO-line level using PO Type filters.
|
| 122 |
+
"""
|
| 123 |
+
# Merge PO Type flags
|
| 124 |
+
df = df.merge(po_type_df, left_on='po_series', right_on='PO Type', how='left')
|
| 125 |
+
|
| 126 |
+
# Filter Fresh Input POs for primary metrics
|
| 127 |
+
fresh_mask = df['To be consider for Total Pkg of Fresh PO'] == 'Yes'
|
| 128 |
+
fresh_df = df[fresh_mask]
|
| 129 |
+
|
| 130 |
+
# Aggregate to SO-line
|
| 131 |
+
order_level = fresh_df.groupby(['COPS_NO', 'COPS_LINENO']).agg({
|
| 132 |
+
'DORQT1': 'first', # Original order qty (truth)
|
| 133 |
+
'ODISQT': 'sum', # Sum of Fresh PO qty
|
| 134 |
+
'RES_QTY': 'sum', # Sum of Fresh reserves
|
| 135 |
+
'ISS_QTY': 'sum', # Sum of Fresh issued (TARGET)
|
| 136 |
+
'pack_qty': 'sum', # Sum of Fresh packing
|
| 137 |
+
}).reset_index()
|
| 138 |
+
|
| 139 |
+
# Add pack_fresh separately (includes Reprocess)
|
| 140 |
+
fresh_pkg_mask = df['To Be consider for Fresh Pkg of Fresh PO'] == 'Yes'
|
| 141 |
+
fresh_pkg_df = df[fresh_pkg_mask]
|
| 142 |
+
pack_fresh = fresh_pkg_df.groupby(['COPS_NO', 'COPS_LINENO'])['pack_fresh'].sum()
|
| 143 |
+
order_level['pack_fresh'] = pack_fresh
|
| 144 |
+
|
| 145 |
+
return order_level
|
| 146 |
+
```
|
| 147 |
+
|
| 148 |
+
**Result:** A clean order-level dataset where:
|
| 149 |
+
- `order_qty` = DORQT1 (ground truth)
|
| 150 |
+
- `issued_qty` = Sum of Fresh Input ISS_QTY (learning target)
|
| 151 |
+
- `reserved_qty` = Sum of Fresh Input RES_QTY
|
| 152 |
+
- `total_pack_qty` = Sum of Fresh Input pack_qty
|
| 153 |
+
- `pack_fresh` = Sum of Fresh + Reprocess pack_fresh
|
| 154 |
+
|
| 155 |
+
### Step 1: Learn Norms from Order-Level Data
|
| 156 |
+
|
| 157 |
+
Using the clean order-level dataset, segment by product characteristics and learn optimal buffers:
|
| 158 |
+
|
| 159 |
+
```python
|
| 160 |
+
# Segment by: route × norms_category × count_category × shade_type × finish_type × po_type
|
| 161 |
+
# Size buckets: le_500, 501_3000, above_3000
|
| 162 |
+
|
| 163 |
+
def learn_order_level_norms(order_df):
|
| 164 |
+
"""
|
| 165 |
+
Learn buffer_pct_q and buffer_fixed_q per segment.
|
| 166 |
+
"""
|
| 167 |
+
segments = SEGMENT_LEVELS # e.g., ['route', 'norms_category', 'count_category', ...]
|
| 168 |
+
|
| 169 |
+
for level in segments:
|
| 170 |
+
group_cols = level + ['size_bucket']
|
| 171 |
+
for key, g in order_df.groupby(group_cols):
|
| 172 |
+
if len(g) < MIN_N:
|
| 173 |
+
continue
|
| 174 |
+
|
| 175 |
+
# Learn optimal buffer using cost optimization
|
| 176 |
+
for q in QUANTILE_GRID:
|
| 177 |
+
buffer = learn_buffer(g['issued_qty'], g['order_qty'], quantile=q)
|
| 178 |
+
shortage = compute_shortage_rate(g['issued_qty'], predicted)
|
| 179 |
+
excess = compute_excess_rate(g['issued_qty'], predicted)
|
| 180 |
+
|
| 181 |
+
# Dual target: shortage <= 5%, excess <= 65%
|
| 182 |
+
if meets_dual_target(shortage, excess):
|
| 183 |
+
return buffer # buffer_pct_q, buffer_fixed_q
|
| 184 |
+
```
|
| 185 |
+
|
| 186 |
+
**Output:** `learned_norms_table.csv` with columns:
|
| 187 |
+
| Column | Description |
|
| 188 |
+
|--------|-------------|
|
| 189 |
+
| segment_level | e.g., "route__norms_category__count_category__shade_type__finish_type__po_type" |
|
| 190 |
+
| segment_key | e.g., "Continuous__Cotton__Below_40s__Dyed__Soft__Fresh Input" |
|
| 191 |
+
| size_bucket | le_500, 501_3000, or above_3000 |
|
| 192 |
+
| buffer_pct_q | Learned percentage buffer (e.g., 5.2%) |
|
| 193 |
+
| buffer_fixed_q | Learned fixed buffer (e.g., 100m) |
|
| 194 |
+
| orders | Sample size in segment |
|
| 195 |
+
| meets_dual_target | Boolean indicating if targets met |
|
| 196 |
+
|
| 197 |
+
### Step 2: Decide How to Update Norms.csv
|
| 198 |
+
|
| 199 |
+
Two options for incorporating learned norms into production:
|
| 200 |
+
|
| 201 |
+
#### Option A (Preferred): Versioned Learned Norms Table
|
| 202 |
+
|
| 203 |
+
Treat `learned_norms_table.csv` as the **primary production norms**:
|
| 204 |
+
- Version it (e.g., `learned_norms_table_v1.csv`)
|
| 205 |
+
- Keep `data/raw/Norms.csv` as historical baseline
|
| 206 |
+
- Decision policy uses learned table when available, falls back to manual norms
|
| 207 |
+
- Easy to update: just replace the learned table file
|
| 208 |
+
|
| 209 |
+
**Workflow:**
|
| 210 |
+
```
|
| 211 |
+
Order → Lookup in learned_norms_table.csv → Apply buffer → Decision
|
| 212 |
+
```
|
| 213 |
+
|
| 214 |
+
#### Option B: Export New Norms.csv Revision (v69)
|
| 215 |
+
|
| 216 |
+
Export learned rules into a new `Norms_v69.csv`:
|
| 217 |
+
- Same structure as current Norms.csv
|
| 218 |
+
- Replaces manual rules with data-driven ones
|
| 219 |
+
- Requires re-running all downstream calculations
|
| 220 |
+
- More disruptive but keeps single source of truth
|
| 221 |
+
|
| 222 |
+
**Recommendation:** Start with Option A (parallel learned table) for rapid iteration, then migrate to Option B once stabilized.
|
| 223 |
+
|
| 224 |
+
### Step 3: Encode Special Rules Explicitly
|
| 225 |
+
|
| 226 |
+
The current Norms.csv has special rules in comments. These should be codified as structured rules:
|
| 227 |
+
|
| 228 |
+
#### Tolerance Rules
|
| 229 |
+
|
| 230 |
+
**Current Problem:** `tolerance_limit` field is missing from transaction data.
|
| 231 |
+
|
| 232 |
+
**Solution Options:**
|
| 233 |
+
1. Add `tolerance_limit` field to source system
|
| 234 |
+
2. Derive from order_description text (contains +/-3%, +/-5%, etc.)
|
| 235 |
+
3. Use policy default (e.g., +/-3% for all orders)
|
| 236 |
+
|
| 237 |
+
```python
|
| 238 |
+
# Structured tolerance policy
|
| 239 |
+
TOLERANCE_POLICY = {
|
| 240 |
+
'default': {'plus': 3, 'minus': 3},
|
| 241 |
+
'+3/-0': {'plus': 3, 'minus': 0},
|
| 242 |
+
'+0/-3': {'plus': 0, 'minus': 3},
|
| 243 |
+
'+5/-0': {'plus': 5, 'minus': 0},
|
| 244 |
+
# ... other tolerance types
|
| 245 |
+
}
|
| 246 |
+
```
|
| 247 |
+
|
| 248 |
+
#### Special Comments as Rules
|
| 249 |
+
|
| 250 |
+
| Rule | Current Status | Implementation |
|
| 251 |
+
|------|----------------|----------------|
|
| 252 |
+
| TAKISADA +100m | Comment only | Flag in data, add 100m buffer |
|
| 253 |
+
| Relax Dryer (XF) +1% | Comment only | is_relax_dryer flag, add 1% |
|
| 254 |
+
| HR/ET/T6S +1% | Comment only | is_hr_finish, is_et_finish, is_t6s_finish flags |
|
| 255 |
+
| Jet/Jigger +4% (non-viscose) | Comment only | route in [Jet, Jigger] and norms_category not in [100%_Viscose, Cotton_Viscose_Modal] |
|
| 256 |
+
| <=500m override | Comment only | size_bucket == 'le_500', apply special rates |
|
| 257 |
+
|
| 258 |
+
```python
|
| 259 |
+
def apply_special_rules(base_reserve, order):
|
| 260 |
+
"""Apply structured special rules to base reserve."""
|
| 261 |
+
reserve = base_reserve
|
| 262 |
+
|
| 263 |
+
# TAKISADA
|
| 264 |
+
if order.get('is_takisada'):
|
| 265 |
+
reserve += 100 # meters
|
| 266 |
+
|
| 267 |
+
# Relax Dryer
|
| 268 |
+
if order.get('is_relax_dryer'):
|
| 269 |
+
reserve *= 1.01 # +1%
|
| 270 |
+
|
| 271 |
+
# Special Finishes
|
| 272 |
+
if any([order.get(f) for f in ['is_hr_finish', 'is_et_finish', 'is_t6s_finish']]):
|
| 273 |
+
reserve *= 1.01 # +1%
|
| 274 |
+
|
| 275 |
+
# Jet/Jigger non-viscose
|
| 276 |
+
if order.get('route') in ['Jet', 'Jigger']:
|
| 277 |
+
if order.get('norms_category') not in ['100%_Viscose', 'Cotton_Viscose_Modal']:
|
| 278 |
+
reserve *= 1.04 # +4%
|
| 279 |
+
|
| 280 |
+
return reserve
|
| 281 |
+
```
|
| 282 |
+
|
| 283 |
+
### Step 4: Validate on Holdout
|
| 284 |
+
|
| 285 |
+
Before deploying improved norms, validate on a holdout set:
|
| 286 |
+
|
| 287 |
+
```python
|
| 288 |
+
def validate_norms(order_df_holdout, learned_norms):
|
| 289 |
+
"""
|
| 290 |
+
Validate learned norms on holdout data.
|
| 291 |
+
"""
|
| 292 |
+
results = []
|
| 293 |
+
|
| 294 |
+
for _, row in order_df_holdout.iterrows():
|
| 295 |
+
# Lookup learned norm
|
| 296 |
+
norm = lookup_learned_norm(learned_norms, row)
|
| 297 |
+
|
| 298 |
+
# Calculate predicted reserve
|
| 299 |
+
predicted = row['order_qty'] + max(
|
| 300 |
+
row['order_qty'] * norm['buffer_pct_q'] / 100,
|
| 301 |
+
norm['buffer_fixed_q']
|
| 302 |
+
)
|
| 303 |
+
|
| 304 |
+
# Calculate metrics
|
| 305 |
+
shortage = max(0, row['order_qty'] - row['pack_fresh'])
|
| 306 |
+
excess = max(0, predicted - row['order_qty'])
|
| 307 |
+
within_tolerance = abs(predicted - row['order_qty']) / row['order_qty'] <= 0.03
|
| 308 |
+
|
| 309 |
+
results.append({
|
| 310 |
+
'shortage': shortage,
|
| 311 |
+
'excess': excess,
|
| 312 |
+
'within_3pct': within_tolerance
|
| 313 |
+
})
|
| 314 |
+
|
| 315 |
+
# Aggregate metrics
|
| 316 |
+
total_shortage = sum(r['shortage'] for r in results)
|
| 317 |
+
total_excess = sum(r['excess'] for r in results)
|
| 318 |
+
within_3pct_pct = sum(r['within_3pct'] for r in results) / len(results) * 100
|
| 319 |
+
|
| 320 |
+
# Calculate cost
|
| 321 |
+
total_cost = SHORTAGE_COST * total_shortage + EXCESS_COST * total_excess
|
| 322 |
+
|
| 323 |
+
return {
|
| 324 |
+
'shortage_rate_pct': total_shortage / sum(r['order_qty'] for r in results) * 100,
|
| 325 |
+
'excess_rate_pct': total_excess / sum(r['order_qty'] for r in results) * 100,
|
| 326 |
+
'within_3pct': within_3pct_pct,
|
| 327 |
+
'total_cost': total_cost
|
| 328 |
+
}
|
| 329 |
+
```
|
| 330 |
+
|
| 331 |
+
**Validation Metrics:**
|
| 332 |
+
| Metric | Target | Description |
|
| 333 |
+
|--------|--------|-------------|
|
| 334 |
+
| Shortage Rate | <5% | % of orders where pack_fresh < order_qty |
|
| 335 |
+
| Excess Rate | <65% | % of orders where reserve > actual |
|
| 336 |
+
| Within ±3% | >90% | % of predictions within 3% of actual |
|
| 337 |
+
| Total Cost | Minimize | shortage_cost × shortage + excess_cost × excess |
|
| 338 |
+
|
| 339 |
+
### Should We Learn from Single Rows?
|
| 340 |
+
|
| 341 |
+
**No** for the primary issuance decision. Single rows are PO-level, not order-level.
|
| 342 |
+
|
| 343 |
+
**Single PO rows are useful for:**
|
| 344 |
+
1. PO-type classification and correct aggregation logic
|
| 345 |
+
2. Special workflows (Reprocess / Re-packing / Short-Fall) handled separately
|
| 346 |
+
3. Understanding multi-PO order structures
|
| 347 |
+
|
| 348 |
+
**Order-level aggregates are required for:**
|
| 349 |
+
1. Learning norms that match business decisions
|
| 350 |
+
2. Training ML models with correct targets
|
| 351 |
+
3. Evaluating policy performance
|
| 352 |
+
|
| 353 |
+
### How This Aligns with Project Plans
|
| 354 |
+
|
| 355 |
+
Both `.cursor/plans/ml_greige_norms_v2_*.plan.md` aim to recommend greige reserve (issued_qty) with norms+ML. They implicitly assume a single decision per order.
|
| 356 |
+
|
| 357 |
+
**What's been missing:** An explicit data-model decision - **SO-line aggregation with PO-Type filters before any norms/ML step**.
|
| 358 |
+
|
| 359 |
+
Without this:
|
| 360 |
+
- Every downstream metric is wrong
|
| 361 |
+
- Norms learning produces incorrect buffers
|
| 362 |
+
- ML models learn corrupted patterns
|
| 363 |
+
- Policy decisions are unreliable
|
| 364 |
+
|
| 365 |
+
**With correct aggregation:**
|
| 366 |
+
- Norms learning produces actionable buffer improvements
|
| 367 |
+
- ML models learn from clean targets
|
| 368 |
+
- Policy decisions are grounded in correct data
|
| 369 |
+
|
| 370 |
+
---
|
| 371 |
+
|
| 372 |
+
## Data Quality and Dataset Sizes
|
| 373 |
+
|
| 374 |
+
### Dataset Tiers from Cleaning Pipeline
|
| 375 |
+
|
| 376 |
+
The data processing pipeline produces three quality tiers:
|
| 377 |
+
|
| 378 |
+
| Dataset | Records | Description | Filters Applied |
|
| 379 |
+
|---------|---------|-------------|-----------------|
|
| 380 |
+
| Full Cleaned | 4,551 | All valid records | Removed 62 with invalid issued_qty |
|
| 381 |
+
| ML-Ready | 4,004 | Valid for ML training | Excluded outliers, bad entries |
|
| 382 |
+
| Conservative | 3,984 | Strict ML subset | Excludes all flagged exceptions |
|
| 383 |
+
| Bad Entries Review | 510 | Flagged for review | Outliers and exceptions |
|
| 384 |
+
| Invalid Issued Removed | 62 | Data quality issues | issued_qty <= 0 |
|
| 385 |
+
|
| 386 |
+
### Data Quality Issues Identified
|
| 387 |
+
|
| 388 |
+
From `data/reports/cleaning_report.txt`:
|
| 389 |
+
|
| 390 |
+
| Issue | Count | Severity | Notes |
|
| 391 |
+
|-------|-------|----------|-------|
|
| 392 |
+
| DORQT1 = 0 with non-zero RES/ISS | 4 rows | Critical | Data entry error - order qty zero but quantities exist |
|
| 393 |
+
| Invalid issued qty removed | 62 | High | issued_qty <= 0 or null |
|
| 394 |
+
| No buffer added (RES = ORDER) | 598 | Medium | Potential rule violation or special process |
|
| 395 |
+
| Under-reserved (RES < ORDER) | 16 | High | Reserve below order quantity |
|
| 396 |
+
| Tiny orders (<=100m) | 72 | Medium | May need special handling |
|
| 397 |
+
| Extreme ratio high | 57 | High | Possible data entry error |
|
| 398 |
+
| Extreme ratio low | 19 | High | Possible data entry error |
|
| 399 |
+
| Extreme issued/reserved | 37 | High | Potential anomalies |
|
| 400 |
+
|
| 401 |
+
### Reference Table Coverage
|
| 402 |
+
|
| 403 |
+
| Reference Table | Coverage | Missing | Notes |
|
| 404 |
+
|-----------------|----------|---------|-------|
|
| 405 |
+
| Shade Family | 95.2% | 4,331/4,551 | High coverage from K4-Prefix |
|
| 406 |
+
| Shade Depth | 92.9% | 4,230/4,551 | High coverage from K4-Suffix |
|
| 407 |
+
| Finish Description | 20.0% | ~3,603 missing | Low coverage - major gap |
|
| 408 |
+
| Special Finish | 29.3% | 1,333 flagged | Based on finish code heuristics |
|
| 409 |
+
| Relax Dryer (XF) | 9.8% | 445 flagged | From finish code pattern |
|
| 410 |
+
| ET Finish | 5.3% | 239 flagged | Resin finish indicator |
|
| 411 |
+
| HR Finish | 2.1% | 97 flagged | Hydro repellent indicator |
|
| 412 |
+
| Unmapped Products | 173 | Various | No direct norms mapping |
|
| 413 |
+
|
| 414 |
+
### Unmapped Product Categories
|
| 415 |
+
|
| 416 |
+
Products without direct Norms.csv mapping:
|
| 417 |
+
- Tencil, Other Cotton Blends
|
| 418 |
+
- Polyester Viscose/Modal
|
| 419 |
+
- 100% Polyester
|
| 420 |
+
- Viscose Other Blends
|
| 421 |
+
|
| 422 |
+
**Impact:** These products require special handling or manual mapping.
|
| 423 |
+
|
| 424 |
+
---
|
| 425 |
+
|
| 426 |
+
## Missing Field: tolerance_limit
|
| 427 |
+
|
| 428 |
+
### The Problem
|
| 429 |
+
|
| 430 |
+
The Norms.csv file contains tolerance adjustment columns:
|
| 431 |
+
- +/-3%, +3/-0%, +2/-0% (columns I, J, K)
|
| 432 |
+
- +/-5%, +/-6%, +/-7% (column L)
|
| 433 |
+
- +/-10% (column M)
|
| 434 |
+
- +0/-3%, +0/-5% (column N)
|
| 435 |
+
- +/-1%, +/2%, +0/-2%, +1/-0% (column O)
|
| 436 |
+
|
| 437 |
+
However, **the transaction data (Details.csv) has no `tolerance_limit` field**.
|
| 438 |
+
|
| 439 |
+
### Evidence
|
| 440 |
+
|
| 441 |
+
From `docs/data_findings_codex.md`:
|
| 442 |
+
> "The tolerance_limit field is missing in Detail.csv, so it cannot be applied deterministically."
|
| 443 |
+
|
| 444 |
+
### Workaround Used
|
| 445 |
+
|
| 446 |
+
Current approach infers tolerance from data residuals vs norms baseline:
|
| 447 |
+
- Residuals cluster at 0%, -1%, +1%, +2%, +3%
|
| 448 |
+
- Smaller cluster near +5% to +6% on certain segments
|
| 449 |
+
|
| 450 |
+
### Required Action
|
| 451 |
+
|
| 452 |
+
To apply tolerances deterministically:
|
| 453 |
+
1. Add `tolerance_limit` field to source data, OR
|
| 454 |
+
2. Derive from order_description field (contains tolerance info in text), OR
|
| 455 |
+
3. Standardize tolerance to a default (e.g., +/-3%)
|
| 456 |
+
|
| 457 |
+
---
|
| 458 |
+
|
| 459 |
+
## Norms Rev 68: Metadata and Linkage Assumptions
|
| 460 |
+
|
| 461 |
+
### Norms File Information
|
| 462 |
+
|
| 463 |
+
| Property | Value |
|
| 464 |
+
|----------|-------|
|
| 465 |
+
| File | `data/raw/Norms.csv` |
|
| 466 |
+
| Revision | 68 |
|
| 467 |
+
| Effective Date | 13-12-2025 |
|
| 468 |
+
| Source | AT1 MKT PD Gr Norms Rev on 13-12-2025 |
|
| 469 |
+
|
| 470 |
+
### Linkage Assumptions (from `docs/data_findings_codex.md`)
|
| 471 |
+
|
| 472 |
+
The pipeline makes several assumptions when linking transaction data to Norms.csv:
|
| 473 |
+
|
| 474 |
+
**A) Route Normalization:**
|
| 475 |
+
| Raw Data | Maps To |
|
| 476 |
+
|----------|---------|
|
| 477 |
+
| Continouse, Continues | Continuous |
|
| 478 |
+
| Jet | Jet Route |
|
| 479 |
+
| Jigger | Jigger Route |
|
| 480 |
+
|
| 481 |
+
**B) Count Band:**
|
| 482 |
+
| Raw Count | Maps To |
|
| 483 |
+
|-----------|---------|
|
| 484 |
+
| count < 40 | Below 40s |
|
| 485 |
+
| count >= 40 | 40s and above |
|
| 486 |
+
| 2/40 (fraction) | Parses to 40 |
|
| 487 |
+
|
| 488 |
+
**C) Finish Normalization:**
|
| 489 |
+
| Shade Type | Finish (Peach/Soft) | Maps To |
|
| 490 |
+
|------------|---------------------|---------|
|
| 491 |
+
| Dyed | Soft | Normal |
|
| 492 |
+
| Dyed | Peach | Peach |
|
| 493 |
+
| FB/RFD | Soft | Peach/Soft |
|
| 494 |
+
| FB/RFD | Peach | Peach |
|
| 495 |
+
|
| 496 |
+
**D) Product Group Mapping:**
|
| 497 |
+
| Data Product Type | Maps To Norms |
|
| 498 |
+
|-------------------|---------------|
|
| 499 |
+
| Cotton Normal, Tencil Cotton | Cotton |
|
| 500 |
+
| Cotton Stretch, Cotton Viscose Stretch, Cotton Modal Stretch | Cotton Stretch (FB/RFD), Stretch (Dyed) |
|
| 501 |
+
| PC Stretch, Polyester Cotton | PC/PC stretch |
|
| 502 |
+
| Bi-Stretch | Bi-stretch Noram (if no nylon), Bi-stretch PC/Nylon (if nylon) |
|
| 503 |
+
| Nylon Stretch | Bi-stretch PC/Nylon or Stretch |
|
| 504 |
+
| Viscose/Modal | Cotton Viscose/Cotton Modal |
|
| 505 |
+
| Others | Other (requires manual handling) |
|
| 506 |
+
|
| 507 |
+
**Linkage Success Rate:**
|
| 508 |
+
- Matched: 3,809 / 4,613 rows (82.6%)
|
| 509 |
+
- Unmatched: 804 rows (17.4%)
|
| 510 |
+
|
| 511 |
+
**Unmatched Concentrations:**
|
| 512 |
+
- Stretch on Jigger
|
| 513 |
+
- Cotton Viscose/Modal on Jet/Jigger
|
| 514 |
+
- Bi-stretch and special/rare categories
|
| 515 |
+
|
| 516 |
+
---
|
| 517 |
+
|
| 518 |
+
## Special Articles Section (Not Reflected in Code)
|
| 519 |
+
|
| 520 |
+
### Norms.csv Special Articles
|
| 521 |
+
|
| 522 |
+
The Norms.csv file contains a "Special Articles" section (rows 58-60) with manually defined rules:
|
| 523 |
+
|
| 524 |
+
| Article | Extra Greige | Finish | Date | Remarks |
|
| 525 |
+
|---------|--------------|--------|------|---------|
|
| 526 |
+
| 12-85240 (A235A013) | 8% | NA | 06/Dec/25 | Due to Less Pkg |
|
| 527 |
+
|
| 528 |
+
### Current Status
|
| 529 |
+
|
| 530 |
+
**The code does NOT implement the Special Articles section.**
|
| 531 |
+
|
| 532 |
+
From `docs/data_findings_codex.md`:
|
| 533 |
+
> "Special Articles (e.g., specific article with 8% extra) - NOT CODED"
|
| 534 |
+
|
| 535 |
+
### Impact
|
| 536 |
+
|
| 537 |
+
Orders matching special articles will not receive the specified extra buffer.
|
| 538 |
+
|
| 539 |
+
### Required Action
|
| 540 |
+
|
| 541 |
+
Add special article lookup table and apply extra buffer for matching articles.
|
| 542 |
+
|
| 543 |
+
---
|
| 544 |
+
|
| 545 |
+
## Feature-Leakage Contract
|
| 546 |
+
|
| 547 |
+
### Pre-Issuance Feature Contract
|
| 548 |
+
|
| 549 |
+
The project implements a formal feature contract to prevent data leakage:
|
| 550 |
+
|
| 551 |
+
**Allowed Features (PRE_ISSUANCE_FEATURES):**
|
| 552 |
+
```
|
| 553 |
+
order_qty, order_size_bucket, norms_order_size, product_type, norms_category,
|
| 554 |
+
route, finish_type, shade_type, count_numeric, count_category, po_type,
|
| 555 |
+
po_series, is_fresh_input, is_reprocess, is_repacking, is_shade_conversion,
|
| 556 |
+
is_short_fall, is_fresh_po_total, is_fresh_po_fresh, shade_family, shade_depth,
|
| 557 |
+
is_special_finish, is_relax_dryer, is_et_finish, is_hr_finish, is_t6s_finish,
|
| 558 |
+
is_takisada, year, month, quarter, day_of_week, week_of_year, is_peak_season,
|
| 559 |
+
norms_expected_reserve
|
| 560 |
+
```
|
| 561 |
+
|
| 562 |
+
### Leakage Check Results
|
| 563 |
+
|
| 564 |
+
From `data/reports/feature_contract_report.md`:
|
| 565 |
+
|
| 566 |
+
**Post-Issuance Columns (NOT allowed at prediction time):**
|
| 567 |
+
```
|
| 568 |
+
reserved_qty, issued_qty, total_pack_qty, pack_fresh, pack_q1, pack_q2,
|
| 569 |
+
pack_q3, pack_qty, pack_q5, pack_q6, packing_date, dispatch_qty, stock_q1,
|
| 570 |
+
stock_q2, stock_q3, stock_q4, stock_q5, stock_q6, packing_efficiency
|
| 571 |
+
```
|
| 572 |
+
|
| 573 |
+
**Guidance:**
|
| 574 |
+
- For training: use only PRE_ISSUANCE_FEATURES
|
| 575 |
+
- Keep post-issuance columns for evaluation/labels only
|
| 576 |
+
- Never use post-issuance features at prediction time
|
| 577 |
+
|
| 578 |
+
---
|
| 579 |
+
|
| 580 |
+
## End-to-End Modeling Stack
|
| 581 |
+
|
| 582 |
+
### Complete Pipeline Overview
|
| 583 |
+
|
| 584 |
+
The project implements a comprehensive ML and analytics pipeline:
|
| 585 |
+
|
| 586 |
+
```
|
| 587 |
+
Details.csv (Transactional Data)
|
| 588 |
+
↓
|
| 589 |
+
data_processing.py (Cleaning + Feature Engineering)
|
| 590 |
+
↓
|
| 591 |
+
cleaned_greige_data_ml_ready_v3.csv
|
| 592 |
+
↓
|
| 593 |
+
┌──────────────────────────────────────────────────────────────┐
|
| 594 |
+
│ NORM CALCULATION │
|
| 595 |
+
│ norms_calculator.py → norms_baseline.csv │
|
| 596 |
+
│ learn_norms_from_data.py → learned_norms_table.csv │
|
| 597 |
+
└──────────────────────────────────────────────────────────────┘
|
| 598 |
+
↓
|
| 599 |
+
┌──────────────────────────────────────────────────────────────┐
|
| 600 |
+
│ MODEL TRAINING │
|
| 601 |
+
│ train_quantile_models.py → models/quantile_models.joblib │
|
| 602 |
+
│ train_hierarchical_models.py → models/hierarchical_models │
|
| 603 |
+
│ calibrate_conformal.py → conformal_adjustments.yaml │
|
| 604 |
+
└──────────────────────────────────────────────────────────────┘
|
| 605 |
+
↓
|
| 606 |
+
┌──────────────────────────────────────────────────────────────┐
|
| 607 |
+
│ EVALUATION & DECISION │
|
| 608 |
+
│ evaluate_cost_policy.py → cost_policy_report.md │
|
| 609 |
+
│ decision_policy.py → decision_policy_output.csv │
|
| 610 |
+
│ tolerance_inference.py → tolerance_policy.yaml │
|
| 611 |
+
└──────────────────────────────────────────────────────────────┘
|
| 612 |
+
↓
|
| 613 |
+
api.py (FastAPI Endpoint for Production)
|
| 614 |
+
```
|
| 615 |
+
|
| 616 |
+
### Scripts and Outputs
|
| 617 |
+
|
| 618 |
+
| Script | Output | Purpose |
|
| 619 |
+
|--------|--------|---------|
|
| 620 |
+
| `data_processing.py` | cleaned_greige_data_ml_ready_v3.csv | Data cleaning |
|
| 621 |
+
| `norms_calculator.py` | cleaned_greige_data_ml_ready_v3_norms_baseline.csv | Apply Norms.csv rules |
|
| 622 |
+
| `learn_norms_from_data.py` | learned_norms_long.csv, learned_norms_table.csv | Data-driven norms |
|
| 623 |
+
| `learned_norms_calculator.py` | cleaned_greige_data_ml_ready_v3_learned_norms.csv | Apply learned norms |
|
| 624 |
+
| `train_quantile_models.py` | models/quantile_models.joblib | Quantile regression |
|
| 625 |
+
| `train_hierarchical_models.py` | models/hierarchical_models.joblib | Hierarchical Bayesian |
|
| 626 |
+
| `calibrate_conformal.py` | configs/conformal_adjustments.yaml | Conformal calibration |
|
| 627 |
+
| `evaluate_cost_policy.py` | cost_policy_report.md | Cost-based evaluation |
|
| 628 |
+
| `decision_policy.py` | decision_policy_output.csv | Decision engine |
|
| 629 |
+
| `tolerance_inference.py` | configs/tolerance_policy.yaml | Tolerance inference |
|
| 630 |
+
|
| 631 |
+
### Generated Configuration Files
|
| 632 |
+
|
| 633 |
+
| File | Purpose |
|
| 634 |
+
|------|---------|
|
| 635 |
+
| `configs/tolerance_policy.yaml` | Tolerance adjustments per segment |
|
| 636 |
+
| `configs/conformal_adjustments.yaml` | Conformal prediction adjustments |
|
| 637 |
+
| `configs/norms_policy.yaml` | Official policy parameters |
|
| 638 |
+
|
| 639 |
+
---
|
| 640 |
+
|
| 641 |
+
## Decision Support API and Guardrails
|
| 642 |
+
|
| 643 |
+
### FastAPI Endpoint
|
| 644 |
+
|
| 645 |
+
The project includes a production-ready API for recommendations:
|
| 646 |
+
|
| 647 |
+
**File:** `src/api.py`
|
| 648 |
+
|
| 649 |
+
**Endpoints:**
|
| 650 |
+
- `POST /recommend` - Get reserve recommendation
|
| 651 |
+
- `POST /decision-policy/run` - Run decision policy on full dataset
|
| 652 |
+
|
| 653 |
+
**Request Schema:**
|
| 654 |
+
```python
|
| 655 |
+
class RecommendationRequest(BaseModel):
|
| 656 |
+
order_qty: float
|
| 657 |
+
route: str
|
| 658 |
+
norms_category: str
|
| 659 |
+
shade_type: str
|
| 660 |
+
finish_type: str
|
| 661 |
+
count_category: str
|
| 662 |
+
po_type: str
|
| 663 |
+
count_numeric: float | None = None
|
| 664 |
+
product_type: str | None = None
|
| 665 |
+
shade_family: str | None = None
|
| 666 |
+
shade_depth: str | None = None
|
| 667 |
+
risk_policy: str | None = "moderate"
|
| 668 |
+
shortage_cost: float | None = None
|
| 669 |
+
excess_cost: float | None = None
|
| 670 |
+
```
|
| 671 |
+
|
| 672 |
+
**Response Schema:**
|
| 673 |
+
```python
|
| 674 |
+
{
|
| 675 |
+
"recommended_reserve": float,
|
| 676 |
+
"baseline_reserve": float,
|
| 677 |
+
"quantile": float,
|
| 678 |
+
"action": str,
|
| 679 |
+
"notes": str
|
| 680 |
+
}
|
| 681 |
+
```
|
| 682 |
+
|
| 683 |
+
**Usage:**
|
| 684 |
+
```bash
|
| 685 |
+
uvicorn src.api:app --reload
|
| 686 |
+
```
|
| 687 |
+
|
| 688 |
+
### Guardrails System
|
| 689 |
+
|
| 690 |
+
**File:** `src/guardrails.py`
|
| 691 |
+
|
| 692 |
+
Guardrails ensure recommendations stay within safe bounds:
|
| 693 |
+
|
| 694 |
+
| Parameter | Default | Purpose |
|
| 695 |
+
|-----------|---------|---------|
|
| 696 |
+
| min_buffer_pct | 0.95 | Minimum recommendation as % of baseline |
|
| 697 |
+
| max_buffer_pct | 1.50 | Maximum recommendation as % of baseline |
|
| 698 |
+
|
| 699 |
+
**Application Logic:**
|
| 700 |
+
```python
|
| 701 |
+
def apply(self, order_qty: float, baseline: float, recommendation: float) -> float:
|
| 702 |
+
rec = max(recommendation, order_qty) # At least order qty
|
| 703 |
+
rec = max(rec, baseline * self.min_buffer_pct) # At least 95% of baseline
|
| 704 |
+
rec = min(rec, baseline * self.max_buffer_pct) # At most 150% of baseline
|
| 705 |
+
return rec
|
| 706 |
+
```
|
| 707 |
+
|
| 708 |
+
---
|
| 709 |
+
|
| 710 |
+
## Business Workflow Assumptions
|
| 711 |
+
|
| 712 |
+
From `data/meetings/Meeting notes.md`:
|
| 713 |
+
|
| 714 |
+
### Operational Process
|
| 715 |
+
|
| 716 |
+
1. **Order Entry**: Customer places order with specified quantity
|
| 717 |
+
2. **PDC Review**: Product Development Center estimates:
|
| 718 |
+
- First: Potential shrinkage (based on construction, EPI/PPI, history)
|
| 719 |
+
- Second: Wastage factors
|
| 720 |
+
- Final: Fabric length for deliverable percentage
|
| 721 |
+
3. **Norms Application**: Standard formula applied (e.g., 3% or 100m whichever is higher up to 3000m)
|
| 722 |
+
4. **Execution**: Production issues greige; top-ups may occur
|
| 723 |
+
5. **Packing**: Final output packed; fresh packing quality assessed
|
| 724 |
+
|
| 725 |
+
### Key Business Rules (from Meeting Notes)
|
| 726 |
+
|
| 727 |
+
| Rule | Value | Source |
|
| 728 |
+
|------|-------|--------|
|
| 729 |
+
| Standard tolerance | +/-3% | Meeting notes |
|
| 730 |
+
| Buffer up to 3000m | 3% or 100m whichever is higher | Meeting notes |
|
| 731 |
+
| "OK" packing percentage | 97-98% | Meeting notes |
|
| 732 |
+
| Expected solution output | Recommendation | Meeting notes |
|
| 733 |
+
| Norms update mechanism | Feedback loop | Meeting notes |
|
| 734 |
+
|
| 735 |
+
### PDC Workflow Details
|
| 736 |
+
|
| 737 |
+
From meeting transcript (Suresh/Bhupesh):
|
| 738 |
+
- PDC suggests shrinkage based on construction and historical data
|
| 739 |
+
- Wastage considered after shrinkage
|
| 740 |
+
- Final fabric length calculated for customer deliverable
|
| 741 |
+
- Tolerance +/-3% is standard (can vary by order)
|
| 742 |
+
- 97-98% fresh packing is considered "OK"
|
| 743 |
+
|
| 744 |
+
---
|
| 745 |
+
|
| 746 |
+
## Details.csv Header Structure
|
| 747 |
+
|
| 748 |
+
### Actual File Structure
|
| 749 |
+
|
| 750 |
+
The Details.csv file has a **3-row header structure**:
|
| 751 |
+
|
| 752 |
+
| Row | Content | Purpose |
|
| 753 |
+
|-----|---------|---------|
|
| 754 |
+
| Row 1 | Descriptive labels (Sale Order No, SO Line, etc.) | Human-readable headers |
|
| 755 |
+
| Row 2 | System/Manual indicators | Data source tracking |
|
| 756 |
+
| Row 3 | Actual column names (COPS_NO, COPS_LINENO, etc.) | Programmatic column names |
|
| 757 |
+
| Row 4+ | Data rows | Transactional data |
|
| 758 |
+
|
| 759 |
+
### Pipeline Assumption
|
| 760 |
+
|
| 761 |
+
The pipeline uses `skiprows=2` which:
|
| 762 |
+
- Skips Row 1 (descriptive labels)
|
| 763 |
+
- Skips Row 2 (System/Manual indicators)
|
| 764 |
+
- Starts reading from Row 3 (actual column names)
|
| 765 |
+
|
| 766 |
+
**Note:** This means the first data row is read with column names from Row 3, but subsequent rows use Row 1 as column names (incorrect behavior).
|
| 767 |
+
|
| 768 |
+
**Correct Approach:**
|
| 769 |
+
- Read all 3 header rows
|
| 770 |
+
- Use Row 3 (COPS_NO, COPS_LINENO, etc.) as column names
|
| 771 |
+
- Discard Rows 1-2
|
| 772 |
+
|
| 773 |
+
---
|
| 774 |
+
|
| 775 |
+
## Older Analysis: PO-Level Assumptions
|
| 776 |
+
|
| 777 |
+
### Pre-Fix Analysis Used ODISQT as Order Qty
|
| 778 |
+
|
| 779 |
+
Earlier analysis documents treated `ODISQT` (PO-level quantity) as "Order Qty", which conflicts with the correct understanding that `DORQT1` (Sales Order level) is the true order quantity.
|
| 780 |
+
|
| 781 |
+
### Affected Documents
|
| 782 |
+
|
| 783 |
+
| Document | Issue |
|
| 784 |
+
|----------|-------|
|
| 785 |
+
| `docs/greige_quantity_analysis_findings.md` | Uses ODISQT as order_qty (PO-level) |
|
| 786 |
+
| `docs/eli5_greige_norms_brief_for_pm_and_client.md` | Uses ODISQT terminology |
|
| 787 |
+
| `docs/data_findings_codex.md` | ODISQT described as "PO Qty" (correct) |
|
| 788 |
+
|
| 789 |
+
### Correct Field Definitions
|
| 790 |
+
|
| 791 |
+
| Field | Raw Name | Correct Usage |
|
| 792 |
+
|-------|----------|---------------|
|
| 793 |
+
| Order Qty (True) | DORQT1 | Ground truth sales order quantity |
|
| 794 |
+
| PO Qty | ODISQT | Quantity in individual PO |
|
| 795 |
+
| Reserved Qty | RES_QTY | Greige reserved based on norms |
|
| 796 |
+
| Issued Qty | ISS_QTY | Actual greige issued |
|
| 797 |
+
| Pack Total | pack_qty | Total packed output |
|
| 798 |
+
| Pack Fresh | pack_fresh | Good quality packed output |
|
| 799 |
+
|
| 800 |
+
### Key Distinction
|
| 801 |
+
|
| 802 |
+
- **DORQT1** = Sum of Fresh Input PO quantities (for each SO line)
|
| 803 |
+
- **ODISQT** = Quantity in individual PO (one row per PO)
|
| 804 |
+
|
| 805 |
+
---
|
| 806 |
+
|
| 807 |
+
## Known Issues and Discrepancies
|
| 808 |
+
|
| 809 |
+
### Issue 1: Input File Name Mismatch
|
| 810 |
+
|
| 811 |
+
**Problem:** The pipeline code (`data_processing.py`) expects `Detail.csv` in `data/raw/`, but the actual transactional data file is `Details.csv` in `data/`.
|
| 812 |
+
|
| 813 |
+
| Expected by Code | Actual File Location | Status |
|
| 814 |
+
|-----------------|---------------------|--------|
|
| 815 |
+
| `data/raw/Detail.csv` | `data/Details.csv` | **MISMATCH** |
|
| 816 |
+
|
| 817 |
+
### Issue 2: Column Mapping
|
| 818 |
+
|
| 819 |
+
| Field | Correct Source Column | Previous Error |
|
| 820 |
+
|-------|----------------------|----------------|
|
| 821 |
+
| Article Code | OCDKE1 | Listed as grey_k1 |
|
| 822 |
+
| Finish Code | OCDKE3 | Listed as Finish |
|
| 823 |
+
| Shade Code | OCDKE4 | Listed as Shade Code |
|
| 824 |
+
| PO Qty (Fair) | ODISQT | Listed as OCDDIL |
|
| 825 |
+
| PO Line | OCDDIL | Listed as po_line |
|
| 826 |
+
| PO Type | PO Series | Listed as HCMTYP |
|
| 827 |
+
| Order Description | HCMTYP | Correct |
|
| 828 |
+
|
| 829 |
+
### Issue 3: PO Type Aggregation Not Implemented
|
| 830 |
+
|
| 831 |
+
The code creates `is_fresh_po_total` and `is_fresh_po_fresh` flags but never uses them for filtering.
|
| 832 |
+
|
| 833 |
+
### Issue 4: Norms Formula Uses Additive Buffer
|
| 834 |
+
|
| 835 |
+
Current code uses:
|
| 836 |
+
```python
|
| 837 |
+
expected = order_qty + extra # WRONG
|
| 838 |
+
```
|
| 839 |
+
|
| 840 |
+
Should use division factor:
|
| 841 |
+
```python
|
| 842 |
+
expected = order_qty / (1 - buffer_pct / 100) # CORRECT
|
| 843 |
+
```
|
| 844 |
+
|
| 845 |
+
---
|
| 846 |
+
|
| 847 |
+
## Data Files Guide
|
| 848 |
+
|
| 849 |
+
### Raw Reference Files
|
| 850 |
+
|
| 851 |
+
#### `data/raw/Norms.csv`
|
| 852 |
+
Master norms document with division factor rules, tolerance columns, special comments, and special articles.
|
| 853 |
+
|
| 854 |
+
#### `data/raw/PO Type.csv`
|
| 855 |
+
Defines how each PO type (FQT, FRG, F01, etc.) should be treated in calculations.
|
| 856 |
+
|
| 857 |
+
#### `data/raw/Shade Category.csv`
|
| 858 |
+
Maps K4-prefix/suffix to shade family and depth.
|
| 859 |
+
|
| 860 |
+
#### `data/raw/Finish Description.csv`
|
| 861 |
+
Contains finish code descriptions and special chemical attributes.
|
| 862 |
+
|
| 863 |
+
### Main Data Files
|
| 864 |
+
|
| 865 |
+
#### `data/Details.csv`
|
| 866 |
+
Primary transactional data with one row per PO per SO line.
|
| 867 |
+
|
| 868 |
+
#### `data/Calculation.csv`
|
| 869 |
+
Example showing correct aggregation for sample order.
|
| 870 |
+
|
| 871 |
+
### Processed Data Files
|
| 872 |
+
|
| 873 |
+
**Post-fix (v4) outputs — order-level (SO-line) datasets**
|
| 874 |
+
These are the current, correct datasets after fixing PO aggregation:
|
| 875 |
+
|
| 876 |
+
- `data/processed/cleaned_greige_order_level_full_v4.csv` (order-level, full)
|
| 877 |
+
- `data/processed/cleaned_greige_order_level_v4.csv` (order-level, ML-ready)
|
| 878 |
+
- `data/processed/cleaned_greige_po_level_v4.csv` (PO-level, audit)
|
| 879 |
+
- `data/processed/cleaned_greige_reprocess_v4.csv` (reprocess/repacking rows only)
|
| 880 |
+
- `data/processed/bad_entries_for_review_v4.csv` (order-level outliers)
|
| 881 |
+
- `data/processed/cleaned_greige_order_level_v4_norms_baseline.csv`
|
| 882 |
+
- `data/processed/cleaned_greige_order_level_v4_learned_norms.csv`
|
| 883 |
+
|
| 884 |
+
**Note:** v3 files and reports were generated **pre-fix** and should be treated as historical references only.
|
| 885 |
+
|
| 886 |
+
| File | Purpose |
|
| 887 |
+
|------|---------|
|
| 888 |
+
| `cleaned_greige_data_ml_ready_v3.csv` | Cleaned data for ML |
|
| 889 |
+
| `cleaned_greige_data_ml_ready_v3_norms_baseline.csv` | With norms baseline |
|
| 890 |
+
| `cleaned_greige_data_ml_ready_v3_learned_norms.csv` | With learned norms |
|
| 891 |
+
| `learned_norms_table.csv` | Data-driven norms lookup |
|
| 892 |
+
| `learned_norms_long.csv` | Detailed norms per segment |
|
| 893 |
+
| `data_dictionary_v3.csv` | Complete column documentation |
|
| 894 |
+
|
| 895 |
+
---
|
| 896 |
+
|
| 897 |
+
## Key Concepts
|
| 898 |
+
|
| 899 |
+
### Division Factor vs. Multiplying Factor
|
| 900 |
+
|
| 901 |
+
**Multiplying Factor (WRONG):**
|
| 902 |
+
```python
|
| 903 |
+
Reserve = Order Qty + (Order Qty * Buffer%)
|
| 904 |
+
# Example: 10000 + (10000 * 6%) = 10600
|
| 905 |
+
```
|
| 906 |
+
|
| 907 |
+
**Division Factor (CORRECT - Per Norms.csv):**
|
| 908 |
+
```python
|
| 909 |
+
Reserve = Order Qty / (1 - Buffer%)
|
| 910 |
+
# Example: 10000 / (1 - 0.06) = 10000 / 0.94 = 10638
|
| 911 |
+
```
|
| 912 |
+
|
| 913 |
+
### Order Size Thresholds
|
| 914 |
+
|
| 915 |
+
| Order Size | Buffer Method |
|
| 916 |
+
|------------|---------------|
|
| 917 |
+
| <= 500m | 15% or 70m (Dyed), 10% or 50m (FB/RFD) |
|
| 918 |
+
| 501 - 3000m | Fixed meters (X% or Ym whichever is higher) |
|
| 919 |
+
| > 3000m | Percentage-based |
|
| 920 |
+
|
| 921 |
+
### PO Type Treatment
|
| 922 |
+
|
| 923 |
+
| Metric | Fresh Input | Reprocess | Other |
|
| 924 |
+
|--------|-------------|-----------|-------|
|
| 925 |
+
| order_qty | Yes | No | No |
|
| 926 |
+
| reserved_qty | Yes | No (1:1) | No |
|
| 927 |
+
| issued_qty | Yes | No | No |
|
| 928 |
+
| total_pack_qty | Yes | No | No |
|
| 929 |
+
| pack_fresh | Yes | Yes | No |
|
| 930 |
+
|
| 931 |
+
---
|
| 932 |
+
|
| 933 |
+
## Implementation Checklist
|
| 934 |
+
|
| 935 |
+
### Critical Fixes Required
|
| 936 |
+
|
| 937 |
+
- [ ] Fix input file path: Change `data/raw/Detail.csv` to `data/Details.csv`
|
| 938 |
+
- [ ] Fix column mapping: Correct OCDDIL -> po_line, ODISQT -> order_qty
|
| 939 |
+
- [ ] Fix PO Type mapping: PO Type from `po_series` column
|
| 940 |
+
- [ ] Implement PO Type aggregation: Use `is_fresh_po_total` and `is_fresh_po_fresh` flags
|
| 941 |
+
- [ ] Fix norms formula: Change to division factor method
|
| 942 |
+
- [ ] Regenerate cleaned data with correct aggregation
|
| 943 |
+
- [ ] Re-run all analysis scripts with fixed data
|
| 944 |
+
- [ ] Validate against Calculation.csv examples
|
| 945 |
+
|
| 946 |
+
### Re-run Required Scripts
|
| 947 |
+
|
| 948 |
+
1. `data_processing.py` - Fix aggregation
|
| 949 |
+
2. `norms_calculator.py` - Fix formula
|
| 950 |
+
3. `learn_norms_from_data.py` - Fix formula
|
| 951 |
+
4. `segment_insights.py` - Regenerate reports
|
| 952 |
+
5. `composite_insights.py` - Regenerate reports
|
| 953 |
+
6. `learned_norms_calculator.py` - Regenerate
|
| 954 |
+
7. `tolerance_inference.py` - Regenerate
|
| 955 |
+
8. `train_quantile_models.py` - Retrain
|
| 956 |
+
9. `decision_policy.py` - Regenerate output
|
| 957 |
+
|
| 958 |
+
---
|
| 959 |
+
|
| 960 |
+
## Appendix: Norms Mapping Logic
|
| 961 |
+
|
| 962 |
+
### Step 1: Determine Parameter (Shade Type)
|
| 963 |
+
| Data shade_type | Maps to Parameter |
|
| 964 |
+
|-----------------|-------------------|
|
| 965 |
+
| Dyed | Dyed |
|
| 966 |
+
| FB | FB |
|
| 967 |
+
| RFD | RFD |
|
| 968 |
+
|
| 969 |
+
### Step 2: Determine Finish
|
| 970 |
+
| Data shade_type | Data finish_type | Maps to Finish |
|
| 971 |
+
|-----------------|------------------|----------------|
|
| 972 |
+
| Dyed | Soft | Normal |
|
| 973 |
+
| Dyed | Peach | Peach |
|
| 974 |
+
| FB/RFD | Soft/Peach | Peach/Soft |
|
| 975 |
+
|
| 976 |
+
### Step 3: Determine Product Group
|
| 977 |
+
| Data norms_category | Data shade_type | Maps to Product Group |
|
| 978 |
+
|---------------------|-----------------|----------------------|
|
| 979 |
+
| Cotton | Any | Cotton |
|
| 980 |
+
| Stretch | FB/RFD | Cotton Stretch |
|
| 981 |
+
| Stretch | Dyed | Stretch |
|
| 982 |
+
| PC/PC_Stretch | Any | PC/PC stretch |
|
| 983 |
+
| Bi_Stretch | Contains nylon | Bi-stretch PC/Nylon |
|
| 984 |
+
| Bi_Stretch | Other | Bi-stretch Noram |
|
| 985 |
+
|
| 986 |
+
### Step 4: Determine Count Band
|
| 987 |
+
| Data count_category | Maps to Count Band |
|
| 988 |
+
|---------------------|-------------------|
|
| 989 |
+
| Below_40s | Below 40s |
|
| 990 |
+
| 40s_and_above | 40s and above |
|
| 991 |
+
|
| 992 |
+
### Step 5-7: Match Rule, Apply Rule, Calculate Reserve
|
| 993 |
+
- Select matching row from Norms.csv
|
| 994 |
+
- Use appropriate rule (Upto 3000m or Above 3000m)
|
| 995 |
+
- Apply special add-ons (TAKISADA, small orders, special finishes)
|
| 996 |
+
- Calculate reserve using division factor
|
| 997 |
+
|
| 998 |
+
---
|
| 999 |
+
|
| 1000 |
+
## Latest Meeting Clarifications (Jan 28, 2026)
|
| 1001 |
+
|
| 1002 |
+
### Target Variable Decision Required
|
| 1003 |
+
|
| 1004 |
+
**Critical:** Actual Gray Opening (ISS_QTY) is manual/physical availability, NOT a calculated value.
|
| 1005 |
+
|
| 1006 |
+
From the meeting with Vardhman team (Suresh Pathania, Balvinder Singh):
|
| 1007 |
+
|
| 1008 |
+
> "The actual gray opening quantity is the physical availability of the gray it can be plus minus something plus minus some percentage of the reserved quantity because the calculated value cannot match 100% with the quantity."
|
| 1009 |
+
>
|
| 1010 |
+
> - Balvinder Singh
|
| 1011 |
+
|
| 1012 |
+
> "Actual gray opening... is just like system. It is manual activity, right? There is no mathematical computation involved in it."
|
| 1013 |
+
>
|
| 1014 |
+
> - Suresh Pathania
|
| 1015 |
+
|
| 1016 |
+
This creates a critical decision point for the model:
|
| 1017 |
+
|
| 1018 |
+
| Option | Target | Implication |
|
| 1019 |
+
|--------|--------|-------------|
|
| 1020 |
+
| A | Predict RES_QTY (reserved quantity) | Matches norms formula - clean target |
|
| 1021 |
+
| B | Predict ISS_QTY (actual opening) | Constrained by inventory - needs availability features |
|
| 1022 |
+
| C | Predict pack_fresh (OK packing) | Business goal - optimize to meet customer order |
|
| 1023 |
+
|
| 1024 |
+
**Risk:** ISS_QTY is manual/constrained - without inventory/availability features, model cannot accurately predict it.
|
| 1025 |
+
|
| 1026 |
+
**Recommendation:** Either add inventory features to predict ISS_QTY, or target RES_QTY (norms recommendation) as the model objective.
|
| 1027 |
+
|
| 1028 |
+
### Process Flow Confirmed
|
| 1029 |
+
|
| 1030 |
+
```
|
| 1031 |
+
Reserved (norms calculated) → Actual Opening (manual availability) → Total Pack (includes rejection) → Pack Fresh (OK)
|
| 1032 |
+
```
|
| 1033 |
+
|
| 1034 |
+
Key metrics from meeting:
|
| 1035 |
+
|
| 1036 |
+
- **Shrinkage:** 5-8% (from reserved to total pack)
|
| 1037 |
+
- **Extra packing:** ~18% (order quantity limit plus 0%)
|
| 1038 |
+
- **Rejections:** Delta between total_pack and pack_fresh
|
| 1039 |
+
|
| 1040 |
+
### Reprocess Handling Confirmed
|
| 1041 |
+
|
| 1042 |
+
From meeting: FRG = reprocess, and "total quantity fresh packing should consider reprocessing."
|
| 1043 |
+
|
| 1044 |
+
This aligns with our aggregation rule:
|
| 1045 |
+
- `pack_fresh` = sum of Fresh + Reprocess (is_fresh_po_fresh = Yes)
|
| 1046 |
+
|
| 1047 |
+
### Granularity Expectations
|
| 1048 |
+
|
| 1049 |
+
Client requires:
|
| 1050 |
+
|
| 1051 |
+
- **Article-wise analysis** - Article-level diagnostics included
|
| 1052 |
+
- **Sale order-wise analysis** - SO-line aggregation implemented
|
| 1053 |
+
|
| 1054 |
+
From meeting: "Single sale order having multiple lines will also constitute to the same product."
|
| 1055 |
+
|
| 1056 |
+
### Data Gaps (Critical Risk)
|
| 1057 |
+
|
| 1058 |
+
Mentioned in meeting but NOT in current dataset:
|
| 1059 |
+
|
| 1060 |
+
| Feature | Importance | Status | Impact |
|
| 1061 |
+
|---------|------------|--------|--------|
|
| 1062 |
+
| GSM (Grams per Square Meter) | High | Missing | Cannot segment by fabric weight |
|
| 1063 |
+
| GLM (Grams per Linear Meter) | High | Missing | Cannot calculate linear density |
|
| 1064 |
+
| Thread count | Medium | Missing | Cannot identify fabric construction |
|
| 1065 |
+
| Tolerance_limit field | High | Missing | Cannot apply deterministic tolerance |
|
| 1066 |
+
| Inventory/availability signal | **Critical** | Missing | Cannot predict actual opening (ISS_QTY) |
|
| 1067 |
+
|
| 1068 |
+
**Impact:** Model cannot predict "what will actually be opened" (ISS_QTY) without inventory availability data.
|
| 1069 |
+
|
| 1070 |
+
---
|
| 1071 |
+
|
| 1072 |
+
## Implementation Notes (v4)
|
| 1073 |
+
|
| 1074 |
+
### Order-Level Aggregation (Primary Workflow)
|
| 1075 |
+
|
| 1076 |
+
- Dataset: `cleaned_greige_order_level_v4.csv`
|
| 1077 |
+
- One row per (COPS_NO, COPS_LINENO)
|
| 1078 |
+
- Fresh Input POs only for order/reserve/issue/total_pack
|
| 1079 |
+
- Fresh + Reprocess for pack_fresh
|
| 1080 |
+
- Attribute conflicts resolved by weighted mode
|
| 1081 |
+
|
| 1082 |
+
### Reprocess Workflow (Separate)
|
| 1083 |
+
|
| 1084 |
+
- Dataset: `cleaned_greige_reprocess_v4.csv`
|
| 1085 |
+
- 1:1 mapping (no buffer applied)
|
| 1086 |
+
- Not mixed with primary norms learning
|
| 1087 |
+
|
| 1088 |
+
### Norms Formula
|
| 1089 |
+
|
| 1090 |
+
- Division factor for percentage rules
|
| 1091 |
+
- Additive for fixed-meter rules
|
| 1092 |
+
|
| 1093 |
+
### Tolerance Policy
|
| 1094 |
+
|
| 1095 |
+
- Field missing from source data
|
| 1096 |
+
- Using inferred policy or default +/-3%
|
| 1097 |
+
|
| 1098 |
+
### Output Datasets (v4)
|
| 1099 |
+
|
| 1100 |
+
| Dataset | Description |
|
| 1101 |
+
|---------|-------------|
|
| 1102 |
+
| `cleaned_greige_order_level_v4.csv` | Order-level aggregated dataset, ML-ready |
|
| 1103 |
+
| `cleaned_greige_order_level_full_v4.csv` | Full order-level with all derived fields |
|
| 1104 |
+
| `cleaned_greige_po_level_v4.csv` | PO-level dataset, retained for audit |
|
| 1105 |
+
| `cleaned_greige_reprocess_v4.csv` | Reprocess/Short-Fall workflow, 1:1 mapping |
|
| 1106 |
+
| `learned_norms_table_v2.csv` | Learned norms v2, order-level learning |
|
| 1107 |
+
|
| 1108 |
+
---
|
| 1109 |
+
|
| 1110 |
+
## Notes
|
| 1111 |
+
|
| 1112 |
+
- Reports generated after v4 aggregation fix use `cleaned_greige_order_level_v4.csv`
|
| 1113 |
+
- Check PO Type.csv for inclusion rules
|
| 1114 |
+
- DORQT1 is ground truth for order quantity
|
| 1115 |
+
- Fresh Input POs sum to equal DORQT1
|
| 1116 |
+
- Reprocess POs are additional fabric
|
| 1117 |
+
- Tolerance_limit field is missing from transaction data
|
| 1118 |
+
- Special Articles section not implemented in code
|
| 1119 |
+
- Feature contract prevents leakage of post-issuance data
|
| 1120 |
+
- API endpoint available for production recommendations
|
| 1121 |
+
- Guardrails ensure recommendations stay within bounds
|
Dockerfile
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM node:20-alpine AS frontend-builder
|
| 2 |
+
WORKDIR /app/frontend
|
| 3 |
+
COPY frontend/package*.json ./
|
| 4 |
+
RUN npm install
|
| 5 |
+
COPY frontend ./
|
| 6 |
+
RUN npm run build
|
| 7 |
+
|
| 8 |
+
FROM python:3.10-slim
|
| 9 |
+
|
| 10 |
+
# Install Node.js
|
| 11 |
+
RUN apt-get update && apt-get install -y \
|
| 12 |
+
curl \
|
| 13 |
+
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
| 14 |
+
&& apt-get install -y nodejs \
|
| 15 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 16 |
+
|
| 17 |
+
WORKDIR /app
|
| 18 |
+
|
| 19 |
+
# Setup Backend
|
| 20 |
+
COPY backend/requirements.txt backend/
|
| 21 |
+
RUN pip install --no-cache-dir -r backend/requirements.txt
|
| 22 |
+
COPY backend backend/
|
| 23 |
+
|
| 24 |
+
# Setup Frontend
|
| 25 |
+
COPY frontend/package*.json frontend/
|
| 26 |
+
WORKDIR /app/frontend
|
| 27 |
+
RUN npm install --omit=dev
|
| 28 |
+
COPY --from=frontend-builder /app/frontend/.next ./.next
|
| 29 |
+
COPY --from=frontend-builder /app/frontend/public ./public
|
| 30 |
+
# We need the source files for Next.js to run in production for App Router depending on setup, but mostly .next and node_modules are enough.
|
| 31 |
+
COPY frontend/next.config.mjs ./
|
| 32 |
+
|
| 33 |
+
WORKDIR /app
|
| 34 |
+
|
| 35 |
+
# Create a start script
|
| 36 |
+
COPY start.sh .
|
| 37 |
+
RUN chmod +x start.sh
|
| 38 |
+
|
| 39 |
+
# Expose the port Hugging Face expects
|
| 40 |
+
EXPOSE 7860
|
| 41 |
+
|
| 42 |
+
# Run both servers
|
| 43 |
+
CMD ["./start.sh"]
|
README.md
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Process Aware AI Dashboard
|
| 3 |
+
emoji: 📊
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
+
---
|
backend/app/data/norms.json
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"id": 1,
|
| 4 |
+
"division_factor": "Dyed",
|
| 5 |
+
"sub_type": "Peach",
|
| 6 |
+
"composition": "Cotton",
|
| 7 |
+
"count_range": "Below 40s",
|
| 8 |
+
"route": "Continuous",
|
| 9 |
+
"rules": {
|
| 10 |
+
"upto_3000m": "7% or 100m",
|
| 11 |
+
"above_3000m": "6% or 100m"
|
| 12 |
+
},
|
| 13 |
+
"tolerance_adjustments": {
|
| 14 |
+
"tolerance_3_percent": "1% Extra",
|
| 15 |
+
"tolerance_5_7_percent": "2% Extra",
|
| 16 |
+
"tolerance_10_percent": "5% Extra",
|
| 17 |
+
"tolerance_plus0_minus3_5": "-1% Less",
|
| 18 |
+
"tolerance_1_2_percent": "As per Std Norms"
|
| 19 |
+
}
|
| 20 |
+
},
|
| 21 |
+
{
|
| 22 |
+
"id": 2,
|
| 23 |
+
"division_factor": "Dyed",
|
| 24 |
+
"sub_type": "Peach",
|
| 25 |
+
"composition": "Cotton",
|
| 26 |
+
"count_range": "40s and above",
|
| 27 |
+
"route": "Continuous",
|
| 28 |
+
"rules": {
|
| 29 |
+
"upto_3000m": "5% or 100m",
|
| 30 |
+
"above_3000m": "4% or 100m"
|
| 31 |
+
},
|
| 32 |
+
"tolerance_adjustments": {
|
| 33 |
+
"tolerance_3_percent": "1% Extra",
|
| 34 |
+
"tolerance_5_7_percent": "2% Extra",
|
| 35 |
+
"tolerance_10_percent": "5% Extra",
|
| 36 |
+
"tolerance_plus0_minus3_5": "-1% Less",
|
| 37 |
+
"tolerance_1_2_percent": "As per Std Norms"
|
| 38 |
+
}
|
| 39 |
+
},
|
| 40 |
+
{
|
| 41 |
+
"id": 3,
|
| 42 |
+
"division_factor": "Dyed",
|
| 43 |
+
"sub_type": "Peach",
|
| 44 |
+
"composition": "PC/ PC stretch",
|
| 45 |
+
"count_range": "Below 40s",
|
| 46 |
+
"route": "Continuous",
|
| 47 |
+
"rules": {
|
| 48 |
+
"upto_3000m": "6% or 100m",
|
| 49 |
+
"above_3000m": "5% or 100m"
|
| 50 |
+
},
|
| 51 |
+
"tolerance_adjustments": {
|
| 52 |
+
"tolerance_3_percent": "1% Extra",
|
| 53 |
+
"tolerance_5_7_percent": "2% Extra",
|
| 54 |
+
"tolerance_10_percent": "5% Extra",
|
| 55 |
+
"tolerance_plus0_minus3_5": "-1% Less",
|
| 56 |
+
"tolerance_1_2_percent": "As per Std Norms"
|
| 57 |
+
}
|
| 58 |
+
},
|
| 59 |
+
{
|
| 60 |
+
"id": 4,
|
| 61 |
+
"division_factor": "Dyed",
|
| 62 |
+
"sub_type": "Peach",
|
| 63 |
+
"composition": "PC/ PC stretch",
|
| 64 |
+
"count_range": "40s and above",
|
| 65 |
+
"route": "Continuous",
|
| 66 |
+
"rules": {
|
| 67 |
+
"upto_3000m": "5% or 100m",
|
| 68 |
+
"above_3000m": "4% or 100m"
|
| 69 |
+
},
|
| 70 |
+
"tolerance_adjustments": {
|
| 71 |
+
"tolerance_3_percent": "1% Extra",
|
| 72 |
+
"tolerance_5_7_percent": "2% Extra",
|
| 73 |
+
"tolerance_10_percent": "5% Extra",
|
| 74 |
+
"tolerance_plus0_minus3_5": "-1% Less",
|
| 75 |
+
"tolerance_1_2_percent": "As per Std Norms"
|
| 76 |
+
}
|
| 77 |
+
},
|
| 78 |
+
{
|
| 79 |
+
"id": 5,
|
| 80 |
+
"division_factor": "Dyed",
|
| 81 |
+
"sub_type": "Peach",
|
| 82 |
+
"composition": "Stretch",
|
| 83 |
+
"count_range": "Below 40s",
|
| 84 |
+
"route": "Continuous",
|
| 85 |
+
"rules": {
|
| 86 |
+
"upto_3000m": "7% or 125m",
|
| 87 |
+
"above_3000m": "5% or 125m"
|
| 88 |
+
},
|
| 89 |
+
"tolerance_adjustments": {
|
| 90 |
+
"tolerance_3_percent": "1% Extra",
|
| 91 |
+
"tolerance_5_7_percent": "2% Extra",
|
| 92 |
+
"tolerance_10_percent": "5% Extra",
|
| 93 |
+
"tolerance_plus0_minus3_5": "-1% Less",
|
| 94 |
+
"tolerance_1_2_percent": "As per Std Norms"
|
| 95 |
+
}
|
| 96 |
+
},
|
| 97 |
+
{
|
| 98 |
+
"id": 6,
|
| 99 |
+
"division_factor": "Dyed",
|
| 100 |
+
"sub_type": "Peach",
|
| 101 |
+
"composition": "Stretch",
|
| 102 |
+
"count_range": "40s and above",
|
| 103 |
+
"route": "Continuous",
|
| 104 |
+
"rules": {
|
| 105 |
+
"upto_3000m": "6% or 125m",
|
| 106 |
+
"above_3000m": "5% or 125m"
|
| 107 |
+
},
|
| 108 |
+
"tolerance_adjustments": {
|
| 109 |
+
"tolerance_3_percent": "1% Extra",
|
| 110 |
+
"tolerance_5_7_percent": "2% Extra",
|
| 111 |
+
"tolerance_10_percent": "5% Extra",
|
| 112 |
+
"tolerance_plus0_minus3_5": "-1% Less",
|
| 113 |
+
"tolerance_1_2_percent": "As per Std Norms"
|
| 114 |
+
}
|
| 115 |
+
},
|
| 116 |
+
{
|
| 117 |
+
"id": 7,
|
| 118 |
+
"division_factor": "Dyed",
|
| 119 |
+
"sub_type": "Normal",
|
| 120 |
+
"composition": "Cotton",
|
| 121 |
+
"count_range": "Below 40s",
|
| 122 |
+
"route": "Continuous",
|
| 123 |
+
"rules": {
|
| 124 |
+
"upto_3000m": "6% or 100m",
|
| 125 |
+
"above_3000m": "5% or 100m"
|
| 126 |
+
},
|
| 127 |
+
"tolerance_adjustments": {
|
| 128 |
+
"tolerance_3_percent": "1% Extra",
|
| 129 |
+
"tolerance_5_7_percent": "2% Extra",
|
| 130 |
+
"tolerance_10_percent": "5% Extra",
|
| 131 |
+
"tolerance_plus0_minus3_5": "-1% Less",
|
| 132 |
+
"tolerance_1_2_percent": "As per Std Norms"
|
| 133 |
+
}
|
| 134 |
+
},
|
| 135 |
+
{
|
| 136 |
+
"id": 8,
|
| 137 |
+
"division_factor": "Dyed",
|
| 138 |
+
"sub_type": "Normal",
|
| 139 |
+
"composition": "Cotton",
|
| 140 |
+
"count_range": "40s and above",
|
| 141 |
+
"route": "Continuous",
|
| 142 |
+
"rules": {
|
| 143 |
+
"upto_3000m": "4% or 100m",
|
| 144 |
+
"above_3000m": "3% or 100m"
|
| 145 |
+
},
|
| 146 |
+
"tolerance_adjustments": {
|
| 147 |
+
"tolerance_3_percent": "1% Extra",
|
| 148 |
+
"tolerance_5_7_percent": "2% Extra",
|
| 149 |
+
"tolerance_10_percent": "5% Extra",
|
| 150 |
+
"tolerance_plus0_minus3_5": "-1% Less",
|
| 151 |
+
"tolerance_1_2_percent": "As per Std Norms"
|
| 152 |
+
}
|
| 153 |
+
},
|
| 154 |
+
{
|
| 155 |
+
"id": 9,
|
| 156 |
+
"division_factor": "Dyed",
|
| 157 |
+
"sub_type": "Normal",
|
| 158 |
+
"composition": "PC/PC stretch",
|
| 159 |
+
"count_range": "Below 40s",
|
| 160 |
+
"route": "Continuous",
|
| 161 |
+
"rules": {
|
| 162 |
+
"upto_3000m": "6% or 100m",
|
| 163 |
+
"above_3000m": "5% or 100m"
|
| 164 |
+
},
|
| 165 |
+
"tolerance_adjustments": {
|
| 166 |
+
"tolerance_3_percent": "1% Extra",
|
| 167 |
+
"tolerance_5_7_percent": "2% Extra",
|
| 168 |
+
"tolerance_10_percent": "5% Extra",
|
| 169 |
+
"tolerance_plus0_minus3_5": "-1% Less",
|
| 170 |
+
"tolerance_1_2_percent": "As per Std Norms"
|
| 171 |
+
}
|
| 172 |
+
},
|
| 173 |
+
{
|
| 174 |
+
"id": 10,
|
| 175 |
+
"division_factor": "Dyed",
|
| 176 |
+
"sub_type": "Normal",
|
| 177 |
+
"composition": "PC/PC stretch",
|
| 178 |
+
"count_range": "40s and above",
|
| 179 |
+
"route": "Continuous",
|
| 180 |
+
"rules": {
|
| 181 |
+
"upto_3000m": "5% or 100m",
|
| 182 |
+
"above_3000m": "4% or 100m"
|
| 183 |
+
},
|
| 184 |
+
"tolerance_adjustments": {
|
| 185 |
+
"tolerance_3_percent": "1% Extra",
|
| 186 |
+
"tolerance_5_7_percent": "2% Extra",
|
| 187 |
+
"tolerance_10_percent": "5% Extra",
|
| 188 |
+
"tolerance_plus0_minus3_5": "-1% Less",
|
| 189 |
+
"tolerance_1_2_percent": "As per Std Norms"
|
| 190 |
+
}
|
| 191 |
+
},
|
| 192 |
+
{
|
| 193 |
+
"id": 11,
|
| 194 |
+
"division_factor": "Dyed",
|
| 195 |
+
"sub_type": "Normal",
|
| 196 |
+
"composition": "Bi-stretch Noram",
|
| 197 |
+
"count_range": "Below 40s",
|
| 198 |
+
"route": "Continuous",
|
| 199 |
+
"rules": {
|
| 200 |
+
"upto_3000m": "40% or 1000m",
|
| 201 |
+
"above_3000m": "35% or 300m"
|
| 202 |
+
},
|
| 203 |
+
"tolerance_adjustments": {
|
| 204 |
+
"tolerance_3_percent": "1% Extra",
|
| 205 |
+
"tolerance_5_7_percent": "2% Extra",
|
| 206 |
+
"tolerance_10_percent": "5% Extra",
|
| 207 |
+
"tolerance_plus0_minus3_5": "-1% Less",
|
| 208 |
+
"tolerance_1_2_percent": "As per Std Norms"
|
| 209 |
+
}
|
| 210 |
+
},
|
| 211 |
+
{
|
| 212 |
+
"id": 12,
|
| 213 |
+
"division_factor": "Dyed",
|
| 214 |
+
"sub_type": "Normal",
|
| 215 |
+
"composition": "Bi-stretch PC/ Nylon",
|
| 216 |
+
"count_range": "All",
|
| 217 |
+
"route": "Continuous",
|
| 218 |
+
"rules": {
|
| 219 |
+
"upto_3000m": "25% or 600m",
|
| 220 |
+
"above_3000m": "20% or 600m"
|
| 221 |
+
},
|
| 222 |
+
"tolerance_adjustments": {
|
| 223 |
+
"tolerance_3_percent": "1% Extra",
|
| 224 |
+
"tolerance_5_7_percent": "2% Extra",
|
| 225 |
+
"tolerance_10_percent": "5% Extra",
|
| 226 |
+
"tolerance_plus0_minus3_5": "-1% Less",
|
| 227 |
+
"tolerance_1_2_percent": "As per Std Norms"
|
| 228 |
+
}
|
| 229 |
+
},
|
| 230 |
+
{
|
| 231 |
+
"id": 13,
|
| 232 |
+
"division_factor": "RFD",
|
| 233 |
+
"sub_type": "Peach/ Soft",
|
| 234 |
+
"composition": "Cotton",
|
| 235 |
+
"count_range": "Below 40s",
|
| 236 |
+
"route": "Continuous",
|
| 237 |
+
"rules": {
|
| 238 |
+
"upto_3000m": "5% or 100m",
|
| 239 |
+
"above_3000m": "4% or 100m"
|
| 240 |
+
},
|
| 241 |
+
"tolerance_adjustments": {
|
| 242 |
+
"tolerance_3_percent": "1% Extra",
|
| 243 |
+
"tolerance_5_7_percent": "2% Extra",
|
| 244 |
+
"tolerance_10_percent": "5% Extra",
|
| 245 |
+
"tolerance_plus0_minus3_5": "-1% Less",
|
| 246 |
+
"tolerance_1_2_percent": "As per Std Norms"
|
| 247 |
+
}
|
| 248 |
+
},
|
| 249 |
+
{
|
| 250 |
+
"id": 14,
|
| 251 |
+
"division_factor": "RFD",
|
| 252 |
+
"sub_type": "Peach/ Soft",
|
| 253 |
+
"composition": "Cotton",
|
| 254 |
+
"count_range": "40s and above",
|
| 255 |
+
"route": "Continuous",
|
| 256 |
+
"rules": {
|
| 257 |
+
"upto_3000m": "3% or 100m",
|
| 258 |
+
"above_3000m": "2% or 100m"
|
| 259 |
+
},
|
| 260 |
+
"tolerance_adjustments": {
|
| 261 |
+
"tolerance_3_percent": "1% Extra",
|
| 262 |
+
"tolerance_5_7_percent": "2% Extra",
|
| 263 |
+
"tolerance_10_percent": "5% Extra",
|
| 264 |
+
"tolerance_plus0_minus3_5": "-1% Less",
|
| 265 |
+
"tolerance_1_2_percent": "As per Std Norms"
|
| 266 |
+
}
|
| 267 |
+
},
|
| 268 |
+
{
|
| 269 |
+
"id": 15,
|
| 270 |
+
"division_factor": "FB",
|
| 271 |
+
"sub_type": "Peach/ Soft",
|
| 272 |
+
"composition": "Cotton",
|
| 273 |
+
"count_range": "Below 40s",
|
| 274 |
+
"route": "Continuous",
|
| 275 |
+
"rules": {
|
| 276 |
+
"upto_3000m": "5% or 100m",
|
| 277 |
+
"above_3000m": "4% or 100m"
|
| 278 |
+
},
|
| 279 |
+
"tolerance_adjustments": {
|
| 280 |
+
"tolerance_3_percent": "1% Extra",
|
| 281 |
+
"tolerance_5_7_percent": "2% Extra",
|
| 282 |
+
"tolerance_10_percent": "5% Extra",
|
| 283 |
+
"tolerance_plus0_minus3_5": "-1% Less",
|
| 284 |
+
"tolerance_1_2_percent": "As per Std Norms"
|
| 285 |
+
}
|
| 286 |
+
},
|
| 287 |
+
{
|
| 288 |
+
"id": 16,
|
| 289 |
+
"division_factor": "FB",
|
| 290 |
+
"sub_type": "Peach/ Soft",
|
| 291 |
+
"composition": "Cotton",
|
| 292 |
+
"count_range": "40s and above",
|
| 293 |
+
"route": "Continuous",
|
| 294 |
+
"rules": {
|
| 295 |
+
"upto_3000m": "3% or 100m",
|
| 296 |
+
"above_3000m": "2% or 100m"
|
| 297 |
+
},
|
| 298 |
+
"tolerance_adjustments": {
|
| 299 |
+
"tolerance_3_percent": "1% Extra",
|
| 300 |
+
"tolerance_5_7_percent": "2% Extra",
|
| 301 |
+
"tolerance_10_percent": "5% Extra",
|
| 302 |
+
"tolerance_plus0_minus3_5": "-1% Less",
|
| 303 |
+
"tolerance_1_2_percent": "As per Std Norms"
|
| 304 |
+
}
|
| 305 |
+
},
|
| 306 |
+
{
|
| 307 |
+
"id": 17,
|
| 308 |
+
"division_factor": "Special",
|
| 309 |
+
"sub_type": "N/A",
|
| 310 |
+
"composition": "100% Modal(Non Print)",
|
| 311 |
+
"count_range": "All",
|
| 312 |
+
"route": "Jet Route",
|
| 313 |
+
"rules": {
|
| 314 |
+
"upto_3000m": "12% or 300m",
|
| 315 |
+
"above_3000m": "10% or 300m"
|
| 316 |
+
},
|
| 317 |
+
"tolerance_adjustments": {
|
| 318 |
+
"tolerance_3_percent": "1% Extra",
|
| 319 |
+
"tolerance_5_7_percent": "2% Extra",
|
| 320 |
+
"tolerance_10_percent": "5% Extra",
|
| 321 |
+
"tolerance_plus0_minus3_5": "-1% Less",
|
| 322 |
+
"tolerance_1_2_percent": "As per Std Norms"
|
| 323 |
+
}
|
| 324 |
+
},
|
| 325 |
+
{
|
| 326 |
+
"id": 18,
|
| 327 |
+
"division_factor": "Special",
|
| 328 |
+
"sub_type": "N/A",
|
| 329 |
+
"composition": "100% Viscose(Non Print)",
|
| 330 |
+
"count_range": "All",
|
| 331 |
+
"route": "Jet Route",
|
| 332 |
+
"rules": {
|
| 333 |
+
"upto_3000m": "18% or 400m",
|
| 334 |
+
"above_3000m": "12% or 400m"
|
| 335 |
+
},
|
| 336 |
+
"tolerance_adjustments": {
|
| 337 |
+
"tolerance_3_percent": "1% Extra",
|
| 338 |
+
"tolerance_5_7_percent": "2% Extra",
|
| 339 |
+
"tolerance_10_percent": "5% Extra",
|
| 340 |
+
"tolerance_plus0_minus3_5": "-1% Less",
|
| 341 |
+
"tolerance_1_2_percent": "As per Std Norms"
|
| 342 |
+
}
|
| 343 |
+
}
|
| 344 |
+
]
|
backend/app/main.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI, HTTPException
|
| 2 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 3 |
+
from app.services.data_service import data_service
|
| 4 |
+
|
| 5 |
+
app = FastAPI(title="Process Aware AI Backend")
|
| 6 |
+
|
| 7 |
+
# Allow CORS for Next.js
|
| 8 |
+
app.add_middleware(
|
| 9 |
+
CORSMiddleware,
|
| 10 |
+
allow_origins=["http://localhost:3000", "http://localhost:8000"],
|
| 11 |
+
allow_credentials=True,
|
| 12 |
+
allow_methods=["*"],
|
| 13 |
+
allow_headers=["*"],
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
@app.on_event("startup")
|
| 17 |
+
async def startup_event():
|
| 18 |
+
# Preload data on startup
|
| 19 |
+
try:
|
| 20 |
+
data_service.load_data()
|
| 21 |
+
except Exception as e:
|
| 22 |
+
print(f"Failed to load data on startup: {e}")
|
| 23 |
+
|
| 24 |
+
@app.get("/")
|
| 25 |
+
def read_root():
|
| 26 |
+
return {"message": "Process Aware AI API is running"}
|
| 27 |
+
|
| 28 |
+
@app.get("/api/dashboard")
|
| 29 |
+
def get_dashboard():
|
| 30 |
+
return data_service.get_dashboard_summary()
|
| 31 |
+
|
| 32 |
+
@app.get("/api/article/{article_id}")
|
| 33 |
+
def get_article(article_id: str):
|
| 34 |
+
return data_service.get_article_insights(article_id)
|
| 35 |
+
|
| 36 |
+
@app.get("/api/scatter")
|
| 37 |
+
def get_scatter():
|
| 38 |
+
return data_service.get_scatter_data()
|
| 39 |
+
|
| 40 |
+
@app.get("/api/definitions")
|
| 41 |
+
def get_definitions():
|
| 42 |
+
return data_service.get_definitions()
|
| 43 |
+
|
| 44 |
+
@app.get("/api/analytics/finish-complexity")
|
| 45 |
+
def get_finish_complexity():
|
| 46 |
+
return data_service.get_finish_complexity()
|
| 47 |
+
|
| 48 |
+
@app.get("/api/analytics/global")
|
| 49 |
+
def get_global_analytics():
|
| 50 |
+
return data_service.get_enhanced_analytics()
|
| 51 |
+
|
| 52 |
+
@app.get("/api/analytics/route-performance")
|
| 53 |
+
def get_route_performance():
|
| 54 |
+
return data_service.get_route_performance()
|
| 55 |
+
|
| 56 |
+
@app.get("/api/data/full")
|
| 57 |
+
def get_full_data(limit: int = 100):
|
| 58 |
+
return data_service.get_full_data(limit)
|
| 59 |
+
|
| 60 |
+
@app.get("/api/order/{order_id}")
|
| 61 |
+
def get_order(order_id: str):
|
| 62 |
+
res = data_service.get_sale_order_details(order_id)
|
| 63 |
+
if "error" in res:
|
| 64 |
+
return {"error": res["error"]}
|
| 65 |
+
return res
|
| 66 |
+
|
| 67 |
+
@app.post("/api/simulate")
|
| 68 |
+
def simulate_simulation(payload: dict):
|
| 69 |
+
# Payload: { "tolerance": 5.0 }
|
| 70 |
+
tolerance = payload.get("tolerance", 0)
|
| 71 |
+
return data_service.simulate_impact(float(tolerance))
|
| 72 |
+
|
| 73 |
+
@app.get("/api/reference/po-types")
|
| 74 |
+
def get_po_types():
|
| 75 |
+
"""Get all PO types with their flags"""
|
| 76 |
+
return data_service.get_po_types()
|
| 77 |
+
|
| 78 |
+
@app.get("/api/reference/finish-descriptions")
|
| 79 |
+
def get_finish_descriptions():
|
| 80 |
+
"""Get all finish descriptions"""
|
| 81 |
+
return data_service.get_finish_descriptions()
|
| 82 |
+
|
| 83 |
+
@app.get("/api/reference/shade-categories")
|
| 84 |
+
def get_shade_categories():
|
| 85 |
+
"""Get all shade categories"""
|
| 86 |
+
return data_service.get_shade_categories()
|
| 87 |
+
|
| 88 |
+
@app.get("/api/reference/norms")
|
| 89 |
+
def get_norms():
|
| 90 |
+
"""Get greige issuance norms"""
|
| 91 |
+
return data_service.get_norms()
|
| 92 |
+
|
| 93 |
+
@app.get("/api/analytics/trends")
|
| 94 |
+
def get_global_trends():
|
| 95 |
+
"""Get comprehensive trend analytics for all entity types"""
|
| 96 |
+
return data_service.get_global_trends()
|
| 97 |
+
|
| 98 |
+
@app.get("/api/predictions/article/{article_id}")
|
| 99 |
+
def get_article_predictions(article_id: str):
|
| 100 |
+
"""Get prediction insights for a specific article"""
|
| 101 |
+
result = data_service.get_article_predictions(article_id)
|
| 102 |
+
if not result:
|
| 103 |
+
raise HTTPException(status_code=404, detail="Article not found")
|
| 104 |
+
return result
|
backend/app/services/data_service.py
ADDED
|
@@ -0,0 +1,2204 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pandas as pd
|
| 2 |
+
import os
|
| 3 |
+
import json
|
| 4 |
+
|
| 5 |
+
_DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "data")
|
| 6 |
+
NORMS_PATH = os.path.join(_DATA_DIR, "AT1 MKT PD Gr Norms Rev on 13-12-2025.xlsx")
|
| 7 |
+
DATA_PATH = os.path.join(_DATA_DIR, "Final Base Data for PD Gr issue Norsm 15-01-26.xlsx")
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class DataService:
|
| 11 |
+
def __init__(self):
|
| 12 |
+
self.master_df = None
|
| 13 |
+
self.detail_df = None
|
| 14 |
+
self.finish_df = None
|
| 15 |
+
self.shade_df = None
|
| 16 |
+
self.po_type_df = None
|
| 17 |
+
self.norms_data = [] # Load norms from JSON
|
| 18 |
+
self.is_loaded = False
|
| 19 |
+
|
| 20 |
+
# Metadata Dictionary for Tooltips
|
| 21 |
+
self.column_definitions = {
|
| 22 |
+
"Article": "The 'DNA' of the fabric. It uniquely identifies the construction, combining Count, Product Type, and core attributes.",
|
| 23 |
+
"Order Qty": "The total length of fabric requested by the customer in this Sale Order.",
|
| 24 |
+
"PO Qty": "The quantity planned for a specific Production Order (one Sale Order can be split into multiple POs).",
|
| 25 |
+
"Reserver Qty as per Std Norms": "The theoretical amount of Greige fabric required based on the static 'Standard Norms' (Rule Engine). Think of this as the 'Base Tax'.",
|
| 26 |
+
"Actual Gr Opening": "The ACTUAL amount of Greige fabric issued by the planner. If this is higher than Reserved, the planner manually added a buffer (Risk Adjustment).",
|
| 27 |
+
"Deviation": "The gap between Actual Issued and Standard Reserved. Positive means the planner added extra; Negative means they under-issued.",
|
| 28 |
+
"Finish": "The chemical process code. Determines the 'recipe' of chemicals (e.g., Teflon, Resin) applied to the fabric.",
|
| 29 |
+
"Shade Code": "Defines the color family (Light, Dark, Extra Dark). Darker shades often require more processing time and shrinkage.",
|
| 30 |
+
"Route": "The machine path (Continuous, Jet, Jigger). Different machines have different waste profiles.",
|
| 31 |
+
"DORQT1": "System internal order quantity reference.",
|
| 32 |
+
"Shortfall": "An event where Actual Output < Order Quantity, requiring a new 'Short Fall PO' (F0S) to make up the difference.",
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
def load_data(self):
|
| 36 |
+
print("Loading comprehensive data...")
|
| 37 |
+
# Initial check moved to specific section to allow partial loading
|
| 38 |
+
|
| 39 |
+
# Load Norms Data
|
| 40 |
+
try:
|
| 41 |
+
# Robust path resolution
|
| 42 |
+
# If running from backend root (main.py), path is app/data/norms.json
|
| 43 |
+
backend_root = os.getcwd() # Should be .../backend
|
| 44 |
+
norms_path = os.path.join(backend_root, "app/data/norms.json")
|
| 45 |
+
|
| 46 |
+
if not os.path.exists(norms_path):
|
| 47 |
+
# Fallback to relative to this file
|
| 48 |
+
norms_path = os.path.join(
|
| 49 |
+
os.path.dirname(os.path.dirname(__file__)), "data/norms.json"
|
| 50 |
+
)
|
| 51 |
+
if os.path.exists(norms_path):
|
| 52 |
+
with open(norms_path, "r") as f:
|
| 53 |
+
self.norms_data = json.load(f)
|
| 54 |
+
print(f"Norms loaded: {len(self.norms_data)} rules")
|
| 55 |
+
else:
|
| 56 |
+
print(f"Warning: Norms data not found at {norms_path}")
|
| 57 |
+
except Exception as e:
|
| 58 |
+
print(f"Error loading norms: {e}")
|
| 59 |
+
|
| 60 |
+
# 1. Load Main Data (Detail) - using header=2 to skip metadata rows
|
| 61 |
+
try:
|
| 62 |
+
# 1. Load Main Data (Detail)
|
| 63 |
+
file_path = DATA_PATH
|
| 64 |
+
if not os.path.exists(file_path):
|
| 65 |
+
print(f"Warning: Primary data file not found at {file_path}")
|
| 66 |
+
# Fallback
|
| 67 |
+
file_path = os.path.join(_DATA_DIR, "Data Set.xlsx")
|
| 68 |
+
|
| 69 |
+
if not os.path.exists(file_path):
|
| 70 |
+
print(f"Error: No data file found at {file_path}")
|
| 71 |
+
self.is_loaded = True # Prevent infinite retry loop if file missing
|
| 72 |
+
return
|
| 73 |
+
|
| 74 |
+
print(f"Loading data from: {file_path}")
|
| 75 |
+
|
| 76 |
+
self.detail_df = pd.read_excel(file_path, sheet_name="Detail", header=2)
|
| 77 |
+
self.finish_df = pd.read_excel(file_path, sheet_name="Finish Description")
|
| 78 |
+
self.po_type_df = pd.read_excel(file_path, sheet_name="PO Type")
|
| 79 |
+
|
| 80 |
+
# 2. Process PO Type Logic
|
| 81 |
+
# Normalize column names
|
| 82 |
+
self.po_type_df.columns = [c.strip() for c in self.po_type_df.columns]
|
| 83 |
+
# Identify relevant columns (names might be slightly different so using index or flexible search)
|
| 84 |
+
# Based on inspection: Col 0: BO Type, Col 2: Total Pkg (Input), Col 3: Fresh Pkg (Output)
|
| 85 |
+
# Index 0 is 'PO Type', 2 is 'To be consider for Total Pkg of Fresh PO', 3 is 'To Be cosnsider for Fresh Pkg of Fresh PO'
|
| 86 |
+
self.po_type_df["is_input"] = (
|
| 87 |
+
self.po_type_df.iloc[:, 2]
|
| 88 |
+
.astype(str)
|
| 89 |
+
.str.upper()
|
| 90 |
+
.apply(lambda x: "YES" in x)
|
| 91 |
+
)
|
| 92 |
+
self.po_type_df["is_output"] = (
|
| 93 |
+
self.po_type_df.iloc[:, 3]
|
| 94 |
+
.astype(str)
|
| 95 |
+
.str.upper()
|
| 96 |
+
.apply(lambda x: "YES" in x)
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
po_logic_map = self.po_type_df.set_index(self.po_type_df.columns[0])[
|
| 100 |
+
["is_input", "is_output"]
|
| 101 |
+
].to_dict("index")
|
| 102 |
+
|
| 103 |
+
# 3. Join Logic to Detail with safer matching
|
| 104 |
+
# EXTRACT PO CODE FROM PO NUMBER (First 3 chars)
|
| 105 |
+
# Example: F0U0000866 -> F0U
|
| 106 |
+
self.detail_df["PO_CODE"] = self.detail_df["PO_NO"].astype(str).str[:3]
|
| 107 |
+
|
| 108 |
+
# Helper to safely get value
|
| 109 |
+
def get_po_flag(x, flag_name):
|
| 110 |
+
return po_logic_map.get(x, {}).get(flag_name, False)
|
| 111 |
+
|
| 112 |
+
self.detail_df["is_input"] = self.detail_df["PO_CODE"].map(
|
| 113 |
+
lambda x: get_po_flag(x, "is_input")
|
| 114 |
+
)
|
| 115 |
+
self.detail_df["is_output"] = self.detail_df["PO_CODE"].map(
|
| 116 |
+
lambda x: get_po_flag(x, "is_output")
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
# 4. Standard Cleaning
|
| 120 |
+
self.detail_df["Finish"] = self.detail_df["Finish (Peach/Soft)"].astype(str)
|
| 121 |
+
self.master_df = self.detail_df # Use directly now, simpler
|
| 122 |
+
|
| 123 |
+
# Numeric Conversion
|
| 124 |
+
cols = ["DORQT1", "RES_QTY", "ISS_QTY", "pack_fresh", "Actual Gr Opening"]
|
| 125 |
+
for c in cols:
|
| 126 |
+
if c in self.master_df.columns:
|
| 127 |
+
self.master_df[c] = pd.to_numeric(
|
| 128 |
+
self.master_df[c], errors="coerce"
|
| 129 |
+
).fillna(0)
|
| 130 |
+
|
| 131 |
+
# Map column names for frontend compatibility (Previous logic)
|
| 132 |
+
self.master_df["Order Qty"] = self.master_df["DORQT1"]
|
| 133 |
+
self.master_df["Reserver Qty as per Std Norms"] = self.master_df["RES_QTY"]
|
| 134 |
+
self.master_df["Actual Gr Opening"] = self.master_df["ISS_QTY"]
|
| 135 |
+
self.master_df["Deviation"] = (
|
| 136 |
+
self.master_df["ISS_QTY"] - self.master_df["RES_QTY"]
|
| 137 |
+
)
|
| 138 |
+
self.master_df["Deviation_Percent"] = (
|
| 139 |
+
self.master_df["Deviation"] / self.master_df["RES_QTY"].replace(0, 1)
|
| 140 |
+
) * 100
|
| 141 |
+
|
| 142 |
+
# Map Article ID
|
| 143 |
+
if "grey_k1_from_DBPD" in self.master_df.columns:
|
| 144 |
+
self.master_df["Article"] = self.master_df["grey_k1_from_DBPD"]
|
| 145 |
+
else:
|
| 146 |
+
self.master_df["Article"] = "Unknown"
|
| 147 |
+
|
| 148 |
+
# Sale Order ID (Best Guess: COPS_NO)
|
| 149 |
+
if "COPS_NO" in self.master_df.columns:
|
| 150 |
+
self.master_df["Sale Order"] = self.master_df["COPS_NO"]
|
| 151 |
+
else:
|
| 152 |
+
self.master_df["Sale Order"] = "Unknown"
|
| 153 |
+
|
| 154 |
+
print(f"Data Loaded. Rows: {len(self.master_df)}")
|
| 155 |
+
self.is_loaded = True
|
| 156 |
+
|
| 157 |
+
except Exception as e:
|
| 158 |
+
print(f"Error loading data: {e}")
|
| 159 |
+
# Fallback mock data if needed or re-raise
|
| 160 |
+
raise e
|
| 161 |
+
|
| 162 |
+
def get_definitions(self):
|
| 163 |
+
return self.column_definitions
|
| 164 |
+
|
| 165 |
+
def get_dashboard_summary(self):
|
| 166 |
+
if not self.is_loaded:
|
| 167 |
+
self.load_data()
|
| 168 |
+
|
| 169 |
+
total_orders = len(self.master_df)
|
| 170 |
+
total_qty = self.master_df["Order Qty"].sum()
|
| 171 |
+
avg_deviation = self.master_df["Deviation_Percent"].mean()
|
| 172 |
+
|
| 173 |
+
# Replace NaNs for JSON
|
| 174 |
+
sample = (
|
| 175 |
+
self.master_df[
|
| 176 |
+
[
|
| 177 |
+
"Article",
|
| 178 |
+
"Finish",
|
| 179 |
+
"Order Qty",
|
| 180 |
+
"Reserver Qty as per Std Norms",
|
| 181 |
+
"Actual Gr Opening",
|
| 182 |
+
"Deviation",
|
| 183 |
+
]
|
| 184 |
+
]
|
| 185 |
+
.head(10)
|
| 186 |
+
.fillna(0)
|
| 187 |
+
.to_dict(orient="records")
|
| 188 |
+
)
|
| 189 |
+
|
| 190 |
+
return {
|
| 191 |
+
"total_orders": total_orders,
|
| 192 |
+
"total_qty_meters": total_qty,
|
| 193 |
+
"avg_deviation_percent": avg_deviation,
|
| 194 |
+
"sample_data": sample,
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
def get_finish_complexity(self):
|
| 198 |
+
# Only consider INPUT data for complexity (initial processing)
|
| 199 |
+
if not self.is_loaded:
|
| 200 |
+
self.load_data()
|
| 201 |
+
df = self.master_df[self.master_df["is_input"] == True]
|
| 202 |
+
stats = df.groupby("Finish")["Deviation_Percent"].mean().reset_index()
|
| 203 |
+
stats = stats.sort_values("Deviation_Percent", ascending=False).head(10)
|
| 204 |
+
return [
|
| 205 |
+
{"attribute": r["Finish"], "avg_deviation": r["Deviation_Percent"]}
|
| 206 |
+
for _, r in stats.iterrows()
|
| 207 |
+
]
|
| 208 |
+
|
| 209 |
+
def get_route_performance(self):
|
| 210 |
+
"""
|
| 211 |
+
Aggregates metrics by Route (Continuous, Jet, Jigger).
|
| 212 |
+
"""
|
| 213 |
+
if not self.is_loaded:
|
| 214 |
+
self.load_data()
|
| 215 |
+
|
| 216 |
+
if "Route" not in self.master_df.columns:
|
| 217 |
+
return []
|
| 218 |
+
|
| 219 |
+
df = self.master_df[self.master_df["is_input"] == True]
|
| 220 |
+
stats = (
|
| 221 |
+
df.groupby("Route")["Deviation_Percent"]
|
| 222 |
+
.agg(["mean", "count"])
|
| 223 |
+
.reset_index()
|
| 224 |
+
)
|
| 225 |
+
stats.rename(columns={"mean": "avg_deviation", "Route": "route"}, inplace=True)
|
| 226 |
+
return stats.fillna(0).to_dict(orient="records")
|
| 227 |
+
|
| 228 |
+
def get_article_insights(self, article_no: str):
|
| 229 |
+
if not self.is_loaded:
|
| 230 |
+
self.load_data()
|
| 231 |
+
|
| 232 |
+
df = self.master_df[
|
| 233 |
+
self.master_df["Article"].astype(str) == str(article_no)
|
| 234 |
+
].copy()
|
| 235 |
+
|
| 236 |
+
if df.empty:
|
| 237 |
+
return {"error": "No data found"}
|
| 238 |
+
|
| 239 |
+
# Article "DNA"
|
| 240 |
+
first_row = df.iloc[0]
|
| 241 |
+
dna = {
|
| 242 |
+
"Article": article_no,
|
| 243 |
+
"Count": first_row.get("Count", "N/A"),
|
| 244 |
+
"Product": first_row.get("Product", "N/A"),
|
| 245 |
+
"Standard_Route": first_row.get("Route", "N/A"),
|
| 246 |
+
"Base_Finish_Example": first_row.get("Finish", "N/A"),
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
return {
|
| 250 |
+
"dna": dna,
|
| 251 |
+
"count": len(df),
|
| 252 |
+
"data": df[
|
| 253 |
+
[
|
| 254 |
+
"PO_NO",
|
| 255 |
+
"Order Qty",
|
| 256 |
+
"Reserver Qty as per Std Norms",
|
| 257 |
+
"Actual Gr Opening",
|
| 258 |
+
"Deviation",
|
| 259 |
+
"Finish",
|
| 260 |
+
"Route",
|
| 261 |
+
]
|
| 262 |
+
]
|
| 263 |
+
.fillna(0)
|
| 264 |
+
.to_dict(orient="records"),
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
def get_full_data(self, limit: int = 100):
|
| 268 |
+
if not self.is_loaded:
|
| 269 |
+
self.load_data()
|
| 270 |
+
|
| 271 |
+
# Show all rows, but include flags
|
| 272 |
+
return self.master_df.head(limit).fillna("").to_dict(orient="records")
|
| 273 |
+
|
| 274 |
+
def get_scatter_data(self):
|
| 275 |
+
"""
|
| 276 |
+
Returns data for Failure Heatmap (Order Qty vs Shrinkage/Deviation)
|
| 277 |
+
"""
|
| 278 |
+
if not self.is_loaded:
|
| 279 |
+
self.load_data()
|
| 280 |
+
|
| 281 |
+
# Deviation = Actual - Reserved
|
| 282 |
+
# We assume 'Actual Gr Opening' is Issued, and 'Reserver Qty as per Std Norms' is Reserved.
|
| 283 |
+
# Filter where Order Qty > 0 to avoid zero division/noise.
|
| 284 |
+
df = self.master_df[self.master_df["Order Qty"] > 0].copy()
|
| 285 |
+
|
| 286 |
+
df["Deviation"] = df["Actual Gr Opening"] - df["Reserver Qty as per Std Norms"]
|
| 287 |
+
df["Deviation_Percent"] = (df["Deviation"] / df["Order Qty"]) * 100
|
| 288 |
+
|
| 289 |
+
# Limit distinct points or aggregate? Scatter needs raw points. Keep it reasonable.
|
| 290 |
+
# Maybe top 2000 points.
|
| 291 |
+
df = df.head(2000)
|
| 292 |
+
|
| 293 |
+
return (
|
| 294 |
+
df[
|
| 295 |
+
[
|
| 296 |
+
"Order Qty",
|
| 297 |
+
"Deviation",
|
| 298 |
+
"Deviation_Percent",
|
| 299 |
+
"Article",
|
| 300 |
+
"Finish",
|
| 301 |
+
"PO_NO",
|
| 302 |
+
]
|
| 303 |
+
]
|
| 304 |
+
.fillna(0)
|
| 305 |
+
.to_dict(orient="records")
|
| 306 |
+
)
|
| 307 |
+
|
| 308 |
+
def get_article_dna(self, article_id: str):
|
| 309 |
+
# "Article Deep Dive" is actually "Sale Order Deep Dive" based on user context,
|
| 310 |
+
# but let's keep Article ID search if unique, otherwise search by COPS_NO?
|
| 311 |
+
# User said: "I enter that article number... connection of two variables"
|
| 312 |
+
# Article No is likely 'Article' or 'Count' + 'Product'.
|
| 313 |
+
# Let's search by COPS_NO (Sale Order) as specific request
|
| 314 |
+
pass
|
| 315 |
+
|
| 316 |
+
def get_sale_order_details(self, sale_order_id: str):
|
| 317 |
+
if not self.is_loaded:
|
| 318 |
+
self.load_data()
|
| 319 |
+
|
| 320 |
+
# Filter by Sale Order ID
|
| 321 |
+
df = self.master_df[self.master_df["Sale Order"] == sale_order_id].copy()
|
| 322 |
+
if df.empty:
|
| 323 |
+
return {"error": "Order not found"}
|
| 324 |
+
|
| 325 |
+
# ---------------------------------------------------------
|
| 326 |
+
# 1. PO Classification (Fresh vs Reprocess)
|
| 327 |
+
# ---------------------------------------------------------
|
| 328 |
+
if "PO_CODE" not in df.columns:
|
| 329 |
+
df["PO_CODE"] = df["PO_NO"].astype(str).str[:3]
|
| 330 |
+
|
| 331 |
+
df["is_fresh"] = df["PO_CODE"].str.startswith("F")
|
| 332 |
+
df["is_reprocess"] = df["PO_CODE"].str.startswith("R")
|
| 333 |
+
|
| 334 |
+
input_rows = df[df["is_input"] == True]
|
| 335 |
+
output_rows = df[df["is_output"] == True]
|
| 336 |
+
|
| 337 |
+
fresh_input_rows = input_rows[input_rows["is_fresh"] == True]
|
| 338 |
+
reprocess_input_rows = df[df["is_reprocess"] == True]
|
| 339 |
+
|
| 340 |
+
# ---------------------------------------------------------
|
| 341 |
+
# 2. Metric Calculations
|
| 342 |
+
# ---------------------------------------------------------
|
| 343 |
+
if "COPS_LINENO" in df.columns:
|
| 344 |
+
total_order_qty = df.groupby("COPS_LINENO")["DORQT1"].first().sum()
|
| 345 |
+
else:
|
| 346 |
+
total_order_qty = df["DORQT1"].drop_duplicates().sum()
|
| 347 |
+
|
| 348 |
+
# FRESH Metrics
|
| 349 |
+
fresh_issued_qty = fresh_input_rows["ISS_QTY"].sum()
|
| 350 |
+
fresh_reserved_qty = fresh_input_rows["RES_QTY"].sum()
|
| 351 |
+
total_pack_fresh = output_rows["pack_fresh"].sum()
|
| 352 |
+
total_packing = (
|
| 353 |
+
output_rows["pack_qty"].sum()
|
| 354 |
+
if "pack_qty" in output_rows.columns
|
| 355 |
+
else output_rows["Total Pkg"].sum()
|
| 356 |
+
)
|
| 357 |
+
|
| 358 |
+
# REPROCESS Metrics
|
| 359 |
+
reprocess_count = df["is_reprocess"].sum()
|
| 360 |
+
reprocess_issued_qty = reprocess_input_rows["ISS_QTY"].sum()
|
| 361 |
+
|
| 362 |
+
# Legacy/Total Metrics
|
| 363 |
+
total_po_qty = (
|
| 364 |
+
input_rows["ODISQT"].sum()
|
| 365 |
+
if "ODISQT" in input_rows.columns
|
| 366 |
+
else input_rows["DORQT1"].sum()
|
| 367 |
+
)
|
| 368 |
+
total_reserved = input_rows["RES_QTY"].sum()
|
| 369 |
+
total_issued = input_rows["ISS_QTY"].sum()
|
| 370 |
+
|
| 371 |
+
# Percentages
|
| 372 |
+
extra_gr_reserved_pct = (
|
| 373 |
+
((total_reserved - total_po_qty) / total_po_qty * 100)
|
| 374 |
+
if total_po_qty > 0
|
| 375 |
+
else 0
|
| 376 |
+
)
|
| 377 |
+
actual_gr_issue_pct = (
|
| 378 |
+
((total_issued - total_po_qty) / total_po_qty * 100)
|
| 379 |
+
if total_po_qty > 0
|
| 380 |
+
else 0
|
| 381 |
+
)
|
| 382 |
+
shrinkage_pct = (
|
| 383 |
+
((total_issued - total_packing) / total_issued * 100)
|
| 384 |
+
if total_issued > 0
|
| 385 |
+
else 0
|
| 386 |
+
)
|
| 387 |
+
fresh_pkg_pct = (
|
| 388 |
+
(total_pack_fresh / total_packing * 100) if total_packing > 0 else 0
|
| 389 |
+
)
|
| 390 |
+
fresh_to_order_pct = (
|
| 391 |
+
(total_pack_fresh / total_order_qty * 100) if total_order_qty > 0 else 0
|
| 392 |
+
)
|
| 393 |
+
|
| 394 |
+
# NEW: Fresh Yield (Efficiency of First Run)
|
| 395 |
+
fresh_yield_pct = (
|
| 396 |
+
(total_pack_fresh / fresh_issued_qty * 100) if fresh_issued_qty > 0 else 0
|
| 397 |
+
)
|
| 398 |
+
|
| 399 |
+
# NEW: Rejection Metrics
|
| 400 |
+
# Rejection Rate = (Reprocess / Total Issued)? Or (Total Issued - Pack Fresh) / Total Issued?
|
| 401 |
+
# User asked for "% of Rejection".
|
| 402 |
+
# If we use Process Loss (Shrinkage), that's one measure.
|
| 403 |
+
# If we use Reprocess Qty ratio, that's another.
|
| 404 |
+
# Let's provide Reprocess Rate.
|
| 405 |
+
reprocess_rate_pct = (
|
| 406 |
+
(reprocess_issued_qty / total_issued * 100) if total_issued > 0 else 0
|
| 407 |
+
)
|
| 408 |
+
|
| 409 |
+
shortfall = total_order_qty - total_pack_fresh
|
| 410 |
+
shortfall_status = "Shortfall" if shortfall > 0 else "Fulfilled"
|
| 411 |
+
|
| 412 |
+
# ---------------------------------------------------------
|
| 413 |
+
# 3. DNA & Metadata
|
| 414 |
+
# ---------------------------------------------------------
|
| 415 |
+
first_row = df.iloc[0]
|
| 416 |
+
dna = {
|
| 417 |
+
# Core Identification
|
| 418 |
+
"Article": str(first_row.get("Article", "")),
|
| 419 |
+
"Grey Code": str(
|
| 420 |
+
first_row.get("grey_k1_from_DBPD", str(first_row.get("grey_k1", "")))
|
| 421 |
+
),
|
| 422 |
+
"Grey Code DB": str(first_row.get("grey_k1_from_DBPD", "")),
|
| 423 |
+
# Product Details
|
| 424 |
+
"Count": str(first_row.get("Count", "")),
|
| 425 |
+
"Product": str(first_row.get("Product", "")),
|
| 426 |
+
"Route": str(first_row.get("Route", "")),
|
| 427 |
+
"Finish": str(
|
| 428 |
+
first_row.get("Finish", str(first_row.get("Finish (Peach/Soft)", "")))
|
| 429 |
+
),
|
| 430 |
+
"Shade Type": str(first_row.get("Shade Type", "")),
|
| 431 |
+
"Material Type": str(first_row.get("HCMTYP", ""))
|
| 432 |
+
if pd.notna(first_row.get("HCMTYP", ""))
|
| 433 |
+
else (
|
| 434 |
+
"Cotton"
|
| 435 |
+
if "COTTON" in str(first_row.get("Product", "")).upper()
|
| 436 |
+
else "Blend"
|
| 437 |
+
),
|
| 438 |
+
# Customer & Segment
|
| 439 |
+
"Customer": str(first_row.get("cust_desc", ""))
|
| 440 |
+
if pd.notna(first_row.get("cust_desc", ""))
|
| 441 |
+
else "N/A",
|
| 442 |
+
"Segment": str(first_row.get("segment_desc", ""))
|
| 443 |
+
if pd.notna(first_row.get("segment_desc", ""))
|
| 444 |
+
else "N/A",
|
| 445 |
+
"Sub-Segment": str(first_row.get("subsegment_name", ""))
|
| 446 |
+
if pd.notna(first_row.get("subsegment_name", ""))
|
| 447 |
+
else "N/A",
|
| 448 |
+
# Order Keys
|
| 449 |
+
"OCDKE1": str(first_row.get("OCDKE1", "")),
|
| 450 |
+
"OCDKE2": str(first_row.get("OCDKE2", "")),
|
| 451 |
+
"OCDKE3": str(first_row.get("OCDKE3", "")),
|
| 452 |
+
"OCDKE4": str(first_row.get("OCDKE4", "")),
|
| 453 |
+
# Dates
|
| 454 |
+
"Dispo Date": str(first_row.get("dispo_date", ""))[:10]
|
| 455 |
+
if pd.notna(first_row.get("dispo_date", ""))
|
| 456 |
+
else "N/A",
|
| 457 |
+
"Pack Date": str(first_row.get("pack_date", ""))[:10]
|
| 458 |
+
if pd.notna(first_row.get("pack_date", ""))
|
| 459 |
+
else "N/A",
|
| 460 |
+
# PO Details
|
| 461 |
+
"PO Series": str(first_row.get("PO_NO", ""))[:3] + "...",
|
| 462 |
+
"Total POs": int(df["PO_NO"].nunique()),
|
| 463 |
+
"Input POs": int(input_rows["PO_NO"].nunique()),
|
| 464 |
+
"Output POs": int(output_rows["PO_NO"].nunique()),
|
| 465 |
+
}
|
| 466 |
+
|
| 467 |
+
# ---------------------------------------------------------
|
| 468 |
+
# 4. Calculation Steps
|
| 469 |
+
# ---------------------------------------------------------
|
| 470 |
+
calculations = {
|
| 471 |
+
"extra_gr_reserved": {
|
| 472 |
+
"label": "Extra Gr %age Reserved",
|
| 473 |
+
"formula": "(Reserved - PO_Qty) / PO_Qty × 100",
|
| 474 |
+
"steps": [
|
| 475 |
+
f"= ({total_reserved:,.0f} - {total_po_qty:,.0f}) / {total_po_qty:,.0f} × 100",
|
| 476 |
+
f"= {total_reserved - total_po_qty:,.0f} / {total_po_qty:,.0f} × 100",
|
| 477 |
+
f"= {extra_gr_reserved_pct:.2f}%",
|
| 478 |
+
],
|
| 479 |
+
"value": round(extra_gr_reserved_pct, 2),
|
| 480 |
+
"interpretation": "Greige reserved above PO demand",
|
| 481 |
+
},
|
| 482 |
+
"actual_gr_issue": {
|
| 483 |
+
"label": "Actual Gr Issue %age",
|
| 484 |
+
"formula": "(Issued - PO_Qty) / PO_Qty × 100",
|
| 485 |
+
"steps": [
|
| 486 |
+
f"= ({total_issued:,.0f} - {total_po_qty:,.0f}) / {total_po_qty:,.0f} × 100",
|
| 487 |
+
f"= {actual_gr_issue_pct:.2f}%",
|
| 488 |
+
],
|
| 489 |
+
"value": round(actual_gr_issue_pct, 2),
|
| 490 |
+
"interpretation": "Total greige issued above PO demand (includes planner adjustment)",
|
| 491 |
+
},
|
| 492 |
+
"shrinkage": {
|
| 493 |
+
"label": "Shrinkage %age (Process Loss)",
|
| 494 |
+
"formula": "(Issued - Total Packing) / Issued × 100",
|
| 495 |
+
"steps": [
|
| 496 |
+
f"= ({total_issued:,.0f} - {total_packing:,.0f}) / {total_issued:,.0f} × 100",
|
| 497 |
+
f"= {shrinkage_pct:.2f}%",
|
| 498 |
+
],
|
| 499 |
+
"value": round(shrinkage_pct, 2),
|
| 500 |
+
"interpretation": "Material lost during processing",
|
| 501 |
+
},
|
| 502 |
+
"fresh_pkg": {
|
| 503 |
+
"label": "Fresh Pkg %age",
|
| 504 |
+
"formula": "Pack Fresh / Total Packing × 100",
|
| 505 |
+
"steps": [
|
| 506 |
+
f"= {total_pack_fresh:,.0f} / {total_packing:,.0f} × 100",
|
| 507 |
+
f"= {fresh_pkg_pct:.2f}%",
|
| 508 |
+
],
|
| 509 |
+
"value": round(fresh_pkg_pct, 2),
|
| 510 |
+
"interpretation": "Proportion of packing that is fresh (first-run quality)",
|
| 511 |
+
},
|
| 512 |
+
"fresh_to_order": {
|
| 513 |
+
"label": "Fresh Packing to Order Qty",
|
| 514 |
+
"formula": "Pack Fresh / Order Qty × 100",
|
| 515 |
+
"steps": [
|
| 516 |
+
f"= {total_pack_fresh:,.0f} / {total_order_qty:,.0f} × 100",
|
| 517 |
+
f"= {fresh_to_order_pct:.2f}%",
|
| 518 |
+
],
|
| 519 |
+
"value": round(fresh_to_order_pct, 2),
|
| 520 |
+
"interpretation": "How much of the original order was fulfilled by fresh production",
|
| 521 |
+
},
|
| 522 |
+
"fresh_yield": {
|
| 523 |
+
"label": "Fresh Process Yield",
|
| 524 |
+
"formula": "Pack Fresh / Fresh Issued × 100",
|
| 525 |
+
"steps": [
|
| 526 |
+
f"= {total_pack_fresh:,.0f} / {fresh_issued_qty:,.0f} × 100",
|
| 527 |
+
f"= {fresh_yield_pct:.2f}%",
|
| 528 |
+
],
|
| 529 |
+
"value": round(fresh_yield_pct, 2),
|
| 530 |
+
"interpretation": "Efficiency of the first run (Fresh Issue only)",
|
| 531 |
+
},
|
| 532 |
+
}
|
| 533 |
+
|
| 534 |
+
# ---------------------------------------------------------
|
| 535 |
+
# 5. Waterfall & Blame Analysis
|
| 536 |
+
# ---------------------------------------------------------
|
| 537 |
+
waterfall = [
|
| 538 |
+
{"label": "Demand", "value": float(total_order_qty), "type": "base"},
|
| 539 |
+
{
|
| 540 |
+
"label": "Policy Gap",
|
| 541 |
+
"value": float(total_reserved - total_order_qty),
|
| 542 |
+
"type": "variance",
|
| 543 |
+
"desc": "Norm Buffer",
|
| 544 |
+
},
|
| 545 |
+
{
|
| 546 |
+
"label": "Execution Adj",
|
| 547 |
+
"value": float(total_issued - total_reserved),
|
| 548 |
+
"type": "variance",
|
| 549 |
+
"desc": "Planner Adj",
|
| 550 |
+
},
|
| 551 |
+
{
|
| 552 |
+
"label": "Reprocess Loop",
|
| 553 |
+
"value": float(reprocess_issued_qty),
|
| 554 |
+
"type": "variance",
|
| 555 |
+
"desc": "Rework Added",
|
| 556 |
+
"is_negative": False,
|
| 557 |
+
},
|
| 558 |
+
{
|
| 559 |
+
"label": "Process Loss",
|
| 560 |
+
"value": float(total_pack_fresh - total_issued),
|
| 561 |
+
"type": "variance",
|
| 562 |
+
"desc": "Net Loss",
|
| 563 |
+
},
|
| 564 |
+
{"label": "Delivered", "value": float(total_pack_fresh), "type": "final"},
|
| 565 |
+
]
|
| 566 |
+
|
| 567 |
+
yield_rate = total_pack_fresh / total_issued if total_issued > 0 else 0
|
| 568 |
+
norm_adequacy = (
|
| 569 |
+
(total_pack_fresh / total_order_qty * 100) if total_order_qty > 0 else 0
|
| 570 |
+
)
|
| 571 |
+
|
| 572 |
+
policy_impact = total_reserved - total_order_qty
|
| 573 |
+
execution_impact = total_issued - total_reserved
|
| 574 |
+
process_impact = total_pack_fresh - total_issued
|
| 575 |
+
total_impact = abs(policy_impact) + abs(execution_impact) + abs(process_impact)
|
| 576 |
+
|
| 577 |
+
blame_breakdown = {
|
| 578 |
+
"policy_impact": float(policy_impact),
|
| 579 |
+
"execution_impact": float(execution_impact),
|
| 580 |
+
"process_impact": float(process_impact),
|
| 581 |
+
"policy_pct": round(abs(policy_impact) / total_impact * 100, 1)
|
| 582 |
+
if total_impact
|
| 583 |
+
else 0,
|
| 584 |
+
"execution_pct": round(abs(execution_impact) / total_impact * 100, 1)
|
| 585 |
+
if total_impact
|
| 586 |
+
else 0,
|
| 587 |
+
"process_pct": round(abs(process_impact) / total_impact * 100, 1)
|
| 588 |
+
if total_impact
|
| 589 |
+
else 0,
|
| 590 |
+
}
|
| 591 |
+
|
| 592 |
+
# ---------------------------------------------------------
|
| 593 |
+
# 6. PO Breakdown
|
| 594 |
+
# ---------------------------------------------------------
|
| 595 |
+
po_breakdown = []
|
| 596 |
+
for po_no, group in df.groupby("PO_NO"):
|
| 597 |
+
po_data = group.iloc[0]
|
| 598 |
+
po_breakdown.append(
|
| 599 |
+
{
|
| 600 |
+
"po_no": str(po_no),
|
| 601 |
+
"po_code": str(po_data.get("PO_CODE", "")),
|
| 602 |
+
"type": "Fresh"
|
| 603 |
+
if po_data["is_fresh"]
|
| 604 |
+
else "Reprocess"
|
| 605 |
+
if po_data["is_reprocess"]
|
| 606 |
+
else "Other",
|
| 607 |
+
"issued_qty": float(group["ISS_QTY"].sum()),
|
| 608 |
+
"pack_fresh": float(group["pack_fresh"].sum()),
|
| 609 |
+
"reserved_qty": float(group["RES_QTY"].sum()),
|
| 610 |
+
"line_no": str(po_data.get("COPS_LINENO", "-")),
|
| 611 |
+
}
|
| 612 |
+
)
|
| 613 |
+
po_breakdown.sort(key=lambda x: (x["type"] != "Fresh", x["po_no"]))
|
| 614 |
+
|
| 615 |
+
# ---------------------------------------------------------
|
| 616 |
+
# 7. Decision Intelligence Metrics
|
| 617 |
+
# ---------------------------------------------------------
|
| 618 |
+
|
| 619 |
+
# 7a. Elasticity (Yield Stability)
|
| 620 |
+
elasticity_value = fresh_yield_pct
|
| 621 |
+
if elasticity_value >= 90:
|
| 622 |
+
elasticity_class = "HIGH"
|
| 623 |
+
elif elasticity_value >= 80:
|
| 624 |
+
elasticity_class = "MEDIUM"
|
| 625 |
+
else:
|
| 626 |
+
elasticity_class = "LOW"
|
| 627 |
+
|
| 628 |
+
elasticity = {
|
| 629 |
+
"classification": elasticity_class,
|
| 630 |
+
"value": round(elasticity_value, 1),
|
| 631 |
+
}
|
| 632 |
+
|
| 633 |
+
# 7b. Intervention ROI
|
| 634 |
+
planner_adj = total_issued - total_reserved
|
| 635 |
+
if planner_adj > 0 and shortfall <= 0:
|
| 636 |
+
roi_status = "High (Saved Order)"
|
| 637 |
+
elif planner_adj > 0 and shortfall > 0:
|
| 638 |
+
roi_status = "Low (Insufficient)"
|
| 639 |
+
elif planner_adj < 0 and shortfall > 0:
|
| 640 |
+
roi_status = "Negative (Caused Shortfall)"
|
| 641 |
+
else:
|
| 642 |
+
roi_status = "Neutral"
|
| 643 |
+
|
| 644 |
+
# 7c. False Yield Warning
|
| 645 |
+
false_yield_warning = bool(fresh_yield_pct > 90 and shortfall > 0)
|
| 646 |
+
|
| 647 |
+
# 7d. Safety Recommendation
|
| 648 |
+
if fresh_yield_pct > 0:
|
| 649 |
+
required_issued = total_order_qty / (fresh_yield_pct / 100)
|
| 650 |
+
safety_rec_val = (
|
| 651 |
+
((required_issued - total_order_qty) / total_order_qty * 100)
|
| 652 |
+
if total_order_qty > 0
|
| 653 |
+
else 0
|
| 654 |
+
)
|
| 655 |
+
else:
|
| 656 |
+
safety_rec_val = 0
|
| 657 |
+
|
| 658 |
+
safety_recommendation = {
|
| 659 |
+
"value": round(safety_rec_val, 1),
|
| 660 |
+
"confidence_low": round(safety_rec_val * 0.9, 1),
|
| 661 |
+
"confidence_high": round(safety_rec_val * 1.1, 1),
|
| 662 |
+
}
|
| 663 |
+
|
| 664 |
+
# 7e. Breakeven Tolerance
|
| 665 |
+
breakeven_tolerance = (
|
| 666 |
+
round(((total_issued - total_order_qty) / total_issued * 100), 1)
|
| 667 |
+
if total_issued > 0
|
| 668 |
+
else 0
|
| 669 |
+
)
|
| 670 |
+
|
| 671 |
+
# 7f. PO Imbalance Detection
|
| 672 |
+
po_imbalance = {"detected": False, "stddev": 0, "details": []}
|
| 673 |
+
if len(input_rows) > 1:
|
| 674 |
+
po_gaps = []
|
| 675 |
+
for _, row in input_rows.iterrows():
|
| 676 |
+
demand = row["DORQT1"]
|
| 677 |
+
reserved = row["RES_QTY"]
|
| 678 |
+
if demand > 0:
|
| 679 |
+
gap_pct = ((reserved - demand) / demand) * 100
|
| 680 |
+
po_gaps.append(
|
| 681 |
+
{
|
| 682 |
+
"po_no": row["PO_NO"],
|
| 683 |
+
"demand": float(demand),
|
| 684 |
+
"reserved": float(reserved),
|
| 685 |
+
"gap_pct": round(gap_pct, 1),
|
| 686 |
+
}
|
| 687 |
+
)
|
| 688 |
+
|
| 689 |
+
if po_gaps:
|
| 690 |
+
import statistics
|
| 691 |
+
|
| 692 |
+
gap_values = [p["gap_pct"] for p in po_gaps]
|
| 693 |
+
if len(gap_values) > 1:
|
| 694 |
+
stddev = statistics.stdev(gap_values)
|
| 695 |
+
po_imbalance = {
|
| 696 |
+
"detected": bool(stddev > 5),
|
| 697 |
+
"stddev": round(stddev, 1),
|
| 698 |
+
"details": po_gaps,
|
| 699 |
+
}
|
| 700 |
+
|
| 701 |
+
# 7g. Minimum Charge Distortion Flag
|
| 702 |
+
min_charge_distortion = False
|
| 703 |
+
if len(input_rows) > 1:
|
| 704 |
+
demands = input_rows["DORQT1"].tolist()
|
| 705 |
+
if len(demands) > 1:
|
| 706 |
+
import statistics
|
| 707 |
+
|
| 708 |
+
mean_demand = statistics.mean(demands)
|
| 709 |
+
stddev_demand = statistics.stdev(demands) if len(demands) > 1 else 0
|
| 710 |
+
demand_cv = (
|
| 711 |
+
(stddev_demand / mean_demand * 100) if mean_demand > 0 else 0
|
| 712 |
+
)
|
| 713 |
+
gap_stddev = po_imbalance.get("stddev", 0)
|
| 714 |
+
min_charge_distortion = bool(demand_cv > 50 and gap_stddev > 3)
|
| 715 |
+
|
| 716 |
+
# 7h. Article Risk Fingerprint
|
| 717 |
+
# norm_reliability = how well norms predicted delivery (norm_adequacy scaled to 0-1)
|
| 718 |
+
norm_reliability = norm_adequacy / 100.0
|
| 719 |
+
|
| 720 |
+
reprocess_rows_out = df[(df["is_output"] == True) & (df["is_input"] == False)]
|
| 721 |
+
reprocess_output = (
|
| 722 |
+
reprocess_rows_out["pack_fresh"].sum()
|
| 723 |
+
if not reprocess_rows_out.empty
|
| 724 |
+
else 0
|
| 725 |
+
)
|
| 726 |
+
reprocess_dependence = (
|
| 727 |
+
(reprocess_output / total_pack_fresh * 100) if total_pack_fresh > 0 else 0
|
| 728 |
+
)
|
| 729 |
+
|
| 730 |
+
if norm_reliability >= 0.98:
|
| 731 |
+
risk_level = "LOW"
|
| 732 |
+
elif norm_reliability >= 0.95:
|
| 733 |
+
risk_level = "MEDIUM"
|
| 734 |
+
else:
|
| 735 |
+
risk_level = "HIGH"
|
| 736 |
+
|
| 737 |
+
risk_fingerprint = {
|
| 738 |
+
"norm_reliability": round(norm_reliability, 3),
|
| 739 |
+
"policy_sensitivity": elasticity_class,
|
| 740 |
+
"reprocessing_dependence": round(reprocess_dependence, 1),
|
| 741 |
+
"risk_level": risk_level,
|
| 742 |
+
}
|
| 743 |
+
|
| 744 |
+
# ---------------------------------------------------------
|
| 745 |
+
# 8. SINGLE RETURN
|
| 746 |
+
# ---------------------------------------------------------
|
| 747 |
+
return {
|
| 748 |
+
"sale_order": sale_order_id,
|
| 749 |
+
"dna": dna,
|
| 750 |
+
"metrics": {
|
| 751 |
+
"Order Qty": float(total_order_qty),
|
| 752 |
+
"PO Qty": float(total_po_qty),
|
| 753 |
+
"Reserved Qty": float(total_reserved),
|
| 754 |
+
"Actual Issued": float(total_issued),
|
| 755 |
+
"Total Packing": float(total_packing),
|
| 756 |
+
"Pack Fresh": float(total_pack_fresh),
|
| 757 |
+
"Shortfall": float(shortfall),
|
| 758 |
+
"Status": shortfall_status,
|
| 759 |
+
"Fresh Yield %": round(fresh_yield_pct, 2),
|
| 760 |
+
"Reprocess Count": int(reprocess_count),
|
| 761 |
+
"Reprocess Qty": float(reprocess_issued_qty),
|
| 762 |
+
"Rejection Rate %": round(reprocess_rate_pct, 2),
|
| 763 |
+
"Extra Gr Reserved %": round(extra_gr_reserved_pct, 2),
|
| 764 |
+
"Actual Gr Issue %": round(actual_gr_issue_pct, 2),
|
| 765 |
+
"Shrinkage %": round(shrinkage_pct, 2),
|
| 766 |
+
"Fresh Pkg %": round(fresh_pkg_pct, 2),
|
| 767 |
+
"Fresh to Order %": round(fresh_to_order_pct, 2),
|
| 768 |
+
},
|
| 769 |
+
"calculations": calculations,
|
| 770 |
+
"intelligence": {
|
| 771 |
+
"waterfall": waterfall,
|
| 772 |
+
"norm_adequacy": round(norm_adequacy, 1),
|
| 773 |
+
"intervention_roi": roi_status,
|
| 774 |
+
"break_even_tolerance": breakeven_tolerance,
|
| 775 |
+
"yield_rate": round(yield_rate * 100, 1),
|
| 776 |
+
"blame_breakdown": blame_breakdown,
|
| 777 |
+
"elasticity": elasticity,
|
| 778 |
+
"false_yield_warning": false_yield_warning,
|
| 779 |
+
"safety_recommendation": safety_recommendation,
|
| 780 |
+
"po_imbalance": po_imbalance,
|
| 781 |
+
"min_charge_distortion": min_charge_distortion,
|
| 782 |
+
"risk_fingerprint": risk_fingerprint,
|
| 783 |
+
},
|
| 784 |
+
"rows": df[
|
| 785 |
+
[
|
| 786 |
+
"PO_NO",
|
| 787 |
+
"PO Type",
|
| 788 |
+
"DORQT1",
|
| 789 |
+
"RES_QTY",
|
| 790 |
+
"ISS_QTY",
|
| 791 |
+
"pack_fresh",
|
| 792 |
+
"is_input",
|
| 793 |
+
"is_output",
|
| 794 |
+
]
|
| 795 |
+
]
|
| 796 |
+
.fillna(0)
|
| 797 |
+
.astype("object")
|
| 798 |
+
.to_dict(orient="records"),
|
| 799 |
+
"po_breakdown": po_breakdown,
|
| 800 |
+
}
|
| 801 |
+
|
| 802 |
+
def get_enhanced_analytics(self):
|
| 803 |
+
"""
|
| 804 |
+
Global analytics across all data.
|
| 805 |
+
"""
|
| 806 |
+
if not self.is_loaded:
|
| 807 |
+
self.load_data()
|
| 808 |
+
|
| 809 |
+
df = self.master_df[self.master_df["Order Qty"] > 0].copy()
|
| 810 |
+
|
| 811 |
+
# 1. Global KPIs
|
| 812 |
+
# 1. Global KPIs
|
| 813 |
+
total_orders = df["Sale Order"].nunique()
|
| 814 |
+
|
| 815 |
+
# Consistent Volume Calculation (Deduplicated)
|
| 816 |
+
if "COPS_LINENO" in df.columns:
|
| 817 |
+
total_volume = (
|
| 818 |
+
df.groupby(["Sale Order", "COPS_LINENO"])["Order Qty"].first().sum()
|
| 819 |
+
)
|
| 820 |
+
else:
|
| 821 |
+
# Fallback: assume one line per order or data pre-aggregated?
|
| 822 |
+
# Safest fallback for now:
|
| 823 |
+
total_volume = df.groupby("Sale Order")["Order Qty"].first().sum()
|
| 824 |
+
|
| 825 |
+
# Calculate Shortfall Risk Rate (Orders where Total Issued < Total Reserved)
|
| 826 |
+
# We need to aggregate at Sale Order level first to compare apples to apples
|
| 827 |
+
order_risk_df = df.groupby("Sale Order").agg(
|
| 828 |
+
{"Actual Gr Opening": "sum", "Reserver Qty as per Std Norms": "sum"}
|
| 829 |
+
)
|
| 830 |
+
risky_orders_count = order_risk_df[
|
| 831 |
+
order_risk_df["Actual Gr Opening"]
|
| 832 |
+
< order_risk_df["Reserver Qty as per Std Norms"]
|
| 833 |
+
].shape[0]
|
| 834 |
+
shortfall_risk_rate = (
|
| 835 |
+
(risky_orders_count / total_orders * 100) if total_orders > 0 else 0
|
| 836 |
+
)
|
| 837 |
+
|
| 838 |
+
# Average Global Yield (Weighted)
|
| 839 |
+
# We don't have Pack Fresh in Detail DF for all orders?
|
| 840 |
+
# Wait, 'OK Packing (Pack Fresh)' IS in Detail DF (mapped in load_data)
|
| 841 |
+
total_pack = df["pack_fresh"].sum()
|
| 842 |
+
total_issued = df["Actual Gr Opening"].sum()
|
| 843 |
+
global_yield = (total_pack / total_issued * 100) if total_issued > 0 else 0
|
| 844 |
+
|
| 845 |
+
kpis = {
|
| 846 |
+
"total_orders": int(total_orders),
|
| 847 |
+
"total_volume_m": int(total_volume),
|
| 848 |
+
"global_yield_pct": round(global_yield, 1),
|
| 849 |
+
"shortfall_risk_pct": round(shortfall_risk_rate, 1),
|
| 850 |
+
}
|
| 851 |
+
|
| 852 |
+
# 2. Distributions
|
| 853 |
+
# By Route
|
| 854 |
+
route_stats = (
|
| 855 |
+
df.groupby("Route")
|
| 856 |
+
.agg(
|
| 857 |
+
{
|
| 858 |
+
"pack_fresh": "sum",
|
| 859 |
+
"Actual Gr Opening": "sum",
|
| 860 |
+
"Order Qty": "count", # Order Count
|
| 861 |
+
}
|
| 862 |
+
)
|
| 863 |
+
.reset_index()
|
| 864 |
+
)
|
| 865 |
+
route_stats["yield"] = (
|
| 866 |
+
route_stats["pack_fresh"] / route_stats["Actual Gr Opening"] * 100
|
| 867 |
+
).fillna(0)
|
| 868 |
+
route_dist = (
|
| 869 |
+
route_stats[["Route", "yield", "Order Qty"]]
|
| 870 |
+
.rename(columns={"Order Qty": "count"})
|
| 871 |
+
.round(1)
|
| 872 |
+
.to_dict(orient="records")
|
| 873 |
+
)
|
| 874 |
+
|
| 875 |
+
# By Finish
|
| 876 |
+
finish_stats = (
|
| 877 |
+
df.groupby("Finish")
|
| 878 |
+
.agg(
|
| 879 |
+
{"pack_fresh": "sum", "Actual Gr Opening": "sum", "Order Qty": "count"}
|
| 880 |
+
)
|
| 881 |
+
.reset_index()
|
| 882 |
+
.rename(columns={"Order Qty": "count"})
|
| 883 |
+
)
|
| 884 |
+
finish_stats["yield"] = (
|
| 885 |
+
finish_stats["pack_fresh"] / finish_stats["Actual Gr Opening"] * 100
|
| 886 |
+
).fillna(0)
|
| 887 |
+
finish_dist = (
|
| 888 |
+
finish_stats.sort_values("count", ascending=False)
|
| 889 |
+
.head(10)[["Finish", "yield", "count"]]
|
| 890 |
+
.round(1)
|
| 891 |
+
.to_dict(orient="records")
|
| 892 |
+
)
|
| 893 |
+
|
| 894 |
+
# By Shade (Shade Type)
|
| 895 |
+
shade_stats = (
|
| 896 |
+
df.groupby("Shade Type")
|
| 897 |
+
.agg(
|
| 898 |
+
{"pack_fresh": "sum", "Actual Gr Opening": "sum", "Order Qty": "count"}
|
| 899 |
+
)
|
| 900 |
+
.reset_index()
|
| 901 |
+
.rename(columns={"Order Qty": "count"})
|
| 902 |
+
)
|
| 903 |
+
shade_stats["yield"] = (
|
| 904 |
+
shade_stats["pack_fresh"] / shade_stats["Actual Gr Opening"] * 100
|
| 905 |
+
).fillna(0)
|
| 906 |
+
shade_dist = (
|
| 907 |
+
shade_stats.sort_values("count", ascending=False)
|
| 908 |
+
.head(10)[["Shade Type", "yield", "count"]]
|
| 909 |
+
.round(1)
|
| 910 |
+
.to_dict(orient="records")
|
| 911 |
+
)
|
| 912 |
+
|
| 913 |
+
# 4. Segment Distribution
|
| 914 |
+
segment_dist = []
|
| 915 |
+
if "segment_desc" in df.columns:
|
| 916 |
+
seg_stats = (
|
| 917 |
+
df.groupby("segment_desc")
|
| 918 |
+
.agg(
|
| 919 |
+
{
|
| 920 |
+
"pack_fresh": "sum",
|
| 921 |
+
"Actual Gr Opening": "sum",
|
| 922 |
+
"Order Qty": ["count", "sum"],
|
| 923 |
+
}
|
| 924 |
+
)
|
| 925 |
+
.reset_index()
|
| 926 |
+
)
|
| 927 |
+
seg_stats.columns = ["segment", "pack_fresh", "issued", "count", "volume"]
|
| 928 |
+
seg_stats["yield"] = (
|
| 929 |
+
seg_stats["pack_fresh"] / seg_stats["issued"] * 100
|
| 930 |
+
).fillna(0)
|
| 931 |
+
segment_dist = (
|
| 932 |
+
seg_stats.sort_values("volume", ascending=False)
|
| 933 |
+
.head(8)[["segment", "yield", "count", "volume"]]
|
| 934 |
+
.round(1)
|
| 935 |
+
.to_dict(orient="records")
|
| 936 |
+
)
|
| 937 |
+
|
| 938 |
+
# 5. Top Customers
|
| 939 |
+
top_customers = []
|
| 940 |
+
if "cust_desc" in df.columns:
|
| 941 |
+
cust_stats = (
|
| 942 |
+
df.groupby("cust_desc")
|
| 943 |
+
.agg(
|
| 944 |
+
{
|
| 945 |
+
"pack_fresh": "sum",
|
| 946 |
+
"Actual Gr Opening": "sum",
|
| 947 |
+
"Order Qty": "sum",
|
| 948 |
+
}
|
| 949 |
+
)
|
| 950 |
+
.reset_index()
|
| 951 |
+
)
|
| 952 |
+
cust_stats["yield"] = (
|
| 953 |
+
cust_stats["pack_fresh"] / cust_stats["Actual Gr Opening"] * 100
|
| 954 |
+
).fillna(0)
|
| 955 |
+
top_customers = (
|
| 956 |
+
cust_stats.sort_values("Order Qty", ascending=False)
|
| 957 |
+
.head(10)[["cust_desc", "yield", "Order Qty"]]
|
| 958 |
+
.round(1)
|
| 959 |
+
.rename(columns={"Order Qty": "volume", "cust_desc": "customer"})
|
| 960 |
+
.to_dict(orient="records")
|
| 961 |
+
)
|
| 962 |
+
|
| 963 |
+
# 6. Global Blame & Waterfall
|
| 964 |
+
# 6. Global Blame & Waterfall
|
| 965 |
+
g_order_qty = total_volume # Use the corrected volume calculated above
|
| 966 |
+
g_reserved = df["Reserver Qty as per Std Norms"].sum()
|
| 967 |
+
g_issued = df["Actual Gr Opening"].sum()
|
| 968 |
+
g_pack_fresh = df["pack_fresh"].sum()
|
| 969 |
+
|
| 970 |
+
g_policy_gap = g_reserved - g_order_qty
|
| 971 |
+
g_exec_gap = g_issued - g_reserved
|
| 972 |
+
g_process_loss = g_pack_fresh - g_issued
|
| 973 |
+
|
| 974 |
+
waterfall = [
|
| 975 |
+
{"label": "Total Demand", "value": float(g_order_qty), "type": "base"},
|
| 976 |
+
{
|
| 977 |
+
"label": "Policy Gap",
|
| 978 |
+
"value": float(g_policy_gap),
|
| 979 |
+
"type": "variance",
|
| 980 |
+
"desc": "Norm vs Demand",
|
| 981 |
+
},
|
| 982 |
+
{
|
| 983 |
+
"label": "Execution Adj",
|
| 984 |
+
"value": float(g_exec_gap),
|
| 985 |
+
"type": "variance",
|
| 986 |
+
"desc": "Issued vs Norm",
|
| 987 |
+
},
|
| 988 |
+
{
|
| 989 |
+
"label": "Process Loss",
|
| 990 |
+
"value": float(g_process_loss),
|
| 991 |
+
"type": "variance",
|
| 992 |
+
"desc": "Defects & Shrinkage",
|
| 993 |
+
},
|
| 994 |
+
{"label": "Delivered", "value": float(g_pack_fresh), "type": "final"},
|
| 995 |
+
]
|
| 996 |
+
|
| 997 |
+
# Global Blame Breakdown (Absolute Magnitude)
|
| 998 |
+
# Global Blame Breakdown (Absolute Magnitude)
|
| 999 |
+
# Note: 'abs_policy' per row is tricky because Norm is at PO level but Demand (Order Qty) is at Order Level?
|
| 1000 |
+
# Actually in input rows, RES_QTY - DORQT1 might be valid per PO if DORQT1 was split?
|
| 1001 |
+
# But here DORQT1 is the full order qty repeated.
|
| 1002 |
+
# So "Policy Gap" per row = (RES_QTY - DORQT1) is WRONG because DORQT1 is too big for a single PO.
|
| 1003 |
+
# We need a different approach for row-wise attribution if we want to sum it up.
|
| 1004 |
+
# But for the pie chart, we can just use the global aggregates we calculated above.
|
| 1005 |
+
|
| 1006 |
+
# Re-calculating global friction based on the corrected aggregates
|
| 1007 |
+
abs_policy = abs(g_policy_gap)
|
| 1008 |
+
abs_exec = abs(g_exec_gap)
|
| 1009 |
+
abs_process = abs(g_process_loss)
|
| 1010 |
+
|
| 1011 |
+
total_friction = abs_policy + abs_exec + abs_process
|
| 1012 |
+
|
| 1013 |
+
blame = {
|
| 1014 |
+
"policy_pct": round(abs_policy / total_friction * 100, 1)
|
| 1015 |
+
if total_friction > 0
|
| 1016 |
+
else 0,
|
| 1017 |
+
"execution_pct": round(abs_exec / total_friction * 100, 1)
|
| 1018 |
+
if total_friction > 0
|
| 1019 |
+
else 0,
|
| 1020 |
+
"process_pct": round(abs_process / total_friction * 100, 1)
|
| 1021 |
+
if total_friction > 0
|
| 1022 |
+
else 0,
|
| 1023 |
+
}
|
| 1024 |
+
|
| 1025 |
+
# 3. Trends (Monthly)
|
| 1026 |
+
trend_data = []
|
| 1027 |
+
if "pack_date" in df.columns:
|
| 1028 |
+
df["date"] = pd.to_datetime(df["pack_date"], errors="coerce")
|
| 1029 |
+
elif "Pack Date" in df.columns:
|
| 1030 |
+
df["date"] = pd.to_datetime(df["Pack Date"], errors="coerce")
|
| 1031 |
+
|
| 1032 |
+
if "date" in df.columns:
|
| 1033 |
+
monthly = (
|
| 1034 |
+
df.groupby(df["date"].dt.to_period("M"))
|
| 1035 |
+
.agg({"pack_fresh": "sum", "Actual Gr Opening": "sum"})
|
| 1036 |
+
.reset_index()
|
| 1037 |
+
)
|
| 1038 |
+
monthly["yield"] = (
|
| 1039 |
+
monthly["pack_fresh"] / monthly["Actual Gr Opening"] * 100
|
| 1040 |
+
).fillna(0)
|
| 1041 |
+
monthly["month"] = monthly["date"].astype(str)
|
| 1042 |
+
trend_data = monthly[["month", "yield"]].to_dict(orient="records")
|
| 1043 |
+
|
| 1044 |
+
return {
|
| 1045 |
+
"kpis": kpis,
|
| 1046 |
+
"distributions": {
|
| 1047 |
+
"route": route_dist,
|
| 1048 |
+
"finish": finish_dist,
|
| 1049 |
+
"shade": shade_dist,
|
| 1050 |
+
"segment": segment_dist,
|
| 1051 |
+
"customer": top_customers,
|
| 1052 |
+
},
|
| 1053 |
+
"trends": trend_data,
|
| 1054 |
+
"global_waterfall": waterfall,
|
| 1055 |
+
"global_blame": blame,
|
| 1056 |
+
}
|
| 1057 |
+
|
| 1058 |
+
def simulate_impact(self, tolerance_percent: float):
|
| 1059 |
+
"""
|
| 1060 |
+
Simulate increasing the Norm by tolerance_percent (0-100).
|
| 1061 |
+
"""
|
| 1062 |
+
if not self.is_loaded:
|
| 1063 |
+
self.load_data()
|
| 1064 |
+
|
| 1065 |
+
df = self.master_df[self.master_df["Order Qty"] > 0].copy()
|
| 1066 |
+
|
| 1067 |
+
# Original Status
|
| 1068 |
+
# Overissuance: Issued > Reserved (planner issued more than norm allowed)
|
| 1069 |
+
df["Original_Overissuance"] = (
|
| 1070 |
+
df["Actual Gr Opening"] > df["Reserver Qty as per Std Norms"]
|
| 1071 |
+
)
|
| 1072 |
+
|
| 1073 |
+
# New Reserved
|
| 1074 |
+
buffer_multiplier = 1 + (tolerance_percent / 100.0)
|
| 1075 |
+
df["New_Reserved"] = df["Reserver Qty as per Std Norms"] * buffer_multiplier
|
| 1076 |
+
|
| 1077 |
+
# New Status
|
| 1078 |
+
df["New_Overissuance"] = df["Actual Gr Opening"] > df["New_Reserved"]
|
| 1079 |
+
|
| 1080 |
+
original_count = int(df["Original_Overissuance"].sum())
|
| 1081 |
+
new_count = int(df["New_Overissuance"].sum())
|
| 1082 |
+
saved_count = original_count - new_count
|
| 1083 |
+
|
| 1084 |
+
# Cost: Extra greige allocated (New Reserved - Original Reserved) summed
|
| 1085 |
+
extra_allocation = (
|
| 1086 |
+
df["New_Reserved"] - df["Reserver Qty as per Std Norms"]
|
| 1087 |
+
).sum()
|
| 1088 |
+
|
| 1089 |
+
return {
|
| 1090 |
+
"tolerance": tolerance_percent,
|
| 1091 |
+
"original_overissuances": original_count,
|
| 1092 |
+
"new_overissuances": new_count,
|
| 1093 |
+
"overissuances_prevented": saved_count,
|
| 1094 |
+
"extra_greige_allocation_meters": float(extra_allocation),
|
| 1095 |
+
}
|
| 1096 |
+
|
| 1097 |
+
def get_po_types(self):
|
| 1098 |
+
"""Get all PO types with their classification"""
|
| 1099 |
+
if not self.is_loaded:
|
| 1100 |
+
self.load_data()
|
| 1101 |
+
|
| 1102 |
+
po_df = self.po_type_df.copy()
|
| 1103 |
+
result = []
|
| 1104 |
+
for _, row in po_df.iterrows():
|
| 1105 |
+
po_type = str(row.iloc[0])
|
| 1106 |
+
description = str(row.iloc[1]) if len(row) > 1 else ""
|
| 1107 |
+
is_input = "YES" in str(row.iloc[2]).upper() if len(row) > 2 else False
|
| 1108 |
+
is_output = "YES" in str(row.iloc[3]).upper() if len(row) > 3 else False
|
| 1109 |
+
result.append(
|
| 1110 |
+
{
|
| 1111 |
+
"code": po_type,
|
| 1112 |
+
"description": description,
|
| 1113 |
+
"is_input": is_input,
|
| 1114 |
+
"is_output": is_output,
|
| 1115 |
+
}
|
| 1116 |
+
)
|
| 1117 |
+
return {"po_types": result}
|
| 1118 |
+
|
| 1119 |
+
def get_finish_descriptions(self):
|
| 1120 |
+
"""Get all finish codes and their special treatments"""
|
| 1121 |
+
if not self.is_loaded:
|
| 1122 |
+
self.load_data()
|
| 1123 |
+
|
| 1124 |
+
finish_df = self.finish_df.copy()
|
| 1125 |
+
result = []
|
| 1126 |
+
# Column 0 is Finish Code, last column is Long Description
|
| 1127 |
+
for _, row in finish_df.iterrows():
|
| 1128 |
+
finish_code = str(row.iloc[0]) if pd.notna(row.iloc[0]) else ""
|
| 1129 |
+
if not finish_code or finish_code == "nan":
|
| 1130 |
+
continue
|
| 1131 |
+
long_desc = str(row.iloc[-1]) if pd.notna(row.iloc[-1]) else ""
|
| 1132 |
+
spl_chemical = (
|
| 1133 |
+
str(row.iloc[1]) if len(row) > 1 and pd.notna(row.iloc[1]) else ""
|
| 1134 |
+
)
|
| 1135 |
+
|
| 1136 |
+
# Collect all special treatments (non-NaN values in columns 2-27)
|
| 1137 |
+
treatments = []
|
| 1138 |
+
for i in range(2, min(len(row) - 1, 28)):
|
| 1139 |
+
val = row.iloc[i]
|
| 1140 |
+
if pd.notna(val) and str(val).strip():
|
| 1141 |
+
treatments.append(str(val).strip())
|
| 1142 |
+
|
| 1143 |
+
result.append(
|
| 1144 |
+
{
|
| 1145 |
+
"code": finish_code,
|
| 1146 |
+
"description": long_desc,
|
| 1147 |
+
"chemical_type": spl_chemical,
|
| 1148 |
+
"treatments": treatments,
|
| 1149 |
+
}
|
| 1150 |
+
)
|
| 1151 |
+
return {"finishes": result}
|
| 1152 |
+
|
| 1153 |
+
def get_shade_categories(self):
|
| 1154 |
+
"""Get all shade categories"""
|
| 1155 |
+
if not self.is_loaded:
|
| 1156 |
+
self.load_data()
|
| 1157 |
+
|
| 1158 |
+
shade_df = self.shade_df.copy()
|
| 1159 |
+
result = []
|
| 1160 |
+
# Skip first row (header), Col 1 is prefix, Col 2 is shade type, Col 3 is shade family
|
| 1161 |
+
for i, row in shade_df.iterrows():
|
| 1162 |
+
if i == 0: # Skip header
|
| 1163 |
+
continue
|
| 1164 |
+
prefix = str(row.iloc[1]) if len(row) > 1 and pd.notna(row.iloc[1]) else ""
|
| 1165 |
+
shade_type = (
|
| 1166 |
+
str(row.iloc[2]) if len(row) > 2 and pd.notna(row.iloc[2]) else ""
|
| 1167 |
+
)
|
| 1168 |
+
shade_family = (
|
| 1169 |
+
str(row.iloc[3]) if len(row) > 3 and pd.notna(row.iloc[3]) else ""
|
| 1170 |
+
)
|
| 1171 |
+
|
| 1172 |
+
if prefix or shade_type:
|
| 1173 |
+
result.append(
|
| 1174 |
+
{
|
| 1175 |
+
"prefix": prefix,
|
| 1176 |
+
"shade_type": shade_type,
|
| 1177 |
+
"shade_family": shade_family,
|
| 1178 |
+
}
|
| 1179 |
+
)
|
| 1180 |
+
return {"shades": result}
|
| 1181 |
+
|
| 1182 |
+
def get_norms(self):
|
| 1183 |
+
"""Get greige issuance norms from the pre-loaded norms.json"""
|
| 1184 |
+
return self.norms_data
|
| 1185 |
+
|
| 1186 |
+
def get_global_trends(self):
|
| 1187 |
+
"""
|
| 1188 |
+
Get comprehensive trend analytics for all entity types.
|
| 1189 |
+
Returns aggregated metrics per: Article, Sale Order, PO, Shade, Route, Finish, Customer, Segment.
|
| 1190 |
+
"""
|
| 1191 |
+
if not self.is_loaded:
|
| 1192 |
+
self.load_data()
|
| 1193 |
+
|
| 1194 |
+
df = self.master_df.copy()
|
| 1195 |
+
|
| 1196 |
+
# FIX: Deduplicate Order Qty for aggregation
|
| 1197 |
+
# Create 'effective_order_qty' which is Order Qty for the first row of each line, 0 for others.
|
| 1198 |
+
subset_cols = (
|
| 1199 |
+
["Sale Order", "COPS_LINENO"]
|
| 1200 |
+
if "COPS_LINENO" in df.columns
|
| 1201 |
+
else ["Sale Order"]
|
| 1202 |
+
)
|
| 1203 |
+
df["effective_order_qty"] = df["Order Qty"]
|
| 1204 |
+
df.loc[
|
| 1205 |
+
df.duplicated(subset=subset_cols, keep="first"), "effective_order_qty"
|
| 1206 |
+
] = 0
|
| 1207 |
+
|
| 1208 |
+
# FIX: Deduplicate RES/ISS - only sum for Input rows (Fresh Greige Input)
|
| 1209 |
+
# Detailed view uses df[df['is_input'] == True]['RES_QTY'].sum()
|
| 1210 |
+
# We simulate this by zeroing out non-input rows
|
| 1211 |
+
df["effective_res_qty"] = df["RES_QTY"].fillna(0)
|
| 1212 |
+
df.loc[df["is_input"] != True, "effective_res_qty"] = 0
|
| 1213 |
+
|
| 1214 |
+
df["effective_iss_qty"] = df["ISS_QTY"].fillna(0)
|
| 1215 |
+
df.loc[df["is_input"] != True, "effective_iss_qty"] = 0
|
| 1216 |
+
|
| 1217 |
+
# Helper function to calculate trends for a groupby column
|
| 1218 |
+
def aggregate_entity(group_col, name_col=None, limit=50):
|
| 1219 |
+
if group_col not in df.columns:
|
| 1220 |
+
return []
|
| 1221 |
+
|
| 1222 |
+
# Use a different column for counting to avoid conflicts
|
| 1223 |
+
# If grouping by PO_NO, use a different column entirely (not Order Qty which we need for sum)
|
| 1224 |
+
count_col = "pack_fresh" if group_col == "PO_NO" else "PO_NO"
|
| 1225 |
+
|
| 1226 |
+
# Enhanced aggregation with more metrics - use list for ALL to ensure consistent naming
|
| 1227 |
+
# Update: Use effective_order_qty/res/iss for correct Sums
|
| 1228 |
+
agg_dict = {
|
| 1229 |
+
"effective_order_qty": ["sum"],
|
| 1230 |
+
"Actual Gr Opening": ["sum"],
|
| 1231 |
+
"pack_fresh": ["sum"],
|
| 1232 |
+
"Deviation_Percent": ["mean", "std", "min", "max"],
|
| 1233 |
+
"effective_res_qty": ["sum"],
|
| 1234 |
+
"effective_iss_qty": ["sum"],
|
| 1235 |
+
}
|
| 1236 |
+
# Add count aggregation - ensure we don't overwrite existing aggregations
|
| 1237 |
+
if count_col in df.columns and count_col != group_col:
|
| 1238 |
+
if count_col in agg_dict:
|
| 1239 |
+
# Column already exists - extend its aggregation list
|
| 1240 |
+
if "count" not in agg_dict[count_col]:
|
| 1241 |
+
agg_dict[count_col] = agg_dict[count_col] + ["count"]
|
| 1242 |
+
else:
|
| 1243 |
+
agg_dict[count_col] = ["count"]
|
| 1244 |
+
|
| 1245 |
+
grouped = df.groupby(group_col).agg(agg_dict)
|
| 1246 |
+
# Flatten multi-level columns - all are tuples now
|
| 1247 |
+
grouped.columns = ["_".join(col).strip() for col in grouped.columns.values]
|
| 1248 |
+
grouped = grouped.reset_index()
|
| 1249 |
+
|
| 1250 |
+
# Calculate derived metrics
|
| 1251 |
+
grouped["yield"] = (
|
| 1252 |
+
grouped["pack_fresh_sum"] / grouped["Actual Gr Opening_sum"] * 100
|
| 1253 |
+
).fillna(0)
|
| 1254 |
+
grouped["volume"] = grouped["effective_order_qty_sum"]
|
| 1255 |
+
|
| 1256 |
+
# Count records
|
| 1257 |
+
count_key = f"{count_col}_count"
|
| 1258 |
+
if count_key in grouped.columns:
|
| 1259 |
+
grouped["count"] = grouped[count_key]
|
| 1260 |
+
else:
|
| 1261 |
+
# Fallback: use size-based count
|
| 1262 |
+
grouped["count"] = df.groupby(group_col).size().values
|
| 1263 |
+
|
| 1264 |
+
# Shortfall = Demand - Output
|
| 1265 |
+
grouped["shortfall"] = (
|
| 1266 |
+
grouped["effective_order_qty_sum"] - grouped["pack_fresh_sum"]
|
| 1267 |
+
)
|
| 1268 |
+
grouped["shortfall_pct"] = (
|
| 1269 |
+
grouped["shortfall"] / grouped["effective_order_qty_sum"] * 100
|
| 1270 |
+
).fillna(0)
|
| 1271 |
+
|
| 1272 |
+
# Success rate: % where output >= demand (approximation using yield)
|
| 1273 |
+
grouped["success_rate"] = ((grouped["yield"] >= 100) * 100).fillna(0)
|
| 1274 |
+
# Actually calculate from row-level data
|
| 1275 |
+
if group_col in df.columns:
|
| 1276 |
+
success_df = df.copy()
|
| 1277 |
+
success_df["is_success"] = (
|
| 1278 |
+
success_df["pack_fresh"] >= success_df["Order Qty"]
|
| 1279 |
+
)
|
| 1280 |
+
success_by_group = (
|
| 1281 |
+
success_df.groupby(group_col)["is_success"].mean() * 100
|
| 1282 |
+
)
|
| 1283 |
+
grouped["success_rate"] = (
|
| 1284 |
+
grouped[group_col].map(success_by_group).fillna(0)
|
| 1285 |
+
)
|
| 1286 |
+
|
| 1287 |
+
# ================================================================
|
| 1288 |
+
# NORM-BASED INSIGHTS: Deviation, Waterfall, Blame, Compliance
|
| 1289 |
+
# ================================================================
|
| 1290 |
+
|
| 1291 |
+
# Norm Deviation = Issued - Reserved (positive = over-allocation)
|
| 1292 |
+
grouped["norm_deviation"] = (
|
| 1293 |
+
grouped["effective_iss_qty_sum"] - grouped["effective_res_qty_sum"]
|
| 1294 |
+
)
|
| 1295 |
+
grouped["norm_deviation_pct"] = (
|
| 1296 |
+
grouped["norm_deviation"] / grouped["effective_res_qty_sum"] * 100
|
| 1297 |
+
).fillna(0)
|
| 1298 |
+
|
| 1299 |
+
# Calculate over/under allocation from row-level data
|
| 1300 |
+
if group_col in df.columns:
|
| 1301 |
+
alloc_df = df.copy()
|
| 1302 |
+
alloc_df["is_over"] = alloc_df["ISS_QTY"] > alloc_df["RES_QTY"]
|
| 1303 |
+
alloc_df["is_under"] = alloc_df["ISS_QTY"] < alloc_df["RES_QTY"]
|
| 1304 |
+
over_by_group = alloc_df.groupby(group_col)["is_over"].mean() * 100
|
| 1305 |
+
under_by_group = alloc_df.groupby(group_col)["is_under"].mean() * 100
|
| 1306 |
+
grouped["over_allocated_pct"] = (
|
| 1307 |
+
grouped[group_col].map(over_by_group).fillna(0)
|
| 1308 |
+
)
|
| 1309 |
+
grouped["under_allocated_pct"] = (
|
| 1310 |
+
grouped[group_col].map(under_by_group).fillna(0)
|
| 1311 |
+
)
|
| 1312 |
+
else:
|
| 1313 |
+
grouped["over_allocated_pct"] = 0
|
| 1314 |
+
grouped["under_allocated_pct"] = 0
|
| 1315 |
+
|
| 1316 |
+
# WATERFALL AGGREGATES
|
| 1317 |
+
# Policy Gap = Reserved - Demand (how much buffer the norm added)
|
| 1318 |
+
grouped["policy_gap"] = (
|
| 1319 |
+
grouped["effective_res_qty_sum"] - grouped["effective_order_qty_sum"]
|
| 1320 |
+
)
|
| 1321 |
+
# Execution Adj = Issued - Reserved (human intervention)
|
| 1322 |
+
grouped["execution_adj"] = (
|
| 1323 |
+
grouped["effective_iss_qty_sum"] - grouped["effective_res_qty_sum"]
|
| 1324 |
+
)
|
| 1325 |
+
# Process Loss = Pack Fresh - Issued (manufacturing reality)
|
| 1326 |
+
grouped["process_loss"] = (
|
| 1327 |
+
grouped["pack_fresh_sum"] - grouped["effective_iss_qty_sum"]
|
| 1328 |
+
)
|
| 1329 |
+
|
| 1330 |
+
# BLAME ATTRIBUTION (which factor is responsible for shortfall?)
|
| 1331 |
+
# Policy, Execution, Process impacts
|
| 1332 |
+
grouped["policy_impact"] = grouped["policy_gap"]
|
| 1333 |
+
grouped["execution_impact"] = grouped["execution_adj"]
|
| 1334 |
+
grouped["process_impact"] = grouped["process_loss"]
|
| 1335 |
+
|
| 1336 |
+
# Calculate blame percentages
|
| 1337 |
+
grouped["total_impact"] = (
|
| 1338 |
+
grouped["policy_impact"].abs()
|
| 1339 |
+
+ grouped["execution_impact"].abs()
|
| 1340 |
+
+ grouped["process_impact"].abs()
|
| 1341 |
+
)
|
| 1342 |
+
grouped["policy_blame_pct"] = (
|
| 1343 |
+
grouped["policy_impact"].abs() / grouped["total_impact"] * 100
|
| 1344 |
+
).fillna(0)
|
| 1345 |
+
grouped["execution_blame_pct"] = (
|
| 1346 |
+
grouped["execution_impact"].abs() / grouped["total_impact"] * 100
|
| 1347 |
+
).fillna(0)
|
| 1348 |
+
grouped["process_blame_pct"] = (
|
| 1349 |
+
grouped["process_impact"].abs() / grouped["total_impact"] * 100
|
| 1350 |
+
).fillna(0)
|
| 1351 |
+
|
| 1352 |
+
# NORM COMPLIANCE SCORE
|
| 1353 |
+
# % of records where allocation is within ±10% of norm
|
| 1354 |
+
if group_col in df.columns:
|
| 1355 |
+
comp_df = df.copy()
|
| 1356 |
+
comp_df["norm_dev_pct"] = (
|
| 1357 |
+
(comp_df["ISS_QTY"] - comp_df["RES_QTY"]) / comp_df["RES_QTY"] * 100
|
| 1358 |
+
).abs()
|
| 1359 |
+
comp_df["is_compliant"] = comp_df["norm_dev_pct"] <= 10
|
| 1360 |
+
compliance_by_group = (
|
| 1361 |
+
comp_df.groupby(group_col)["is_compliant"].mean() * 100
|
| 1362 |
+
)
|
| 1363 |
+
grouped["norm_compliance"] = (
|
| 1364 |
+
grouped[group_col].map(compliance_by_group).fillna(0)
|
| 1365 |
+
)
|
| 1366 |
+
else:
|
| 1367 |
+
grouped["norm_compliance"] = 0
|
| 1368 |
+
|
| 1369 |
+
# Norm Reliability = Delivery Success Rate
|
| 1370 |
+
grouped["norm_reliability"] = grouped["success_rate"]
|
| 1371 |
+
|
| 1372 |
+
# Deviation metrics (existing - from Deviation_Percent column)
|
| 1373 |
+
grouped["deviation_avg"] = grouped["Deviation_Percent_mean"]
|
| 1374 |
+
grouped["deviation_std"] = grouped["Deviation_Percent_std"].fillna(0)
|
| 1375 |
+
grouped["deviation_min"] = grouped["Deviation_Percent_min"]
|
| 1376 |
+
grouped["deviation_max"] = grouped["Deviation_Percent_max"]
|
| 1377 |
+
|
| 1378 |
+
# Efficiency score (0-100): weighted combination of yield, compliance, and success
|
| 1379 |
+
grouped["efficiency_score"] = (
|
| 1380 |
+
grouped["yield"] * 0.4
|
| 1381 |
+
+ grouped["norm_compliance"] * 0.3
|
| 1382 |
+
+ grouped["success_rate"] * 0.3
|
| 1383 |
+
).clip(lower=0, upper=100)
|
| 1384 |
+
|
| 1385 |
+
# Risk level based on yield, shortfall, and norm compliance
|
| 1386 |
+
def calc_risk(row):
|
| 1387 |
+
if (
|
| 1388 |
+
row["yield"] < 80
|
| 1389 |
+
or row["shortfall_pct"] > 20
|
| 1390 |
+
or row["norm_compliance"] < 50
|
| 1391 |
+
):
|
| 1392 |
+
return "high"
|
| 1393 |
+
elif (
|
| 1394 |
+
row["yield"] < 90
|
| 1395 |
+
or row["shortfall_pct"] > 10
|
| 1396 |
+
or row["norm_compliance"] < 70
|
| 1397 |
+
):
|
| 1398 |
+
return "medium"
|
| 1399 |
+
else:
|
| 1400 |
+
return "low"
|
| 1401 |
+
|
| 1402 |
+
grouped["risk_level"] = grouped.apply(calc_risk, axis=1)
|
| 1403 |
+
|
| 1404 |
+
# Determine trend based on yield vs average
|
| 1405 |
+
avg_yield = grouped["yield"].mean()
|
| 1406 |
+
|
| 1407 |
+
def get_trend(row):
|
| 1408 |
+
if row["yield"] > avg_yield + 2:
|
| 1409 |
+
return "up"
|
| 1410 |
+
elif row["yield"] < avg_yield - 2:
|
| 1411 |
+
return "down"
|
| 1412 |
+
else:
|
| 1413 |
+
return "stable"
|
| 1414 |
+
|
| 1415 |
+
grouped["trend"] = grouped.apply(get_trend, axis=1)
|
| 1416 |
+
|
| 1417 |
+
# Sort by volume (most important first) and add rank
|
| 1418 |
+
grouped = grouped.sort_values("volume", ascending=False).head(limit)
|
| 1419 |
+
grouped["rank"] = range(1, len(grouped) + 1)
|
| 1420 |
+
|
| 1421 |
+
# Helper to safely convert floats (handle inf/nan)
|
| 1422 |
+
import math
|
| 1423 |
+
|
| 1424 |
+
def safe_float(val, default=0):
|
| 1425 |
+
try:
|
| 1426 |
+
f = float(val)
|
| 1427 |
+
if math.isnan(f) or math.isinf(f):
|
| 1428 |
+
return default
|
| 1429 |
+
return f
|
| 1430 |
+
except:
|
| 1431 |
+
return default
|
| 1432 |
+
|
| 1433 |
+
result = []
|
| 1434 |
+
for _, row in grouped.iterrows():
|
| 1435 |
+
result.append(
|
| 1436 |
+
{
|
| 1437 |
+
"id": str(row[group_col]),
|
| 1438 |
+
"name": str(row[name_col])
|
| 1439 |
+
if name_col and name_col in grouped.columns
|
| 1440 |
+
else str(row[group_col]),
|
| 1441 |
+
"rank": int(row["rank"]),
|
| 1442 |
+
"count": int(safe_float(row["count"])),
|
| 1443 |
+
"volume": safe_float(row["volume"]),
|
| 1444 |
+
"yield": round(safe_float(row["yield"]), 1),
|
| 1445 |
+
"trend": row["trend"],
|
| 1446 |
+
# Shortfall metrics
|
| 1447 |
+
"shortfall": round(safe_float(row["shortfall"]), 0),
|
| 1448 |
+
"shortfall_pct": round(safe_float(row["shortfall_pct"]), 1),
|
| 1449 |
+
"success_rate": round(safe_float(row["success_rate"]), 1),
|
| 1450 |
+
# NORM-BASED INSIGHTS
|
| 1451 |
+
"norm_deviation": {
|
| 1452 |
+
"absolute": round(safe_float(row["norm_deviation"]), 0),
|
| 1453 |
+
"percent": round(safe_float(row["norm_deviation_pct"]), 1),
|
| 1454 |
+
"over_allocated_pct": round(
|
| 1455 |
+
safe_float(row["over_allocated_pct"]), 1
|
| 1456 |
+
),
|
| 1457 |
+
"under_allocated_pct": round(
|
| 1458 |
+
safe_float(row["under_allocated_pct"]), 1
|
| 1459 |
+
),
|
| 1460 |
+
},
|
| 1461 |
+
"waterfall": {
|
| 1462 |
+
"demand": round(
|
| 1463 |
+
safe_float(row["effective_order_qty_sum"]), 0
|
| 1464 |
+
),
|
| 1465 |
+
"policy_gap": round(safe_float(row["policy_gap"]), 0),
|
| 1466 |
+
"execution_adj": round(safe_float(row["execution_adj"]), 0),
|
| 1467 |
+
"process_loss": round(safe_float(row["process_loss"]), 0),
|
| 1468 |
+
"delivered": round(safe_float(row["pack_fresh_sum"]), 0),
|
| 1469 |
+
},
|
| 1470 |
+
"blame": {
|
| 1471 |
+
"policy_pct": round(safe_float(row["policy_blame_pct"]), 1),
|
| 1472 |
+
"execution_pct": round(
|
| 1473 |
+
safe_float(row["execution_blame_pct"]), 1
|
| 1474 |
+
),
|
| 1475 |
+
"process_pct": round(
|
| 1476 |
+
safe_float(row["process_blame_pct"]), 1
|
| 1477 |
+
),
|
| 1478 |
+
},
|
| 1479 |
+
"compliance": {
|
| 1480 |
+
"norm_compliance": round(
|
| 1481 |
+
safe_float(row["norm_compliance"]), 1
|
| 1482 |
+
),
|
| 1483 |
+
"norm_reliability": round(
|
| 1484 |
+
safe_float(row["norm_reliability"]), 1
|
| 1485 |
+
),
|
| 1486 |
+
},
|
| 1487 |
+
# Existing deviation from Deviation_Percent column
|
| 1488 |
+
"deviation": {
|
| 1489 |
+
"avg": round(safe_float(row["deviation_avg"]), 2),
|
| 1490 |
+
"std": round(safe_float(row["deviation_std"]), 2),
|
| 1491 |
+
"min": round(safe_float(row["deviation_min"]), 2),
|
| 1492 |
+
"max": round(safe_float(row["deviation_max"]), 2),
|
| 1493 |
+
},
|
| 1494 |
+
"efficiency_score": round(
|
| 1495 |
+
safe_float(row["efficiency_score"]), 1
|
| 1496 |
+
),
|
| 1497 |
+
"risk_level": row["risk_level"],
|
| 1498 |
+
"greige_issued": round(
|
| 1499 |
+
safe_float(row["effective_iss_qty_sum"]), 0
|
| 1500 |
+
),
|
| 1501 |
+
"greige_reserved": round(
|
| 1502 |
+
safe_float(row["effective_res_qty_sum"]), 0
|
| 1503 |
+
),
|
| 1504 |
+
}
|
| 1505 |
+
)
|
| 1506 |
+
return result
|
| 1507 |
+
|
| 1508 |
+
# Build Article identifier if not present
|
| 1509 |
+
if "Article" not in df.columns:
|
| 1510 |
+
if "Product" in df.columns and "Count" in df.columns:
|
| 1511 |
+
df["Article"] = (
|
| 1512 |
+
df["Product"].astype(str) + " " + df["Count"].astype(str)
|
| 1513 |
+
)
|
| 1514 |
+
else:
|
| 1515 |
+
df["Article"] = "Unknown"
|
| 1516 |
+
|
| 1517 |
+
# Aggregate per entity type
|
| 1518 |
+
articles = aggregate_entity("Article", limit=100)
|
| 1519 |
+
sale_orders = aggregate_entity("Sale Order", limit=2000)
|
| 1520 |
+
po_numbers = aggregate_entity("PO_NO", limit=100)
|
| 1521 |
+
|
| 1522 |
+
# Shade Type
|
| 1523 |
+
shades = []
|
| 1524 |
+
if "Shade Type" in df.columns:
|
| 1525 |
+
shades = aggregate_entity("Shade Type", limit=20)
|
| 1526 |
+
|
| 1527 |
+
# Route
|
| 1528 |
+
routes = []
|
| 1529 |
+
if "Route" in df.columns:
|
| 1530 |
+
routes = aggregate_entity("Route", limit=10)
|
| 1531 |
+
|
| 1532 |
+
# Finish
|
| 1533 |
+
finishes = []
|
| 1534 |
+
if "Finish" in df.columns:
|
| 1535 |
+
finishes = aggregate_entity("Finish", limit=20)
|
| 1536 |
+
|
| 1537 |
+
# Customer
|
| 1538 |
+
customers = []
|
| 1539 |
+
if "cust_desc" in df.columns:
|
| 1540 |
+
# Rename for consistency
|
| 1541 |
+
df["Customer"] = df["cust_desc"]
|
| 1542 |
+
customers = aggregate_entity("Customer", limit=50)
|
| 1543 |
+
|
| 1544 |
+
# Segment
|
| 1545 |
+
segments = []
|
| 1546 |
+
if "segment_desc" in df.columns:
|
| 1547 |
+
df["Segment"] = df["segment_desc"]
|
| 1548 |
+
segments = aggregate_entity("Segment", limit=20)
|
| 1549 |
+
|
| 1550 |
+
# Count Trend (Yarn Count)
|
| 1551 |
+
counts = []
|
| 1552 |
+
if "Count" in df.columns:
|
| 1553 |
+
counts = aggregate_entity("Count", limit=30)
|
| 1554 |
+
|
| 1555 |
+
# Product Type
|
| 1556 |
+
products = []
|
| 1557 |
+
if "Product" in df.columns:
|
| 1558 |
+
products = aggregate_entity("Product", limit=20)
|
| 1559 |
+
|
| 1560 |
+
return {
|
| 1561 |
+
"articles": articles,
|
| 1562 |
+
"sale_orders": sale_orders,
|
| 1563 |
+
"po_numbers": po_numbers,
|
| 1564 |
+
"shades": shades,
|
| 1565 |
+
"routes": routes,
|
| 1566 |
+
"finishes": finishes,
|
| 1567 |
+
"customers": customers,
|
| 1568 |
+
"segments": segments,
|
| 1569 |
+
"counts": counts,
|
| 1570 |
+
"products": products,
|
| 1571 |
+
"summary": {
|
| 1572 |
+
"total_articles": len(articles),
|
| 1573 |
+
"total_sale_orders": len(sale_orders),
|
| 1574 |
+
"total_pos": len(po_numbers),
|
| 1575 |
+
"avg_yield": round(
|
| 1576 |
+
df["pack_fresh"].sum() / df["Actual Gr Opening"].sum() * 100, 1
|
| 1577 |
+
)
|
| 1578 |
+
if df["Actual Gr Opening"].sum() > 0
|
| 1579 |
+
else 0,
|
| 1580 |
+
},
|
| 1581 |
+
}
|
| 1582 |
+
|
| 1583 |
+
def _determine_norm_params(self, row):
|
| 1584 |
+
"""Map generic article attributes to Norm Calculator parameters"""
|
| 1585 |
+
params = {
|
| 1586 |
+
"division_factor": "",
|
| 1587 |
+
"sub_type": "",
|
| 1588 |
+
"composition": "",
|
| 1589 |
+
"count_range": "",
|
| 1590 |
+
}
|
| 1591 |
+
|
| 1592 |
+
# 1. Division Factor
|
| 1593 |
+
shade = str(row.get("Shade Type", "")).upper()
|
| 1594 |
+
if "FB" in shade:
|
| 1595 |
+
params["division_factor"] = "FB"
|
| 1596 |
+
elif "RFD" in shade:
|
| 1597 |
+
params["division_factor"] = "RFD"
|
| 1598 |
+
else:
|
| 1599 |
+
params["division_factor"] = "Dyed"
|
| 1600 |
+
|
| 1601 |
+
# Check for Special Division (Viscose/Modal)
|
| 1602 |
+
prod = str(row.get("Product", "")).upper()
|
| 1603 |
+
if "MODAL" in prod or "VISCOSE" in prod:
|
| 1604 |
+
params["division_factor"] = "Special"
|
| 1605 |
+
|
| 1606 |
+
# 2. Sub Type
|
| 1607 |
+
finish = str(row.get("Finish", "")).upper()
|
| 1608 |
+
div = params["division_factor"]
|
| 1609 |
+
|
| 1610 |
+
if div == "Special":
|
| 1611 |
+
params["sub_type"] = "N/A"
|
| 1612 |
+
elif div in ["RFD", "FB"]:
|
| 1613 |
+
params["sub_type"] = "Peach/ Soft"
|
| 1614 |
+
else: # Dyed
|
| 1615 |
+
if "PEACH" in finish:
|
| 1616 |
+
params["sub_type"] = "Peach"
|
| 1617 |
+
else:
|
| 1618 |
+
params["sub_type"] = "Normal"
|
| 1619 |
+
|
| 1620 |
+
# 3. Composition
|
| 1621 |
+
if div == "Special":
|
| 1622 |
+
if "MODAL" in prod:
|
| 1623 |
+
params["composition"] = "100% Modal(Non Print)"
|
| 1624 |
+
elif "VISCOSE" in prod:
|
| 1625 |
+
params["composition"] = "100% Viscose(Non Print)"
|
| 1626 |
+
else:
|
| 1627 |
+
params["composition"] = (
|
| 1628 |
+
"100% Modal(Non Print)" # Default fallback for special
|
| 1629 |
+
)
|
| 1630 |
+
else:
|
| 1631 |
+
if "BI-STRETCH" in prod and "PC" in prod:
|
| 1632 |
+
params["composition"] = "Bi-stretch PC/ Nylon"
|
| 1633 |
+
elif "BI-STRETCH" in prod:
|
| 1634 |
+
params["composition"] = "Bi-stretch Noram"
|
| 1635 |
+
elif "STRETCH" in prod and "PC" in prod:
|
| 1636 |
+
params["composition"] = "PC/ PC stretch"
|
| 1637 |
+
elif "STRETCH" in prod:
|
| 1638 |
+
params["composition"] = "Stretch"
|
| 1639 |
+
elif "PC" in prod:
|
| 1640 |
+
params["composition"] = "PC/ PC stretch"
|
| 1641 |
+
elif "COTTON" in prod:
|
| 1642 |
+
params["composition"] = "Cotton"
|
| 1643 |
+
else:
|
| 1644 |
+
params["composition"] = "Cotton" # Default fallback
|
| 1645 |
+
|
| 1646 |
+
# 4. Count Range
|
| 1647 |
+
count_val = str(row.get("Count", "0"))
|
| 1648 |
+
import re
|
| 1649 |
+
|
| 1650 |
+
nums = re.findall(r"\d+", count_val)
|
| 1651 |
+
if nums:
|
| 1652 |
+
val = max([int(n) for n in nums])
|
| 1653 |
+
if val < 40:
|
| 1654 |
+
params["count_range"] = "Below 40s"
|
| 1655 |
+
else:
|
| 1656 |
+
params["count_range"] = "40s and above"
|
| 1657 |
+
else:
|
| 1658 |
+
params["count_range"] = "Below 40s"
|
| 1659 |
+
|
| 1660 |
+
# Special Count Range Override
|
| 1661 |
+
if div == "Special":
|
| 1662 |
+
params["count_range"] = "All"
|
| 1663 |
+
|
| 1664 |
+
return params
|
| 1665 |
+
|
| 1666 |
+
def get_article_predictions(self, article_id):
|
| 1667 |
+
if not self.is_loaded:
|
| 1668 |
+
self.load_data()
|
| 1669 |
+
|
| 1670 |
+
# Filter for Article (Exact Match)
|
| 1671 |
+
df = self.master_df[self.master_df["Article"] == article_id].copy()
|
| 1672 |
+
|
| 1673 |
+
if df.empty:
|
| 1674 |
+
return None
|
| 1675 |
+
|
| 1676 |
+
# Aggregations for Insights
|
| 1677 |
+
# Total Volume (Deduplicated)
|
| 1678 |
+
subset_cols = (
|
| 1679 |
+
["Sale Order", "COPS_LINENO"]
|
| 1680 |
+
if "COPS_LINENO" in df.columns
|
| 1681 |
+
else ["Sale Order"]
|
| 1682 |
+
)
|
| 1683 |
+
total_vol = df.drop_duplicates(subset=subset_cols)["Order Qty"].sum()
|
| 1684 |
+
|
| 1685 |
+
# Input/Output Sums
|
| 1686 |
+
input_rows = df[df["is_input"] == True]
|
| 1687 |
+
total_input = input_rows["Actual Gr Opening"].sum()
|
| 1688 |
+
total_output = df["pack_fresh"].sum()
|
| 1689 |
+
|
| 1690 |
+
avg_yield = (total_output / total_input * 100) if total_input > 0 else 0
|
| 1691 |
+
|
| 1692 |
+
# Historical Shortfall Analysis
|
| 1693 |
+
# Calculate Shortfall % per order
|
| 1694 |
+
# Need to aggregate per Sale Order first
|
| 1695 |
+
|
| 1696 |
+
# List of Sale Orders
|
| 1697 |
+
orders = []
|
| 1698 |
+
so_groups = df.groupby(["Sale Order"])
|
| 1699 |
+
for so_id, group in so_groups:
|
| 1700 |
+
# Handle tuple key if list used in groupby
|
| 1701 |
+
raw_id = so_id[0] if isinstance(so_id, tuple) else so_id
|
| 1702 |
+
actual_id = str(raw_id)
|
| 1703 |
+
|
| 1704 |
+
# Volume for this order (deduplicated)
|
| 1705 |
+
so_vol = group.drop_duplicates(subset=subset_cols)["Order Qty"].sum()
|
| 1706 |
+
so_output = group["pack_fresh"].sum()
|
| 1707 |
+
so_input = group[group["is_input"] == True]["Actual Gr Opening"].sum()
|
| 1708 |
+
|
| 1709 |
+
orders.append(
|
| 1710 |
+
{
|
| 1711 |
+
"id": actual_id,
|
| 1712 |
+
"volume": float(so_vol),
|
| 1713 |
+
"input": float(so_input),
|
| 1714 |
+
"output": float(so_output),
|
| 1715 |
+
"yield": float(so_output / so_input * 100 if so_input > 0 else 0),
|
| 1716 |
+
"dates": {
|
| 1717 |
+
"dispo": str(group["DISPO DATE"].iloc[0])
|
| 1718 |
+
if "DISPO DATE" in group.columns
|
| 1719 |
+
else None
|
| 1720 |
+
},
|
| 1721 |
+
}
|
| 1722 |
+
)
|
| 1723 |
+
|
| 1724 |
+
# Normalize Data for Norm Calculator
|
| 1725 |
+
row = df.iloc[0]
|
| 1726 |
+
norm_params = self._determine_norm_params(row)
|
| 1727 |
+
|
| 1728 |
+
# ============ AI Prediction Engine (Robust, Norm-Based) ============
|
| 1729 |
+
import statistics
|
| 1730 |
+
import re
|
| 1731 |
+
|
| 1732 |
+
# Helper function to parse norm rule strings like "8% or 250m"
|
| 1733 |
+
def parse_rule(rule_str):
|
| 1734 |
+
"""Parse '8% or 250m' into (percentage, fixed_minimum)"""
|
| 1735 |
+
pattern = r"(\d+(?:\.\d+)?)\s*%\s*or\s*(\d+(?:\.\d+)?)\s*m"
|
| 1736 |
+
match = re.match(pattern, str(rule_str), re.IGNORECASE)
|
| 1737 |
+
if match:
|
| 1738 |
+
return float(match.group(1)), float(match.group(2))
|
| 1739 |
+
return 0.0, 0.0
|
| 1740 |
+
|
| 1741 |
+
# Helper to parse tolerance adjustments like "1% Extra" or "-1% Less"
|
| 1742 |
+
def parse_tolerance_adj(tol_str):
|
| 1743 |
+
"""Parse '1% Extra' -> +1, '-1% Less' -> -1, 'As per Std Norms' -> 0"""
|
| 1744 |
+
if not tol_str or "std" in str(tol_str).lower():
|
| 1745 |
+
return 0.0
|
| 1746 |
+
extra_match = re.search(
|
| 1747 |
+
r"(\d+(?:\.\d+)?)\s*%\s*extra", str(tol_str), re.IGNORECASE
|
| 1748 |
+
)
|
| 1749 |
+
if extra_match:
|
| 1750 |
+
return float(extra_match.group(1))
|
| 1751 |
+
less_match = re.search(
|
| 1752 |
+
r"-?\s*(\d+(?:\.\d+)?)\s*%\s*less", str(tol_str), re.IGNORECASE
|
| 1753 |
+
)
|
| 1754 |
+
if less_match:
|
| 1755 |
+
return -float(less_match.group(1))
|
| 1756 |
+
return 0.0
|
| 1757 |
+
|
| 1758 |
+
# Step 1: Find applicable norm rule
|
| 1759 |
+
norm_rule = None
|
| 1760 |
+
for norm in self.norms_data:
|
| 1761 |
+
if (
|
| 1762 |
+
norm.get("division_factor") == norm_params.get("division_factor")
|
| 1763 |
+
and norm.get("sub_type") == norm_params.get("sub_type")
|
| 1764 |
+
and norm.get("composition") == norm_params.get("composition")
|
| 1765 |
+
and norm.get("count_range") == norm_params.get("count_range")
|
| 1766 |
+
):
|
| 1767 |
+
norm_rule = norm
|
| 1768 |
+
break
|
| 1769 |
+
|
| 1770 |
+
# Fallback: Find best match if exact match not found
|
| 1771 |
+
if not norm_rule:
|
| 1772 |
+
for norm in self.norms_data:
|
| 1773 |
+
if norm.get("division_factor") == norm_params.get(
|
| 1774 |
+
"division_factor"
|
| 1775 |
+
) and norm.get("sub_type") == norm_params.get("sub_type"):
|
| 1776 |
+
norm_rule = norm
|
| 1777 |
+
break
|
| 1778 |
+
|
| 1779 |
+
# Default norm if nothing found
|
| 1780 |
+
if not norm_rule:
|
| 1781 |
+
norm_rule = {
|
| 1782 |
+
"rules": {"upto_3000m": "8% or 200m", "above_3000m": "6% or 200m"},
|
| 1783 |
+
"tolerance_adjustments": {
|
| 1784 |
+
"tolerance_3_percent": "1% Extra",
|
| 1785 |
+
"tolerance_5_7_percent": "2% Extra",
|
| 1786 |
+
"tolerance_10_percent": "5% Extra",
|
| 1787 |
+
"tolerance_plus0_minus3_5": "-1% Less",
|
| 1788 |
+
"tolerance_1_2_percent": "As per Std Norms",
|
| 1789 |
+
},
|
| 1790 |
+
}
|
| 1791 |
+
|
| 1792 |
+
# Step 2: Parse norm rule percentages
|
| 1793 |
+
rule_upto_3000 = norm_rule.get("rules", {}).get("upto_3000m", "8% or 200m")
|
| 1794 |
+
rule_above_3000 = norm_rule.get("rules", {}).get("above_3000m", "6% or 200m")
|
| 1795 |
+
|
| 1796 |
+
pct_upto_3000, min_upto_3000 = parse_rule(rule_upto_3000)
|
| 1797 |
+
pct_above_3000, min_above_3000 = parse_rule(rule_above_3000)
|
| 1798 |
+
|
| 1799 |
+
# Use a weighted average based on historical order volumes
|
| 1800 |
+
small_orders = [o for o in orders if o["volume"] <= 3000]
|
| 1801 |
+
large_orders = [o for o in orders if o["volume"] > 3000]
|
| 1802 |
+
|
| 1803 |
+
if len(small_orders) > len(large_orders):
|
| 1804 |
+
avg_norm_pct = pct_upto_3000
|
| 1805 |
+
avg_min_charge = min_upto_3000
|
| 1806 |
+
else:
|
| 1807 |
+
avg_norm_pct = pct_above_3000
|
| 1808 |
+
avg_min_charge = min_above_3000
|
| 1809 |
+
|
| 1810 |
+
# Step 3: OUTCOME-BASED Analysis - Learn from what WORKED
|
| 1811 |
+
# Separate orders by outcome (fulfilled vs shortfall)
|
| 1812 |
+
fulfilled_orders = [] # Orders where output >= volume (successfully delivered)
|
| 1813 |
+
unfulfilled_orders = [] # Orders where output < volume
|
| 1814 |
+
|
| 1815 |
+
yields = []
|
| 1816 |
+
actual_reservation_pcts = [] # (Issued - Order) / Order * 100
|
| 1817 |
+
fulfillment_rates = [] # Output / Order * 100
|
| 1818 |
+
|
| 1819 |
+
for order in orders:
|
| 1820 |
+
if order["input"] > 0 and order["volume"] > 0:
|
| 1821 |
+
yields.append(order["yield"])
|
| 1822 |
+
reservation_pct = (
|
| 1823 |
+
(order["input"] - order["volume"]) / order["volume"]
|
| 1824 |
+
) * 100
|
| 1825 |
+
actual_reservation_pcts.append(reservation_pct)
|
| 1826 |
+
fulfillment = (order["output"] / order["volume"]) * 100
|
| 1827 |
+
fulfillment_rates.append(fulfillment)
|
| 1828 |
+
|
| 1829 |
+
# Classify by outcome
|
| 1830 |
+
# Handle EDGE CASE: Partial orders where Input < Volume
|
| 1831 |
+
# These aren't "failures" of reservation, they are just partial deliveries
|
| 1832 |
+
# If yield is valid, we can still learn from them!
|
| 1833 |
+
is_valid_process = False
|
| 1834 |
+
if order["output"] >= order["volume"]:
|
| 1835 |
+
is_valid_process = True # Fully successful
|
| 1836 |
+
elif order["input"] < order["volume"] and order["yield"] > 80.0:
|
| 1837 |
+
# Partial delivery with sane yield - treat as valid data point
|
| 1838 |
+
is_valid_process = True
|
| 1839 |
+
|
| 1840 |
+
if is_valid_process:
|
| 1841 |
+
# Calculate EFFICIENT reservation
|
| 1842 |
+
# ... (rest of logic)
|
| 1843 |
+
if order["yield"] > 0:
|
| 1844 |
+
eff_res_pct = ((100.0 / order["yield"]) - 1.0) * 100.0
|
| 1845 |
+
else:
|
| 1846 |
+
eff_res_pct = reservation_pct # Fallback
|
| 1847 |
+
|
| 1848 |
+
fulfilled_orders.append(
|
| 1849 |
+
{
|
| 1850 |
+
"reservation_pct": reservation_pct, # Actual used
|
| 1851 |
+
"efficient_reservation_pct": eff_res_pct, # Ideally needed
|
| 1852 |
+
"yield": order["yield"],
|
| 1853 |
+
"fulfillment": fulfillment,
|
| 1854 |
+
}
|
| 1855 |
+
)
|
| 1856 |
+
else:
|
| 1857 |
+
unfulfilled_orders.append(
|
| 1858 |
+
{
|
| 1859 |
+
"reservation_pct": reservation_pct,
|
| 1860 |
+
"yield": order["yield"],
|
| 1861 |
+
"fulfillment": fulfillment,
|
| 1862 |
+
}
|
| 1863 |
+
)
|
| 1864 |
+
|
| 1865 |
+
# Statistical calculations for yields
|
| 1866 |
+
if len(yields) >= 2:
|
| 1867 |
+
yield_avg = statistics.mean(yields)
|
| 1868 |
+
yield_min = min(yields)
|
| 1869 |
+
yield_max = max(yields)
|
| 1870 |
+
yield_std = statistics.stdev(yields)
|
| 1871 |
+
elif len(yields) == 1:
|
| 1872 |
+
yield_avg = yields[0]
|
| 1873 |
+
yield_min = yields[0]
|
| 1874 |
+
yield_max = yields[0]
|
| 1875 |
+
yield_std = 0.0
|
| 1876 |
+
else:
|
| 1877 |
+
yield_avg = 100.0
|
| 1878 |
+
yield_min = 100.0
|
| 1879 |
+
yield_max = 100.0
|
| 1880 |
+
yield_std = 0.0
|
| 1881 |
+
|
| 1882 |
+
avg_actual_reservation = (
|
| 1883 |
+
statistics.mean(actual_reservation_pcts)
|
| 1884 |
+
if actual_reservation_pcts
|
| 1885 |
+
else avg_norm_pct
|
| 1886 |
+
)
|
| 1887 |
+
avg_fulfillment = (
|
| 1888 |
+
statistics.mean(fulfillment_rates) if fulfillment_rates else 100.0
|
| 1889 |
+
)
|
| 1890 |
+
|
| 1891 |
+
# Step 4: SMART ANALYSIS - What reservation % leads to success?
|
| 1892 |
+
# Calculate success rate and analyze successful orders
|
| 1893 |
+
success_rate = (len(fulfilled_orders) / len(orders) * 100) if orders else 100.0
|
| 1894 |
+
shortfall_rate = 100.0 - success_rate
|
| 1895 |
+
|
| 1896 |
+
# Key insight: What reservation % worked for FULFILLED orders?
|
| 1897 |
+
# CHANGED: Use EFFICIENT_RESERVATION (what was needed) instead of ACTUAL (what was used)
|
| 1898 |
+
# This prevents learning "waste" (e.g., if operators always added 10% but only needed 2%)
|
| 1899 |
+
if fulfilled_orders:
|
| 1900 |
+
successful_reservations = [
|
| 1901 |
+
o["efficient_reservation_pct"] for o in fulfilled_orders
|
| 1902 |
+
]
|
| 1903 |
+
min_successful_reservation = min(successful_reservations)
|
| 1904 |
+
avg_successful_reservation = statistics.mean(successful_reservations)
|
| 1905 |
+
median_successful_reservation = statistics.median(successful_reservations)
|
| 1906 |
+
|
| 1907 |
+
# Use the 25th percentile of successful orders as recommended
|
| 1908 |
+
# This is CONSERVATIVE - most successful orders worked with this or less
|
| 1909 |
+
successful_reservations_sorted = sorted(successful_reservations)
|
| 1910 |
+
p25_idx = max(0, int(len(successful_reservations_sorted) * 0.25))
|
| 1911 |
+
p75_idx = min(
|
| 1912 |
+
len(successful_reservations_sorted) - 1,
|
| 1913 |
+
int(len(successful_reservations_sorted) * 0.75),
|
| 1914 |
+
)
|
| 1915 |
+
p25_reservation = successful_reservations_sorted[p25_idx]
|
| 1916 |
+
p75_reservation = successful_reservations_sorted[p75_idx]
|
| 1917 |
+
else:
|
| 1918 |
+
min_successful_reservation = avg_norm_pct
|
| 1919 |
+
avg_successful_reservation = avg_norm_pct
|
| 1920 |
+
median_successful_reservation = avg_norm_pct
|
| 1921 |
+
p25_reservation = avg_norm_pct
|
| 1922 |
+
p75_reservation = avg_norm_pct
|
| 1923 |
+
|
| 1924 |
+
# Step 5: Calculate SMART AI recommendation
|
| 1925 |
+
# Start with norm, but only adjust if there's evidence of issues
|
| 1926 |
+
|
| 1927 |
+
# Compare norm vs what actually worked
|
| 1928 |
+
performance_gap = avg_actual_reservation - avg_norm_pct
|
| 1929 |
+
|
| 1930 |
+
# KEY INSIGHT: Reservation % doesn't strongly predict success
|
| 1931 |
+
# Orders fail due to manufacturing issues, not low reservation
|
| 1932 |
+
# So recommend based on what WORKED for successful orders
|
| 1933 |
+
|
| 1934 |
+
if fulfilled_orders:
|
| 1935 |
+
# Filter outliers using IQR
|
| 1936 |
+
successful_reservations_sorted = sorted(successful_reservations)
|
| 1937 |
+
q1_idx = int(len(successful_reservations_sorted) * 0.25)
|
| 1938 |
+
q3_idx = int(len(successful_reservations_sorted) * 0.75)
|
| 1939 |
+
q1 = successful_reservations_sorted[q1_idx]
|
| 1940 |
+
q3 = successful_reservations_sorted[q3_idx]
|
| 1941 |
+
iqr = q3 - q1
|
| 1942 |
+
lower_bound = q1 - 1.5 * iqr
|
| 1943 |
+
upper_bound = q3 + 1.5 * iqr
|
| 1944 |
+
|
| 1945 |
+
# Filter to "typical" successful orders (exclude outliers)
|
| 1946 |
+
typical_reservations = [
|
| 1947 |
+
r for r in successful_reservations if lower_bound <= r <= upper_bound
|
| 1948 |
+
]
|
| 1949 |
+
|
| 1950 |
+
if typical_reservations:
|
| 1951 |
+
typical_median = statistics.median(typical_reservations)
|
| 1952 |
+
# Use the typical median as the recommendation
|
| 1953 |
+
# Add tiny buffer for yield variance if process is inconsistent
|
| 1954 |
+
small_buffer = min(yield_std * 0.1, 1.0) if yield_std > 5 else 0
|
| 1955 |
+
ai_adjustment = (typical_median - avg_norm_pct) + small_buffer
|
| 1956 |
+
else:
|
| 1957 |
+
# Fall back to regular median
|
| 1958 |
+
ai_adjustment = median_successful_reservation - avg_norm_pct
|
| 1959 |
+
else:
|
| 1960 |
+
# No successful orders - learn from failures
|
| 1961 |
+
# If we used X% and failed, we need MORE than X%
|
| 1962 |
+
if unfulfilled_orders:
|
| 1963 |
+
max_failed_reservation = max(
|
| 1964 |
+
[o["reservation_pct"] for o in unfulfilled_orders]
|
| 1965 |
+
)
|
| 1966 |
+
# Recommend significantly more than what failed
|
| 1967 |
+
# Use max of norm or max_failed, then add a robust buffer
|
| 1968 |
+
base_pct = max(avg_norm_pct, max_failed_reservation)
|
| 1969 |
+
ai_adjustment = (base_pct - avg_norm_pct) + 2.0
|
| 1970 |
+
else:
|
| 1971 |
+
# No orders at all, or edge case
|
| 1972 |
+
ai_adjustment = min(yield_std * 0.2, 2.0)
|
| 1973 |
+
|
| 1974 |
+
# Cap the adjustment - don't over-recommend
|
| 1975 |
+
# Max adjustment is 5% above norm or typical median + 1%, whichever is lower
|
| 1976 |
+
max_adjustment = 5.0
|
| 1977 |
+
if fulfilled_orders and typical_reservations:
|
| 1978 |
+
max_adjustment = max(typical_median - avg_norm_pct + 1.0, 2.0)
|
| 1979 |
+
ai_adjustment = min(ai_adjustment, max_adjustment)
|
| 1980 |
+
|
| 1981 |
+
recommended_reservation_pct = max(0.0, avg_norm_pct + ai_adjustment)
|
| 1982 |
+
|
| 1983 |
+
# Step 6: Build tolerance-based recommendations
|
| 1984 |
+
tolerance_recommendations = {}
|
| 1985 |
+
for tol_key, tol_val in norm_rule.get("tolerance_adjustments", {}).items():
|
| 1986 |
+
tol_adj = parse_tolerance_adj(tol_val)
|
| 1987 |
+
adjusted_norm = avg_norm_pct + tol_adj
|
| 1988 |
+
# AI recommendation on top of tolerance-adjusted norm
|
| 1989 |
+
ai_rec = adjusted_norm + ai_adjustment
|
| 1990 |
+
tolerance_recommendations[tol_key] = {
|
| 1991 |
+
"tolerance_label": tol_val,
|
| 1992 |
+
"norm_pct": round(adjusted_norm, 1),
|
| 1993 |
+
"ai_recommendation_pct": round(ai_rec, 1),
|
| 1994 |
+
}
|
| 1995 |
+
|
| 1996 |
+
# Build explanation based on outcome analysis
|
| 1997 |
+
explanation_parts = []
|
| 1998 |
+
if success_rate >= 95:
|
| 1999 |
+
explanation_parts.append(
|
| 2000 |
+
f"{round(success_rate, 0)}% of orders fulfilled successfully"
|
| 2001 |
+
)
|
| 2002 |
+
if success_rate < 95 and success_rate >= 70:
|
| 2003 |
+
explanation_parts.append(f"Good success rate ({round(success_rate, 0)}%)")
|
| 2004 |
+
if success_rate < 70:
|
| 2005 |
+
explanation_parts.append(
|
| 2006 |
+
f"Only {round(success_rate, 0)}% fulfilled - review needed"
|
| 2007 |
+
)
|
| 2008 |
+
|
| 2009 |
+
if fulfilled_orders:
|
| 2010 |
+
explanation_parts.append(
|
| 2011 |
+
f"Successful orders used {round(median_successful_reservation, 1)}% median reservation"
|
| 2012 |
+
)
|
| 2013 |
+
elif unfulfilled_orders:
|
| 2014 |
+
explanation_parts.append(
|
| 2015 |
+
f"All {len(orders)} orders failed. Would have needed ~{round(avg_norm_pct + ai_adjustment - 1.0, 1)}% extra"
|
| 2016 |
+
)
|
| 2017 |
+
|
| 2018 |
+
if ai_adjustment <= 0.5 and ai_adjustment >= -0.5 and success_rate > 50:
|
| 2019 |
+
explanation_parts.append(
|
| 2020 |
+
"Norms appear adequate based on historical success"
|
| 2021 |
+
)
|
| 2022 |
+
elif ai_adjustment < -0.5 and success_rate > 90:
|
| 2023 |
+
explanation_parts.append(
|
| 2024 |
+
f"Norms are excessive. Safe to reduce by {abs(round(ai_adjustment, 1))}%"
|
| 2025 |
+
)
|
| 2026 |
+
|
| 2027 |
+
explanation = (
|
| 2028 |
+
". ".join(explanation_parts)
|
| 2029 |
+
if explanation_parts
|
| 2030 |
+
else "Historical performance aligns with norms"
|
| 2031 |
+
)
|
| 2032 |
+
|
| 2033 |
+
# Confidence based on data quality
|
| 2034 |
+
if len(orders) >= 10:
|
| 2035 |
+
confidence = "high"
|
| 2036 |
+
elif len(orders) >= 5:
|
| 2037 |
+
confidence = "medium"
|
| 2038 |
+
else:
|
| 2039 |
+
confidence = "low"
|
| 2040 |
+
|
| 2041 |
+
ai_prediction = {
|
| 2042 |
+
"historical_orders": len(orders),
|
| 2043 |
+
"yield_stats": {
|
| 2044 |
+
"avg": float(round(yield_avg, 1)),
|
| 2045 |
+
"min": float(round(yield_min, 1)),
|
| 2046 |
+
"max": float(round(yield_max, 1)),
|
| 2047 |
+
"std_dev": float(round(yield_std, 2)),
|
| 2048 |
+
},
|
| 2049 |
+
"norm_analysis": {
|
| 2050 |
+
"applicable_rule_upto_3000": rule_upto_3000,
|
| 2051 |
+
"applicable_rule_above_3000": rule_above_3000,
|
| 2052 |
+
"base_norm_pct": float(round(avg_norm_pct, 1)),
|
| 2053 |
+
"min_charge_m": float(avg_min_charge),
|
| 2054 |
+
},
|
| 2055 |
+
"historical_analysis": {
|
| 2056 |
+
"avg_actual_reservation_pct": float(round(avg_actual_reservation, 1)),
|
| 2057 |
+
"avg_fulfillment_pct": float(round(avg_fulfillment, 1)),
|
| 2058 |
+
"success_rate_pct": float(round(success_rate, 1)),
|
| 2059 |
+
"shortfall_rate_pct": float(round(shortfall_rate, 1)),
|
| 2060 |
+
"fulfilled_orders": len(fulfilled_orders),
|
| 2061 |
+
"median_successful_reservation_pct": float(
|
| 2062 |
+
round(median_successful_reservation, 1)
|
| 2063 |
+
)
|
| 2064 |
+
if fulfilled_orders
|
| 2065 |
+
else 0.0,
|
| 2066 |
+
"performance_gap_pct": float(round(performance_gap, 1)),
|
| 2067 |
+
},
|
| 2068 |
+
"recommendation": {
|
| 2069 |
+
"suggested_reservation_pct": float(
|
| 2070 |
+
round(recommended_reservation_pct, 1)
|
| 2071 |
+
),
|
| 2072 |
+
"ai_adjustment_pct": float(round(ai_adjustment, 1)),
|
| 2073 |
+
"explanation": explanation,
|
| 2074 |
+
},
|
| 2075 |
+
"tolerance_recommendations": tolerance_recommendations,
|
| 2076 |
+
"avg_process_loss_pct": float(round(100 - yield_avg, 1)),
|
| 2077 |
+
"recommended_multiplier": float(
|
| 2078 |
+
round(1 + recommended_reservation_pct / 100, 3)
|
| 2079 |
+
),
|
| 2080 |
+
"confidence": confidence,
|
| 2081 |
+
}
|
| 2082 |
+
|
| 2083 |
+
return {
|
| 2084 |
+
"article_id": article_id,
|
| 2085 |
+
"details": {
|
| 2086 |
+
"product": str(row.get("Product", "")),
|
| 2087 |
+
"count": str(row.get("Count", "")),
|
| 2088 |
+
"finish": str(row.get("Finish", "")),
|
| 2089 |
+
"route": str(row.get("Route", "")),
|
| 2090 |
+
},
|
| 2091 |
+
"norm_params": norm_params,
|
| 2092 |
+
"stats": {
|
| 2093 |
+
"total_volume": float(round(total_vol, 0)),
|
| 2094 |
+
"avg_yield": float(round(avg_yield, 1)),
|
| 2095 |
+
"total_orders": len(orders),
|
| 2096 |
+
"total_input": float(round(total_input, 0)),
|
| 2097 |
+
"total_output": float(round(total_output, 0)),
|
| 2098 |
+
},
|
| 2099 |
+
"ai_prediction": ai_prediction,
|
| 2100 |
+
"orders": sorted(orders, key=lambda x: x["volume"], reverse=True),
|
| 2101 |
+
}
|
| 2102 |
+
|
| 2103 |
+
def get_po_types(self):
|
| 2104 |
+
if not self.is_loaded:
|
| 2105 |
+
self.load_data()
|
| 2106 |
+
data = []
|
| 2107 |
+
if self.po_type_df is not None:
|
| 2108 |
+
df = self.po_type_df.copy()
|
| 2109 |
+
# Rename if columns exist, otherwise assume order: 0 is code, 1 is description
|
| 2110 |
+
rename_map = {}
|
| 2111 |
+
if "PO Type" in df.columns: rename_map["PO Type"] = "code"
|
| 2112 |
+
# Fallback based on column index
|
| 2113 |
+
if "code" not in rename_map.values() and len(df.columns) > 0:
|
| 2114 |
+
df["code"] = df.iloc[:, 0]
|
| 2115 |
+
if len(df.columns) > 1 and "description" not in df.columns:
|
| 2116 |
+
df["description"] = df.iloc[:, 1]
|
| 2117 |
+
|
| 2118 |
+
if rename_map:
|
| 2119 |
+
df.rename(columns=rename_map, inplace=True)
|
| 2120 |
+
|
| 2121 |
+
data = df.fillna("").to_dict(orient="records")
|
| 2122 |
+
return {"po_types": data}
|
| 2123 |
+
|
| 2124 |
+
def get_finish_descriptions(self):
|
| 2125 |
+
if not self.is_loaded:
|
| 2126 |
+
self.load_data()
|
| 2127 |
+
data = []
|
| 2128 |
+
if self.finish_df is not None:
|
| 2129 |
+
# Map 'Finish Code' -> 'code', 'Finish Description' -> 'description'
|
| 2130 |
+
# Verify column names first. Based on debug script, Likely 'Finish' or 'Finish Code'
|
| 2131 |
+
# Let's check headers from debug output or assume standard
|
| 2132 |
+
# If mapping is needed:
|
| 2133 |
+
df = self.finish_df.copy()
|
| 2134 |
+
# Rename if columns exist
|
| 2135 |
+
rename_map = {}
|
| 2136 |
+
if "Finish Code" in df.columns: rename_map["Finish Code"] = "code"
|
| 2137 |
+
if "Finish Description" in df.columns: rename_map["Finish Description"] = "description"
|
| 2138 |
+
# Fallback if names are different
|
| 2139 |
+
if "code" not in rename_map.values() and len(df.columns) > 0:
|
| 2140 |
+
df["code"] = df.iloc[:, 0] # Assume first col is code
|
| 2141 |
+
if "description" not in rename_map.values() and len(df.columns) > 1:
|
| 2142 |
+
df["description"] = df.iloc[:, 1] # Assume second is desc
|
| 2143 |
+
|
| 2144 |
+
if rename_map:
|
| 2145 |
+
df.rename(columns=rename_map, inplace=True)
|
| 2146 |
+
|
| 2147 |
+
data = df.fillna("").to_dict(orient="records")
|
| 2148 |
+
return {"finishes": data}
|
| 2149 |
+
|
| 2150 |
+
def get_shade_categories(self):
|
| 2151 |
+
if not self.is_loaded:
|
| 2152 |
+
self.load_data()
|
| 2153 |
+
data = []
|
| 2154 |
+
if self.master_df is not None and "Shade Type" in self.master_df.columns:
|
| 2155 |
+
# Create structure: { prefix, shade_type, shade_family }
|
| 2156 |
+
# Shade Type is like "L - Light", "M - Medium"
|
| 2157 |
+
shades = self.master_df["Shade Type"].dropna().unique().tolist()
|
| 2158 |
+
for s in sorted([str(x) for x in shades]):
|
| 2159 |
+
parts = s.split("-")
|
| 2160 |
+
prefix = parts[0].strip() if len(parts) > 0 else ""
|
| 2161 |
+
family = parts[1].strip() if len(parts) > 1 else s
|
| 2162 |
+
data.append({
|
| 2163 |
+
"prefix": prefix,
|
| 2164 |
+
"shade_type": s,
|
| 2165 |
+
"shade_family": family
|
| 2166 |
+
})
|
| 2167 |
+
return {"shades": data}
|
| 2168 |
+
|
| 2169 |
+
def get_norms(self):
|
| 2170 |
+
if not self.is_loaded:
|
| 2171 |
+
self.load_data()
|
| 2172 |
+
|
| 2173 |
+
# Norms are loaded from JSON, likely have keys: division_factor, sub_type, composition, count_range, rules
|
| 2174 |
+
# Frontend expects: shade_type, finish_type, fabric_type, bt_wt_norm, top_wt_norm
|
| 2175 |
+
|
| 2176 |
+
mapped_norms = []
|
| 2177 |
+
for n in self.norms_data:
|
| 2178 |
+
# Map parameters to frontend columns
|
| 2179 |
+
# Division Factor -> Shade Type (roughly)
|
| 2180 |
+
# Sub Type -> Finish
|
| 2181 |
+
# Composition -> Fabric
|
| 2182 |
+
|
| 2183 |
+
# Parse rule "8% or 200m" -> take % as norm for display?
|
| 2184 |
+
# Frontend shows "bt_wt_norm" and "top_wt_norm"
|
| 2185 |
+
# Let's map "upto_3000m" rule to "bt_wt_norm" (Bottom Weight / Small Order?)
|
| 2186 |
+
# And "above_3000m" rule to "top_wt_norm" (Top Weight / Large Order?)
|
| 2187 |
+
|
| 2188 |
+
rules = n.get("rules", {})
|
| 2189 |
+
rule_small = rules.get("upto_3000m", "-")
|
| 2190 |
+
rule_large = rules.get("above_3000m", "-")
|
| 2191 |
+
|
| 2192 |
+
mapped_norms.append({
|
| 2193 |
+
"shade_type": n.get("division_factor", ""),
|
| 2194 |
+
"finish_type": n.get("sub_type", ""),
|
| 2195 |
+
"fabric_type": n.get("composition", ""),
|
| 2196 |
+
"bt_wt_norm": rule_small,
|
| 2197 |
+
"top_wt_norm": rule_large,
|
| 2198 |
+
"count_range": n.get("count_range", "")
|
| 2199 |
+
})
|
| 2200 |
+
|
| 2201 |
+
return {"norms": mapped_norms}
|
| 2202 |
+
|
| 2203 |
+
|
| 2204 |
+
data_service = DataService()
|
backend/debug_data_columns.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import os
|
| 4 |
+
import sys
|
| 5 |
+
|
| 6 |
+
# Define path exactly as in data_service.py
|
| 7 |
+
DATA_PATH = "/run/media/ishpreet/New Volume/Auribises/Vardhman Textiles/Final Base Data for PD Gr issue Norsm 15-01-26.xlsx"
|
| 8 |
+
|
| 9 |
+
print(f"Checking {DATA_PATH}...")
|
| 10 |
+
|
| 11 |
+
if not os.path.exists(DATA_PATH):
|
| 12 |
+
print("❌ File NOT found!")
|
| 13 |
+
# Check fallback
|
| 14 |
+
fallback = "/run/media/ishpreet/New Volume/Auribises/Vardhman Textiles/Data Set.xlsx"
|
| 15 |
+
if os.path.exists(fallback):
|
| 16 |
+
print(f"⚠️ Fallback found: {fallback}")
|
| 17 |
+
DATA_PATH = fallback
|
| 18 |
+
else:
|
| 19 |
+
print("❌ Fallback also NOT found.")
|
| 20 |
+
sys.exit(1)
|
| 21 |
+
|
| 22 |
+
try:
|
| 23 |
+
print("Reading Excel file (this might take a moment)...")
|
| 24 |
+
xl = pd.ExcelFile(DATA_PATH)
|
| 25 |
+
print(f"Sheet names: {xl.sheet_names}")
|
| 26 |
+
|
| 27 |
+
if "Finish Description" in xl.sheet_names:
|
| 28 |
+
df = pd.read_excel(xl, "Finish Description", nrows=5)
|
| 29 |
+
print("\n--- Finish Description Columns ---")
|
| 30 |
+
print(list(df.columns))
|
| 31 |
+
print(df.head(2).to_string())
|
| 32 |
+
|
| 33 |
+
if "PO Type" in xl.sheet_names:
|
| 34 |
+
df = pd.read_excel(xl, "PO Type", nrows=5)
|
| 35 |
+
print("\n--- PO Type Columns ---")
|
| 36 |
+
print(list(df.columns))
|
| 37 |
+
print(df.head(2).to_string())
|
| 38 |
+
|
| 39 |
+
# Check Detail/Master for Shade Type
|
| 40 |
+
if "Detail" in xl.sheet_names:
|
| 41 |
+
df = pd.read_excel(xl, "Detail", nrows=5, header=2) # Adjust header if needed
|
| 42 |
+
print("\n--- Detail Columns (header=2) ---")
|
| 43 |
+
cols = list(df.columns)
|
| 44 |
+
print(cols[:10], "...")
|
| 45 |
+
if "Shade Type" in cols:
|
| 46 |
+
print("✅ 'Shade Type' column FOUND in Detail.")
|
| 47 |
+
else:
|
| 48 |
+
print("❌ 'Shade Type' column NOT FOUND in Detail.")
|
| 49 |
+
# Check if it's there with header=0?
|
| 50 |
+
df0 = pd.read_excel(xl, "Detail", nrows=5, header=0)
|
| 51 |
+
if "Shade Type" in df0.columns:
|
| 52 |
+
print("⚠️ 'Shade Type' found with header=0.")
|
| 53 |
+
|
| 54 |
+
except Exception as e:
|
| 55 |
+
print(f"❌ Error reading Excel: {e}")
|
backend/debug_data_fix.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import sys
|
| 3 |
+
import os
|
| 4 |
+
|
| 5 |
+
# Ensure backend is in path
|
| 6 |
+
sys.path.append(os.getcwd())
|
| 7 |
+
|
| 8 |
+
try:
|
| 9 |
+
from app.services.data_service import data_service
|
| 10 |
+
|
| 11 |
+
print("Attempting to load data...")
|
| 12 |
+
data_service.load_data()
|
| 13 |
+
|
| 14 |
+
print("\n--- PO Types ---")
|
| 15 |
+
if data_service.po_type_df is not None:
|
| 16 |
+
print(f"Shape: {data_service.po_type_df.shape}")
|
| 17 |
+
print("Preview:")
|
| 18 |
+
print(data_service.po_type_df.head().to_string())
|
| 19 |
+
else:
|
| 20 |
+
print("FAIL: po_type_df is None")
|
| 21 |
+
|
| 22 |
+
print("\n--- Finish Descriptions ---")
|
| 23 |
+
if data_service.finish_df is not None:
|
| 24 |
+
print(f"Shape: {data_service.finish_df.shape}")
|
| 25 |
+
print("Preview:")
|
| 26 |
+
print(data_service.finish_df.head().to_string())
|
| 27 |
+
else:
|
| 28 |
+
print("FAIL: finish_df is None")
|
| 29 |
+
|
| 30 |
+
print("\n--- Shade Categories ---")
|
| 31 |
+
if data_service.master_df is not None:
|
| 32 |
+
if "Shade Type" in data_service.master_df.columns:
|
| 33 |
+
shades = data_service.master_df["Shade Type"].dropna().unique()
|
| 34 |
+
print(f"Found {len(shades)} shades.")
|
| 35 |
+
print(f"Sample: {shades[:10]}")
|
| 36 |
+
|
| 37 |
+
# Check for types
|
| 38 |
+
types = set(type(x) for x in shades)
|
| 39 |
+
print(f"Types found: {types}")
|
| 40 |
+
else:
|
| 41 |
+
print("FAIL: 'Shade Type' column not found in master_df.")
|
| 42 |
+
print(f"Columns: {data_service.master_df.columns.tolist()}")
|
| 43 |
+
else:
|
| 44 |
+
print("FAIL: master_df is None")
|
| 45 |
+
|
| 46 |
+
except Exception as e:
|
| 47 |
+
import traceback
|
| 48 |
+
traceback.print_exc()
|
backend/debug_data_simple.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import sys
|
| 3 |
+
import os
|
| 4 |
+
import pandas as pd
|
| 5 |
+
import json
|
| 6 |
+
|
| 7 |
+
# Ensure backend works
|
| 8 |
+
sys.path.append(os.getcwd())
|
| 9 |
+
|
| 10 |
+
# Hardcode paths from data_service.py to test directly
|
| 11 |
+
DATA_PATH = "/run/media/ishpreet/New Volume/Auribises/Vardhman Textiles/Final Base Data for PD Gr issue Norsm 15-01-26.xlsx"
|
| 12 |
+
NORMS_PATH = "/run/media/ishpreet/New Volume/Auribises/Vardhman Textiles/AT1 MKT PD Gr Norms Rev on 13-12-2025.xlsx"
|
| 13 |
+
|
| 14 |
+
print(f"Checking Data Path: {DATA_PATH}")
|
| 15 |
+
if os.path.exists(DATA_PATH):
|
| 16 |
+
print("✅ Main Data File found")
|
| 17 |
+
try:
|
| 18 |
+
print("Reading 'PO Type' sheet...")
|
| 19 |
+
po_df = pd.read_excel(DATA_PATH, sheet_name="PO Type")
|
| 20 |
+
print(f"✅ PO Type Loaded completely. Shape: {po_df.shape}")
|
| 21 |
+
print(po_df.head().to_string())
|
| 22 |
+
|
| 23 |
+
print("\nReading 'Finish Description' sheet...")
|
| 24 |
+
finish_df = pd.read_excel(DATA_PATH, sheet_name="Finish Description")
|
| 25 |
+
print(f"✅ Finish Description Loaded completely. Shape: {finish_df.shape}")
|
| 26 |
+
print(finish_df.head().to_string())
|
| 27 |
+
|
| 28 |
+
except Exception as e:
|
| 29 |
+
print(f"❌ Error reading Excel: {e}")
|
| 30 |
+
else:
|
| 31 |
+
print("❌ Main Data File NOT found")
|
| 32 |
+
# Check fallback
|
| 33 |
+
fallback = "/run/media/ishpreet/New Volume/Auribises/Vardhman Textiles/Data Set.xlsx"
|
| 34 |
+
if os.path.exists(fallback):
|
| 35 |
+
print(f"⚠️ Fallback found at {fallback}")
|
| 36 |
+
else:
|
| 37 |
+
print(f"❌ Fallback NOT found either")
|
| 38 |
+
|
| 39 |
+
print("\nChecking Norms...")
|
| 40 |
+
norms_json = os.path.join(os.getcwd(), "data/norms.json")
|
| 41 |
+
if os.path.exists(norms_json):
|
| 42 |
+
print(f"✅ Norms JSON found at {norms_json}")
|
| 43 |
+
try:
|
| 44 |
+
with open(norms_json, "r") as f:
|
| 45 |
+
norms = json.load(f)
|
| 46 |
+
print(f"✅ Norms loaded: {len(norms)} entries")
|
| 47 |
+
except Exception as e:
|
| 48 |
+
print(f"❌ Error reading Norms JSON: {e}")
|
| 49 |
+
else:
|
| 50 |
+
print(f"❌ Norms JSON NOT found at {norms_json}")
|
backend/debug_norms_only.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import os
|
| 3 |
+
import json
|
| 4 |
+
import sys
|
| 5 |
+
|
| 6 |
+
# Mocking the location of data_service.py
|
| 7 |
+
# We are currently in /backend
|
| 8 |
+
# data_service is in /backend/app/services/data_service.py
|
| 9 |
+
# So we need to simulate that path structure to test relative path logic
|
| 10 |
+
|
| 11 |
+
# Let's define the path logic exactly as in data_service.py
|
| 12 |
+
# We can use a dummy file as the anchor
|
| 13 |
+
dummy_file = os.path.join(os.getcwd(), "app/services/data_service.py")
|
| 14 |
+
print(f"Simulating __file__ as: {dummy_file}")
|
| 15 |
+
|
| 16 |
+
norms_path = os.path.join(
|
| 17 |
+
os.path.dirname(os.path.dirname(dummy_file)), "data/norms.json"
|
| 18 |
+
)
|
| 19 |
+
print(f"Resolved norms path: {norms_path}")
|
| 20 |
+
|
| 21 |
+
if os.path.exists(norms_path):
|
| 22 |
+
print("✅ File found!")
|
| 23 |
+
try:
|
| 24 |
+
with open(norms_path, "r") as f:
|
| 25 |
+
data = json.load(f)
|
| 26 |
+
print(f"✅ Loaded {len(data)} items.")
|
| 27 |
+
print("First item keys:", data[0].keys())
|
| 28 |
+
except Exception as e:
|
| 29 |
+
print(f"❌ JSON Load Error: {e}")
|
| 30 |
+
else:
|
| 31 |
+
print("❌ File NOT found at resolved path.")
|
| 32 |
+
# Debug listing
|
| 33 |
+
parent = os.path.dirname(norms_path)
|
| 34 |
+
print(f"Listing parent dir: {parent}")
|
| 35 |
+
if os.path.exists(parent):
|
| 36 |
+
print(os.listdir(parent))
|
| 37 |
+
else:
|
| 38 |
+
print("Parent dir does not exist.")
|
backend/requirements.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi
|
| 2 |
+
uvicorn
|
| 3 |
+
pandas
|
| 4 |
+
openpyxl
|
| 5 |
+
python-multipart
|
| 6 |
+
xlsxwriter
|
backend/tests/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Test suite for Process Aware AI - Data Service
|
| 3 |
+
|
| 4 |
+
This module contains comprehensive tests for verifying:
|
| 5 |
+
1. Data loading and column mappings
|
| 6 |
+
2. Calculation formulas match Excel logic
|
| 7 |
+
3. All 970 sale orders produce correct metrics
|
| 8 |
+
4. All 496 articles have valid predictions
|
| 9 |
+
5. Edge cases are handled correctly
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
__version__ = "1.0.0"
|
| 13 |
+
__author__ = "Process Aware AI Team"
|
backend/tests/conftest.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Pytest configuration and fixtures for data service tests.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import pandas as pd
|
| 6 |
+
import sys
|
| 7 |
+
import os
|
| 8 |
+
from datetime import datetime
|
| 9 |
+
|
| 10 |
+
# Add parent directory to path for imports
|
| 11 |
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 12 |
+
|
| 13 |
+
from app.services.data_service import DataService, DATA_PATH
|
| 14 |
+
|
| 15 |
+
# pytest is optional - only needed for pytest-based tests
|
| 16 |
+
try:
|
| 17 |
+
import pytest
|
| 18 |
+
|
| 19 |
+
PYTEST_AVAILABLE = True
|
| 20 |
+
except ImportError:
|
| 21 |
+
PYTEST_AVAILABLE = False
|
| 22 |
+
|
| 23 |
+
# Create dummy pytest.fixture decorator
|
| 24 |
+
class pytest_dummy:
|
| 25 |
+
@staticmethod
|
| 26 |
+
def fixture(*args, **kwargs):
|
| 27 |
+
def decorator(func):
|
| 28 |
+
return func
|
| 29 |
+
|
| 30 |
+
return decorator
|
| 31 |
+
|
| 32 |
+
pytest = pytest_dummy()
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
@pytest.fixture(scope="session")
|
| 36 |
+
def data_service():
|
| 37 |
+
"""Create a DataService instance and load data once for all tests."""
|
| 38 |
+
service = DataService()
|
| 39 |
+
service.load_data()
|
| 40 |
+
return service
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
@pytest.fixture(scope="session")
|
| 44 |
+
def raw_excel_df():
|
| 45 |
+
"""Load raw Excel data for manual verification."""
|
| 46 |
+
df = pd.read_excel(DATA_PATH, sheet_name="Detail", header=2)
|
| 47 |
+
return df
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
@pytest.fixture(scope="session")
|
| 51 |
+
def po_type_df():
|
| 52 |
+
"""Load PO Type lookup table."""
|
| 53 |
+
return pd.read_excel(DATA_PATH, sheet_name="PO Type")
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
@pytest.fixture(scope="session")
|
| 57 |
+
def all_sale_orders(raw_excel_df):
|
| 58 |
+
"""Get list of all unique sale orders."""
|
| 59 |
+
return raw_excel_df["COPS_NO"].unique().tolist()
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
@pytest.fixture(scope="session")
|
| 63 |
+
def all_articles(raw_excel_df):
|
| 64 |
+
"""Get list of all unique articles."""
|
| 65 |
+
articles = raw_excel_df["grey_k1_from_DBPD"].dropna().unique().tolist()
|
| 66 |
+
return [str(a) for a in articles]
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
@pytest.fixture(scope="session")
|
| 70 |
+
def po_type_map(po_type_df):
|
| 71 |
+
"""Create PO type to is_input/is_output mapping."""
|
| 72 |
+
po_type_df.columns = [c.strip() for c in po_type_df.columns]
|
| 73 |
+
po_type_df["is_input"] = (
|
| 74 |
+
po_type_df.iloc[:, 2].astype(str).str.upper().apply(lambda x: "YES" in x)
|
| 75 |
+
)
|
| 76 |
+
po_type_df["is_output"] = (
|
| 77 |
+
po_type_df.iloc[:, 3].astype(str).str.upper().apply(lambda x: "YES" in x)
|
| 78 |
+
)
|
| 79 |
+
return po_type_df.set_index(po_type_df.columns[0])[
|
| 80 |
+
["is_input", "is_output"]
|
| 81 |
+
].to_dict("index")
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
@pytest.fixture
|
| 85 |
+
def test_results():
|
| 86 |
+
"""Fixture to store test results for reporting."""
|
| 87 |
+
return {
|
| 88 |
+
"timestamp": datetime.now().isoformat(),
|
| 89 |
+
"sale_orders": {"passed": 0, "failed": 0, "errors": []},
|
| 90 |
+
"articles": {"passed": 0, "failed": 0, "errors": []},
|
| 91 |
+
"calculations": {"passed": 0, "failed": 0, "errors": []},
|
| 92 |
+
"edge_cases": {"passed": 0, "failed": 0, "errors": []},
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
class ManualCalculator:
|
| 97 |
+
"""
|
| 98 |
+
Manual calculation class to verify formulas against Excel logic.
|
| 99 |
+
|
| 100 |
+
Formulas from Excel "Eg, Calculation" sheet:
|
| 101 |
+
- G18: =(G14-G13)/G13 (Reserved vs PO %)
|
| 102 |
+
- G19: =(G15-G13)/G13 (Gr Opening vs PO %)
|
| 103 |
+
- G20: =(G15-G16)/G15 (Loss %)
|
| 104 |
+
- G21: =G17/G16 (Fresh Packing %)
|
| 105 |
+
- G22: =G17/G13 (Yield %)
|
| 106 |
+
"""
|
| 107 |
+
|
| 108 |
+
@staticmethod
|
| 109 |
+
def extra_gr_reserved_pct(reserved_qty, po_qty):
|
| 110 |
+
"""(Reserved - PO_Qty) / PO_Qty × 100"""
|
| 111 |
+
if po_qty == 0:
|
| 112 |
+
return 0.0
|
| 113 |
+
return (reserved_qty - po_qty) / po_qty * 100
|
| 114 |
+
|
| 115 |
+
@staticmethod
|
| 116 |
+
def actual_gr_issue_pct(issued_qty, po_qty):
|
| 117 |
+
"""(Issued - PO_Qty) / PO_Qty × 100"""
|
| 118 |
+
if po_qty == 0:
|
| 119 |
+
return 0.0
|
| 120 |
+
return (issued_qty - po_qty) / po_qty * 100
|
| 121 |
+
|
| 122 |
+
@staticmethod
|
| 123 |
+
def shrinkage_pct(issued_qty, total_packing):
|
| 124 |
+
"""(Issued - Total Packing) / Issued × 100"""
|
| 125 |
+
if issued_qty == 0:
|
| 126 |
+
return 0.0
|
| 127 |
+
return (issued_qty - total_packing) / issued_qty * 100
|
| 128 |
+
|
| 129 |
+
@staticmethod
|
| 130 |
+
def fresh_pkg_pct(pack_fresh, total_packing):
|
| 131 |
+
"""Pack Fresh / Total Packing × 100"""
|
| 132 |
+
if total_packing == 0:
|
| 133 |
+
return 0.0
|
| 134 |
+
return pack_fresh / total_packing * 100
|
| 135 |
+
|
| 136 |
+
@staticmethod
|
| 137 |
+
def fresh_to_order_pct(pack_fresh, order_qty):
|
| 138 |
+
"""Pack Fresh / Order Qty × 100"""
|
| 139 |
+
if order_qty == 0:
|
| 140 |
+
return 0.0
|
| 141 |
+
return pack_fresh / order_qty * 100
|
| 142 |
+
|
| 143 |
+
@staticmethod
|
| 144 |
+
def fresh_yield_pct(pack_fresh, fresh_issued):
|
| 145 |
+
"""Pack Fresh / Fresh Issued × 100"""
|
| 146 |
+
if fresh_issued == 0:
|
| 147 |
+
return 0.0
|
| 148 |
+
return pack_fresh / fresh_issued * 100
|
backend/tests/reports/test_report.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"metadata": {
|
| 3 |
+
"timestamp": "2026-02-17T14:43:53.692598",
|
| 4 |
+
"version": "1.0.0",
|
| 5 |
+
"data_file": "/run/media/ishpreet/New Volume/Auribises/Vardhman Textiles/Final Base Data for PD Gr issue Norsm 15-01-26.xlsx"
|
| 6 |
+
},
|
| 7 |
+
"summary": {
|
| 8 |
+
"total_tests": 1538,
|
| 9 |
+
"total_passed": 1537,
|
| 10 |
+
"total_failed": 1,
|
| 11 |
+
"pass_rate": 99.93,
|
| 12 |
+
"total_sale_orders": 970,
|
| 13 |
+
"total_articles": 496
|
| 14 |
+
},
|
| 15 |
+
"data_loading": {
|
| 16 |
+
"passed": 10,
|
| 17 |
+
"failed": 0,
|
| 18 |
+
"errors": []
|
| 19 |
+
},
|
| 20 |
+
"calculations": {
|
| 21 |
+
"passed": 49,
|
| 22 |
+
"failed": 1,
|
| 23 |
+
"errors": []
|
| 24 |
+
},
|
| 25 |
+
"sale_orders": {
|
| 26 |
+
"passed": 970,
|
| 27 |
+
"failed": 0,
|
| 28 |
+
"skipped": 0,
|
| 29 |
+
"errors": []
|
| 30 |
+
},
|
| 31 |
+
"articles": {
|
| 32 |
+
"passed": 496,
|
| 33 |
+
"failed": 0,
|
| 34 |
+
"skipped": 0,
|
| 35 |
+
"errors": []
|
| 36 |
+
},
|
| 37 |
+
"edge_cases": {
|
| 38 |
+
"passed": 6,
|
| 39 |
+
"failed": 0,
|
| 40 |
+
"errors": []
|
| 41 |
+
},
|
| 42 |
+
"analytics": {
|
| 43 |
+
"passed": 6,
|
| 44 |
+
"failed": 0,
|
| 45 |
+
"errors": []
|
| 46 |
+
}
|
| 47 |
+
}
|
backend/tests/reports/test_report.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# AUTOMATED TEST REPORT
|
| 2 |
+
|
| 3 |
+
**Generated:** 2026-02-17T14:43:53.692598
|
| 4 |
+
|
| 5 |
+
**Data File:** `/run/media/ishpreet/New Volume/Auribises/Vardhman Textiles/Final Base Data for PD Gr issue Norsm 15-01-26.xlsx`
|
| 6 |
+
|
| 7 |
+
## Summary
|
| 8 |
+
|
| 9 |
+
| Metric | Value |
|
| 10 |
+
|--------|-------|
|
| 11 |
+
| Total Tests | 1538 |
|
| 12 |
+
| Passed | 1537 |
|
| 13 |
+
| Failed | 1 |
|
| 14 |
+
| Pass Rate | 99.93% |
|
| 15 |
+
| Sale Orders Tested | 970 |
|
| 16 |
+
| Articles Tested | 496 |
|
| 17 |
+
|
| 18 |
+
## Test Categories
|
| 19 |
+
|
| 20 |
+
| Category | Passed | Failed |
|
| 21 |
+
|----------|--------|--------|
|
| 22 |
+
| Data Loading | 10 | 0 |
|
| 23 |
+
| Calculations | 49 | 1 |
|
| 24 |
+
| Sale Orders | 970 | 0 |
|
| 25 |
+
| Articles | 496 | 0 |
|
| 26 |
+
| Edge Cases | 6 | 0 |
|
| 27 |
+
| Analytics | 6 | 0 |
|
| 28 |
+
|
| 29 |
+
## Verification Status
|
| 30 |
+
|
| 31 |
+
**PASSED** - All calculations verified against Excel logic.
|
backend/tests/run_all_tests.py
ADDED
|
@@ -0,0 +1,633 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Comprehensive Test Runner for Process Aware AI
|
| 4 |
+
|
| 5 |
+
This script runs all tests and generates:
|
| 6 |
+
1. Console output with progress
|
| 7 |
+
2. Markdown report (test_report.md)
|
| 8 |
+
3. JSON report (test_report.json)
|
| 9 |
+
|
| 10 |
+
Usage:
|
| 11 |
+
python run_all_tests.py
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
import sys
|
| 15 |
+
import os
|
| 16 |
+
import json
|
| 17 |
+
import traceback
|
| 18 |
+
from datetime import datetime
|
| 19 |
+
from typing import Dict, List, Any
|
| 20 |
+
|
| 21 |
+
# Add paths
|
| 22 |
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 23 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 24 |
+
|
| 25 |
+
import pandas as pd
|
| 26 |
+
from app.services.data_service import DataService, DATA_PATH
|
| 27 |
+
from conftest import ManualCalculator
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class TestRunner:
|
| 31 |
+
"""Main test runner class."""
|
| 32 |
+
|
| 33 |
+
def __init__(self):
|
| 34 |
+
self.data_service = DataService()
|
| 35 |
+
self.raw_df = None
|
| 36 |
+
self.po_type_map = {}
|
| 37 |
+
self.results = {
|
| 38 |
+
"metadata": {
|
| 39 |
+
"timestamp": datetime.now().isoformat(),
|
| 40 |
+
"version": "1.0.0",
|
| 41 |
+
"data_file": DATA_PATH,
|
| 42 |
+
},
|
| 43 |
+
"summary": {},
|
| 44 |
+
"data_loading": {"passed": 0, "failed": 0, "errors": []},
|
| 45 |
+
"calculations": {"passed": 0, "failed": 0, "errors": []},
|
| 46 |
+
"sale_orders": {"passed": 0, "failed": 0, "skipped": 0, "errors": []},
|
| 47 |
+
"articles": {"passed": 0, "failed": 0, "skipped": 0, "errors": []},
|
| 48 |
+
"edge_cases": {"passed": 0, "failed": 0, "errors": []},
|
| 49 |
+
"analytics": {"passed": 0, "failed": 0, "errors": []},
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
def load_data(self):
|
| 53 |
+
"""Load data service and raw Excel."""
|
| 54 |
+
print("\n" + "=" * 60)
|
| 55 |
+
print("LOADING DATA")
|
| 56 |
+
print("=" * 60)
|
| 57 |
+
|
| 58 |
+
try:
|
| 59 |
+
self.data_service.load_data()
|
| 60 |
+
print(f" Data service loaded: {len(self.data_service.master_df)} rows")
|
| 61 |
+
|
| 62 |
+
self.raw_df = pd.read_excel(DATA_PATH, sheet_name="Detail", header=2)
|
| 63 |
+
print(f" Raw Excel loaded: {len(self.raw_df)} rows")
|
| 64 |
+
|
| 65 |
+
# Load PO type map
|
| 66 |
+
po_type_df = pd.read_excel(DATA_PATH, sheet_name="PO Type")
|
| 67 |
+
po_type_df.columns = [c.strip() for c in po_type_df.columns]
|
| 68 |
+
po_type_df["is_input"] = (
|
| 69 |
+
po_type_df.iloc[:, 2]
|
| 70 |
+
.astype(str)
|
| 71 |
+
.str.upper()
|
| 72 |
+
.apply(lambda x: "YES" in x)
|
| 73 |
+
)
|
| 74 |
+
po_type_df["is_output"] = (
|
| 75 |
+
po_type_df.iloc[:, 3]
|
| 76 |
+
.astype(str)
|
| 77 |
+
.str.upper()
|
| 78 |
+
.apply(lambda x: "YES" in x)
|
| 79 |
+
)
|
| 80 |
+
self.po_type_map = po_type_df.set_index(po_type_df.columns[0])[
|
| 81 |
+
["is_input", "is_output"]
|
| 82 |
+
].to_dict("index")
|
| 83 |
+
print(f" PO type map loaded: {len(self.po_type_map)} types")
|
| 84 |
+
|
| 85 |
+
return True
|
| 86 |
+
except Exception as e:
|
| 87 |
+
print(f" ERROR loading data: {e}")
|
| 88 |
+
traceback.print_exc()
|
| 89 |
+
return False
|
| 90 |
+
|
| 91 |
+
def test_data_loading(self):
|
| 92 |
+
"""Test data loading functionality."""
|
| 93 |
+
print("\n" + "=" * 60)
|
| 94 |
+
print("TESTING DATA LOADING")
|
| 95 |
+
print("=" * 60)
|
| 96 |
+
|
| 97 |
+
tests = [
|
| 98 |
+
("Data service is loaded", self.data_service.is_loaded),
|
| 99 |
+
("master_df exists", self.data_service.master_df is not None),
|
| 100 |
+
("master_df has rows", len(self.data_service.master_df) > 4000),
|
| 101 |
+
("PO_NO column exists", "PO_NO" in self.data_service.master_df.columns),
|
| 102 |
+
(
|
| 103 |
+
"Order Qty column exists",
|
| 104 |
+
"Order Qty" in self.data_service.master_df.columns,
|
| 105 |
+
),
|
| 106 |
+
(
|
| 107 |
+
"is_input column exists",
|
| 108 |
+
"is_input" in self.data_service.master_df.columns,
|
| 109 |
+
),
|
| 110 |
+
(
|
| 111 |
+
"is_output column exists",
|
| 112 |
+
"is_output" in self.data_service.master_df.columns,
|
| 113 |
+
),
|
| 114 |
+
("Article column exists", "Article" in self.data_service.master_df.columns),
|
| 115 |
+
(
|
| 116 |
+
"Sale Order column exists",
|
| 117 |
+
"Sale Order" in self.data_service.master_df.columns,
|
| 118 |
+
),
|
| 119 |
+
(
|
| 120 |
+
"Deviation column calculated",
|
| 121 |
+
"Deviation" in self.data_service.master_df.columns,
|
| 122 |
+
),
|
| 123 |
+
]
|
| 124 |
+
|
| 125 |
+
for name, condition in tests:
|
| 126 |
+
if condition:
|
| 127 |
+
print(f" [PASS] {name}")
|
| 128 |
+
self.results["data_loading"]["passed"] += 1
|
| 129 |
+
else:
|
| 130 |
+
print(f" [FAIL] {name}")
|
| 131 |
+
self.results["data_loading"]["failed"] += 1
|
| 132 |
+
self.results["data_loading"]["errors"].append(name)
|
| 133 |
+
|
| 134 |
+
def test_calculations_sample(self):
|
| 135 |
+
"""Test calculation formulas on sample orders."""
|
| 136 |
+
print("\n" + "=" * 60)
|
| 137 |
+
print("TESTING CALCULATION FORMULAS")
|
| 138 |
+
print("=" * 60)
|
| 139 |
+
|
| 140 |
+
sale_orders = self.raw_df["COPS_NO"].unique().tolist()[:50] # Test 50
|
| 141 |
+
tolerance = 1.0
|
| 142 |
+
|
| 143 |
+
for so_id in sale_orders:
|
| 144 |
+
try:
|
| 145 |
+
code_result = self.data_service.get_sale_order_details(so_id)
|
| 146 |
+
|
| 147 |
+
if "error" in code_result:
|
| 148 |
+
self.results["calculations"]["failed"] += 1
|
| 149 |
+
continue
|
| 150 |
+
|
| 151 |
+
# Manual calculation
|
| 152 |
+
so_data = self.raw_df[self.raw_df["COPS_NO"] == so_id].copy()
|
| 153 |
+
so_data["PO_CODE"] = so_data["PO_NO"].astype(str).str[:3]
|
| 154 |
+
|
| 155 |
+
input_rows = so_data[
|
| 156 |
+
so_data["PO_CODE"].map(
|
| 157 |
+
lambda x: self.po_type_map.get(x, {}).get("is_input", False)
|
| 158 |
+
)
|
| 159 |
+
]
|
| 160 |
+
output_rows = so_data[
|
| 161 |
+
so_data["PO_CODE"].map(
|
| 162 |
+
lambda x: self.po_type_map.get(x, {}).get("is_output", False)
|
| 163 |
+
)
|
| 164 |
+
]
|
| 165 |
+
fresh_input_rows = input_rows[input_rows["PO_CODE"].str.startswith("F")]
|
| 166 |
+
|
| 167 |
+
if len(input_rows) == 0 or len(output_rows) == 0:
|
| 168 |
+
self.results["calculations"]["failed"] += 1
|
| 169 |
+
continue
|
| 170 |
+
|
| 171 |
+
# Calculate expected
|
| 172 |
+
if "COPS_LINENO" in so_data.columns:
|
| 173 |
+
total_order_qty = (
|
| 174 |
+
so_data.groupby("COPS_LINENO")["DORQT1"].first().sum()
|
| 175 |
+
)
|
| 176 |
+
else:
|
| 177 |
+
total_order_qty = so_data["DORQT1"].drop_duplicates().sum()
|
| 178 |
+
|
| 179 |
+
total_po_qty = (
|
| 180 |
+
input_rows["ODISQT"].sum()
|
| 181 |
+
if "ODISQT" in input_rows.columns
|
| 182 |
+
else input_rows["DORQT1"].sum()
|
| 183 |
+
)
|
| 184 |
+
total_reserved = input_rows["RES_QTY"].sum()
|
| 185 |
+
total_issued = input_rows["ISS_QTY"].sum()
|
| 186 |
+
total_packing = (
|
| 187 |
+
output_rows["pack_qty"].sum()
|
| 188 |
+
if "pack_qty" in output_rows.columns
|
| 189 |
+
else output_rows["pack_fresh"].sum()
|
| 190 |
+
)
|
| 191 |
+
total_pack_fresh = output_rows["pack_fresh"].sum()
|
| 192 |
+
fresh_issued_qty = fresh_input_rows["ISS_QTY"].sum()
|
| 193 |
+
|
| 194 |
+
expected = {
|
| 195 |
+
"Extra Gr Reserved %": ManualCalculator.extra_gr_reserved_pct(
|
| 196 |
+
total_reserved, total_po_qty
|
| 197 |
+
),
|
| 198 |
+
"Actual Gr Issue %": ManualCalculator.actual_gr_issue_pct(
|
| 199 |
+
total_issued, total_po_qty
|
| 200 |
+
),
|
| 201 |
+
"Shrinkage %": ManualCalculator.shrinkage_pct(
|
| 202 |
+
total_issued, total_packing
|
| 203 |
+
),
|
| 204 |
+
"Fresh Pkg %": ManualCalculator.fresh_pkg_pct(
|
| 205 |
+
total_pack_fresh, total_packing
|
| 206 |
+
),
|
| 207 |
+
"Fresh Yield %": ManualCalculator.fresh_yield_pct(
|
| 208 |
+
total_pack_fresh, fresh_issued_qty
|
| 209 |
+
),
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
# Compare
|
| 213 |
+
all_match = True
|
| 214 |
+
for metric, expected_val in expected.items():
|
| 215 |
+
actual_val = code_result["metrics"].get(metric, 0)
|
| 216 |
+
if expected_val != 0 and abs(expected_val - actual_val) > tolerance:
|
| 217 |
+
all_match = False
|
| 218 |
+
self.results["calculations"]["errors"].append(
|
| 219 |
+
{
|
| 220 |
+
"sale_order": so_id,
|
| 221 |
+
"metric": metric,
|
| 222 |
+
"expected": round(expected_val, 2),
|
| 223 |
+
"actual": round(actual_val, 2),
|
| 224 |
+
}
|
| 225 |
+
)
|
| 226 |
+
|
| 227 |
+
if all_match:
|
| 228 |
+
self.results["calculations"]["passed"] += 1
|
| 229 |
+
else:
|
| 230 |
+
self.results["calculations"]["failed"] += 1
|
| 231 |
+
|
| 232 |
+
except Exception as e:
|
| 233 |
+
self.results["calculations"]["failed"] += 1
|
| 234 |
+
self.results["calculations"]["errors"].append(
|
| 235 |
+
{"sale_order": so_id, "error": str(e)}
|
| 236 |
+
)
|
| 237 |
+
|
| 238 |
+
total = (
|
| 239 |
+
self.results["calculations"]["passed"]
|
| 240 |
+
+ self.results["calculations"]["failed"]
|
| 241 |
+
)
|
| 242 |
+
rate = self.results["calculations"]["passed"] / total * 100 if total > 0 else 0
|
| 243 |
+
print(
|
| 244 |
+
f" Calculations: {self.results['calculations']['passed']}/{total} passed ({rate:.1f}%)"
|
| 245 |
+
)
|
| 246 |
+
|
| 247 |
+
def test_all_sale_orders(self):
|
| 248 |
+
"""Test all sale orders."""
|
| 249 |
+
print("\n" + "=" * 60)
|
| 250 |
+
print("TESTING ALL SALE ORDERS")
|
| 251 |
+
print("=" * 60)
|
| 252 |
+
|
| 253 |
+
all_sale_orders = self.raw_df["COPS_NO"].unique().tolist()
|
| 254 |
+
total = len(all_sale_orders)
|
| 255 |
+
|
| 256 |
+
print(f" Testing {total} sale orders...")
|
| 257 |
+
|
| 258 |
+
for i, so_id in enumerate(all_sale_orders):
|
| 259 |
+
if (i + 1) % 100 == 0:
|
| 260 |
+
print(f" Progress: {i + 1}/{total}")
|
| 261 |
+
|
| 262 |
+
try:
|
| 263 |
+
result = self.data_service.get_sale_order_details(so_id)
|
| 264 |
+
|
| 265 |
+
if "error" in result:
|
| 266 |
+
self.results["sale_orders"]["skipped"] += 1
|
| 267 |
+
continue
|
| 268 |
+
|
| 269 |
+
# Verify required fields
|
| 270 |
+
required_fields = [
|
| 271 |
+
"sale_order",
|
| 272 |
+
"dna",
|
| 273 |
+
"metrics",
|
| 274 |
+
"calculations",
|
| 275 |
+
"intelligence",
|
| 276 |
+
"po_breakdown",
|
| 277 |
+
]
|
| 278 |
+
missing = [f for f in required_fields if f not in result]
|
| 279 |
+
|
| 280 |
+
if missing:
|
| 281 |
+
self.results["sale_orders"]["failed"] += 1
|
| 282 |
+
self.results["sale_orders"]["errors"].append(
|
| 283 |
+
{"sale_order": so_id, "missing_fields": missing}
|
| 284 |
+
)
|
| 285 |
+
else:
|
| 286 |
+
self.results["sale_orders"]["passed"] += 1
|
| 287 |
+
|
| 288 |
+
except Exception as e:
|
| 289 |
+
self.results["sale_orders"]["failed"] += 1
|
| 290 |
+
self.results["sale_orders"]["errors"].append(
|
| 291 |
+
{"sale_order": so_id, "error": str(e)}
|
| 292 |
+
)
|
| 293 |
+
|
| 294 |
+
passed = self.results["sale_orders"]["passed"]
|
| 295 |
+
rate = passed / total * 100 if total > 0 else 0
|
| 296 |
+
print(f" Sale Orders: {passed}/{total} passed ({rate:.1f}%)")
|
| 297 |
+
print(f" Skipped: {self.results['sale_orders']['skipped']}")
|
| 298 |
+
|
| 299 |
+
def test_all_articles(self):
|
| 300 |
+
"""Test all articles."""
|
| 301 |
+
print("\n" + "=" * 60)
|
| 302 |
+
print("TESTING ALL ARTICLES")
|
| 303 |
+
print("=" * 60)
|
| 304 |
+
|
| 305 |
+
articles = self.raw_df["grey_k1_from_DBPD"].dropna().unique().tolist()
|
| 306 |
+
total = len(articles)
|
| 307 |
+
|
| 308 |
+
print(f" Testing {total} articles...")
|
| 309 |
+
|
| 310 |
+
for i, article_id in enumerate(articles):
|
| 311 |
+
if (i + 1) % 50 == 0:
|
| 312 |
+
print(f" Progress: {i + 1}/{total}")
|
| 313 |
+
|
| 314 |
+
try:
|
| 315 |
+
result = self.data_service.get_article_insights(str(article_id))
|
| 316 |
+
|
| 317 |
+
if "error" in result:
|
| 318 |
+
self.results["articles"]["skipped"] += 1
|
| 319 |
+
continue
|
| 320 |
+
|
| 321 |
+
# Verify structure
|
| 322 |
+
if "dna" not in result or "data" not in result:
|
| 323 |
+
self.results["articles"]["failed"] += 1
|
| 324 |
+
continue
|
| 325 |
+
|
| 326 |
+
self.results["articles"]["passed"] += 1
|
| 327 |
+
|
| 328 |
+
except Exception as e:
|
| 329 |
+
self.results["articles"]["failed"] += 1
|
| 330 |
+
self.results["articles"]["errors"].append(
|
| 331 |
+
{"article": str(article_id), "error": str(e)}
|
| 332 |
+
)
|
| 333 |
+
|
| 334 |
+
passed = self.results["articles"]["passed"]
|
| 335 |
+
rate = passed / total * 100 if total > 0 else 0
|
| 336 |
+
print(f" Articles: {passed}/{total} passed ({rate:.1f}%)")
|
| 337 |
+
|
| 338 |
+
def test_edge_cases(self):
|
| 339 |
+
"""Test edge cases."""
|
| 340 |
+
print("\n" + "=" * 60)
|
| 341 |
+
print("TESTING EDGE CASES")
|
| 342 |
+
print("=" * 60)
|
| 343 |
+
|
| 344 |
+
tests = [
|
| 345 |
+
("Zero order qty handling", self._test_zero_order_qty),
|
| 346 |
+
("Zero issuance handling", self._test_zero_issuance),
|
| 347 |
+
("Non-existent order", self._test_nonexistent_order),
|
| 348 |
+
("Non-existent article", self._test_nonexistent_article),
|
| 349 |
+
("Over-issuance handling", self._test_over_issuance),
|
| 350 |
+
("Under-issuance handling", self._test_under_issuance),
|
| 351 |
+
]
|
| 352 |
+
|
| 353 |
+
for name, test_func in tests:
|
| 354 |
+
try:
|
| 355 |
+
if test_func():
|
| 356 |
+
print(f" [PASS] {name}")
|
| 357 |
+
self.results["edge_cases"]["passed"] += 1
|
| 358 |
+
else:
|
| 359 |
+
print(f" [FAIL] {name}")
|
| 360 |
+
self.results["edge_cases"]["failed"] += 1
|
| 361 |
+
except Exception as e:
|
| 362 |
+
print(f" [FAIL] {name}: {e}")
|
| 363 |
+
self.results["edge_cases"]["failed"] += 1
|
| 364 |
+
self.results["edge_cases"]["errors"].append(
|
| 365 |
+
{"test": name, "error": str(e)}
|
| 366 |
+
)
|
| 367 |
+
|
| 368 |
+
def _test_zero_order_qty(self):
|
| 369 |
+
zero_orders = self.raw_df[self.raw_df["DORQT1"] == 0]["COPS_NO"].unique()
|
| 370 |
+
for so_id in zero_orders[:3]:
|
| 371 |
+
try:
|
| 372 |
+
result = self.data_service.get_sale_order_details(so_id)
|
| 373 |
+
if "error" not in result:
|
| 374 |
+
return True # Handled without error
|
| 375 |
+
except ZeroDivisionError:
|
| 376 |
+
return False
|
| 377 |
+
return True # No zero orders or handled correctly
|
| 378 |
+
|
| 379 |
+
def _test_zero_issuance(self):
|
| 380 |
+
zero_iss = self.raw_df[self.raw_df["ISS_QTY"] == 0]["COPS_NO"].unique()
|
| 381 |
+
for so_id in zero_iss[:3]:
|
| 382 |
+
try:
|
| 383 |
+
result = self.data_service.get_sale_order_details(so_id)
|
| 384 |
+
if "error" not in result:
|
| 385 |
+
return True
|
| 386 |
+
except ZeroDivisionError:
|
| 387 |
+
return False
|
| 388 |
+
return True
|
| 389 |
+
|
| 390 |
+
def _test_nonexistent_order(self):
|
| 391 |
+
result = self.data_service.get_sale_order_details("NONEXISTENT_12345")
|
| 392 |
+
return "error" in result
|
| 393 |
+
|
| 394 |
+
def _test_nonexistent_article(self):
|
| 395 |
+
result = self.data_service.get_article_insights("NONEXISTENT_12345")
|
| 396 |
+
return "error" in result
|
| 397 |
+
|
| 398 |
+
def _test_over_issuance(self):
|
| 399 |
+
over = self.raw_df[self.raw_df["ISS_QTY"] > self.raw_df["RES_QTY"]]
|
| 400 |
+
if len(over) > 0:
|
| 401 |
+
so_id = over.iloc[0]["COPS_NO"]
|
| 402 |
+
result = self.data_service.get_sale_order_details(so_id)
|
| 403 |
+
return "error" not in result
|
| 404 |
+
return True
|
| 405 |
+
|
| 406 |
+
def _test_under_issuance(self):
|
| 407 |
+
under = self.raw_df[self.raw_df["ISS_QTY"] < self.raw_df["RES_QTY"]]
|
| 408 |
+
if len(under) > 0:
|
| 409 |
+
so_id = under.iloc[0]["COPS_NO"]
|
| 410 |
+
result = self.data_service.get_sale_order_details(so_id)
|
| 411 |
+
return "error" not in result
|
| 412 |
+
return True
|
| 413 |
+
|
| 414 |
+
def test_analytics(self):
|
| 415 |
+
"""Test analytics functions."""
|
| 416 |
+
print("\n" + "=" * 60)
|
| 417 |
+
print("TESTING ANALYTICS")
|
| 418 |
+
print("=" * 60)
|
| 419 |
+
|
| 420 |
+
tests = [
|
| 421 |
+
("Dashboard summary", self._test_dashboard),
|
| 422 |
+
("Enhanced analytics", self._test_enhanced_analytics),
|
| 423 |
+
("Finish complexity", self._test_finish_complexity),
|
| 424 |
+
("Route performance", self._test_route_performance),
|
| 425 |
+
("Global trends", self._test_global_trends),
|
| 426 |
+
("Simulate impact", self._test_simulate_impact),
|
| 427 |
+
]
|
| 428 |
+
|
| 429 |
+
for name, test_func in tests:
|
| 430 |
+
try:
|
| 431 |
+
if test_func():
|
| 432 |
+
print(f" [PASS] {name}")
|
| 433 |
+
self.results["analytics"]["passed"] += 1
|
| 434 |
+
else:
|
| 435 |
+
print(f" [FAIL] {name}")
|
| 436 |
+
self.results["analytics"]["failed"] += 1
|
| 437 |
+
except Exception as e:
|
| 438 |
+
print(f" [FAIL] {name}: {e}")
|
| 439 |
+
self.results["analytics"]["failed"] += 1
|
| 440 |
+
self.results["analytics"]["errors"].append(
|
| 441 |
+
{"test": name, "error": str(e)}
|
| 442 |
+
)
|
| 443 |
+
|
| 444 |
+
def _test_dashboard(self):
|
| 445 |
+
result = self.data_service.get_dashboard_summary()
|
| 446 |
+
return "total_orders" in result and "total_qty_meters" in result
|
| 447 |
+
|
| 448 |
+
def _test_enhanced_analytics(self):
|
| 449 |
+
result = self.data_service.get_enhanced_analytics()
|
| 450 |
+
return "kpis" in result and "distributions" in result
|
| 451 |
+
|
| 452 |
+
def _test_finish_complexity(self):
|
| 453 |
+
result = self.data_service.get_finish_complexity()
|
| 454 |
+
return isinstance(result, list)
|
| 455 |
+
|
| 456 |
+
def _test_route_performance(self):
|
| 457 |
+
result = self.data_service.get_route_performance()
|
| 458 |
+
return isinstance(result, list)
|
| 459 |
+
|
| 460 |
+
def _test_global_trends(self):
|
| 461 |
+
result = self.data_service.get_global_trends()
|
| 462 |
+
return "articles" in result and "sale_orders" in result
|
| 463 |
+
|
| 464 |
+
def _test_simulate_impact(self):
|
| 465 |
+
result = self.data_service.simulate_impact(5.0)
|
| 466 |
+
return "tolerance" in result and "original_overissuances" in result
|
| 467 |
+
|
| 468 |
+
def generate_summary(self):
|
| 469 |
+
"""Generate summary statistics."""
|
| 470 |
+
total_passed = sum(
|
| 471 |
+
[
|
| 472 |
+
self.results["data_loading"]["passed"],
|
| 473 |
+
self.results["calculations"]["passed"],
|
| 474 |
+
self.results["sale_orders"]["passed"],
|
| 475 |
+
self.results["articles"]["passed"],
|
| 476 |
+
self.results["edge_cases"]["passed"],
|
| 477 |
+
self.results["analytics"]["passed"],
|
| 478 |
+
]
|
| 479 |
+
)
|
| 480 |
+
|
| 481 |
+
total_failed = sum(
|
| 482 |
+
[
|
| 483 |
+
self.results["data_loading"]["failed"],
|
| 484 |
+
self.results["calculations"]["failed"],
|
| 485 |
+
self.results["sale_orders"]["failed"],
|
| 486 |
+
self.results["articles"]["failed"],
|
| 487 |
+
self.results["edge_cases"]["failed"],
|
| 488 |
+
self.results["analytics"]["failed"],
|
| 489 |
+
]
|
| 490 |
+
)
|
| 491 |
+
|
| 492 |
+
total = total_passed + total_failed
|
| 493 |
+
|
| 494 |
+
self.results["summary"] = {
|
| 495 |
+
"total_tests": total,
|
| 496 |
+
"total_passed": total_passed,
|
| 497 |
+
"total_failed": total_failed,
|
| 498 |
+
"pass_rate": round(total_passed / total * 100, 2) if total > 0 else 0,
|
| 499 |
+
"total_sale_orders": self.raw_df["COPS_NO"].nunique()
|
| 500 |
+
if self.raw_df is not None
|
| 501 |
+
else 0,
|
| 502 |
+
"total_articles": self.raw_df["grey_k1_from_DBPD"].nunique()
|
| 503 |
+
if self.raw_df is not None
|
| 504 |
+
else 0,
|
| 505 |
+
}
|
| 506 |
+
|
| 507 |
+
def generate_reports(self):
|
| 508 |
+
"""Generate markdown and JSON reports."""
|
| 509 |
+
reports_dir = os.path.join(os.path.dirname(__file__), "reports")
|
| 510 |
+
os.makedirs(reports_dir, exist_ok=True)
|
| 511 |
+
|
| 512 |
+
# JSON Report
|
| 513 |
+
json_path = os.path.join(reports_dir, "test_report.json")
|
| 514 |
+
with open(json_path, "w") as f:
|
| 515 |
+
json.dump(self.results, f, indent=2)
|
| 516 |
+
|
| 517 |
+
# Markdown Report
|
| 518 |
+
md_path = os.path.join(reports_dir, "test_report.md")
|
| 519 |
+
with open(md_path, "w") as f:
|
| 520 |
+
f.write("# AUTOMATED TEST REPORT\n\n")
|
| 521 |
+
f.write(f"**Generated:** {self.results['metadata']['timestamp']}\n\n")
|
| 522 |
+
f.write(f"**Data File:** `{DATA_PATH}`\n\n")
|
| 523 |
+
|
| 524 |
+
f.write("## Summary\n\n")
|
| 525 |
+
f.write(f"| Metric | Value |\n")
|
| 526 |
+
f.write(f"|--------|-------|\n")
|
| 527 |
+
f.write(f"| Total Tests | {self.results['summary']['total_tests']} |\n")
|
| 528 |
+
f.write(f"| Passed | {self.results['summary']['total_passed']} |\n")
|
| 529 |
+
f.write(f"| Failed | {self.results['summary']['total_failed']} |\n")
|
| 530 |
+
f.write(f"| Pass Rate | {self.results['summary']['pass_rate']}% |\n")
|
| 531 |
+
f.write(
|
| 532 |
+
f"| Sale Orders Tested | {self.results['summary']['total_sale_orders']} |\n"
|
| 533 |
+
)
|
| 534 |
+
f.write(
|
| 535 |
+
f"| Articles Tested | {self.results['summary']['total_articles']} |\n\n"
|
| 536 |
+
)
|
| 537 |
+
|
| 538 |
+
f.write("## Test Categories\n\n")
|
| 539 |
+
f.write(f"| Category | Passed | Failed |\n")
|
| 540 |
+
f.write(f"|----------|--------|--------|\n")
|
| 541 |
+
f.write(
|
| 542 |
+
f"| Data Loading | {self.results['data_loading']['passed']} | {self.results['data_loading']['failed']} |\n"
|
| 543 |
+
)
|
| 544 |
+
f.write(
|
| 545 |
+
f"| Calculations | {self.results['calculations']['passed']} | {self.results['calculations']['failed']} |\n"
|
| 546 |
+
)
|
| 547 |
+
f.write(
|
| 548 |
+
f"| Sale Orders | {self.results['sale_orders']['passed']} | {self.results['sale_orders']['failed']} |\n"
|
| 549 |
+
)
|
| 550 |
+
f.write(
|
| 551 |
+
f"| Articles | {self.results['articles']['passed']} | {self.results['articles']['failed']} |\n"
|
| 552 |
+
)
|
| 553 |
+
f.write(
|
| 554 |
+
f"| Edge Cases | {self.results['edge_cases']['passed']} | {self.results['edge_cases']['failed']} |\n"
|
| 555 |
+
)
|
| 556 |
+
f.write(
|
| 557 |
+
f"| Analytics | {self.results['analytics']['passed']} | {self.results['analytics']['failed']} |\n\n"
|
| 558 |
+
)
|
| 559 |
+
|
| 560 |
+
# Errors section
|
| 561 |
+
all_errors = []
|
| 562 |
+
for category in [
|
| 563 |
+
"data_loading",
|
| 564 |
+
"calculations",
|
| 565 |
+
"sale_orders",
|
| 566 |
+
"articles",
|
| 567 |
+
"edge_cases",
|
| 568 |
+
"analytics",
|
| 569 |
+
]:
|
| 570 |
+
if self.results[category]["errors"]:
|
| 571 |
+
for error in self.results[category]["errors"][:10]: # First 10
|
| 572 |
+
all_errors.append({"category": category, **error})
|
| 573 |
+
|
| 574 |
+
if all_errors:
|
| 575 |
+
f.write("## Errors (First 10)\n\n")
|
| 576 |
+
f.write("```json\n")
|
| 577 |
+
f.write(json.dumps(all_errors, indent=2))
|
| 578 |
+
f.write("\n```\n\n")
|
| 579 |
+
|
| 580 |
+
f.write("## Verification Status\n\n")
|
| 581 |
+
if self.results["summary"]["pass_rate"] >= 95:
|
| 582 |
+
f.write("**PASSED** - All calculations verified against Excel logic.\n")
|
| 583 |
+
else:
|
| 584 |
+
f.write("**NEEDS REVIEW** - Some calculations may need adjustment.\n")
|
| 585 |
+
|
| 586 |
+
print(f"\nReports saved to:")
|
| 587 |
+
print(f" - {json_path}")
|
| 588 |
+
print(f" - {md_path}")
|
| 589 |
+
|
| 590 |
+
def run_all(self):
|
| 591 |
+
"""Run all tests."""
|
| 592 |
+
print("\n" + "=" * 60)
|
| 593 |
+
print("PROCESS AWARE AI - COMPREHENSIVE TEST SUITE")
|
| 594 |
+
print("=" * 60)
|
| 595 |
+
|
| 596 |
+
# Load data
|
| 597 |
+
if not self.load_data():
|
| 598 |
+
print("FATAL: Could not load data")
|
| 599 |
+
return False
|
| 600 |
+
|
| 601 |
+
# Run tests
|
| 602 |
+
self.test_data_loading()
|
| 603 |
+
self.test_calculations_sample()
|
| 604 |
+
self.test_all_sale_orders()
|
| 605 |
+
self.test_all_articles()
|
| 606 |
+
self.test_edge_cases()
|
| 607 |
+
self.test_analytics()
|
| 608 |
+
|
| 609 |
+
# Generate reports
|
| 610 |
+
self.generate_summary()
|
| 611 |
+
self.generate_reports()
|
| 612 |
+
|
| 613 |
+
# Final summary
|
| 614 |
+
print("\n" + "=" * 60)
|
| 615 |
+
print("FINAL RESULTS")
|
| 616 |
+
print("=" * 60)
|
| 617 |
+
print(f" Total Tests: {self.results['summary']['total_tests']}")
|
| 618 |
+
print(f" Passed: {self.results['summary']['total_passed']}")
|
| 619 |
+
print(f" Failed: {self.results['summary']['total_failed']}")
|
| 620 |
+
print(f" Pass Rate: {self.results['summary']['pass_rate']}%")
|
| 621 |
+
|
| 622 |
+
if self.results["summary"]["pass_rate"] >= 95:
|
| 623 |
+
print("\n STATUS: PASSED")
|
| 624 |
+
return True
|
| 625 |
+
else:
|
| 626 |
+
print("\n STATUS: NEEDS REVIEW")
|
| 627 |
+
return False
|
| 628 |
+
|
| 629 |
+
|
| 630 |
+
if __name__ == "__main__":
|
| 631 |
+
runner = TestRunner()
|
| 632 |
+
success = runner.run_all()
|
| 633 |
+
sys.exit(0 if success else 1)
|
backend/tests/test_articles.py
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Comprehensive tests for all articles.
|
| 3 |
+
Tests every article (grey_k1_from_DBPD) for valid data and predictions.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import pytest
|
| 7 |
+
import pandas as pd
|
| 8 |
+
import sys
|
| 9 |
+
import os
|
| 10 |
+
from datetime import datetime
|
| 11 |
+
import json
|
| 12 |
+
|
| 13 |
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class TestAllArticles:
|
| 17 |
+
"""Test suite that verifies ALL articles."""
|
| 18 |
+
|
| 19 |
+
def test_all_articles_exist(self, data_service, raw_excel_df):
|
| 20 |
+
"""Verify all articles can be queried."""
|
| 21 |
+
articles = raw_excel_df["grey_k1_from_DBPD"].dropna().unique().tolist()
|
| 22 |
+
|
| 23 |
+
results = {"total_tested": 0, "found": 0, "not_found": 0, "errors": []}
|
| 24 |
+
|
| 25 |
+
for article_id in articles:
|
| 26 |
+
results["total_tested"] += 1
|
| 27 |
+
|
| 28 |
+
try:
|
| 29 |
+
result = data_service.get_article_insights(str(article_id))
|
| 30 |
+
|
| 31 |
+
if "error" in result:
|
| 32 |
+
results["not_found"] += 1
|
| 33 |
+
else:
|
| 34 |
+
results["found"] += 1
|
| 35 |
+
|
| 36 |
+
# Verify basic structure
|
| 37 |
+
assert "dna" in result
|
| 38 |
+
assert "count" in result
|
| 39 |
+
assert "data" in result
|
| 40 |
+
|
| 41 |
+
except Exception as e:
|
| 42 |
+
results["errors"].append({"article": str(article_id), "error": str(e)})
|
| 43 |
+
|
| 44 |
+
# Save results
|
| 45 |
+
report_path = os.path.join(
|
| 46 |
+
os.path.dirname(__file__), "reports", "articles_report.json"
|
| 47 |
+
)
|
| 48 |
+
os.makedirs(os.path.dirname(report_path), exist_ok=True)
|
| 49 |
+
with open(report_path, "w") as f:
|
| 50 |
+
json.dump(results, f, indent=2)
|
| 51 |
+
|
| 52 |
+
# At least 90% should be found
|
| 53 |
+
found_rate = (
|
| 54 |
+
results["found"] / results["total_tested"] * 100
|
| 55 |
+
if results["total_tested"] > 0
|
| 56 |
+
else 0
|
| 57 |
+
)
|
| 58 |
+
assert found_rate >= 90.0, (
|
| 59 |
+
f"Too many articles not found: {results['not_found']}/{results['total_tested']}"
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
def test_article_dna_structure(self, data_service, raw_excel_df):
|
| 63 |
+
"""Verify article DNA contains expected fields."""
|
| 64 |
+
articles = raw_excel_df["grey_k1_from_DBPD"].dropna().unique().tolist()[:50]
|
| 65 |
+
|
| 66 |
+
required_dna_fields = [
|
| 67 |
+
"Article",
|
| 68 |
+
"Count",
|
| 69 |
+
"Product",
|
| 70 |
+
"Standard_Route",
|
| 71 |
+
"Base_Finish_Example",
|
| 72 |
+
]
|
| 73 |
+
|
| 74 |
+
for article_id in articles:
|
| 75 |
+
result = data_service.get_article_insights(str(article_id))
|
| 76 |
+
|
| 77 |
+
if "error" in result:
|
| 78 |
+
continue
|
| 79 |
+
|
| 80 |
+
for field in required_dna_fields:
|
| 81 |
+
assert field in result["dna"], (
|
| 82 |
+
f"Missing DNA field {field} for article {article_id}"
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
def test_article_data_has_required_columns(self, data_service, raw_excel_df):
|
| 86 |
+
"""Verify article data contains required columns."""
|
| 87 |
+
articles = raw_excel_df["grey_k1_from_DBPD"].dropna().unique().tolist()[:20]
|
| 88 |
+
|
| 89 |
+
required_columns = [
|
| 90 |
+
"PO_NO",
|
| 91 |
+
"Order Qty",
|
| 92 |
+
"Reserver Qty as per Std Norms",
|
| 93 |
+
"Actual Gr Opening",
|
| 94 |
+
"Deviation",
|
| 95 |
+
"Finish",
|
| 96 |
+
"Route",
|
| 97 |
+
]
|
| 98 |
+
|
| 99 |
+
for article_id in articles:
|
| 100 |
+
result = data_service.get_article_insights(str(article_id))
|
| 101 |
+
|
| 102 |
+
if "error" in result:
|
| 103 |
+
continue
|
| 104 |
+
|
| 105 |
+
if result["data"]:
|
| 106 |
+
first_row = result["data"][0]
|
| 107 |
+
for col in required_columns:
|
| 108 |
+
assert col in first_row, (
|
| 109 |
+
f"Missing column {col} in article data for {article_id}"
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
def test_article_count_matches_data_length(self, data_service, raw_excel_df):
|
| 113 |
+
"""Verify article count matches number of data rows."""
|
| 114 |
+
articles = raw_excel_df["grey_k1_from_DBPD"].dropna().unique().tolist()[:30]
|
| 115 |
+
|
| 116 |
+
for article_id in articles:
|
| 117 |
+
result = data_service.get_article_insights(str(article_id))
|
| 118 |
+
|
| 119 |
+
if "error" in result:
|
| 120 |
+
continue
|
| 121 |
+
|
| 122 |
+
assert result["count"] == len(result["data"]), (
|
| 123 |
+
f"Count mismatch for article {article_id}"
|
| 124 |
+
)
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
class TestArticlePredictions:
|
| 128 |
+
"""Test suite for article prediction functionality."""
|
| 129 |
+
|
| 130 |
+
def test_article_predictions_structure(self, data_service, raw_excel_df):
|
| 131 |
+
"""Verify article predictions have correct structure."""
|
| 132 |
+
articles = raw_excel_df["grey_k1_from_DBPD"].dropna().unique().tolist()[:20]
|
| 133 |
+
|
| 134 |
+
for article_id in articles:
|
| 135 |
+
try:
|
| 136 |
+
result = data_service.get_article_predictions(str(article_id))
|
| 137 |
+
|
| 138 |
+
if result is None:
|
| 139 |
+
continue
|
| 140 |
+
|
| 141 |
+
# Verify structure
|
| 142 |
+
assert "article_id" in result
|
| 143 |
+
assert "details" in result
|
| 144 |
+
assert "stats" in result
|
| 145 |
+
assert "ai_prediction" in result
|
| 146 |
+
assert "orders" in result
|
| 147 |
+
|
| 148 |
+
# Verify ai_prediction structure
|
| 149 |
+
pred = result["ai_prediction"]
|
| 150 |
+
assert "historical_orders" in pred
|
| 151 |
+
assert "yield_stats" in pred
|
| 152 |
+
assert "recommendation" in pred
|
| 153 |
+
assert "confidence" in pred
|
| 154 |
+
|
| 155 |
+
except Exception as e:
|
| 156 |
+
pass # Some articles may not have predictions
|
| 157 |
+
|
| 158 |
+
def test_article_yield_stats_reasonable(self, data_service, raw_excel_df):
|
| 159 |
+
"""Verify yield stats are within reasonable ranges."""
|
| 160 |
+
articles = raw_excel_df["grey_k1_from_DBPD"].dropna().unique().tolist()[:30]
|
| 161 |
+
|
| 162 |
+
for article_id in articles:
|
| 163 |
+
try:
|
| 164 |
+
result = data_service.get_article_predictions(str(article_id))
|
| 165 |
+
|
| 166 |
+
if result is None:
|
| 167 |
+
continue
|
| 168 |
+
|
| 169 |
+
yield_stats = result["ai_prediction"]["yield_stats"]
|
| 170 |
+
|
| 171 |
+
# Yield should be between 0 and 200 (allowing for edge cases)
|
| 172 |
+
assert 0 <= yield_stats["avg"] <= 200, (
|
| 173 |
+
f"Unreasonable yield avg for {article_id}"
|
| 174 |
+
)
|
| 175 |
+
assert 0 <= yield_stats["min"] <= 200, (
|
| 176 |
+
f"Unreasonable yield min for {article_id}"
|
| 177 |
+
)
|
| 178 |
+
assert 0 <= yield_stats["max"] <= 200, (
|
| 179 |
+
f"Unreasonable yield max for {article_id}"
|
| 180 |
+
)
|
| 181 |
+
|
| 182 |
+
except Exception as e:
|
| 183 |
+
pass
|
| 184 |
+
|
| 185 |
+
def test_article_recommendation_reasonable(self, data_service, raw_excel_df):
|
| 186 |
+
"""Verify recommendation values are reasonable."""
|
| 187 |
+
articles = raw_excel_df["grey_k1_from_DBPD"].dropna().unique().tolist()[:30]
|
| 188 |
+
|
| 189 |
+
for article_id in articles:
|
| 190 |
+
try:
|
| 191 |
+
result = data_service.get_article_predictions(str(article_id))
|
| 192 |
+
|
| 193 |
+
if result is None:
|
| 194 |
+
continue
|
| 195 |
+
|
| 196 |
+
rec = result["ai_prediction"]["recommendation"]
|
| 197 |
+
|
| 198 |
+
# Suggested reservation should be between -50% and +50%
|
| 199 |
+
assert -50 <= rec["suggested_reservation_pct"] <= 50, (
|
| 200 |
+
f"Unreasonable reservation suggestion for {article_id}"
|
| 201 |
+
)
|
| 202 |
+
|
| 203 |
+
except Exception as e:
|
| 204 |
+
pass
|
| 205 |
+
|
| 206 |
+
def test_article_confidence_levels(self, data_service, raw_excel_df):
|
| 207 |
+
"""Verify confidence levels are valid."""
|
| 208 |
+
articles = raw_excel_df["grey_k1_from_DBPD"].dropna().unique().tolist()[:30]
|
| 209 |
+
|
| 210 |
+
valid_confidence = ["high", "medium", "low"]
|
| 211 |
+
|
| 212 |
+
for article_id in articles:
|
| 213 |
+
try:
|
| 214 |
+
result = data_service.get_article_predictions(str(article_id))
|
| 215 |
+
|
| 216 |
+
if result is None:
|
| 217 |
+
continue
|
| 218 |
+
|
| 219 |
+
confidence = result["ai_prediction"]["confidence"]
|
| 220 |
+
assert confidence in valid_confidence, (
|
| 221 |
+
f"Invalid confidence level for {article_id}"
|
| 222 |
+
)
|
| 223 |
+
|
| 224 |
+
except Exception as e:
|
| 225 |
+
pass
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
class TestArticleAggregation:
|
| 229 |
+
"""Test suite for article-level aggregation logic."""
|
| 230 |
+
|
| 231 |
+
def test_article_total_volume_matches(self, data_service, raw_excel_df):
|
| 232 |
+
"""Verify article total volume matches sum of orders."""
|
| 233 |
+
articles = raw_excel_df["grey_k1_from_DBPD"].dropna().unique().tolist()[:20]
|
| 234 |
+
|
| 235 |
+
for article_id in articles:
|
| 236 |
+
try:
|
| 237 |
+
result = data_service.get_article_predictions(str(article_id))
|
| 238 |
+
|
| 239 |
+
if result is None:
|
| 240 |
+
continue
|
| 241 |
+
|
| 242 |
+
# Verify stats
|
| 243 |
+
stats = result["stats"]
|
| 244 |
+
|
| 245 |
+
# Total volume should be positive
|
| 246 |
+
assert stats["total_volume"] >= 0, f"Negative volume for {article_id}"
|
| 247 |
+
|
| 248 |
+
# Total orders should match orders list
|
| 249 |
+
assert stats["total_orders"] == len(result["orders"]), (
|
| 250 |
+
f"Order count mismatch for {article_id}"
|
| 251 |
+
)
|
| 252 |
+
|
| 253 |
+
except Exception as e:
|
| 254 |
+
pass
|
| 255 |
+
|
| 256 |
+
def test_article_orders_have_required_fields(self, data_service, raw_excel_df):
|
| 257 |
+
"""Verify each order in article has required fields."""
|
| 258 |
+
articles = raw_excel_df["grey_k1_from_DBPD"].dropna().unique().tolist()[:20]
|
| 259 |
+
|
| 260 |
+
required_fields = ["id", "volume", "input", "output", "yield"]
|
| 261 |
+
|
| 262 |
+
for article_id in articles:
|
| 263 |
+
try:
|
| 264 |
+
result = data_service.get_article_predictions(str(article_id))
|
| 265 |
+
|
| 266 |
+
if result is None or not result["orders"]:
|
| 267 |
+
continue
|
| 268 |
+
|
| 269 |
+
for order in result["orders"][:5]: # Check first 5 orders
|
| 270 |
+
for field in required_fields:
|
| 271 |
+
assert field in order, (
|
| 272 |
+
f"Missing field {field} in order for {article_id}"
|
| 273 |
+
)
|
| 274 |
+
|
| 275 |
+
except Exception as e:
|
| 276 |
+
pass
|
backend/tests/test_calculations.py
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Tests for calculation formulas.
|
| 3 |
+
Verifies that all percentage calculations match the Excel formulas exactly.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import pytest
|
| 7 |
+
import pandas as pd
|
| 8 |
+
import sys
|
| 9 |
+
import os
|
| 10 |
+
|
| 11 |
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 12 |
+
|
| 13 |
+
from conftest import ManualCalculator
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class TestCalculationFormulas:
|
| 17 |
+
"""
|
| 18 |
+
Test suite for verifying calculation formulas.
|
| 19 |
+
|
| 20 |
+
Excel formulas from "Eg, Calculation" sheet:
|
| 21 |
+
- G18: =(G14-G13)/G13 (Extra Gr Reserved %)
|
| 22 |
+
- G19: =(G15-G13)/G13 (Actual Gr Issue %)
|
| 23 |
+
- G20: =(G15-G16)/G15 (Shrinkage %)
|
| 24 |
+
- G21: =G17/G16 (Fresh Pkg %)
|
| 25 |
+
- G22: =G17/G13 (Fresh to Order %)
|
| 26 |
+
"""
|
| 27 |
+
|
| 28 |
+
TOLERANCE = 0.5 # Allow 0.5% tolerance for floating point differences
|
| 29 |
+
|
| 30 |
+
def test_extra_gr_reserved_formula(self, data_service, raw_excel_df, po_type_map):
|
| 31 |
+
"""Test: (Reserved - PO_Qty) / PO_Qty × 100"""
|
| 32 |
+
# Get a sample sale order
|
| 33 |
+
sample_so = raw_excel_df["COPS_NO"].iloc[0]
|
| 34 |
+
|
| 35 |
+
# Get code result
|
| 36 |
+
code_result = data_service.get_sale_order_details(sample_so)
|
| 37 |
+
|
| 38 |
+
if "error" in code_result:
|
| 39 |
+
pytest.skip(f"Sale order {sample_so} not found")
|
| 40 |
+
|
| 41 |
+
# Manual calculation
|
| 42 |
+
so_data = raw_excel_df[raw_excel_df["COPS_NO"] == sample_so]
|
| 43 |
+
so_data["PO_CODE"] = so_data["PO_NO"].astype(str).str[:3]
|
| 44 |
+
|
| 45 |
+
input_rows = so_data[
|
| 46 |
+
so_data["PO_CODE"].map(
|
| 47 |
+
lambda x: po_type_map.get(x, {}).get("is_input", False)
|
| 48 |
+
)
|
| 49 |
+
]
|
| 50 |
+
|
| 51 |
+
total_reserved = input_rows["RES_QTY"].sum()
|
| 52 |
+
total_po_qty = (
|
| 53 |
+
input_rows["ODISQT"].sum()
|
| 54 |
+
if "ODISQT" in input_rows.columns
|
| 55 |
+
else input_rows["DORQT1"].sum()
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
expected = ManualCalculator.extra_gr_reserved_pct(total_reserved, total_po_qty)
|
| 59 |
+
actual = code_result["metrics"]["Extra Gr Reserved %"]
|
| 60 |
+
|
| 61 |
+
assert abs(expected - actual) <= self.TOLERANCE, (
|
| 62 |
+
f"Extra Gr Reserved % mismatch: expected {expected:.2f}, got {actual:.2f}"
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
def test_actual_gr_issue_formula(self, data_service, raw_excel_df, po_type_map):
|
| 66 |
+
"""Test: (Issued - PO_Qty) / PO_Qty × 100"""
|
| 67 |
+
sample_so = raw_excel_df["COPS_NO"].iloc[0]
|
| 68 |
+
|
| 69 |
+
code_result = data_service.get_sale_order_details(sample_so)
|
| 70 |
+
|
| 71 |
+
if "error" in code_result:
|
| 72 |
+
pytest.skip(f"Sale order {sample_so} not found")
|
| 73 |
+
|
| 74 |
+
# Manual calculation
|
| 75 |
+
so_data = raw_excel_df[raw_excel_df["COPS_NO"] == sample_so]
|
| 76 |
+
so_data["PO_CODE"] = so_data["PO_NO"].astype(str).str[:3]
|
| 77 |
+
|
| 78 |
+
input_rows = so_data[
|
| 79 |
+
so_data["PO_CODE"].map(
|
| 80 |
+
lambda x: po_type_map.get(x, {}).get("is_input", False)
|
| 81 |
+
)
|
| 82 |
+
]
|
| 83 |
+
|
| 84 |
+
total_issued = input_rows["ISS_QTY"].sum()
|
| 85 |
+
total_po_qty = (
|
| 86 |
+
input_rows["ODISQT"].sum()
|
| 87 |
+
if "ODISQT" in input_rows.columns
|
| 88 |
+
else input_rows["DORQT1"].sum()
|
| 89 |
+
)
|
| 90 |
+
|
| 91 |
+
expected = ManualCalculator.actual_gr_issue_pct(total_issued, total_po_qty)
|
| 92 |
+
actual = code_result["metrics"]["Actual Gr Issue %"]
|
| 93 |
+
|
| 94 |
+
assert abs(expected - actual) <= self.TOLERANCE, (
|
| 95 |
+
f"Actual Gr Issue % mismatch: expected {expected:.2f}, got {actual:.2f}"
|
| 96 |
+
)
|
| 97 |
+
|
| 98 |
+
def test_shrinkage_formula(self, data_service, raw_excel_df, po_type_map):
|
| 99 |
+
"""Test: (Issued - Total Packing) / Issued × 100"""
|
| 100 |
+
sample_so = raw_excel_df["COPS_NO"].iloc[0]
|
| 101 |
+
|
| 102 |
+
code_result = data_service.get_sale_order_details(sample_so)
|
| 103 |
+
|
| 104 |
+
if "error" in code_result:
|
| 105 |
+
pytest.skip(f"Sale order {sample_so} not found")
|
| 106 |
+
|
| 107 |
+
# Manual calculation
|
| 108 |
+
so_data = raw_excel_df[raw_excel_df["COPS_NO"] == sample_so]
|
| 109 |
+
so_data["PO_CODE"] = so_data["PO_NO"].astype(str).str[:3]
|
| 110 |
+
|
| 111 |
+
input_rows = so_data[
|
| 112 |
+
so_data["PO_CODE"].map(
|
| 113 |
+
lambda x: po_type_map.get(x, {}).get("is_input", False)
|
| 114 |
+
)
|
| 115 |
+
]
|
| 116 |
+
output_rows = so_data[
|
| 117 |
+
so_data["PO_CODE"].map(
|
| 118 |
+
lambda x: po_type_map.get(x, {}).get("is_output", False)
|
| 119 |
+
)
|
| 120 |
+
]
|
| 121 |
+
|
| 122 |
+
total_issued = input_rows["ISS_QTY"].sum()
|
| 123 |
+
total_packing = (
|
| 124 |
+
output_rows["pack_qty"].sum()
|
| 125 |
+
if "pack_qty" in output_rows.columns
|
| 126 |
+
else output_rows["pack_fresh"].sum()
|
| 127 |
+
)
|
| 128 |
+
|
| 129 |
+
expected = ManualCalculator.shrinkage_pct(total_issued, total_packing)
|
| 130 |
+
actual = code_result["metrics"]["Shrinkage %"]
|
| 131 |
+
|
| 132 |
+
assert abs(expected - actual) <= self.TOLERANCE, (
|
| 133 |
+
f"Shrinkage % mismatch: expected {expected:.2f}, got {actual:.2f}"
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
def test_fresh_pkg_formula(self, data_service, raw_excel_df, po_type_map):
|
| 137 |
+
"""Test: Pack Fresh / Total Packing × 100"""
|
| 138 |
+
sample_so = raw_excel_df["COPS_NO"].iloc[0]
|
| 139 |
+
|
| 140 |
+
code_result = data_service.get_sale_order_details(sample_so)
|
| 141 |
+
|
| 142 |
+
if "error" in code_result:
|
| 143 |
+
pytest.skip(f"Sale order {sample_so} not found")
|
| 144 |
+
|
| 145 |
+
# Manual calculation
|
| 146 |
+
so_data = raw_excel_df[raw_excel_df["COPS_NO"] == sample_so]
|
| 147 |
+
so_data["PO_CODE"] = so_data["PO_NO"].astype(str).str[:3]
|
| 148 |
+
|
| 149 |
+
output_rows = so_data[
|
| 150 |
+
so_data["PO_CODE"].map(
|
| 151 |
+
lambda x: po_type_map.get(x, {}).get("is_output", False)
|
| 152 |
+
)
|
| 153 |
+
]
|
| 154 |
+
|
| 155 |
+
total_pack_fresh = output_rows["pack_fresh"].sum()
|
| 156 |
+
total_packing = (
|
| 157 |
+
output_rows["pack_qty"].sum()
|
| 158 |
+
if "pack_qty" in output_rows.columns
|
| 159 |
+
else total_pack_fresh
|
| 160 |
+
)
|
| 161 |
+
|
| 162 |
+
expected = ManualCalculator.fresh_pkg_pct(total_pack_fresh, total_packing)
|
| 163 |
+
actual = code_result["metrics"]["Fresh Pkg %"]
|
| 164 |
+
|
| 165 |
+
assert abs(expected - actual) <= self.TOLERANCE, (
|
| 166 |
+
f"Fresh Pkg % mismatch: expected {expected:.2f}, got {actual:.2f}"
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
+
def test_fresh_yield_formula(self, data_service, raw_excel_df, po_type_map):
|
| 170 |
+
"""Test: Pack Fresh / Fresh Issued × 100"""
|
| 171 |
+
sample_so = raw_excel_df["COPS_NO"].iloc[0]
|
| 172 |
+
|
| 173 |
+
code_result = data_service.get_sale_order_details(sample_so)
|
| 174 |
+
|
| 175 |
+
if "error" in code_result:
|
| 176 |
+
pytest.skip(f"Sale order {sample_so} not found")
|
| 177 |
+
|
| 178 |
+
# Manual calculation
|
| 179 |
+
so_data = raw_excel_df[raw_excel_df["COPS_NO"] == sample_so]
|
| 180 |
+
so_data["PO_CODE"] = so_data["PO_NO"].astype(str).str[:3]
|
| 181 |
+
so_data["is_fresh"] = so_data["PO_CODE"].str.startswith("F")
|
| 182 |
+
|
| 183 |
+
fresh_input_rows = so_data[
|
| 184 |
+
(
|
| 185 |
+
so_data["PO_CODE"].map(
|
| 186 |
+
lambda x: po_type_map.get(x, {}).get("is_input", False)
|
| 187 |
+
)
|
| 188 |
+
)
|
| 189 |
+
& (so_data["is_fresh"] == True)
|
| 190 |
+
]
|
| 191 |
+
output_rows = so_data[
|
| 192 |
+
so_data["PO_CODE"].map(
|
| 193 |
+
lambda x: po_type_map.get(x, {}).get("is_output", False)
|
| 194 |
+
)
|
| 195 |
+
]
|
| 196 |
+
|
| 197 |
+
fresh_issued_qty = fresh_input_rows["ISS_QTY"].sum()
|
| 198 |
+
total_pack_fresh = output_rows["pack_fresh"].sum()
|
| 199 |
+
|
| 200 |
+
expected = ManualCalculator.fresh_yield_pct(total_pack_fresh, fresh_issued_qty)
|
| 201 |
+
actual = code_result["metrics"]["Fresh Yield %"]
|
| 202 |
+
|
| 203 |
+
assert abs(expected - actual) <= self.TOLERANCE, (
|
| 204 |
+
f"Fresh Yield % mismatch: expected {expected:.2f}, got {actual:.2f}"
|
| 205 |
+
)
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
class TestWaterfallCalculations:
|
| 209 |
+
"""Test suite for waterfall and blame calculations."""
|
| 210 |
+
|
| 211 |
+
def test_waterfall_values_add_up(self, data_service, all_sale_orders):
|
| 212 |
+
"""Verify waterfall values sum correctly."""
|
| 213 |
+
# Test first 5 sale orders
|
| 214 |
+
for so_id in all_sale_orders[:5]:
|
| 215 |
+
result = data_service.get_sale_order_details(so_id)
|
| 216 |
+
|
| 217 |
+
if "error" in result:
|
| 218 |
+
continue
|
| 219 |
+
|
| 220 |
+
waterfall = result["intelligence"]["waterfall"]
|
| 221 |
+
|
| 222 |
+
# Demand + Policy Gap + Execution Adj + Process Loss should approximately equal Delivered
|
| 223 |
+
# Note: This is a simplification; actual waterfall may have more complex logic
|
| 224 |
+
|
| 225 |
+
# Just verify all values are numeric
|
| 226 |
+
for item in waterfall:
|
| 227 |
+
assert isinstance(item["value"], (int, float)), (
|
| 228 |
+
f"Waterfall value should be numeric: {item}"
|
| 229 |
+
)
|
| 230 |
+
|
| 231 |
+
def test_blame_percentages_sum_to_100(self, data_service, all_sale_orders):
|
| 232 |
+
"""Verify blame percentages sum to 100%."""
|
| 233 |
+
for so_id in all_sale_orders[:10]:
|
| 234 |
+
result = data_service.get_sale_order_details(so_id)
|
| 235 |
+
|
| 236 |
+
if "error" in result:
|
| 237 |
+
continue
|
| 238 |
+
|
| 239 |
+
blame = result["intelligence"]["blame_breakdown"]
|
| 240 |
+
|
| 241 |
+
total_pct = (
|
| 242 |
+
blame["policy_pct"] + blame["execution_pct"] + blame["process_pct"]
|
| 243 |
+
)
|
| 244 |
+
|
| 245 |
+
# Allow small tolerance for rounding
|
| 246 |
+
assert abs(total_pct - 100) <= 0.2, (
|
| 247 |
+
f"Blame percentages should sum to 100%, got {total_pct}% for {so_id}"
|
| 248 |
+
)
|
| 249 |
+
|
| 250 |
+
def test_waterfall_demand_equals_order_qty(self, data_service, all_sale_orders):
|
| 251 |
+
"""Verify waterfall Demand equals Order Qty."""
|
| 252 |
+
for so_id in all_sale_orders[:5]:
|
| 253 |
+
result = data_service.get_sale_order_details(so_id)
|
| 254 |
+
|
| 255 |
+
if "error" in result:
|
| 256 |
+
continue
|
| 257 |
+
|
| 258 |
+
demand = result["intelligence"]["waterfall"][0][
|
| 259 |
+
"value"
|
| 260 |
+
] # First item is Demand
|
| 261 |
+
order_qty = result["metrics"]["Order Qty"]
|
| 262 |
+
|
| 263 |
+
assert abs(demand - order_qty) < 0.01, (
|
| 264 |
+
f"Demand should equal Order Qty for {so_id}"
|
| 265 |
+
)
|
| 266 |
+
|
| 267 |
+
def test_waterfall_delivered_equals_pack_fresh(self, data_service, all_sale_orders):
|
| 268 |
+
"""Verify waterfall Delivered equals Pack Fresh."""
|
| 269 |
+
for so_id in all_sale_orders[:5]:
|
| 270 |
+
result = data_service.get_sale_order_details(so_id)
|
| 271 |
+
|
| 272 |
+
if "error" in result:
|
| 273 |
+
continue
|
| 274 |
+
|
| 275 |
+
delivered = result["intelligence"]["waterfall"][-1][
|
| 276 |
+
"value"
|
| 277 |
+
] # Last item is Delivered
|
| 278 |
+
pack_fresh = result["metrics"]["Pack Fresh"]
|
| 279 |
+
|
| 280 |
+
assert abs(delivered - pack_fresh) < 0.01, (
|
| 281 |
+
f"Delivered should equal Pack Fresh for {so_id}"
|
| 282 |
+
)
|
backend/tests/test_data_loading.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Tests for data loading functionality.
|
| 3 |
+
Verifies that data is loaded correctly and all columns are mapped properly.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import pytest
|
| 7 |
+
import pandas as pd
|
| 8 |
+
import sys
|
| 9 |
+
import os
|
| 10 |
+
|
| 11 |
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 12 |
+
|
| 13 |
+
from conftest import ManualCalculator
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class TestDataLoading:
|
| 17 |
+
"""Test suite for data loading verification."""
|
| 18 |
+
|
| 19 |
+
def test_data_service_loads_successfully(self, data_service):
|
| 20 |
+
"""Verify data service loads without errors."""
|
| 21 |
+
assert data_service.is_loaded is True
|
| 22 |
+
assert data_service.master_df is not None
|
| 23 |
+
|
| 24 |
+
def test_master_df_has_correct_columns(self, data_service):
|
| 25 |
+
"""Verify all required columns exist in master_df."""
|
| 26 |
+
required_columns = [
|
| 27 |
+
"PO_NO",
|
| 28 |
+
"COPS_NO",
|
| 29 |
+
"DORQT1",
|
| 30 |
+
"RES_QTY",
|
| 31 |
+
"ISS_QTY",
|
| 32 |
+
"pack_fresh",
|
| 33 |
+
"pack_qty",
|
| 34 |
+
"Order Qty",
|
| 35 |
+
"Actual Gr Opening",
|
| 36 |
+
"Reserver Qty as per Std Norms",
|
| 37 |
+
"Deviation",
|
| 38 |
+
"Deviation_Percent",
|
| 39 |
+
"Article",
|
| 40 |
+
"Sale Order",
|
| 41 |
+
"Finish",
|
| 42 |
+
"Route",
|
| 43 |
+
"is_input",
|
| 44 |
+
"is_output",
|
| 45 |
+
]
|
| 46 |
+
for col in required_columns:
|
| 47 |
+
assert col in data_service.master_df.columns, f"Missing column: {col}"
|
| 48 |
+
|
| 49 |
+
def test_master_df_has_data(self, data_service):
|
| 50 |
+
"""Verify master_df contains expected number of rows."""
|
| 51 |
+
# Original file has 4613 rows
|
| 52 |
+
assert len(data_service.master_df) > 4000, "Too few rows loaded"
|
| 53 |
+
|
| 54 |
+
def test_po_type_flags_set_correctly(self, data_service, po_type_map):
|
| 55 |
+
"""Verify is_input and is_output flags match PO Type mapping."""
|
| 56 |
+
df = data_service.master_df
|
| 57 |
+
|
| 58 |
+
# Sample check: F0U should be input=True, output=True
|
| 59 |
+
f0u_rows = df[df["PO_CODE"] == "F0U"]
|
| 60 |
+
if len(f0u_rows) > 0:
|
| 61 |
+
assert all(f0u_rows["is_input"] == True), "F0U should have is_input=True"
|
| 62 |
+
assert all(f0u_rows["is_output"] == True), "F0U should have is_output=True"
|
| 63 |
+
|
| 64 |
+
# F0N should be input=False, output=False
|
| 65 |
+
f0n_rows = df[df["PO_CODE"] == "F0N"]
|
| 66 |
+
if len(f0n_rows) > 0:
|
| 67 |
+
assert all(f0n_rows["is_input"] == False), "F0N should have is_input=False"
|
| 68 |
+
assert all(f0n_rows["is_output"] == False), (
|
| 69 |
+
"F0N should have is_output=False"
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
# FRG (Reprocess) should be input=False, output=True
|
| 73 |
+
frg_rows = df[df["PO_CODE"] == "FRG"]
|
| 74 |
+
if len(frg_rows) > 0:
|
| 75 |
+
assert all(frg_rows["is_input"] == False), "FRG should have is_input=False"
|
| 76 |
+
assert all(frg_rows["is_output"] == True), "FRG should have is_output=True"
|
| 77 |
+
|
| 78 |
+
def test_numeric_columns_are_numeric(self, data_service):
|
| 79 |
+
"""Verify numeric columns have correct data types."""
|
| 80 |
+
df = data_service.master_df
|
| 81 |
+
|
| 82 |
+
numeric_cols = [
|
| 83 |
+
"DORQT1",
|
| 84 |
+
"RES_QTY",
|
| 85 |
+
"ISS_QTY",
|
| 86 |
+
"pack_fresh",
|
| 87 |
+
"pack_qty",
|
| 88 |
+
"Order Qty",
|
| 89 |
+
"Actual Gr Opening",
|
| 90 |
+
"Reserver Qty as per Std Norms",
|
| 91 |
+
]
|
| 92 |
+
|
| 93 |
+
for col in numeric_cols:
|
| 94 |
+
assert pd.api.types.is_numeric_dtype(df[col]), f"{col} should be numeric"
|
| 95 |
+
|
| 96 |
+
def test_deviation_calculated_correctly(self, data_service):
|
| 97 |
+
"""Verify Deviation column = ISS_QTY - RES_QTY."""
|
| 98 |
+
df = data_service.master_df
|
| 99 |
+
sample = df.head(100)
|
| 100 |
+
|
| 101 |
+
for idx, row in sample.iterrows():
|
| 102 |
+
expected = row["ISS_QTY"] - row["RES_QTY"]
|
| 103 |
+
actual = row["Deviation"]
|
| 104 |
+
assert abs(expected - actual) < 0.01, f"Deviation mismatch at {idx}"
|
| 105 |
+
|
| 106 |
+
def test_deviation_percent_calculated_correctly(self, data_service):
|
| 107 |
+
"""Verify Deviation_Percent = (Deviation / RES_QTY) * 100."""
|
| 108 |
+
df = data_service.master_df
|
| 109 |
+
sample = df.head(100)
|
| 110 |
+
|
| 111 |
+
for idx, row in sample.iterrows():
|
| 112 |
+
if row["RES_QTY"] > 0:
|
| 113 |
+
expected = (row["Deviation"] / row["RES_QTY"]) * 100
|
| 114 |
+
actual = row["Deviation_Percent"]
|
| 115 |
+
assert abs(expected - actual) < 0.1, (
|
| 116 |
+
f"Deviation_Percent mismatch at {idx}"
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
def test_article_column_mapped(self, data_service):
|
| 120 |
+
"""Verify Article column is mapped from grey_k1_from_DBPD."""
|
| 121 |
+
df = data_service.master_df
|
| 122 |
+
|
| 123 |
+
# Check that Article column has values
|
| 124 |
+
non_null = df["Article"].notna().sum()
|
| 125 |
+
assert non_null > 4000, "Too many null Article values"
|
| 126 |
+
|
| 127 |
+
def test_sale_order_column_mapped(self, data_service):
|
| 128 |
+
"""Verify Sale Order column is mapped from COPS_NO."""
|
| 129 |
+
df = data_service.master_df
|
| 130 |
+
|
| 131 |
+
# Check unique sale orders
|
| 132 |
+
unique_orders = df["Sale Order"].nunique()
|
| 133 |
+
assert unique_orders > 900, f"Expected ~970 sale orders, got {unique_orders}"
|
| 134 |
+
|
| 135 |
+
def test_finish_column_exists(self, data_service):
|
| 136 |
+
"""Verify Finish column is properly created."""
|
| 137 |
+
df = data_service.master_df
|
| 138 |
+
|
| 139 |
+
# Check that Finish column has values
|
| 140 |
+
assert "Finish" in df.columns
|
| 141 |
+
|
| 142 |
+
# Check expected values
|
| 143 |
+
unique_finishes = df["Finish"].unique()
|
| 144 |
+
# Should have values like 'Soft', 'Peach', etc.
|
| 145 |
+
assert len(unique_finishes) > 0
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
class TestDataConsistency:
|
| 149 |
+
"""Test suite for data consistency checks."""
|
| 150 |
+
|
| 151 |
+
def test_no_duplicate_columns(self, data_service):
|
| 152 |
+
"""Verify no duplicate column names."""
|
| 153 |
+
cols = data_service.master_df.columns.tolist()
|
| 154 |
+
assert len(cols) == len(set(cols)), "Duplicate column names found"
|
| 155 |
+
|
| 156 |
+
def test_po_code_extracted_correctly(self, data_service):
|
| 157 |
+
"""Verify PO_CODE is first 3 characters of PO_NO."""
|
| 158 |
+
df = data_service.master_df
|
| 159 |
+
sample = df.head(100)
|
| 160 |
+
|
| 161 |
+
for idx, row in sample.iterrows():
|
| 162 |
+
expected = str(row["PO_NO"])[:3]
|
| 163 |
+
actual = row["PO_CODE"]
|
| 164 |
+
assert actual == expected, f"PO_CODE mismatch at {idx}"
|
| 165 |
+
|
| 166 |
+
def test_order_qty_equals_dorqt1(self, data_service):
|
| 167 |
+
"""Verify Order Qty is mapped from DORQT1."""
|
| 168 |
+
df = data_service.master_df
|
| 169 |
+
sample = df.head(100)
|
| 170 |
+
|
| 171 |
+
for idx, row in sample.iterrows():
|
| 172 |
+
assert row["Order Qty"] == row["DORQT1"], f"Order Qty mismatch at {idx}"
|
| 173 |
+
|
| 174 |
+
def test_actual_gr_opening_equals_iss_qty(self, data_service):
|
| 175 |
+
"""Verify Actual Gr Opening is mapped from ISS_QTY."""
|
| 176 |
+
df = data_service.master_df
|
| 177 |
+
sample = df.head(100)
|
| 178 |
+
|
| 179 |
+
for idx, row in sample.iterrows():
|
| 180 |
+
assert row["Actual Gr Opening"] == row["ISS_QTY"], (
|
| 181 |
+
f"Actual Gr Opening mismatch at {idx}"
|
| 182 |
+
)
|
| 183 |
+
|
| 184 |
+
def test_reserved_qty_equals_res_qty(self, data_service):
|
| 185 |
+
"""Verify Reserver Qty is mapped from RES_QTY."""
|
| 186 |
+
df = data_service.master_df
|
| 187 |
+
sample = df.head(100)
|
| 188 |
+
|
| 189 |
+
for idx, row in sample.iterrows():
|
| 190 |
+
assert row["Reserver Qty as per Std Norms"] == row["RES_QTY"], (
|
| 191 |
+
f"Reserved Qty mismatch at {idx}"
|
| 192 |
+
)
|
backend/tests/test_edge_cases.py
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Tests for edge cases.
|
| 3 |
+
Verifies handling of zero values, negative deviations, impossible yields, etc.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import pytest
|
| 7 |
+
import pandas as pd
|
| 8 |
+
import sys
|
| 9 |
+
import os
|
| 10 |
+
|
| 11 |
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class TestEdgeCases:
|
| 15 |
+
"""Test suite for edge case handling."""
|
| 16 |
+
|
| 17 |
+
def test_zero_order_qty_handled(self, data_service, raw_excel_df):
|
| 18 |
+
"""Verify zero order qty doesn't cause division errors."""
|
| 19 |
+
# Find orders with zero order qty
|
| 20 |
+
zero_orders = raw_excel_df[raw_excel_df["DORQT1"] == 0]["COPS_NO"].unique()
|
| 21 |
+
|
| 22 |
+
for so_id in zero_orders[:5]:
|
| 23 |
+
try:
|
| 24 |
+
result = data_service.get_sale_order_details(so_id)
|
| 25 |
+
# Should not raise an error
|
| 26 |
+
assert "error" not in result or result.get("error") == "Order not found"
|
| 27 |
+
except ZeroDivisionError:
|
| 28 |
+
pytest.fail(f"ZeroDivisionError for order with zero qty: {so_id}")
|
| 29 |
+
|
| 30 |
+
def test_zero_issuance_handled(self, data_service, raw_excel_df):
|
| 31 |
+
"""Verify zero issuance doesn't cause division errors."""
|
| 32 |
+
# Find orders with zero issuance
|
| 33 |
+
zero_iss = raw_excel_df[raw_excel_df["ISS_QTY"] == 0]["COPS_NO"].unique()
|
| 34 |
+
|
| 35 |
+
for so_id in zero_iss[:5]:
|
| 36 |
+
try:
|
| 37 |
+
result = data_service.get_sale_order_details(so_id)
|
| 38 |
+
assert "error" not in result or result.get("error") == "Order not found"
|
| 39 |
+
except ZeroDivisionError:
|
| 40 |
+
pytest.fail(f"ZeroDivisionError for order with zero issuance: {so_id}")
|
| 41 |
+
|
| 42 |
+
def test_zero_pack_fresh_handled(self, data_service, raw_excel_df):
|
| 43 |
+
"""Verify zero pack_fresh doesn't cause errors."""
|
| 44 |
+
zero_pack = raw_excel_df[raw_excel_df["pack_fresh"] == 0]["COPS_NO"].unique()
|
| 45 |
+
|
| 46 |
+
for so_id in zero_pack[:5]:
|
| 47 |
+
try:
|
| 48 |
+
result = data_service.get_sale_order_details(so_id)
|
| 49 |
+
if "error" not in result:
|
| 50 |
+
# Fresh Yield should be 0 or handled
|
| 51 |
+
fresh_yield = result["metrics"].get("Fresh Yield %", 0)
|
| 52 |
+
assert fresh_yield >= 0
|
| 53 |
+
except Exception as e:
|
| 54 |
+
pytest.fail(f"Error for order with zero pack_fresh {so_id}: {e}")
|
| 55 |
+
|
| 56 |
+
def test_under_issuance_negative_deviation(self, data_service, raw_excel_df):
|
| 57 |
+
"""Verify under-issuance (ISS_QTY < RES_QTY) produces negative deviation."""
|
| 58 |
+
under_issued = raw_excel_df[raw_excel_df["ISS_QTY"] < raw_excel_df["RES_QTY"]]
|
| 59 |
+
|
| 60 |
+
if len(under_issued) > 0:
|
| 61 |
+
sample = under_issued.iloc[0]
|
| 62 |
+
so_id = sample["COPS_NO"]
|
| 63 |
+
|
| 64 |
+
result = data_service.get_sale_order_details(so_id)
|
| 65 |
+
|
| 66 |
+
if "error" not in result:
|
| 67 |
+
# Deviation should be negative for under-issuance
|
| 68 |
+
total_issued = result["metrics"]["Actual Issued"]
|
| 69 |
+
total_reserved = result["metrics"]["Reserved Qty"]
|
| 70 |
+
|
| 71 |
+
# For this specific PO, check deviation
|
| 72 |
+
deviation = total_issued - total_reserved
|
| 73 |
+
# Note: This is at order level, so might not always be negative
|
| 74 |
+
|
| 75 |
+
def test_over_issuance_positive_deviation(self, data_service, raw_excel_df):
|
| 76 |
+
"""Verify over-issuance (ISS_QTY > RES_QTY) produces positive deviation."""
|
| 77 |
+
over_issued = raw_excel_df[raw_excel_df["ISS_QTY"] > raw_excel_df["RES_QTY"]]
|
| 78 |
+
|
| 79 |
+
if len(over_issued) > 0:
|
| 80 |
+
sample = over_issued.iloc[0]
|
| 81 |
+
so_id = sample["COPS_NO"]
|
| 82 |
+
|
| 83 |
+
result = data_service.get_sale_order_details(so_id)
|
| 84 |
+
|
| 85 |
+
if "error" not in result:
|
| 86 |
+
# At least some metrics should indicate over-issuance
|
| 87 |
+
actual_gr_issue = result["metrics"]["Actual Gr Issue %"]
|
| 88 |
+
# Should be positive if over-issued
|
| 89 |
+
|
| 90 |
+
def test_impossible_yield_over_100(self, data_service, raw_excel_df):
|
| 91 |
+
"""Verify yield > 100% is handled (possible with reprocess data)."""
|
| 92 |
+
# Find orders where pack_fresh > ISS_QTY
|
| 93 |
+
impossible = raw_excel_df[raw_excel_df["pack_fresh"] > raw_excel_df["ISS_QTY"]]
|
| 94 |
+
|
| 95 |
+
for idx, row in impossible.head(5).iterrows():
|
| 96 |
+
so_id = row["COPS_NO"]
|
| 97 |
+
|
| 98 |
+
try:
|
| 99 |
+
result = data_service.get_sale_order_details(so_id)
|
| 100 |
+
if "error" not in result:
|
| 101 |
+
fresh_yield = result["metrics"].get("Fresh Yield %", 0)
|
| 102 |
+
# Yield can be > 100 due to reprocess - just verify no crash
|
| 103 |
+
assert fresh_yield >= 0
|
| 104 |
+
except Exception as e:
|
| 105 |
+
pytest.fail(f"Error handling yield > 100%: {e}")
|
| 106 |
+
|
| 107 |
+
def test_multiple_po_types_in_order(self, data_service, raw_excel_df):
|
| 108 |
+
"""Verify orders with multiple PO types are handled correctly."""
|
| 109 |
+
# Find orders with multiple POs
|
| 110 |
+
so_counts = raw_excel_df.groupby("COPS_NO")["PO_NO"].nunique()
|
| 111 |
+
multi_po_sos = so_counts[so_counts > 2].index.tolist()
|
| 112 |
+
|
| 113 |
+
for so_id in multi_po_sos[:5]:
|
| 114 |
+
try:
|
| 115 |
+
result = data_service.get_sale_order_details(so_id)
|
| 116 |
+
if "error" not in result:
|
| 117 |
+
# Verify PO breakdown exists
|
| 118 |
+
assert "po_breakdown" in result
|
| 119 |
+
assert len(result["po_breakdown"]) > 1
|
| 120 |
+
except Exception as e:
|
| 121 |
+
pytest.fail(f"Error handling multi-PO order {so_id}: {e}")
|
| 122 |
+
|
| 123 |
+
def test_order_with_reprocess(self, data_service, raw_excel_df):
|
| 124 |
+
"""Verify orders with reprocess POs are handled correctly."""
|
| 125 |
+
# Find orders with Reprocess PO type
|
| 126 |
+
reprocess_orders = raw_excel_df[raw_excel_df["PO Type"] == "Reprocess"][
|
| 127 |
+
"COPS_NO"
|
| 128 |
+
].unique()
|
| 129 |
+
|
| 130 |
+
for so_id in reprocess_orders[:5]:
|
| 131 |
+
try:
|
| 132 |
+
result = data_service.get_sale_order_details(so_id)
|
| 133 |
+
if "error" not in result:
|
| 134 |
+
# Should have reprocess count > 0
|
| 135 |
+
reprocess_count = result["metrics"].get("Reprocess Count", 0)
|
| 136 |
+
assert reprocess_count >= 0
|
| 137 |
+
except Exception as e:
|
| 138 |
+
pytest.fail(f"Error handling reprocess order {so_id}: {e}")
|
| 139 |
+
|
| 140 |
+
def test_order_with_shortfall(self, data_service, raw_excel_df):
|
| 141 |
+
"""Verify orders with Short Fall PO type are handled correctly."""
|
| 142 |
+
shortfall_orders = raw_excel_df[raw_excel_df["PO Type"] == "Short Fall"][
|
| 143 |
+
"COPS_NO"
|
| 144 |
+
].unique()
|
| 145 |
+
|
| 146 |
+
for so_id in shortfall_orders[:5]:
|
| 147 |
+
try:
|
| 148 |
+
result = data_service.get_sale_order_details(so_id)
|
| 149 |
+
if "error" not in result:
|
| 150 |
+
# Shortfall should be tracked
|
| 151 |
+
shortfall = result["metrics"].get("Shortfall", 0)
|
| 152 |
+
status = result["metrics"].get("Status", "")
|
| 153 |
+
# Either Shortfall or Fulfilled
|
| 154 |
+
assert status in ["Shortfall", "Fulfilled"]
|
| 155 |
+
except Exception as e:
|
| 156 |
+
pytest.fail(f"Error handling shortfall order {so_id}: {e}")
|
| 157 |
+
|
| 158 |
+
def test_order_not_found(self, data_service):
|
| 159 |
+
"""Verify non-existent order returns error gracefully."""
|
| 160 |
+
result = data_service.get_sale_order_details("NONEXISTENT_ORDER_12345")
|
| 161 |
+
|
| 162 |
+
assert "error" in result
|
| 163 |
+
assert result["error"] == "Order not found"
|
| 164 |
+
|
| 165 |
+
def test_article_not_found(self, data_service):
|
| 166 |
+
"""Verify non-existent article returns error gracefully."""
|
| 167 |
+
result = data_service.get_article_insights("NONEXISTENT_ARTICLE_12345")
|
| 168 |
+
|
| 169 |
+
assert "error" in result
|
| 170 |
+
assert result["error"] == "No data found"
|
| 171 |
+
|
| 172 |
+
def test_single_po_order(self, data_service, raw_excel_df):
|
| 173 |
+
"""Verify orders with single PO are handled correctly."""
|
| 174 |
+
so_counts = raw_excel_df.groupby("COPS_NO")["PO_NO"].nunique()
|
| 175 |
+
single_po_sos = so_counts[so_counts == 1].index.tolist()
|
| 176 |
+
|
| 177 |
+
for so_id in single_po_sos[:5]:
|
| 178 |
+
try:
|
| 179 |
+
result = data_service.get_sale_order_details(so_id)
|
| 180 |
+
if "error" not in result:
|
| 181 |
+
assert len(result["po_breakdown"]) == 1
|
| 182 |
+
except Exception as e:
|
| 183 |
+
pytest.fail(f"Error handling single-PO order {so_id}: {e}")
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
class TestInputOutputClassification:
|
| 187 |
+
"""Test suite for is_input and is_output classification."""
|
| 188 |
+
|
| 189 |
+
def test_fresh_input_classification(self, data_service):
|
| 190 |
+
"""Verify Fresh Input POs have is_input=True."""
|
| 191 |
+
df = data_service.master_df
|
| 192 |
+
fresh_codes = [
|
| 193 |
+
"F0U",
|
| 194 |
+
"F01",
|
| 195 |
+
"FQT",
|
| 196 |
+
"FBT",
|
| 197 |
+
"F0A",
|
| 198 |
+
"F0P",
|
| 199 |
+
"F0Q",
|
| 200 |
+
"F0X",
|
| 201 |
+
"F0Z",
|
| 202 |
+
"FBY",
|
| 203 |
+
"FFX",
|
| 204 |
+
"FMW",
|
| 205 |
+
"FOB",
|
| 206 |
+
"FPT",
|
| 207 |
+
"FPX",
|
| 208 |
+
"FPY",
|
| 209 |
+
]
|
| 210 |
+
|
| 211 |
+
for code in fresh_codes:
|
| 212 |
+
rows = df[df["PO_CODE"] == code]
|
| 213 |
+
if len(rows) > 0:
|
| 214 |
+
assert all(rows["is_input"] == True), (
|
| 215 |
+
f"{code} should have is_input=True"
|
| 216 |
+
)
|
| 217 |
+
|
| 218 |
+
def test_reprocess_output_classification(self, data_service):
|
| 219 |
+
"""Verify Reprocess POs have is_output=True but is_input=False."""
|
| 220 |
+
df = data_service.master_df
|
| 221 |
+
|
| 222 |
+
frg_rows = df[df["PO_CODE"] == "FRG"]
|
| 223 |
+
if len(frg_rows) > 0:
|
| 224 |
+
assert all(frg_rows["is_input"] == False), "FRG should have is_input=False"
|
| 225 |
+
assert all(frg_rows["is_output"] == True), "FRG should have is_output=True"
|
| 226 |
+
|
| 227 |
+
frp_rows = df[df["PO_CODE"] == "FRP"]
|
| 228 |
+
if len(frp_rows) > 0:
|
| 229 |
+
assert all(frp_rows["is_input"] == False), "FRP should have is_input=False"
|
| 230 |
+
assert all(frp_rows["is_output"] == True), "FRP should have is_output=True"
|
| 231 |
+
|
| 232 |
+
def test_no_fresh_po_classification(self, data_service):
|
| 233 |
+
"""Verify No Fresh PO has is_input=False, is_output=False."""
|
| 234 |
+
df = data_service.master_df
|
| 235 |
+
|
| 236 |
+
f0n_rows = df[df["PO_CODE"] == "F0N"]
|
| 237 |
+
if len(f0n_rows) > 0:
|
| 238 |
+
assert all(f0n_rows["is_input"] == False), "F0N should have is_input=False"
|
| 239 |
+
assert all(f0n_rows["is_output"] == False), (
|
| 240 |
+
"F0N should have is_output=False"
|
| 241 |
+
)
|
| 242 |
+
|
| 243 |
+
def test_short_fall_classification(self, data_service):
|
| 244 |
+
"""Verify Short Fall has is_input=False, is_output=False."""
|
| 245 |
+
df = data_service.master_df
|
| 246 |
+
|
| 247 |
+
f0s_rows = df[df["PO_CODE"] == "F0S"]
|
| 248 |
+
if len(f0s_rows) > 0:
|
| 249 |
+
assert all(f0s_rows["is_input"] == False), "F0S should have is_input=False"
|
| 250 |
+
assert all(f0s_rows["is_output"] == False), (
|
| 251 |
+
"F0S should have is_output=False"
|
| 252 |
+
)
|
backend/tests/test_sale_orders.py
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Comprehensive tests for all sale orders.
|
| 3 |
+
Tests every sale order against manual calculations.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import pytest
|
| 7 |
+
import pandas as pd
|
| 8 |
+
import sys
|
| 9 |
+
import os
|
| 10 |
+
from datetime import datetime
|
| 11 |
+
import json
|
| 12 |
+
|
| 13 |
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 14 |
+
|
| 15 |
+
from conftest import ManualCalculator
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class TestAllSaleOrders:
|
| 19 |
+
"""Test suite that verifies ALL sale orders against manual calculations."""
|
| 20 |
+
|
| 21 |
+
TOLERANCE = 1.0 # Allow 1% tolerance for floating point differences
|
| 22 |
+
|
| 23 |
+
@pytest.fixture(scope="class")
|
| 24 |
+
def test_results(self):
|
| 25 |
+
"""Initialize results storage."""
|
| 26 |
+
return {"total_tested": 0, "passed": 0, "failed": 0, "skipped": 0, "errors": []}
|
| 27 |
+
|
| 28 |
+
def test_all_sale_orders_calculations(
|
| 29 |
+
self, data_service, raw_excel_df, po_type_map
|
| 30 |
+
):
|
| 31 |
+
"""Test all sale orders against manual calculations."""
|
| 32 |
+
results = {
|
| 33 |
+
"total_tested": 0,
|
| 34 |
+
"passed": 0,
|
| 35 |
+
"failed": 0,
|
| 36 |
+
"skipped": 0,
|
| 37 |
+
"errors": [],
|
| 38 |
+
"start_time": datetime.now().isoformat(),
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
all_sale_orders = raw_excel_df["COPS_NO"].unique().tolist()
|
| 42 |
+
|
| 43 |
+
for so_id in all_sale_orders:
|
| 44 |
+
results["total_tested"] += 1
|
| 45 |
+
|
| 46 |
+
try:
|
| 47 |
+
# Get code result
|
| 48 |
+
code_result = data_service.get_sale_order_details(so_id)
|
| 49 |
+
|
| 50 |
+
if "error" in code_result:
|
| 51 |
+
results["skipped"] += 1
|
| 52 |
+
continue
|
| 53 |
+
|
| 54 |
+
# Manual calculation from raw data
|
| 55 |
+
so_data = raw_excel_df[raw_excel_df["COPS_NO"] == so_id].copy()
|
| 56 |
+
so_data["PO_CODE"] = so_data["PO_NO"].astype(str).str[:3]
|
| 57 |
+
|
| 58 |
+
# Determine input/output rows
|
| 59 |
+
input_rows = so_data[
|
| 60 |
+
so_data["PO_CODE"].map(
|
| 61 |
+
lambda x: po_type_map.get(x, {}).get("is_input", False)
|
| 62 |
+
)
|
| 63 |
+
]
|
| 64 |
+
output_rows = so_data[
|
| 65 |
+
so_data["PO_CODE"].map(
|
| 66 |
+
lambda x: po_type_map.get(x, {}).get("is_output", False)
|
| 67 |
+
)
|
| 68 |
+
]
|
| 69 |
+
fresh_input_rows = input_rows[input_rows["PO_CODE"].str.startswith("F")]
|
| 70 |
+
|
| 71 |
+
# Calculate values manually
|
| 72 |
+
if "COPS_LINENO" in so_data.columns:
|
| 73 |
+
total_order_qty = (
|
| 74 |
+
so_data.groupby("COPS_LINENO")["DORQT1"].first().sum()
|
| 75 |
+
)
|
| 76 |
+
else:
|
| 77 |
+
total_order_qty = so_data["DORQT1"].drop_duplicates().sum()
|
| 78 |
+
|
| 79 |
+
total_po_qty = (
|
| 80 |
+
input_rows["ODISQT"].sum()
|
| 81 |
+
if "ODISQT" in input_rows.columns
|
| 82 |
+
else input_rows["DORQT1"].sum()
|
| 83 |
+
)
|
| 84 |
+
total_reserved = input_rows["RES_QTY"].sum()
|
| 85 |
+
total_issued = input_rows["ISS_QTY"].sum()
|
| 86 |
+
total_packing = (
|
| 87 |
+
output_rows["pack_qty"].sum()
|
| 88 |
+
if "pack_qty" in output_rows.columns
|
| 89 |
+
else output_rows["pack_fresh"].sum()
|
| 90 |
+
)
|
| 91 |
+
total_pack_fresh = output_rows["pack_fresh"].sum()
|
| 92 |
+
fresh_issued_qty = fresh_input_rows["ISS_QTY"].sum()
|
| 93 |
+
|
| 94 |
+
# Calculate expected percentages
|
| 95 |
+
expected = {
|
| 96 |
+
"Extra Gr Reserved %": ManualCalculator.extra_gr_reserved_pct(
|
| 97 |
+
total_reserved, total_po_qty
|
| 98 |
+
),
|
| 99 |
+
"Actual Gr Issue %": ManualCalculator.actual_gr_issue_pct(
|
| 100 |
+
total_issued, total_po_qty
|
| 101 |
+
),
|
| 102 |
+
"Shrinkage %": ManualCalculator.shrinkage_pct(
|
| 103 |
+
total_issued, total_packing
|
| 104 |
+
),
|
| 105 |
+
"Fresh Pkg %": ManualCalculator.fresh_pkg_pct(
|
| 106 |
+
total_pack_fresh, total_packing
|
| 107 |
+
),
|
| 108 |
+
"Fresh Yield %": ManualCalculator.fresh_yield_pct(
|
| 109 |
+
total_pack_fresh, fresh_issued_qty
|
| 110 |
+
),
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
# Compare with code result
|
| 114 |
+
failed_metrics = []
|
| 115 |
+
for metric, expected_val in expected.items():
|
| 116 |
+
actual_val = code_result["metrics"].get(metric, 0)
|
| 117 |
+
|
| 118 |
+
# Handle edge cases
|
| 119 |
+
if expected_val == 0 and actual_val == 0:
|
| 120 |
+
continue
|
| 121 |
+
if expected_val == 0:
|
| 122 |
+
continue
|
| 123 |
+
|
| 124 |
+
diff = abs(expected_val - actual_val)
|
| 125 |
+
if diff > self.TOLERANCE:
|
| 126 |
+
failed_metrics.append(
|
| 127 |
+
{
|
| 128 |
+
"metric": metric,
|
| 129 |
+
"expected": round(expected_val, 2),
|
| 130 |
+
"actual": round(actual_val, 2),
|
| 131 |
+
"difference": round(diff, 2),
|
| 132 |
+
}
|
| 133 |
+
)
|
| 134 |
+
|
| 135 |
+
if failed_metrics:
|
| 136 |
+
results["failed"] += 1
|
| 137 |
+
results["errors"].append(
|
| 138 |
+
{
|
| 139 |
+
"sale_order": so_id,
|
| 140 |
+
"type": "calculation_mismatch",
|
| 141 |
+
"details": failed_metrics,
|
| 142 |
+
}
|
| 143 |
+
)
|
| 144 |
+
else:
|
| 145 |
+
results["passed"] += 1
|
| 146 |
+
|
| 147 |
+
except Exception as e:
|
| 148 |
+
results["failed"] += 1
|
| 149 |
+
results["errors"].append(
|
| 150 |
+
{"sale_order": so_id, "type": "exception", "details": str(e)}
|
| 151 |
+
)
|
| 152 |
+
|
| 153 |
+
results["end_time"] = datetime.now().isoformat()
|
| 154 |
+
results["pass_rate"] = (
|
| 155 |
+
round(results["passed"] / results["total_tested"] * 100, 2)
|
| 156 |
+
if results["total_tested"] > 0
|
| 157 |
+
else 0
|
| 158 |
+
)
|
| 159 |
+
|
| 160 |
+
# Save results
|
| 161 |
+
report_path = os.path.join(
|
| 162 |
+
os.path.dirname(__file__), "reports", "sale_orders_report.json"
|
| 163 |
+
)
|
| 164 |
+
os.makedirs(os.path.dirname(report_path), exist_ok=True)
|
| 165 |
+
with open(report_path, "w") as f:
|
| 166 |
+
json.dump(results, f, indent=2)
|
| 167 |
+
|
| 168 |
+
# Assert minimum pass rate (should be 95%+)
|
| 169 |
+
assert results["pass_rate"] >= 95.0, (
|
| 170 |
+
f"Pass rate too low: {results['pass_rate']}%. Failed: {results['failed']}"
|
| 171 |
+
)
|
| 172 |
+
|
| 173 |
+
def test_sale_order_shortfall_calculation(self, data_service, raw_excel_df):
|
| 174 |
+
"""Verify shortfall = Order Qty - Pack Fresh."""
|
| 175 |
+
all_sale_orders = raw_excel_df["COPS_NO"].unique().tolist()
|
| 176 |
+
|
| 177 |
+
errors = []
|
| 178 |
+
|
| 179 |
+
for so_id in all_sale_orders[:100]: # Test first 100
|
| 180 |
+
try:
|
| 181 |
+
code_result = data_service.get_sale_order_details(so_id)
|
| 182 |
+
|
| 183 |
+
if "error" in code_result:
|
| 184 |
+
continue
|
| 185 |
+
|
| 186 |
+
so_data = raw_excel_df[raw_excel_df["COPS_NO"] == so_id]
|
| 187 |
+
|
| 188 |
+
if "COPS_LINENO" in so_data.columns:
|
| 189 |
+
order_qty = so_data.groupby("COPS_LINENO")["DORQT1"].first().sum()
|
| 190 |
+
else:
|
| 191 |
+
order_qty = so_data["DORQT1"].drop_duplicates().sum()
|
| 192 |
+
|
| 193 |
+
pack_fresh = so_data["pack_fresh"].sum()
|
| 194 |
+
expected_shortfall = order_qty - pack_fresh
|
| 195 |
+
actual_shortfall = code_result["metrics"]["Shortfall"]
|
| 196 |
+
|
| 197 |
+
if abs(expected_shortfall - actual_shortfall) > 1:
|
| 198 |
+
errors.append(
|
| 199 |
+
{
|
| 200 |
+
"sale_order": so_id,
|
| 201 |
+
"expected": expected_shortfall,
|
| 202 |
+
"actual": actual_shortfall,
|
| 203 |
+
}
|
| 204 |
+
)
|
| 205 |
+
except Exception as e:
|
| 206 |
+
pass
|
| 207 |
+
|
| 208 |
+
assert len(errors) == 0, f"Shortfall calculation errors: {errors[:5]}"
|
| 209 |
+
|
| 210 |
+
def test_sale_order_status_determination(self, data_service, raw_excel_df):
|
| 211 |
+
"""Verify status is 'Shortfall' when shortfall > 0, else 'Fulfilled'."""
|
| 212 |
+
all_sale_orders = raw_excel_df["COPS_NO"].unique().tolist()
|
| 213 |
+
|
| 214 |
+
errors = []
|
| 215 |
+
|
| 216 |
+
for so_id in all_sale_orders[:100]:
|
| 217 |
+
try:
|
| 218 |
+
code_result = data_service.get_sale_order_details(so_id)
|
| 219 |
+
|
| 220 |
+
if "error" in code_result:
|
| 221 |
+
continue
|
| 222 |
+
|
| 223 |
+
shortfall = code_result["metrics"]["Shortfall"]
|
| 224 |
+
status = code_result["metrics"]["Status"]
|
| 225 |
+
|
| 226 |
+
expected_status = "Shortfall" if shortfall > 0 else "Fulfilled"
|
| 227 |
+
|
| 228 |
+
if status != expected_status:
|
| 229 |
+
errors.append(
|
| 230 |
+
{
|
| 231 |
+
"sale_order": so_id,
|
| 232 |
+
"shortfall": shortfall,
|
| 233 |
+
"expected_status": expected_status,
|
| 234 |
+
"actual_status": status,
|
| 235 |
+
}
|
| 236 |
+
)
|
| 237 |
+
except Exception as e:
|
| 238 |
+
pass
|
| 239 |
+
|
| 240 |
+
assert len(errors) == 0, f"Status determination errors: {errors[:5]}"
|
backend/validate_ai_logic.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
import os
|
| 3 |
+
import pandas as pd
|
| 4 |
+
import statistics
|
| 5 |
+
|
| 6 |
+
# Add current directory to path so we can import app
|
| 7 |
+
sys.path.append(os.getcwd())
|
| 8 |
+
|
| 9 |
+
from app.services.data_service import data_service
|
| 10 |
+
|
| 11 |
+
def run_validation():
|
| 12 |
+
print("Loading data...")
|
| 13 |
+
data_service.load_data()
|
| 14 |
+
|
| 15 |
+
if data_service.master_df is None:
|
| 16 |
+
print("Error: Could not load data.")
|
| 17 |
+
return
|
| 18 |
+
|
| 19 |
+
# Get unique articles from master_df
|
| 20 |
+
# Column might be 'Article No' or 'Article' - check data_service.load_data
|
| 21 |
+
# Usually it's mapped. Let's start with 'Article' if mapped, or 'Material'
|
| 22 |
+
# In data_service.py, keys are likely from CSV.
|
| 23 |
+
# Let's verify column name. SAFE bet is to iterate what get_article_predictions expects.
|
| 24 |
+
# It takes article_id.
|
| 25 |
+
# Let's try to get unique values from the dataframe.
|
| 26 |
+
|
| 27 |
+
df = data_service.master_df
|
| 28 |
+
# Identify article column
|
| 29 |
+
article_col = 'Article' if 'Article' in df.columns else 'Material'
|
| 30 |
+
# If not found, try to list columns
|
| 31 |
+
if article_col not in df.columns:
|
| 32 |
+
print(f"Columns: {df.columns}")
|
| 33 |
+
return
|
| 34 |
+
|
| 35 |
+
all_articles = df[article_col].dropna().unique()
|
| 36 |
+
print(f"Found {len(all_articles)} unique articles. Running AI validation...")
|
| 37 |
+
|
| 38 |
+
results = []
|
| 39 |
+
|
| 40 |
+
# Run for all
|
| 41 |
+
count = 0
|
| 42 |
+
for article in all_articles:
|
| 43 |
+
count += 1
|
| 44 |
+
if count % 100 == 0:
|
| 45 |
+
print(f"Processed {count}/{len(all_articles)}...")
|
| 46 |
+
|
| 47 |
+
try:
|
| 48 |
+
pred = data_service.get_article_predictions(article)
|
| 49 |
+
ai = pred['ai_prediction']
|
| 50 |
+
rec = ai['recommendation']
|
| 51 |
+
norm = ai['norm_analysis']
|
| 52 |
+
stats = ai['historical_analysis']
|
| 53 |
+
yield_stats = ai['yield_stats']
|
| 54 |
+
|
| 55 |
+
results.append({
|
| 56 |
+
'Article': article,
|
| 57 |
+
'OrderCount': ai['historical_orders'],
|
| 58 |
+
'SuccessRate': stats['success_rate_pct'],
|
| 59 |
+
'NormPct': norm['base_norm_pct'],
|
| 60 |
+
'RecPct': rec['suggested_reservation_pct'],
|
| 61 |
+
'Adjustment': rec['ai_adjustment_pct'],
|
| 62 |
+
'YieldAvg': yield_stats['avg'],
|
| 63 |
+
'YieldStd': yield_stats['std_dev']
|
| 64 |
+
})
|
| 65 |
+
except Exception as e:
|
| 66 |
+
print(f"Error processing {article}: {e}")
|
| 67 |
+
pass
|
| 68 |
+
|
| 69 |
+
print(f"Collected results for {len(results)} articles.")
|
| 70 |
+
if not results:
|
| 71 |
+
print("No results collected! Exiting.")
|
| 72 |
+
return
|
| 73 |
+
|
| 74 |
+
res_df = pd.DataFrame(results)
|
| 75 |
+
res_df['Savings'] = res_df['NormPct'] - res_df['RecPct']
|
| 76 |
+
|
| 77 |
+
# Save to CSV for inspection
|
| 78 |
+
res_df.to_csv('validation_results.csv', index=False)
|
| 79 |
+
|
| 80 |
+
print("\n" + "="*40)
|
| 81 |
+
print(" VALIDATION SUMMARY")
|
| 82 |
+
print("="*40)
|
| 83 |
+
print(f"Total Articles Analyzed: {len(res_df)}")
|
| 84 |
+
print(f"Articles with Savings (> 0.5%): {len(res_df[res_df['Savings'] > 0.5])}")
|
| 85 |
+
print(f"Articles with More Buffer (< -0.5%): {len(res_df[res_df['Savings'] < -0.5])}")
|
| 86 |
+
print(f"Average Savings across plant: {res_df['Savings'].mean():.2f}%")
|
| 87 |
+
|
| 88 |
+
print("\n--- TOP 5 SAVINGS OPPORTUNITIES (Less Waste) ---")
|
| 89 |
+
print(res_df[res_df['OrderCount'] > 5].sort_values('Savings', ascending=False).head(5)[['Article', 'OrderCount', 'NormPct', 'RecPct', 'Savings', 'SuccessRate']])
|
| 90 |
+
|
| 91 |
+
print("\n--- TOP 5 RISK MITIGATION (More Safety) ---")
|
| 92 |
+
print(res_df[res_df['OrderCount'] > 5].sort_values('Savings', ascending=True).head(5)[['Article', 'OrderCount', 'NormPct', 'RecPct', 'Savings', 'SuccessRate']])
|
| 93 |
+
|
| 94 |
+
# Anomalies
|
| 95 |
+
neg_recs = res_df[res_df['RecPct'] < 0]
|
| 96 |
+
if not neg_recs.empty:
|
| 97 |
+
print(f"\n[CRITICAL] Found {len(neg_recs)} articles with NEGATIVE recommendation!")
|
| 98 |
+
print(neg_recs[['Article', 'RecPct']])
|
| 99 |
+
|
| 100 |
+
high_recs = res_df[res_df['RecPct'] > 15]
|
| 101 |
+
if not high_recs.empty:
|
| 102 |
+
print(f"\n[WARNING] Found {len(high_recs)} articles with > 15% recommendation!")
|
| 103 |
+
print(high_recs[['Article', 'RecPct', 'OrderCount']])
|
| 104 |
+
|
| 105 |
+
# Check specifically for the 'Partial Order' edge cases (high yields but high failure rate if not handled)
|
| 106 |
+
# We can't easily identify them here without looking at raw orders, but we can see if Rec % is reasonable.
|
| 107 |
+
|
| 108 |
+
if __name__ == "__main__":
|
| 109 |
+
run_validation()
|
backend/validation_output.txt
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Traceback (most recent call last):
|
| 2 |
+
File "/run/media/ishpreet/New Volume/Auribises/Vardhman Textiles/process-aware-ai/backend/validate_ai_logic.py", line 9, in <module>
|
| 3 |
+
from app.services.data_service import get_article_predictions, data_store, load_data
|
| 4 |
+
ImportError: cannot import name 'get_article_predictions' from 'app.services.data_service' (/run/media/ishpreet/New Volume/Auribises/Vardhman Textiles/process-aware-ai/backend/app/services/data_service.py)
|
backend/validation_output_v2.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Loading data...
|
| 2 |
+
Loading comprehensive data...
|
| 3 |
+
Norms loaded: 18 rules
|
| 4 |
+
Data Loaded. Rows: 4613
|
| 5 |
+
Found 496 unique articles. Running AI validation...
|
| 6 |
+
Processed 100/496...
|
| 7 |
+
Processed 200/496...
|
| 8 |
+
Processed 300/496...
|
| 9 |
+
Processed 400/496...
|
| 10 |
+
Traceback (most recent call last):
|
| 11 |
+
File "/run/media/ishpreet/New Volume/Auribises/Vardhman Textiles/process-aware-ai/backend/validate_ai_logic.py", line 104, in <module>
|
| 12 |
+
run_validation()
|
| 13 |
+
~~~~~~~~~~~~~~^^
|
| 14 |
+
File "/run/media/ishpreet/New Volume/Auribises/Vardhman Textiles/process-aware-ai/backend/validate_ai_logic.py", line 70, in run_validation
|
| 15 |
+
res_df['Savings'] = res_df['NormPct'] - res_df['RecPct']
|
| 16 |
+
~~~~~~^^^^^^^^^^^
|
| 17 |
+
File "/run/media/ishpreet/New Volume/Auribises/Vardhman Textiles/process-aware-ai/venv/lib/python3.13/site-packages/pandas/core/frame.py", line 4378, in __getitem__
|
| 18 |
+
indexer = self.columns.get_loc(key)
|
| 19 |
+
File "/run/media/ishpreet/New Volume/Auribises/Vardhman Textiles/process-aware-ai/venv/lib/python3.13/site-packages/pandas/core/indexes/range.py", line 525, in get_loc
|
| 20 |
+
raise KeyError(key)
|
| 21 |
+
KeyError: 'NormPct'
|
backend/validation_output_v3.txt
ADDED
|
@@ -0,0 +1,507 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Loading data...
|
| 2 |
+
Loading comprehensive data...
|
| 3 |
+
Norms loaded: 18 rules
|
| 4 |
+
Data Loaded. Rows: 4613
|
| 5 |
+
Found 496 unique articles. Running AI validation...
|
| 6 |
+
Error processing 18006BA: 'total_orders'
|
| 7 |
+
Error processing A240B236HMF: 'total_orders'
|
| 8 |
+
Error processing 150303BAMMZ2: 'total_orders'
|
| 9 |
+
Error processing 150264BAMMB5: 'total_orders'
|
| 10 |
+
Error processing A160C825HMMN: 'total_orders'
|
| 11 |
+
Error processing 14015BAMMB3: 'total_orders'
|
| 12 |
+
Error processing A120C903BACKWLR: 'total_orders'
|
| 13 |
+
Error processing A112A373BDOK: 'total_orders'
|
| 14 |
+
Error processing 14015BACM: 'total_orders'
|
| 15 |
+
Error processing 12091BDKK: 'total_orders'
|
| 16 |
+
Error processing 12000082BAOO: 'total_orders'
|
| 17 |
+
Error processing A120D628BAKKW: 'total_orders'
|
| 18 |
+
Error processing 160303BAMCBA2: 'total_orders'
|
| 19 |
+
Error processing 18015BAMMO: 'total_orders'
|
| 20 |
+
Error processing A160B540BAMMB: 'total_orders'
|
| 21 |
+
Error processing 14015BAMMB5: 'total_orders'
|
| 22 |
+
Error processing 140367BACM: 'total_orders'
|
| 23 |
+
Error processing 150K05BAMM1: 'total_orders'
|
| 24 |
+
Error processing A1600116BAMCNE: 'total_orders'
|
| 25 |
+
Error processing A145A415HMMN: 'total_orders'
|
| 26 |
+
Error processing A120B960BAKKO: 'total_orders'
|
| 27 |
+
Error processing A132B149BADD: 'total_orders'
|
| 28 |
+
Error processing 18006BAMMB: 'total_orders'
|
| 29 |
+
Error processing 15001204BAMCBW2: 'total_orders'
|
| 30 |
+
Error processing 160866BAMMB3: 'total_orders'
|
| 31 |
+
Error processing 18015BAMMY: 'total_orders'
|
| 32 |
+
Error processing A140C147OSPPR: 'total_orders'
|
| 33 |
+
Error processing 130129BAMMB: 'total_orders'
|
| 34 |
+
Error processing A140F646BAMMBW: 'total_orders'
|
| 35 |
+
Error processing A280B070BAPCCNY: 'total_orders'
|
| 36 |
+
Error processing A180A494BAMM: 'total_orders'
|
| 37 |
+
Error processing A114A170BAMM2: 'total_orders'
|
| 38 |
+
Error processing A240B096BAMC: 'total_orders'
|
| 39 |
+
Error processing A114A170BAMM: 'total_orders'
|
| 40 |
+
Error processing A140G679BAMCB: 'total_orders'
|
| 41 |
+
Error processing A150B589BAMCBWN3: 'total_orders'
|
| 42 |
+
Error processing A160C540OSPP: 'total_orders'
|
| 43 |
+
Error processing A130E731BAMF: 'total_orders'
|
| 44 |
+
Error processing A150E765BAMCN: 'total_orders'
|
| 45 |
+
Error processing A132B659BAMC: 'total_orders'
|
| 46 |
+
Error processing A150B589BAMCBDWN: 'total_orders'
|
| 47 |
+
Error processing A150B589BAMCBWN5: 'total_orders'
|
| 48 |
+
Error processing A140F473BAMM: 'total_orders'
|
| 49 |
+
Error processing 16009BDMM: 'total_orders'
|
| 50 |
+
Error processing A130A343BAKK: 'total_orders'
|
| 51 |
+
Error processing A120C693BAKKDW4: 'total_orders'
|
| 52 |
+
Error processing A132B340BADD: 'total_orders'
|
| 53 |
+
Error processing 16000294BAPPY: 'total_orders'
|
| 54 |
+
Error processing A240B224BAMM: 'total_orders'
|
| 55 |
+
Error processing A280B359BAPPNY: 'total_orders'
|
| 56 |
+
Error processing A160D231OSPP: 'total_orders'
|
| 57 |
+
Error processing A250A292BAVV: 'total_orders'
|
| 58 |
+
Error processing A130E341BAMKDW4: 'total_orders'
|
| 59 |
+
Error processing 12091BAKKB: 'total_orders'
|
| 60 |
+
Error processing A140D740BAMKB5: 'total_orders'
|
| 61 |
+
Error processing A140E428BAMD: 'total_orders'
|
| 62 |
+
Error processing A112A540BAKCW4: 'total_orders'
|
| 63 |
+
Error processing 14095BAMM: 'total_orders'
|
| 64 |
+
Error processing 150442BAMCB6: 'total_orders'
|
| 65 |
+
Error processing A150D909BAMC: 'total_orders'
|
| 66 |
+
Error processing 16000094BAMMA: 'total_orders'
|
| 67 |
+
Error processing 12000248BACD: 'total_orders'
|
| 68 |
+
Error processing 12000322BADD: 'total_orders'
|
| 69 |
+
Error processing 12400011BAKK: 'total_orders'
|
| 70 |
+
Error processing 16009OSPPY: 'total_orders'
|
| 71 |
+
Error processing A130F167BAMM: 'total_orders'
|
| 72 |
+
Error processing A120B495BAMM: 'total_orders'
|
| 73 |
+
Error processing A160C479OSPPY: 'total_orders'
|
| 74 |
+
Error processing A150D946BAMMBN: 'total_orders'
|
| 75 |
+
Error processing A160D209OSPPY: 'total_orders'
|
| 76 |
+
Error processing A116B163BAMMO: 'total_orders'
|
| 77 |
+
Error processing A120E602BAMFWY: 'total_orders'
|
| 78 |
+
Error processing A280A354BAMCBWN3: 'total_orders'
|
| 79 |
+
Error processing A180A212BAMCBN2: 'total_orders'
|
| 80 |
+
Error processing 14095BAMMO: 'total_orders'
|
| 81 |
+
Error processing A140A512BAMMO: 'total_orders'
|
| 82 |
+
Error processing A140C479BAMMW+: 'total_orders'
|
| 83 |
+
Error processing A1400474BAMFWQVJ: 'total_orders'
|
| 84 |
+
Error processing 12000251BAMC: 'total_orders'
|
| 85 |
+
Error processing 132340BADKD4: 'total_orders'
|
| 86 |
+
Error processing A120E061BAKK: 'total_orders'
|
| 87 |
+
Error processing A1600187BAMMB2: 'total_orders'
|
| 88 |
+
Error processing A160D253BAMM: 'total_orders'
|
| 89 |
+
Error processing 16000294BAMMZAN6: 'total_orders'
|
| 90 |
+
Error processing A120D302BACC: 'total_orders'
|
| 91 |
+
Error processing A160A742BADDA2: 'total_orders'
|
| 92 |
+
Error processing A116B584BAOO: 'total_orders'
|
| 93 |
+
Error processing 18006BAMMY: 'total_orders'
|
| 94 |
+
Error processing 13257BADKD4: 'total_orders'
|
| 95 |
+
Error processing 12031BA: 'total_orders'
|
| 96 |
+
Error processing A145A593BAMCW: 'total_orders'
|
| 97 |
+
Error processing A240B242OSPPY: 'total_orders'
|
| 98 |
+
Error processing A160C540OSPPY: 'total_orders'
|
| 99 |
+
Error processing 12047BAOE: 'total_orders'
|
| 100 |
+
Error processing A120E967OSPP: 'total_orders'
|
| 101 |
+
Error processing 160866OSPPY: 'total_orders'
|
| 102 |
+
Error processing 16000066BAMM: 'total_orders'
|
| 103 |
+
Error processing A140H306OSPP: 'total_orders'
|
| 104 |
+
Error processing A160D289OSPP: 'total_orders'
|
| 105 |
+
Processed 100/496...
|
| 106 |
+
Error processing A160D284OSPP: 'total_orders'
|
| 107 |
+
Error processing 150442BAMCB8: 'total_orders'
|
| 108 |
+
Error processing A120C359BAMM: 'total_orders'
|
| 109 |
+
Error processing 12200001BAKKW: 'total_orders'
|
| 110 |
+
Error processing A120C365BAMM: 'total_orders'
|
| 111 |
+
Error processing A140E042BAMMB5: 'total_orders'
|
| 112 |
+
Error processing 13200056BADFWA: 'total_orders'
|
| 113 |
+
Error processing 140324BAMCB3: 'total_orders'
|
| 114 |
+
Error processing A240B237BDRRB: 'total_orders'
|
| 115 |
+
Error processing A150C873BAMFDW3: 'total_orders'
|
| 116 |
+
Error processing 160866BAPPY: 'total_orders'
|
| 117 |
+
Error processing A150C873BAMFZW3: 'total_orders'
|
| 118 |
+
Error processing A150C873BAMFBQW: 'total_orders'
|
| 119 |
+
Error processing A150C873BAPFYDQW: 'total_orders'
|
| 120 |
+
Error processing A170A188BAMCBN: 'total_orders'
|
| 121 |
+
Error processing A150D683BAMCB2: 'total_orders'
|
| 122 |
+
Error processing 16000204BAMMB3: 'total_orders'
|
| 123 |
+
Error processing 16000204BAMM: 'total_orders'
|
| 124 |
+
Error processing A150C602BAMC: 'total_orders'
|
| 125 |
+
Error processing A140E013BAMM: 'total_orders'
|
| 126 |
+
Error processing 14514BAMM: 'total_orders'
|
| 127 |
+
Error processing A280B038BAEFWN2: 'total_orders'
|
| 128 |
+
Error processing A280B211BAMCBW: 'total_orders'
|
| 129 |
+
Error processing A280B211BAMCBW2: 'total_orders'
|
| 130 |
+
Error processing A280B212BAMCBW: 'total_orders'
|
| 131 |
+
Error processing A280B212BAMCBW2: 'total_orders'
|
| 132 |
+
Error processing A150C873BAMFDQW: 'total_orders'
|
| 133 |
+
Error processing C140M751BAMKC3: 'total_orders'
|
| 134 |
+
Error processing A140G193BAMCB: 'total_orders'
|
| 135 |
+
Error processing A140E645BADDQW2: 'total_orders'
|
| 136 |
+
Error processing A140F185BAMMB3: 'total_orders'
|
| 137 |
+
Error processing A140E777BAMFBWQN3: 'total_orders'
|
| 138 |
+
Error processing 130M90BADDVJ: 'total_orders'
|
| 139 |
+
Error processing A140E013BAMMW2: 'total_orders'
|
| 140 |
+
Error processing A140G672BAMCB: 'total_orders'
|
| 141 |
+
Error processing A150D683BAMCB3: 'total_orders'
|
| 142 |
+
Error processing A150C873BAMFZW8: 'total_orders'
|
| 143 |
+
Error processing A150C873BAMFDW6: 'total_orders'
|
| 144 |
+
Error processing A140E777BAMFBWN3: 'total_orders'
|
| 145 |
+
Error processing 14005BACM: 'total_orders'
|
| 146 |
+
Error processing A111A004OSPP: 'total_orders'
|
| 147 |
+
Error processing A240A241OSKK: 'total_orders'
|
| 148 |
+
Error processing A130D024BACCWY: 'total_orders'
|
| 149 |
+
Error processing A132A234BADC: 'total_orders'
|
| 150 |
+
Error processing A140F790BAMC: 'total_orders'
|
| 151 |
+
Error processing A160C485BAMC: 'total_orders'
|
| 152 |
+
Error processing A132B125BADKDW4: 'total_orders'
|
| 153 |
+
Error processing A140F815BAMMB3: 'total_orders'
|
| 154 |
+
Error processing A150D189BAMMZ: 'total_orders'
|
| 155 |
+
Error processing A130C663BACCW: 'total_orders'
|
| 156 |
+
Error processing A140A649BAMCA: 'total_orders'
|
| 157 |
+
Error processing 160563BAMMO: 'total_orders'
|
| 158 |
+
Error processing 260300BAEEBA2: 'total_orders'
|
| 159 |
+
Error processing A120B732BAOK*: 'total_orders'
|
| 160 |
+
Error processing A150C547BAMCB3: 'total_orders'
|
| 161 |
+
Error processing A280B195BAMCW: 'total_orders'
|
| 162 |
+
Error processing A140G692BAMMB: 'total_orders'
|
| 163 |
+
Error processing 13000579BAMCW4: 'total_orders'
|
| 164 |
+
Error processing A160C567BAMMBN: 'total_orders'
|
| 165 |
+
Error processing A132B125BADCW: 'total_orders'
|
| 166 |
+
Error processing 16000432BAMM: 'total_orders'
|
| 167 |
+
Error processing A140E471BAMFZQN3: 'total_orders'
|
| 168 |
+
Error processing 12000349BAMCEA2: 'total_orders'
|
| 169 |
+
Error processing A160C445BAMC: 'total_orders'
|
| 170 |
+
Error processing A140A865BDMKD: 'total_orders'
|
| 171 |
+
Error processing A140G722BAMKW4: 'total_orders'
|
| 172 |
+
Error processing 16000232BAMMN: 'total_orders'
|
| 173 |
+
Error processing 130D07BAMMA: 'total_orders'
|
| 174 |
+
Error processing 116580BA: 'total_orders'
|
| 175 |
+
Error processing A140F645BAMM: 'total_orders'
|
| 176 |
+
Error processing A132B311BADD: 'total_orders'
|
| 177 |
+
Error processing A132A744BADD: 'total_orders'
|
| 178 |
+
Error processing A130B742BADCW: 'total_orders'
|
| 179 |
+
Error processing A120E174BAKDW: 'total_orders'
|
| 180 |
+
Error processing 13200074BADD: 'total_orders'
|
| 181 |
+
Error processing 13200046BADD: 'total_orders'
|
| 182 |
+
Error processing A120D009BAMM: 'total_orders'
|
| 183 |
+
Error processing A130E163BAMFQ5: 'total_orders'
|
| 184 |
+
Error processing A145A530BAMCBWQN: 'total_orders'
|
| 185 |
+
Error processing A140D699BAMFZWQN5: 'total_orders'
|
| 186 |
+
Error processing A140E777BAMFSWQN: 'total_orders'
|
| 187 |
+
Error processing A140A865BAMK: 'total_orders'
|
| 188 |
+
Error processing A130A882BACCBW2: 'total_orders'
|
| 189 |
+
Error processing A145A530BAMCWQN: 'total_orders'
|
| 190 |
+
Error processing 14000910BAMD: 'total_orders'
|
| 191 |
+
Error processing 14001037BAMM: 'total_orders'
|
| 192 |
+
Error processing A140A865BAMKD: 'total_orders'
|
| 193 |
+
Error processing A150C547BAMCB5: 'total_orders'
|
| 194 |
+
Error processing A121A058BAKKW: 'total_orders'
|
| 195 |
+
Error processing A150E721BAMMZN: 'total_orders'
|
| 196 |
+
Error processing 14500150BAMM: 'total_orders'
|
| 197 |
+
Error processing 13000503BAMFBW2: 'total_orders'
|
| 198 |
+
Error processing 14000398BAMVR: 'total_orders'
|
| 199 |
+
Error processing A120C726BAOO: 'total_orders'
|
| 200 |
+
Error processing A132B606BADD: 'total_orders'
|
| 201 |
+
Error processing A220A109BAOOB: 'total_orders'
|
| 202 |
+
Error processing A130A723BAMKDW4: 'total_orders'
|
| 203 |
+
Error processing A130D001OSPP2: 'total_orders'
|
| 204 |
+
Error processing A130D900BAMC: 'total_orders'
|
| 205 |
+
Error processing A2A0A551BAEE: 'total_orders'
|
| 206 |
+
Processed 200/496...
|
| 207 |
+
Error processing A140D698BAMFZWN5: 'total_orders'
|
| 208 |
+
Error processing A140F030BADFWRA2: 'total_orders'
|
| 209 |
+
Error processing A220A109BAKOB: 'total_orders'
|
| 210 |
+
Error processing A150F001BAMM: 'total_orders'
|
| 211 |
+
Error processing A132A415BDMCD: 'total_orders'
|
| 212 |
+
Error processing 28000099BAEFBW2: 'total_orders'
|
| 213 |
+
Error processing 16009BA: 'total_orders'
|
| 214 |
+
Error processing A130E918BDMCD: 'total_orders'
|
| 215 |
+
Error processing A150E531BAMKW4: 'total_orders'
|
| 216 |
+
Error processing 160A63BAMMZ7: 'total_orders'
|
| 217 |
+
Error processing 130176BDCM: 'total_orders'
|
| 218 |
+
Error processing 16000108BAMM: 'total_orders'
|
| 219 |
+
Error processing 12000313BAKCA: 'total_orders'
|
| 220 |
+
Error processing A110A579BAOF2: 'total_orders'
|
| 221 |
+
Error processing A130E202BAVMBV3: 'total_orders'
|
| 222 |
+
Error processing A130E202BAVMBV: 'total_orders'
|
| 223 |
+
Error processing A150B593BAMMA: 'total_orders'
|
| 224 |
+
Error processing A235A012BAMOO: 'total_orders'
|
| 225 |
+
Error processing A150E177BAKKNW: 'total_orders'
|
| 226 |
+
Error processing 13000275BAMM: 'total_orders'
|
| 227 |
+
Error processing 160811BAMM: 'total_orders'
|
| 228 |
+
Error processing A160C426BAMC: 'total_orders'
|
| 229 |
+
Error processing A160B951BAMM: 'total_orders'
|
| 230 |
+
Error processing 14000909BAMM: 'total_orders'
|
| 231 |
+
Error processing 12000267BAKK: 'total_orders'
|
| 232 |
+
Error processing 12000267BADD: 'total_orders'
|
| 233 |
+
Error processing A180A212BAMCBN: 'total_orders'
|
| 234 |
+
Error processing A1400474BAMFBW6: 'total_orders'
|
| 235 |
+
Error processing A130F127BAMF: 'total_orders'
|
| 236 |
+
Error processing A140B826BDMCDW4: 'total_orders'
|
| 237 |
+
Error processing A150E173BDMCD: 'total_orders'
|
| 238 |
+
Error processing 140448BAMMB2: 'total_orders'
|
| 239 |
+
Error processing 16072BACM: 'total_orders'
|
| 240 |
+
Error processing A130D900BAMCDW4: 'total_orders'
|
| 241 |
+
Error processing 140367BDB: 'total_orders'
|
| 242 |
+
Error processing 14500033BAMMB: 'total_orders'
|
| 243 |
+
Error processing 14500033BAMMB3: 'total_orders'
|
| 244 |
+
Error processing 160625BDMMA: 'total_orders'
|
| 245 |
+
Error processing A160C426BAMCA: 'total_orders'
|
| 246 |
+
Error processing A160C445BAMC2: 'total_orders'
|
| 247 |
+
Error processing A130F162BAMF: 'total_orders'
|
| 248 |
+
Error processing 280A95BAMC2: 'total_orders'
|
| 249 |
+
Error processing C140M503BAMM: 'total_orders'
|
| 250 |
+
Error processing C140M503BAMM*: 'total_orders'
|
| 251 |
+
Error processing A140D158BAMCDW4: 'total_orders'
|
| 252 |
+
Error processing A116A743BDOCDW4: 'total_orders'
|
| 253 |
+
Error processing A235A013BAMCO: 'total_orders'
|
| 254 |
+
Error processing 16000237BAMC: 'total_orders'
|
| 255 |
+
Error processing A145A488BAMKDW4: 'total_orders'
|
| 256 |
+
Error processing A150D161BDMCDW4: 'total_orders'
|
| 257 |
+
Error processing A160C974BAMM: 'total_orders'
|
| 258 |
+
Error processing A160C974BAMMA: 'total_orders'
|
| 259 |
+
Error processing 25065BAMMA: 'total_orders'
|
| 260 |
+
Error processing A145A624BAMM: 'total_orders'
|
| 261 |
+
Error processing A150D112BAMM: 'total_orders'
|
| 262 |
+
Error processing A150D112BAMMB6: 'total_orders'
|
| 263 |
+
Error processing A140C096BAMM: 'total_orders'
|
| 264 |
+
Error processing A150B047BDMCD: 'total_orders'
|
| 265 |
+
Error processing A132B509BAMF: 'total_orders'
|
| 266 |
+
Error processing A132B170BAMCW4: 'total_orders'
|
| 267 |
+
Error processing A132B170BAMCBW34: 'total_orders'
|
| 268 |
+
Error processing A130B170BAMCW4: 'total_orders'
|
| 269 |
+
Error processing A132B898BADK: 'total_orders'
|
| 270 |
+
Error processing A160C589BAMMWZN: 'total_orders'
|
| 271 |
+
Error processing A140G004BAMMBN2: 'total_orders'
|
| 272 |
+
Error processing A130C646BAMC: 'total_orders'
|
| 273 |
+
Error processing A120E828BAMCW4: 'total_orders'
|
| 274 |
+
Error processing 160A63BA: 'total_orders'
|
| 275 |
+
Error processing A140F086BDMCDA: 'total_orders'
|
| 276 |
+
Error processing A116B252BAMFW: 'total_orders'
|
| 277 |
+
Error processing A130E897BAMKB: 'total_orders'
|
| 278 |
+
Error processing A150A567BAMMB3: 'total_orders'
|
| 279 |
+
Error processing 14252BAMMB: 'total_orders'
|
| 280 |
+
Error processing 18006OSPP: 'total_orders'
|
| 281 |
+
Error processing A280B152BAMMBN3: 'total_orders'
|
| 282 |
+
Error processing 240B16BAMM: 'total_orders'
|
| 283 |
+
Error processing A130F336BAMC: 'total_orders'
|
| 284 |
+
Error processing A140G372BAKK: 'total_orders'
|
| 285 |
+
Error processing A280A898BAMC3: 'total_orders'
|
| 286 |
+
Error processing A280A898BAMC2: 'total_orders'
|
| 287 |
+
Error processing A140F319BDMD: 'total_orders'
|
| 288 |
+
Error processing A110A699BAOOB: 'total_orders'
|
| 289 |
+
Error processing 150442BAMCB7: 'total_orders'
|
| 290 |
+
Error processing A240A975BDMKD: 'total_orders'
|
| 291 |
+
Error processing A120D162BAKK: 'total_orders'
|
| 292 |
+
Error processing A132A901BADKW4: 'total_orders'
|
| 293 |
+
Error processing A130D162BAMCW4: 'total_orders'
|
| 294 |
+
Error processing A140G753BAMD: 'total_orders'
|
| 295 |
+
Error processing 140324BAMCDW2: 'total_orders'
|
| 296 |
+
Error processing 140324BDMCDW4: 'total_orders'
|
| 297 |
+
Error processing 160563BA: 'total_orders'
|
| 298 |
+
Error processing A140G585BADK: 'total_orders'
|
| 299 |
+
Error processing A160A384BAMCW2: 'total_orders'
|
| 300 |
+
Error processing 16000173OSMM: 'total_orders'
|
| 301 |
+
Error processing A160C612BAMC: 'total_orders'
|
| 302 |
+
Error processing 150188BAMM: 'total_orders'
|
| 303 |
+
Error processing A160A390BAMC2: 'total_orders'
|
| 304 |
+
Error processing 160483BAMCC2: 'total_orders'
|
| 305 |
+
Error processing A250A194BAMMN: 'total_orders'
|
| 306 |
+
Error processing A140A865BAMKB6: 'total_orders'
|
| 307 |
+
Processed 300/496...
|
| 308 |
+
Error processing A160C920BAMMZN: 'total_orders'
|
| 309 |
+
Error processing A280B204BAMMN: 'total_orders'
|
| 310 |
+
Error processing A160B572BAMMCN: 'total_orders'
|
| 311 |
+
Error processing A120D892BAMM: 'total_orders'
|
| 312 |
+
Error processing 160874BAMM: 'total_orders'
|
| 313 |
+
Error processing 160874BAMMZ: 'total_orders'
|
| 314 |
+
Error processing A150B593BAMMBA3: 'total_orders'
|
| 315 |
+
Error processing 150264BA: 'total_orders'
|
| 316 |
+
Error processing 150264OSMM: 'total_orders'
|
| 317 |
+
Error processing A240B231BAMCN: 'total_orders'
|
| 318 |
+
Error processing 160866BAMM2: 'total_orders'
|
| 319 |
+
Error processing A140B410BAMKBW34: 'total_orders'
|
| 320 |
+
Error processing 14000928BAMFQ: 'total_orders'
|
| 321 |
+
Error processing 14226BAMMB: 'total_orders'
|
| 322 |
+
Error processing A130F425BAKK: 'total_orders'
|
| 323 |
+
Error processing A140F601BAMCW2: 'total_orders'
|
| 324 |
+
Error processing A140D755BAMMO: 'total_orders'
|
| 325 |
+
Error processing 14005BAMMB3: 'total_orders'
|
| 326 |
+
Error processing A230A364BAMMO: 'total_orders'
|
| 327 |
+
Error processing 15000458BAMK: 'total_orders'
|
| 328 |
+
Error processing 130K00BAMK: 'total_orders'
|
| 329 |
+
Error processing 130K00BDDKD: 'total_orders'
|
| 330 |
+
Error processing A150E252BAMM: 'total_orders'
|
| 331 |
+
Error processing 14000925BAMMBN2: 'total_orders'
|
| 332 |
+
Error processing A140C562BAMMWE: 'total_orders'
|
| 333 |
+
Error processing A150D718BAMM: 'total_orders'
|
| 334 |
+
Error processing A150D718BAMMB: 'total_orders'
|
| 335 |
+
Error processing A130B609OSPP2: 'total_orders'
|
| 336 |
+
Error processing 16000173BAMMA: 'total_orders'
|
| 337 |
+
Error processing 14095BAMMC: 'total_orders'
|
| 338 |
+
Error processing A150D243BAMM: 'total_orders'
|
| 339 |
+
Error processing A160C925BAMM: 'total_orders'
|
| 340 |
+
Error processing A160B951BAMMB2: 'total_orders'
|
| 341 |
+
Error processing A132A824BAMCN: 'total_orders'
|
| 342 |
+
Error processing A250A209BAERN: 'total_orders'
|
| 343 |
+
Error processing A130F295BAMM: 'total_orders'
|
| 344 |
+
Error processing 14000453BAMO: 'total_orders'
|
| 345 |
+
Error processing A140B826BAMCSWA4: 'total_orders'
|
| 346 |
+
Error processing A160B146BAMMB3: 'total_orders'
|
| 347 |
+
Error processing 16000418BAMM2: 'total_orders'
|
| 348 |
+
Error processing A140G461BAMCWFV: 'total_orders'
|
| 349 |
+
Error processing 140U31BAMKW4: 'total_orders'
|
| 350 |
+
Error processing 16000251BAMO: 'total_orders'
|
| 351 |
+
Error processing A280B358BAPKDW4: 'total_orders'
|
| 352 |
+
Error processing A150D650BAMCN2: 'total_orders'
|
| 353 |
+
Error processing 120G43BAMCBW: 'total_orders'
|
| 354 |
+
Error processing A150D837BAMKW4: 'total_orders'
|
| 355 |
+
Error processing 150303BA: 'total_orders'
|
| 356 |
+
Error processing A130E918BDVCD: 'total_orders'
|
| 357 |
+
Error processing A150D650BAMCN: 'total_orders'
|
| 358 |
+
Error processing A140G966BAMM: 'total_orders'
|
| 359 |
+
Error processing 16000232BAMMBN3: 'total_orders'
|
| 360 |
+
Error processing A140H105BAMD: 'total_orders'
|
| 361 |
+
Error processing A150E338BAMMW: 'total_orders'
|
| 362 |
+
Error processing A132A415BAMC: 'total_orders'
|
| 363 |
+
Error processing A132B905BAMKNDW4: 'total_orders'
|
| 364 |
+
Error processing 14000910BAMDB3: 'total_orders'
|
| 365 |
+
Error processing 16000429BADDR: 'total_orders'
|
| 366 |
+
Error processing A160A742OSPP2: 'total_orders'
|
| 367 |
+
Error processing A132B847BADCW: 'total_orders'
|
| 368 |
+
Error processing A150E383BAMCZ: 'total_orders'
|
| 369 |
+
Error processing A140E253BAMF2: 'total_orders'
|
| 370 |
+
Error processing A132B672BAMKN: 'total_orders'
|
| 371 |
+
Error processing 13000093BAMM: 'total_orders'
|
| 372 |
+
Error processing A140A622OSKV: 'total_orders'
|
| 373 |
+
Error processing A124A548BAKK: 'total_orders'
|
| 374 |
+
Error processing A1400474BAMFW3: 'total_orders'
|
| 375 |
+
Error processing 15001253BAMMW: 'total_orders'
|
| 376 |
+
Error processing A140E780BAMO: 'total_orders'
|
| 377 |
+
Error processing A160B260BADD: 'total_orders'
|
| 378 |
+
Error processing A120C882BACC: 'total_orders'
|
| 379 |
+
Error processing A140F842BAMMB: 'total_orders'
|
| 380 |
+
Error processing A140D755BAMMY: 'total_orders'
|
| 381 |
+
Error processing A145A571BAMM: 'total_orders'
|
| 382 |
+
Error processing A160C450BAMC2: 'total_orders'
|
| 383 |
+
Error processing A180A303BAMM: 'total_orders'
|
| 384 |
+
Error processing 140324BAMCBW5: 'total_orders'
|
| 385 |
+
Error processing A140F925BAMM: 'total_orders'
|
| 386 |
+
Error processing A116A361BDOKBW4: 'total_orders'
|
| 387 |
+
Error processing 140448BAMMB: 'total_orders'
|
| 388 |
+
Error processing 15000147BAMCB3: 'total_orders'
|
| 389 |
+
Error processing 15000147BAMC: 'total_orders'
|
| 390 |
+
Error processing A132A895BADK: 'total_orders'
|
| 391 |
+
Error processing A140F417BAMC: 'total_orders'
|
| 392 |
+
Error processing A130D146BAMK: 'total_orders'
|
| 393 |
+
Error processing A260A443BAMKDWA4: 'total_orders'
|
| 394 |
+
Error processing A130F523BAMKDW4: 'total_orders'
|
| 395 |
+
Error processing 11649BA: 'total_orders'
|
| 396 |
+
Error processing A140C562BAMMBW2: 'total_orders'
|
| 397 |
+
Error processing A280B371BAMMN: 'total_orders'
|
| 398 |
+
Error processing A2A0A628BAMMNZ: 'total_orders'
|
| 399 |
+
Error processing A260A512BAERN: 'total_orders'
|
| 400 |
+
Error processing A130E354BACC: 'total_orders'
|
| 401 |
+
Error processing A145A571BAMMQY: 'total_orders'
|
| 402 |
+
Error processing 12400043BACC: 'total_orders'
|
| 403 |
+
Error processing 28000025BAMMB2: 'total_orders'
|
| 404 |
+
Error processing A130D384BAKK: 'total_orders'
|
| 405 |
+
Error processing 14095BAMPC: 'total_orders'
|
| 406 |
+
Error processing A120B115BAKK: 'total_orders'
|
| 407 |
+
Error processing A3A0A015BAMC: 'total_orders'
|
| 408 |
+
Processed 400/496...
|
| 409 |
+
Error processing A250A185BAMCDWN4: 'total_orders'
|
| 410 |
+
Error processing A280A297BAMMN: 'total_orders'
|
| 411 |
+
Error processing A124A663BAKK: 'total_orders'
|
| 412 |
+
Error processing A1400090BAMM: 'total_orders'
|
| 413 |
+
Error processing 12000155BAOK: 'total_orders'
|
| 414 |
+
Error processing A130D863BADKDW4: 'total_orders'
|
| 415 |
+
Error processing 16000185BAMM: 'total_orders'
|
| 416 |
+
Error processing A150F525BAMM: 'total_orders'
|
| 417 |
+
Error processing 15001397BAMMB: 'total_orders'
|
| 418 |
+
Error processing 14015BDMM2: 'total_orders'
|
| 419 |
+
Error processing A160C900BAMO: 'total_orders'
|
| 420 |
+
Error processing 15000051BAMMA: 'total_orders'
|
| 421 |
+
Error processing A140D745BAMFBN3: 'total_orders'
|
| 422 |
+
Error processing A160C117BAMM: 'total_orders'
|
| 423 |
+
Error processing 15001398BAMM: 'total_orders'
|
| 424 |
+
Error processing A140C096BAMMB2: 'total_orders'
|
| 425 |
+
Error processing 12000248BACD2: 'total_orders'
|
| 426 |
+
Error processing 26000068BAPP: 'total_orders'
|
| 427 |
+
Error processing 140V33BAMMS: 'total_orders'
|
| 428 |
+
Error processing 15001294BAMMW: 'total_orders'
|
| 429 |
+
Error processing 110159BD: 'total_orders'
|
| 430 |
+
Error processing A140F555BDMCDW4: 'total_orders'
|
| 431 |
+
Error processing A140H160BAMM: 'total_orders'
|
| 432 |
+
Error processing A114A170BAMMQY2: 'total_orders'
|
| 433 |
+
Error processing 12400011BAKK2: 'total_orders'
|
| 434 |
+
Error processing A280B031BAPMBN: 'total_orders'
|
| 435 |
+
Error processing 150S05BAMM: 'total_orders'
|
| 436 |
+
Error processing 160874BAMMZ2: 'total_orders'
|
| 437 |
+
Error processing A140H239BAMMB: 'total_orders'
|
| 438 |
+
Error processing 140448BAMPB: 'total_orders'
|
| 439 |
+
Error processing A140H035BDMM: 'total_orders'
|
| 440 |
+
Error processing A150E803BDMM: 'total_orders'
|
| 441 |
+
Error processing A160C973BAMMB: 'total_orders'
|
| 442 |
+
Error processing A160C952BAMC: 'total_orders'
|
| 443 |
+
Error processing A160D251BAMM: 'total_orders'
|
| 444 |
+
Error processing 14500101BADDRA2: 'total_orders'
|
| 445 |
+
Error processing A160C978BAMD: 'total_orders'
|
| 446 |
+
Error processing 150G63BAMMB2: 'total_orders'
|
| 447 |
+
Error processing A130F458BACM: 'total_orders'
|
| 448 |
+
Error processing A112A464BAKPW: 'total_orders'
|
| 449 |
+
Error processing A140B631BADDR2: 'total_orders'
|
| 450 |
+
Error processing A150E247BAMM: 'total_orders'
|
| 451 |
+
Error processing A1400491BAMC2: 'total_orders'
|
| 452 |
+
Error processing 16009BAMMZ: 'total_orders'
|
| 453 |
+
Error processing 12000313BDKC: 'total_orders'
|
| 454 |
+
Error processing 140J17BDMKD: 'total_orders'
|
| 455 |
+
Error processing A280A898BAPC: 'total_orders'
|
| 456 |
+
Error processing 140324BAMCWY: 'total_orders'
|
| 457 |
+
Error processing A160C072BAMM: 'total_orders'
|
| 458 |
+
Error processing A160B137BAMOD: 'total_orders'
|
| 459 |
+
Error processing A140E186BAMC: 'total_orders'
|
| 460 |
+
Error processing 24000170BAPP: 'total_orders'
|
| 461 |
+
Error processing A1400006BAMC: 'total_orders'
|
| 462 |
+
Error processing A140F212BAMFBW3: 'total_orders'
|
| 463 |
+
Error processing 180109BAMMZ: 'total_orders'
|
| 464 |
+
Error processing 150J31BDMM: 'total_orders'
|
| 465 |
+
Error processing 150J31BAPP: 'total_orders'
|
| 466 |
+
Error processing A112A575BAOK: 'total_orders'
|
| 467 |
+
Error processing 140324BA2: 'total_orders'
|
| 468 |
+
Error processing 160866BAMMB6: 'total_orders'
|
| 469 |
+
Error processing 150264BD: 'total_orders'
|
| 470 |
+
Error processing A150B047BAMC: 'total_orders'
|
| 471 |
+
Error processing 140324BAMCBW3: 'total_orders'
|
| 472 |
+
Error processing A116B330BAKFWA: 'total_orders'
|
| 473 |
+
Error processing A130D422BADD: 'total_orders'
|
| 474 |
+
Error processing A140F549BAMCCW4: 'total_orders'
|
| 475 |
+
Error processing A140F549BAMCCDW4: 'total_orders'
|
| 476 |
+
Error processing 12000017BAMKZ2: 'total_orders'
|
| 477 |
+
Error processing 12200001BAKKWJV: 'total_orders'
|
| 478 |
+
Error processing A140G714BAMMVJ: 'total_orders'
|
| 479 |
+
Error processing 14000756BAMMVJ: 'total_orders'
|
| 480 |
+
Error processing 130M28BAMK4: 'total_orders'
|
| 481 |
+
Error processing A150C547BAMCB$: 'total_orders'
|
| 482 |
+
Error processing 13200056BADFWRAVJ: 'total_orders'
|
| 483 |
+
Error processing 140371BAMMB3: 'total_orders'
|
| 484 |
+
Error processing A132A453BADFWRAVJ*: 'total_orders'
|
| 485 |
+
Error processing 15000715BAMMW: 'total_orders'
|
| 486 |
+
Error processing A140D454BAMM: 'total_orders'
|
| 487 |
+
Error processing 140I21BA: 'total_orders'
|
| 488 |
+
Error processing 14000638BAMM: 'total_orders'
|
| 489 |
+
Error processing 12200001BAKKWJVA: 'total_orders'
|
| 490 |
+
Error processing A1400406BDCC: 'total_orders'
|
| 491 |
+
Error processing 18006BAMMO: 'total_orders'
|
| 492 |
+
Error processing 120G88BA: 'total_orders'
|
| 493 |
+
Error processing 120G88BAMOE: 'total_orders'
|
| 494 |
+
Error processing 120G65BAOO: 'total_orders'
|
| 495 |
+
Error processing 116555BA: 'total_orders'
|
| 496 |
+
Error processing 11400036BAMMQY: 'total_orders'
|
| 497 |
+
Error processing 14000313BAMMBA5: 'total_orders'
|
| 498 |
+
Error processing A1320030BADKW4: 'total_orders'
|
| 499 |
+
Error processing A140D939BAMFZQNA9: 'total_orders'
|
| 500 |
+
Error processing A114A170BAMMQY: 'total_orders'
|
| 501 |
+
Error processing A130D787BACC: 'total_orders'
|
| 502 |
+
Error processing A114A170BAMMQFEV: 'total_orders'
|
| 503 |
+
Error processing A150B588BAMCB: 'total_orders'
|
| 504 |
+
Error processing 150A83BAMCB: 'total_orders'
|
| 505 |
+
Error processing 150A83BAMC: 'total_orders'
|
| 506 |
+
Collected results for 0 articles.
|
| 507 |
+
No results collected! Exiting.
|
backend/validation_output_v5.txt
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Loading data...
|
| 2 |
+
Loading comprehensive data...
|
| 3 |
+
Norms loaded: 18 rules
|
| 4 |
+
Data Loaded. Rows: 4613
|
| 5 |
+
Found 496 unique articles. Running AI validation...
|
| 6 |
+
Processed 100/496...
|
| 7 |
+
Processed 200/496...
|
| 8 |
+
Processed 300/496...
|
| 9 |
+
Processed 400/496...
|
| 10 |
+
Collected results for 496 articles.
|
| 11 |
+
|
| 12 |
+
========================================
|
| 13 |
+
VALIDATION SUMMARY
|
| 14 |
+
========================================
|
| 15 |
+
Total Articles Analyzed: 496
|
| 16 |
+
Articles with Savings (> 0.5%): 160
|
| 17 |
+
Articles with More Buffer (< -0.5%): 283
|
| 18 |
+
Average Savings across plant: -1.95%
|
| 19 |
+
|
| 20 |
+
--- TOP 5 SAVINGS OPPORTUNITIES (Less Waste) ---
|
| 21 |
+
Article OrderCount NormPct RecPct Savings SuccessRate
|
| 22 |
+
264 A130C646BAMC 7 12.0 0.0 12.0 14.3
|
| 23 |
+
11 A120D628BAKKW 8 7.0 3.3 3.7 87.5
|
| 24 |
+
16 140367BACM 7 3.0 0.0 3.0 85.7
|
| 25 |
+
43 16009BDMM 31 3.0 0.0 3.0 93.5
|
| 26 |
+
9 12091BDKK 8 6.0 4.3 1.7 50.0
|
| 27 |
+
|
| 28 |
+
--- TOP 5 RISK MITIGATION (More Safety) ---
|
| 29 |
+
Article OrderCount NormPct RecPct Savings SuccessRate
|
| 30 |
+
467 12200001BAKKWJV 9 6.0 28.2 -22.2 22.2
|
| 31 |
+
204 28000099BAEFBW2 7 5.0 15.4 -10.4 14.3
|
| 32 |
+
239 A130F162BAMF 11 5.0 10.4 -5.4 81.8
|
| 33 |
+
110 A150C873BAMFZW3 6 5.0 10.0 -5.0 0.0
|
| 34 |
+
165 16000232BAMMN 7 3.0 6.9 -3.9 57.1
|
| 35 |
+
|
| 36 |
+
[WARNING] Found 31 articles with > 15% recommendation!
|
| 37 |
+
Article RecPct OrderCount
|
| 38 |
+
4 A160C825HMMN 17.1 1
|
| 39 |
+
7 A112A373BDOK 22.5 2
|
| 40 |
+
17 150K05BAMM1 40.8 2
|
| 41 |
+
21 A132B149BADD 43.5 1
|
| 42 |
+
26 A140C147OSPPR 44.5 2
|
| 43 |
+
69 A160D209OSPPY 59.5 2
|
| 44 |
+
85 A160A742BADDA2 17.5 3
|
| 45 |
+
139 A111A004OSPP 20.0 1
|
| 46 |
+
140 A240A241OSKK 19.1 1
|
| 47 |
+
172 A120E174BAKDW 21.9 1
|
| 48 |
+
173 13200074BADD 20.0 1
|
| 49 |
+
179 A140E777BAMFSWQN 16.4 1
|
| 50 |
+
193 A132B606BADD 20.0 1
|
| 51 |
+
196 A130D001OSPP2 20.0 1
|
| 52 |
+
204 28000099BAEFBW2 15.4 7
|
| 53 |
+
217 A150E177BAKKNW 24.7 4
|
| 54 |
+
262 A160C589BAMMWZN 41.0 3
|
| 55 |
+
265 A120E828BAMCW4 20.4 1
|
| 56 |
+
286 A140G753BAMD 21.4 2
|
| 57 |
+
290 A140G585BADK 23.0 1
|
| 58 |
+
305 A150B593BAMMBA3 99.9 2
|
| 59 |
+
326 A130B609OSPP2 17.0 1
|
| 60 |
+
357 A160A742OSPP2 15.1 1
|
| 61 |
+
363 A140A622OSKV 20.0 1
|
| 62 |
+
368 A160B260BADD 18.7 2
|
| 63 |
+
379 15000147BAMCB3 53.0 3
|
| 64 |
+
434 14500101BADDRA2 23.0 1
|
| 65 |
+
439 A140B631BADDR2 20.0 1
|
| 66 |
+
461 140324BAMCBW3 69.5 1
|
| 67 |
+
467 12200001BAKKWJV 28.2 9
|
| 68 |
+
479 12200001BAKKWJVA 41.9 4
|
backend/validation_results.csv
ADDED
|
@@ -0,0 +1,497 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Article,OrderCount,SuccessRate,NormPct,RecPct,Adjustment,YieldAvg,YieldStd,Savings
|
| 2 |
+
18006BA,36,63.9,3.0,1.4,-1.6,96.8,12.65,1.6
|
| 3 |
+
A240B236HMF,1,100.0,6.0,14.7,8.7,87.2,0.0,-8.7
|
| 4 |
+
150303BAMMZ2,5,100.0,2.0,4.2,2.2,95.8,2.68,-2.2
|
| 5 |
+
150264BAMMB5,5,80.0,3.0,8.5,5.5,92.0,6.32,-5.5
|
| 6 |
+
A160C825HMMN,1,100.0,4.0,17.1,13.1,85.4,0.0,-13.100000000000001
|
| 7 |
+
14015BAMMB3,7,85.7,3.0,6.5,3.5,92.1,6.12,-3.5
|
| 8 |
+
A120C903BACKWLR,1,0.0,6.0,11.0,5.0,62.6,0.0,-5.0
|
| 9 |
+
A112A373BDOK,2,50.0,6.0,22.5,16.5,81.6,0.0,-16.5
|
| 10 |
+
14015BACM,23,69.6,3.0,4.1,1.1,91.2,17.07,-1.0999999999999996
|
| 11 |
+
12091BDKK,8,50.0,6.0,4.3,-1.7,97.2,5.21,1.7000000000000002
|
| 12 |
+
12000082BAOO,8,75.0,5.0,6.6,1.6,93.7,6.86,-1.5999999999999996
|
| 13 |
+
A120D628BAKKW,8,87.5,7.0,3.3,-3.7,86.2,39.72,3.7
|
| 14 |
+
160303BAMCBA2,3,100.0,5.0,2.2,-2.8,97.0,2.08,2.8
|
| 15 |
+
18015BAMMO,1,100.0,3.0,5.2,2.2,95.0,0.0,-2.2
|
| 16 |
+
A160B540BAMMB,2,0.0,5.0,10.0,5.0,52.7,3.94,-5.0
|
| 17 |
+
14015BAMMB5,1,100.0,4.0,7.6,3.6,92.9,0.0,-3.5999999999999996
|
| 18 |
+
140367BACM,7,85.7,3.0,0.0,-3.1,98.9,2.97,3.0
|
| 19 |
+
150K05BAMM1,2,100.0,3.0,40.8,37.8,76.4,27.31,-37.8
|
| 20 |
+
A1600116BAMCNE,3,66.7,5.0,2.9,-2.1,88.1,17.3,2.1
|
| 21 |
+
A145A415HMMN,1,100.0,4.0,3.5,-0.5,96.6,0.0,0.5
|
| 22 |
+
A120B960BAKKO,2,50.0,4.0,8.7,4.7,93.3,1.83,-4.699999999999999
|
| 23 |
+
A132B149BADD,1,100.0,18.0,43.5,25.5,69.7,0.0,-25.5
|
| 24 |
+
18006BAMMB,1,100.0,4.0,0.0,-17.1,115.1,0.0,4.0
|
| 25 |
+
15001204BAMCBW2,3,33.3,5.0,13.7,8.7,92.3,3.84,-8.7
|
| 26 |
+
160866BAMMB3,19,73.7,3.0,4.1,1.1,97.0,8.35,-1.0999999999999996
|
| 27 |
+
18015BAMMY,1,100.0,3.0,0.0,-6.4,103.5,0.0,3.0
|
| 28 |
+
A140C147OSPPR,2,100.0,12.0,44.5,32.5,72.5,20.32,-32.5
|
| 29 |
+
130129BAMMB,3,66.7,5.0,2.8,-2.2,97.3,0.79,2.2
|
| 30 |
+
A140F646BAMMBW,9,88.9,3.0,2.3,-0.7,95.6,5.17,0.7000000000000002
|
| 31 |
+
A280B070BAPCCNY,2,100.0,4.0,1.2,-2.8,98.8,0.12,2.8
|
| 32 |
+
A180A494BAMM,1,100.0,3.0,0.0,-4.5,101.5,0.0,3.0
|
| 33 |
+
A114A170BAMM2,8,75.0,5.0,4.3,-0.7,88.2,22.68,0.7000000000000002
|
| 34 |
+
A240B096BAMC,1,100.0,6.0,1.7,-4.3,98.3,0.0,4.3
|
| 35 |
+
A114A170BAMM,1,100.0,5.0,0.7,-4.3,99.3,0.0,4.3
|
| 36 |
+
A140G679BAMCB,1,0.0,4.0,7.6,3.6,94.3,0.0,-3.5999999999999996
|
| 37 |
+
A150B589BAMCBWN3,4,75.0,5.0,5.0,-0.0,101.4,15.22,0.0
|
| 38 |
+
A160C540OSPP,2,50.0,3.0,2.6,-0.4,96.7,1.09,0.3999999999999999
|
| 39 |
+
A130E731BAMF,1,100.0,5.0,3.8,-1.2,96.3,0.0,1.2000000000000002
|
| 40 |
+
A150E765BAMCN,3,100.0,5.0,4.4,-0.6,98.2,4.72,0.5999999999999996
|
| 41 |
+
A132B659BAMC,1,100.0,6.0,11.6,5.6,89.6,0.0,-5.6
|
| 42 |
+
A150B589BAMCBDWN,1,0.0,5.0,10.0,5.0,78.4,0.0,-5.0
|
| 43 |
+
A150B589BAMCBWN5,1,0.0,5.0,10.0,5.0,0.0,0.0,-5.0
|
| 44 |
+
A140F473BAMM,2,100.0,3.0,6.0,3.0,94.3,2.33,-3.0
|
| 45 |
+
16009BDMM,31,93.5,3.0,0.0,-3.9,100.6,1.91,3.0
|
| 46 |
+
A130A343BAKK,2,0.0,5.0,7.0,2.0,94.9,0.0,-2.0
|
| 47 |
+
A120C693BAKKDW4,1,100.0,5.0,12.8,7.8,88.7,0.0,-7.800000000000001
|
| 48 |
+
A132B340BADD,2,50.0,12.0,5.0,-7.0,92.0,5.28,7.0
|
| 49 |
+
16000294BAPPY,2,100.0,3.0,1.0,-2.0,99.0,0.94,2.0
|
| 50 |
+
A240B224BAMM,1,100.0,4.0,3.1,-0.9,97.0,0.0,0.8999999999999999
|
| 51 |
+
A280B359BAPPNY,2,50.0,2.0,5.0,3.0,92.6,3.62,-3.0
|
| 52 |
+
A160D231OSPP,1,0.0,3.0,3.0,0.0,100.0,0.0,0.0
|
| 53 |
+
A250A292BAVV,1,100.0,2.0,3.0,1.0,97.1,0.0,-1.0
|
| 54 |
+
A130E341BAMKDW4,2,50.0,5.0,2.7,-2.3,94.1,4.62,2.3
|
| 55 |
+
12091BAKKB,3,100.0,6.0,5.6,-0.4,94.6,0.87,0.40000000000000036
|
| 56 |
+
A140D740BAMKB5,2,100.0,4.0,1.9,-2.1,98.2,1.01,2.1
|
| 57 |
+
A140E428BAMD,1,100.0,12.0,5.8,-6.2,94.5,0.0,6.2
|
| 58 |
+
A112A540BAKCW4,1,100.0,5.0,4.3,-0.7,95.9,0.0,0.7000000000000002
|
| 59 |
+
14095BAMM,13,76.9,3.0,3.2,0.2,100.6,20.68,-0.20000000000000018
|
| 60 |
+
150442BAMCB6,9,88.9,4.0,2.6,-1.4,100.1,6.06,1.4
|
| 61 |
+
A150D909BAMC,3,33.3,3.0,6.9,3.9,93.3,0.64,-3.9000000000000004
|
| 62 |
+
16000094BAMMA,1,100.0,3.0,5.1,2.1,95.1,0.0,-2.0999999999999996
|
| 63 |
+
12000248BACD,2,0.0,12.0,14.0,2.0,91.0,4.0,-2.0
|
| 64 |
+
12000322BADD,1,0.0,12.0,14.0,2.0,59.4,0.0,-2.0
|
| 65 |
+
12400011BAKK,1,100.0,5.0,7.0,2.0,93.5,0.0,-2.0
|
| 66 |
+
16009OSPPY,1,100.0,3.0,0.0,-3.5,100.5,0.0,3.0
|
| 67 |
+
A130F167BAMM,1,100.0,5.0,3.9,-1.1,96.2,0.0,1.1
|
| 68 |
+
A120B495BAMM,2,50.0,6.0,6.7,0.7,89.1,7.42,-0.7000000000000002
|
| 69 |
+
A160C479OSPPY,2,0.0,3.0,8.0,5.0,82.8,0.0,-5.0
|
| 70 |
+
A150D946BAMMBN,2,100.0,3.0,1.6,-1.4,98.4,0.55,1.4
|
| 71 |
+
A160D209OSPPY,2,100.0,3.0,59.5,56.5,69.0,28.49,-56.5
|
| 72 |
+
A116B163BAMMO,1,0.0,6.0,11.0,5.0,86.1,0.0,-5.0
|
| 73 |
+
A120E602BAMFWY,1,0.0,6.0,11.0,5.0,83.2,0.0,-5.0
|
| 74 |
+
A280A354BAMCBWN3,2,100.0,5.0,6.4,1.4,94.0,2.54,-1.4000000000000004
|
| 75 |
+
A180A212BAMCBN2,4,100.0,5.0,6.3,1.3,104.2,18.93,-1.2999999999999998
|
| 76 |
+
14095BAMMO,2,50.0,4.0,0.5,-3.5,98.9,0.81,3.5
|
| 77 |
+
A140A512BAMMO,1,100.0,3.0,1.4,-1.6,98.6,0.0,1.6
|
| 78 |
+
A140C479BAMMW+,2,50.0,2.0,2.7,0.7,93.1,7.07,-0.7000000000000002
|
| 79 |
+
A1400474BAMFWQVJ,1,100.0,5.0,3.5,-1.5,96.6,0.0,1.5
|
| 80 |
+
12000251BAMC,1,0.0,6.0,8.2,2.2,85.7,0.0,-2.1999999999999993
|
| 81 |
+
132340BADKD4,1,100.0,7.0,4.1,-2.9,96.1,0.0,2.9000000000000004
|
| 82 |
+
A120E061BAKK,3,0.0,7.0,11.5,4.5,92.0,1.61,-4.5
|
| 83 |
+
A1600187BAMMB2,6,66.7,4.0,3.9,-0.1,80.9,39.59,0.10000000000000009
|
| 84 |
+
A160D253BAMM,1,100.0,3.0,0.0,-56.1,213.4,0.0,3.0
|
| 85 |
+
16000294BAMMZAN6,2,100.0,3.0,0.7,-2.3,99.3,3.39,2.3
|
| 86 |
+
A120D302BACC,1,100.0,5.0,1.0,-4.0,99.0,0.0,4.0
|
| 87 |
+
A160A742BADDA2,3,33.3,10.0,17.5,7.5,85.2,3.82,-7.5
|
| 88 |
+
A116B584BAOO,1,100.0,6.0,7.8,1.8,92.8,0.0,-1.7999999999999998
|
| 89 |
+
18006BAMMY,2,100.0,2.0,0.2,-1.8,99.8,2.21,1.8
|
| 90 |
+
13257BADKD4,8,37.5,4.0,4.8,0.8,91.3,6.95,-0.7999999999999998
|
| 91 |
+
12031BA,1,0.0,6.0,8.0,2.0,93.7,0.0,-2.0
|
| 92 |
+
A145A593BAMCW,1,0.0,6.0,10.3,4.3,87.1,0.0,-4.300000000000001
|
| 93 |
+
A240B242OSPPY,1,0.0,3.0,6.2,3.2,94.7,0.0,-3.2
|
| 94 |
+
A160C540OSPPY,1,100.0,2.0,1.4,-0.6,98.7,0.0,0.6000000000000001
|
| 95 |
+
12047BAOE,4,25.0,4.0,5.0,1.0,94.7,1.12,-1.0
|
| 96 |
+
A120E967OSPP,1,0.0,12.0,14.0,2.0,61.2,0.0,-2.0
|
| 97 |
+
160866OSPPY,1,100.0,3.0,14.2,11.2,87.6,0.0,-11.2
|
| 98 |
+
16000066BAMM,5,80.0,3.0,4.4,1.4,96.1,0.85,-1.4000000000000004
|
| 99 |
+
A140H306OSPP,1,0.0,3.0,5.3,2.3,96.7,0.0,-2.3
|
| 100 |
+
A160D289OSPP,1,0.0,3.0,5.1,2.1,83.5,0.0,-2.0999999999999996
|
| 101 |
+
A160D284OSPP,1,100.0,2.0,3.0,1.0,97.1,0.0,-1.0
|
| 102 |
+
150442BAMCB8,1,100.0,5.0,1.1,-3.9,98.9,0.0,3.9
|
| 103 |
+
A120C359BAMM,1,100.0,4.0,6.9,2.9,93.6,0.0,-2.9000000000000004
|
| 104 |
+
12200001BAKKW,1,0.0,6.0,6.0,0.0,100.0,0.0,0.0
|
| 105 |
+
A120C365BAMM,3,33.3,6.0,2.0,-4.0,95.9,3.1,4.0
|
| 106 |
+
A140E042BAMMB5,2,100.0,2.0,1.3,-0.7,98.7,1.4,0.7
|
| 107 |
+
13200056BADFWA,3,100.0,6.0,3.0,-3.0,97.3,0.67,3.0
|
| 108 |
+
140324BAMCB3,2,100.0,5.0,4.5,-0.5,95.7,1.53,0.5
|
| 109 |
+
A240B237BDRRB,1,0.0,2.0,4.0,2.0,0.0,0.0,-2.0
|
| 110 |
+
A150C873BAMFDW3,5,60.0,5.0,5.2,0.2,92.5,3.52,-0.20000000000000018
|
| 111 |
+
160866BAPPY,2,50.0,3.0,6.9,3.9,87.4,9.9,-3.9000000000000004
|
| 112 |
+
A150C873BAMFZW3,6,0.0,5.0,10.0,5.0,91.6,2.57,-5.0
|
| 113 |
+
A150C873BAMFBQW,2,0.0,5.0,10.0,5.0,85.2,4.95,-5.0
|
| 114 |
+
A150C873BAPFYDQW,1,100.0,5.0,10.1,5.1,90.8,0.0,-5.1
|
| 115 |
+
A170A188BAMCBN,6,50.0,5.0,5.0,0.0,92.9,9.68,0.0
|
| 116 |
+
A150D683BAMCB2,2,100.0,5.0,5.7,0.7,94.6,2.37,-0.7000000000000002
|
| 117 |
+
16000204BAMMB3,1,100.0,2.0,1.4,-0.6,98.6,0.0,0.6000000000000001
|
| 118 |
+
16000204BAMM,2,50.0,2.0,0.1,-1.9,97.8,3.06,1.9
|
| 119 |
+
A150C602BAMC,2,50.0,5.0,4.2,-0.8,96.0,0.04,0.7999999999999998
|
| 120 |
+
A140E013BAMM,3,100.0,6.0,2.2,-3.8,98.1,3.93,3.8
|
| 121 |
+
14514BAMM,4,100.0,2.0,1.7,-0.3,97.9,1.93,0.30000000000000004
|
| 122 |
+
A280B038BAEFWN2,2,50.0,5.0,9.6,4.6,90.2,1.43,-4.6
|
| 123 |
+
A280B211BAMCBW,2,50.0,5.0,2.2,-2.8,96.9,1.42,2.8
|
| 124 |
+
A280B211BAMCBW2,1,0.0,5.0,7.1,2.1,93.5,0.0,-2.0999999999999996
|
| 125 |
+
A280B212BAMCBW,1,100.0,4.0,2.5,-1.5,97.6,0.0,1.5
|
| 126 |
+
A280B212BAMCBW2,1,100.0,5.0,12.8,7.8,88.7,0.0,-7.800000000000001
|
| 127 |
+
A150C873BAMFDQW,1,100.0,5.0,0.0,-9.2,104.4,0.0,5.0
|
| 128 |
+
C140M751BAMKC3,1,100.0,4.0,2.8,-1.2,97.3,0.0,1.2000000000000002
|
| 129 |
+
A140G193BAMCB,2,0.0,5.0,7.0,2.0,94.8,1.15,-2.0
|
| 130 |
+
A140E645BADDQW2,1,100.0,6.0,4.0,-2.0,96.2,0.0,2.0
|
| 131 |
+
A140F185BAMMB3,1,100.0,3.0,2.1,-0.9,98.0,0.0,0.8999999999999999
|
| 132 |
+
A140E777BAMFBWQN3,2,50.0,5.0,5.8,0.8,95.3,1.09,-0.7999999999999998
|
| 133 |
+
130M90BADDVJ,1,100.0,6.0,4.4,-1.6,95.8,0.0,1.5999999999999996
|
| 134 |
+
A140E013BAMMW2,1,100.0,6.0,5.7,-0.3,94.6,0.0,0.2999999999999998
|
| 135 |
+
A140G672BAMCB,1,0.0,5.0,7.0,2.0,92.8,0.0,-2.0
|
| 136 |
+
A150D683BAMCB3,1,0.0,5.0,9.1,4.1,91.7,0.0,-4.1
|
| 137 |
+
A150C873BAMFZW8,1,0.0,5.0,10.0,5.0,91.6,0.0,-5.0
|
| 138 |
+
A150C873BAMFDW6,1,0.0,5.0,7.0,2.0,77.4,0.0,-2.0
|
| 139 |
+
A140E777BAMFBWN3,1,100.0,4.0,4.4,0.4,95.8,0.0,-0.40000000000000036
|
| 140 |
+
14005BACM,4,25.0,3.0,1.3,-1.7,96.7,2.52,1.7
|
| 141 |
+
A111A004OSPP,1,0.0,18.0,20.0,2.0,60.1,0.0,-2.0
|
| 142 |
+
A240A241OSKK,1,100.0,12.0,19.1,7.1,83.9,0.0,-7.100000000000001
|
| 143 |
+
A130D024BACCWY,1,0.0,5.0,7.0,2.0,78.4,0.0,-2.0
|
| 144 |
+
A132A234BADC,1,0.0,5.0,7.0,2.0,97.3,0.0,-2.0
|
| 145 |
+
A140F790BAMC,1,100.0,3.0,4.5,1.5,95.7,0.0,-1.5
|
| 146 |
+
A160C485BAMC,1,0.0,5.0,7.0,2.0,93.8,0.0,-2.0
|
| 147 |
+
A132B125BADKDW4,1,0.0,4.0,6.0,2.0,93.2,0.0,-2.0
|
| 148 |
+
A140F815BAMMB3,1,0.0,3.0,5.0,2.0,55.8,0.0,-2.0
|
| 149 |
+
A150D189BAMMZ,4,25.0,2.0,4.1,2.1,95.8,1.31,-2.0999999999999996
|
| 150 |
+
A130C663BACCW,1,0.0,5.0,7.0,2.0,85.2,0.0,-2.0
|
| 151 |
+
A140A649BAMCA,1,0.0,5.0,7.0,2.0,98.0,0.0,-2.0
|
| 152 |
+
160563BAMMO,1,0.0,3.0,5.0,2.0,99.6,0.0,-2.0
|
| 153 |
+
260300BAEEBA2,2,0.0,3.0,5.0,2.0,86.4,11.76,-2.0
|
| 154 |
+
A120B732BAOK*,1,0.0,5.0,7.0,2.0,90.3,0.0,-2.0
|
| 155 |
+
A150C547BAMCB3,1,0.0,5.0,7.0,2.0,98.9,0.0,-2.0
|
| 156 |
+
A280B195BAMCW,1,0.0,5.0,7.0,2.0,87.3,0.0,-2.0
|
| 157 |
+
A140G692BAMMB,2,100.0,2.0,2.8,0.8,97.4,3.84,-0.7999999999999998
|
| 158 |
+
13000579BAMCW4,1,0.0,5.0,7.0,2.0,81.4,0.0,-2.0
|
| 159 |
+
A160C567BAMMBN,1,100.0,3.0,8.6,5.6,92.0,0.0,-5.6
|
| 160 |
+
A132B125BADCW,1,100.0,5.0,4.9,-0.1,95.3,0.0,0.09999999999999964
|
| 161 |
+
16000432BAMM,1,0.0,3.0,5.0,2.0,97.7,0.0,-2.0
|
| 162 |
+
A140E471BAMFZQN3,2,50.0,4.0,2.5,-1.5,91.3,10.21,1.5
|
| 163 |
+
12000349BAMCEA2,2,50.0,4.0,0.8,-3.2,98.8,0.68,3.2
|
| 164 |
+
A160C445BAMC,1,100.0,5.0,2.3,-2.7,97.8,0.0,2.7
|
| 165 |
+
A140A865BDMKD,1,100.0,4.0,1.2,-2.8,98.8,0.0,2.8
|
| 166 |
+
A140G722BAMKW4,1,0.0,5.0,7.0,2.0,98.6,0.0,-2.0
|
| 167 |
+
16000232BAMMN,7,57.1,3.0,6.9,3.9,92.1,9.16,-3.9000000000000004
|
| 168 |
+
130D07BAMMA,1,0.0,5.0,7.0,2.0,96.5,0.0,-2.0
|
| 169 |
+
116580BA,3,66.7,5.0,14.2,9.2,88.3,4.67,-9.2
|
| 170 |
+
A140F645BAMM,1,0.0,3.0,5.0,2.0,98.4,0.0,-2.0
|
| 171 |
+
A132B311BADD,1,0.0,5.0,7.0,2.0,98.2,0.0,-2.0
|
| 172 |
+
A132A744BADD,3,66.7,12.0,4.1,-7.9,86.2,18.73,7.9
|
| 173 |
+
A130B742BADCW,2,50.0,4.0,3.7,-0.3,93.0,4.85,0.2999999999999998
|
| 174 |
+
A120E174BAKDW,1,100.0,12.0,21.9,9.9,82.0,0.0,-9.899999999999999
|
| 175 |
+
13200074BADD,1,0.0,18.0,20.0,2.0,97.6,0.0,-2.0
|
| 176 |
+
13200046BADD,1,100.0,18.0,8.1,-9.9,92.5,0.0,9.9
|
| 177 |
+
A120D009BAMM,1,0.0,5.0,7.0,2.0,95.4,0.0,-2.0
|
| 178 |
+
A130E163BAMFQ5,1,100.0,5.0,6.2,1.2,94.1,0.0,-1.2000000000000002
|
| 179 |
+
A145A530BAMCBWQN,1,0.0,5.0,7.0,2.0,74.3,0.0,-2.0
|
| 180 |
+
A140D699BAMFZWQN5,1,100.0,5.0,0.6,-4.4,99.4,0.0,4.4
|
| 181 |
+
A140E777BAMFSWQN,1,100.0,5.0,16.4,11.4,85.9,0.0,-11.399999999999999
|
| 182 |
+
A140A865BAMK,3,100.0,4.0,1.7,-2.3,98.7,1.24,2.3
|
| 183 |
+
A130A882BACCBW2,2,50.0,4.0,0.0,-24.0,98.9,39.1,4.0
|
| 184 |
+
A145A530BAMCWQN,2,100.0,4.0,0.0,-5.7,101.8,2.03,4.0
|
| 185 |
+
14000910BAMD,1,0.0,10.0,12.0,2.0,96.6,0.0,-2.0
|
| 186 |
+
14001037BAMM,3,66.7,2.0,6.0,4.0,95.3,1.85,-4.0
|
| 187 |
+
A140A865BAMKD,1,100.0,5.0,3.8,-1.2,96.3,0.0,1.2000000000000002
|
| 188 |
+
A150C547BAMCB5,2,100.0,5.0,0.0,-6.9,102.6,5.44,5.0
|
| 189 |
+
A121A058BAKKW,2,0.0,4.0,6.0,2.0,92.5,1.09,-2.0
|
| 190 |
+
A150E721BAMMZN,3,66.7,3.0,2.1,-0.9,97.5,2.16,0.8999999999999999
|
| 191 |
+
14500150BAMM,1,100.0,3.0,0.0,-3.4,100.4,0.0,3.0
|
| 192 |
+
13000503BAMFBW2,1,0.0,4.0,6.0,2.0,92.7,0.0,-2.0
|
| 193 |
+
14000398BAMVR,1,100.0,12.0,8.0,-4.0,92.6,0.0,4.0
|
| 194 |
+
A120C726BAOO,1,0.0,5.0,10.0,5.0,84.8,0.0,-5.0
|
| 195 |
+
A132B606BADD,1,0.0,18.0,20.0,2.0,72.8,0.0,-2.0
|
| 196 |
+
A220A109BAOOB,4,0.0,4.0,6.0,2.0,93.3,0.98,-2.0
|
| 197 |
+
A130A723BAMKDW4,3,33.3,12.0,9.9,-2.1,91.5,1.12,2.0999999999999996
|
| 198 |
+
A130D001OSPP2,1,0.0,18.0,20.0,2.0,83.8,0.0,-2.0
|
| 199 |
+
A130D900BAMC,2,50.0,4.0,0.1,-3.9,98.1,2.46,3.9
|
| 200 |
+
A2A0A551BAEE,1,0.0,12.0,14.0,2.0,83.1,0.0,-2.0
|
| 201 |
+
A140D698BAMFZWN5,1,0.0,5.0,7.0,2.0,93.9,0.0,-2.0
|
| 202 |
+
A140F030BADFWRA2,1,0.0,12.0,14.0,2.0,90.3,0.0,-2.0
|
| 203 |
+
A220A109BAKOB,2,0.0,4.0,6.0,2.0,94.0,0.04,-2.0
|
| 204 |
+
A150F001BAMM,1,100.0,3.0,5.7,2.7,94.6,0.0,-2.7
|
| 205 |
+
A132A415BDMCD,5,60.0,5.0,6.5,1.5,92.9,2.46,-1.5
|
| 206 |
+
28000099BAEFBW2,7,14.3,5.0,15.4,10.4,90.0,2.07,-10.4
|
| 207 |
+
16009BA,1,0.0,3.0,3.0,0.0,100.0,0.0,0.0
|
| 208 |
+
A130E918BDMCD,1,0.0,6.0,6.0,0.0,100.0,0.0,0.0
|
| 209 |
+
A150E531BAMKW4,1,0.0,5.0,7.0,2.0,94.7,0.0,-2.0
|
| 210 |
+
160A63BAMMZ7,13,61.5,5.0,5.3,0.3,94.8,1.38,-0.2999999999999998
|
| 211 |
+
130176BDCM,2,100.0,5.0,5.5,0.5,94.8,0.46,-0.5
|
| 212 |
+
16000108BAMM,3,100.0,3.0,5.5,2.5,94.8,0.94,-2.5
|
| 213 |
+
12000313BAKCA,1,0.0,5.0,8.4,3.4,92.7,0.0,-3.4000000000000004
|
| 214 |
+
A110A579BAOF2,1,0.0,5.0,10.0,5.0,87.1,0.0,-5.0
|
| 215 |
+
A130E202BAVMBV3,1,100.0,5.0,0.6,-4.4,99.4,0.0,4.4
|
| 216 |
+
A130E202BAVMBV,1,100.0,5.0,0.0,-19.7,117.2,0.0,5.0
|
| 217 |
+
A150B593BAMMA,4,25.0,5.0,2.1,-2.9,95.3,1.77,2.9
|
| 218 |
+
A235A012BAMOO,2,50.0,5.0,0.0,-39.1,125.0,41.01,5.0
|
| 219 |
+
A150E177BAKKNW,4,50.0,4.0,24.7,20.7,68.6,14.9,-20.7
|
| 220 |
+
13000275BAMM,3,33.3,6.0,0.0,-28.1,109.0,29.8,6.0
|
| 221 |
+
160811BAMM,2,100.0,3.0,0.0,-9.3,109.9,20.76,3.0
|
| 222 |
+
A160C426BAMC,1,100.0,3.0,1.2,-1.8,98.8,0.0,1.8
|
| 223 |
+
A160B951BAMM,5,60.0,4.0,2.6,-1.4,96.0,4.85,1.4
|
| 224 |
+
14000909BAMM,4,75.0,3.0,4.7,1.7,95.7,2.78,-1.7000000000000002
|
| 225 |
+
12000267BAKK,1,0.0,6.0,9.1,3.1,86.5,0.0,-3.0999999999999996
|
| 226 |
+
12000267BADD,1,0.0,5.0,9.6,4.6,88.6,0.0,-4.6
|
| 227 |
+
A180A212BAMCBN,2,100.0,4.0,0.0,-5.5,103.0,9.47,4.0
|
| 228 |
+
A1400474BAMFBW6,2,100.0,5.0,4.2,-0.8,95.9,0.33,0.7999999999999998
|
| 229 |
+
A130F127BAMF,1,0.0,5.0,10.0,5.0,86.4,0.0,-5.0
|
| 230 |
+
A140B826BDMCDW4,1,0.0,5.0,7.0,2.0,93.9,0.0,-2.0
|
| 231 |
+
A150E173BDMCD,3,100.0,5.0,5.0,-0.0,93.0,4.51,0.0
|
| 232 |
+
140448BAMMB2,1,100.0,4.0,1.9,-2.1,98.2,0.0,2.1
|
| 233 |
+
16072BACM,16,75.0,3.0,5.7,2.7,95.0,1.61,-2.7
|
| 234 |
+
A130D900BAMCDW4,1,100.0,6.0,5.9,-0.1,94.5,0.0,0.09999999999999964
|
| 235 |
+
140367BDB,1,100.0,3.0,2.7,-0.3,97.4,0.0,0.2999999999999998
|
| 236 |
+
14500033BAMMB,1,100.0,2.0,0.8,-1.2,99.2,0.0,1.2
|
| 237 |
+
14500033BAMMB3,2,50.0,2.0,3.9,1.9,95.3,1.32,-1.9
|
| 238 |
+
160625BDMMA,3,33.3,2.0,2.9,0.9,97.2,0.23,-0.8999999999999999
|
| 239 |
+
A160C426BAMCA,7,85.7,3.0,5.4,2.4,94.3,1.86,-2.4000000000000004
|
| 240 |
+
A160C445BAMC2,2,100.0,4.0,13.2,9.2,89.7,10.2,-9.2
|
| 241 |
+
A130F162BAMF,11,81.8,5.0,10.4,5.4,87.7,12.71,-5.4
|
| 242 |
+
280A95BAMC2,3,33.3,5.0,10.9,5.9,90.5,1.08,-5.9
|
| 243 |
+
C140M503BAMM,3,66.7,3.0,7.3,4.3,93.8,1.24,-4.3
|
| 244 |
+
C140M503BAMM*,1,100.0,3.0,4.3,1.3,95.8,0.0,-1.2999999999999998
|
| 245 |
+
A140D158BAMCDW4,2,0.0,5.0,10.0,5.0,89.3,0.0,-5.0
|
| 246 |
+
A116A743BDOCDW4,1,0.0,5.0,7.6,2.6,91.3,0.0,-2.5999999999999996
|
| 247 |
+
A235A013BAMCO,3,66.7,5.0,0.0,-19.7,109.9,50.77,5.0
|
| 248 |
+
16000237BAMC,3,66.7,3.0,14.5,11.5,89.8,7.62,-11.5
|
| 249 |
+
A145A488BAMKDW4,1,100.0,5.0,6.3,1.3,94.0,0.0,-1.2999999999999998
|
| 250 |
+
A150D161BDMCDW4,1,100.0,5.0,10.8,5.8,90.2,0.0,-5.800000000000001
|
| 251 |
+
A160C974BAMM,1,0.0,2.0,4.5,2.5,93.3,0.0,-2.5
|
| 252 |
+
A160C974BAMMA,3,66.7,3.0,4.1,1.1,96.3,2.45,-1.0999999999999996
|
| 253 |
+
25065BAMMA,5,40.0,3.0,8.7,5.7,91.3,2.1,-5.699999999999999
|
| 254 |
+
A145A624BAMM,1,100.0,3.0,2.5,-0.5,97.5,0.0,0.5
|
| 255 |
+
A150D112BAMM,3,0.0,3.0,8.0,5.0,88.2,6.62,-5.0
|
| 256 |
+
A150D112BAMMB6,2,50.0,2.0,3.9,1.9,94.8,2.01,-1.9
|
| 257 |
+
A140C096BAMM,9,66.7,2.0,0.8,-1.2,98.4,4.2,1.2
|
| 258 |
+
A150B047BDMCD,1,100.0,5.0,10.2,5.2,90.8,0.0,-5.199999999999999
|
| 259 |
+
A132B509BAMF,3,66.7,6.0,0.0,-11.1,81.1,51.35,6.0
|
| 260 |
+
A132B170BAMCW4,1,0.0,5.0,8.2,3.2,93.4,0.0,-3.1999999999999993
|
| 261 |
+
A132B170BAMCBW34,1,0.0,4.0,6.0,2.0,94.3,0.0,-2.0
|
| 262 |
+
A130B170BAMCW4,1,100.0,7.0,6.9,-0.1,93.6,0.0,0.09999999999999964
|
| 263 |
+
A132B898BADK,1,100.0,5.0,3.4,-1.6,96.8,0.0,1.6
|
| 264 |
+
A160C589BAMMWZN,3,66.7,4.0,41.0,37.0,77.3,30.18,-37.0
|
| 265 |
+
A140G004BAMMBN2,1,100.0,2.0,4.3,2.3,95.9,0.0,-2.3
|
| 266 |
+
A130C646BAMC,7,14.3,12.0,0.0,-27.2,93.0,13.79,12.0
|
| 267 |
+
A120E828BAMCW4,1,100.0,5.0,20.4,15.4,83.0,0.0,-15.399999999999999
|
| 268 |
+
160A63BA,10,40.0,5.0,7.3,2.3,91.0,7.03,-2.3
|
| 269 |
+
A140F086BDMCDA,1,100.0,4.0,6.9,2.9,93.6,0.0,-2.9000000000000004
|
| 270 |
+
A116B252BAMFW,1,100.0,5.0,0.0,-5.0,100.0,0.0,5.0
|
| 271 |
+
A130E897BAMKB,1,100.0,10.0,0.0,-13.3,103.4,0.0,10.0
|
| 272 |
+
A150A567BAMMB3,1,100.0,3.0,4.6,1.6,95.6,0.0,-1.5999999999999996
|
| 273 |
+
14252BAMMB,6,100.0,2.0,1.2,-0.8,98.0,1.85,0.8
|
| 274 |
+
18006OSPP,1,100.0,4.0,0.0,-45.9,172.0,0.0,4.0
|
| 275 |
+
A280B152BAMMBN3,4,25.0,2.0,6.3,4.3,93.6,1.22,-4.3
|
| 276 |
+
240B16BAMM,2,100.0,3.0,7.0,4.0,93.5,1.94,-4.0
|
| 277 |
+
A130F336BAMC,1,0.0,5.0,10.0,5.0,83.8,0.0,-5.0
|
| 278 |
+
A140G372BAKK,1,0.0,5.0,8.8,3.8,91.6,0.0,-3.8000000000000007
|
| 279 |
+
A280A898BAMC3,4,100.0,6.0,1.9,-4.1,97.6,1.59,4.1
|
| 280 |
+
A280A898BAMC2,1,100.0,6.0,8.4,2.4,92.3,0.0,-2.4000000000000004
|
| 281 |
+
A140F319BDMD,2,100.0,3.0,7.1,4.1,93.4,1.73,-4.1
|
| 282 |
+
A110A699BAOOB,1,0.0,5.0,7.0,2.0,93.3,0.0,-2.0
|
| 283 |
+
150442BAMCB7,1,100.0,5.0,3.1,-1.9,97.0,0.0,1.9
|
| 284 |
+
A240A975BDMKD,5,80.0,5.0,5.6,0.6,94.8,1.38,-0.5999999999999996
|
| 285 |
+
A120D162BAKK,1,100.0,6.0,2.1,-3.9,98.0,0.0,3.9
|
| 286 |
+
A132A901BADKW4,5,60.0,7.0,11.1,4.1,83.4,19.96,-4.1
|
| 287 |
+
A130D162BAMCW4,1,100.0,5.0,7.4,2.4,93.1,0.0,-2.4000000000000004
|
| 288 |
+
A140G753BAMD,2,100.0,3.0,21.4,18.4,84.0,12.97,-18.4
|
| 289 |
+
140324BAMCDW2,7,85.7,5.0,3.3,-1.7,95.8,1.61,1.7000000000000002
|
| 290 |
+
140324BDMCDW4,3,100.0,6.0,5.4,-0.6,91.6,7.19,0.5999999999999996
|
| 291 |
+
160563BA,1,100.0,3.0,4.0,1.0,96.1,0.0,-1.0
|
| 292 |
+
A140G585BADK,1,0.0,18.0,23.0,5.0,77.2,0.0,-5.0
|
| 293 |
+
A160A384BAMCW2,4,100.0,5.0,2.5,-2.5,97.0,2.66,2.5
|
| 294 |
+
16000173OSMM,5,60.0,3.0,4.4,1.4,87.9,18.4,-1.4000000000000004
|
| 295 |
+
A160C612BAMC,1,0.0,5.0,9.9,4.9,91.9,0.0,-4.9
|
| 296 |
+
150188BAMM,1,100.0,3.0,6.6,3.6,93.8,0.0,-3.5999999999999996
|
| 297 |
+
A160A390BAMC2,1,100.0,5.0,4.9,-0.1,95.3,0.0,0.09999999999999964
|
| 298 |
+
160483BAMCC2,1,100.0,5.0,0.0,-28.2,130.3,0.0,5.0
|
| 299 |
+
A250A194BAMMN,1,0.0,3.0,8.0,5.0,91.2,0.0,-5.0
|
| 300 |
+
A140A865BAMKB6,1,100.0,5.0,8.5,3.5,92.2,0.0,-3.5
|
| 301 |
+
A160C920BAMMZN,1,0.0,3.0,8.0,5.0,91.5,0.0,-5.0
|
| 302 |
+
A280B204BAMMN,1,100.0,3.0,5.2,2.2,95.1,0.0,-2.2
|
| 303 |
+
A160B572BAMMCN,1,100.0,5.0,3.4,-1.6,96.7,0.0,1.6
|
| 304 |
+
A120D892BAMM,1,100.0,5.0,11.7,6.7,89.6,0.0,-6.699999999999999
|
| 305 |
+
160874BAMM,4,100.0,3.0,2.8,-0.2,99.3,3.82,0.20000000000000018
|
| 306 |
+
160874BAMMZ,1,100.0,2.0,1.2,-0.8,98.8,0.0,0.8
|
| 307 |
+
A150B593BAMMBA3,2,50.0,5.0,99.9,94.9,71.3,29.76,-94.9
|
| 308 |
+
150264BA,4,75.0,2.0,3.6,1.6,95.7,1.97,-1.6
|
| 309 |
+
150264OSMM,1,0.0,2.0,4.0,2.0,97.3,0.0,-2.0
|
| 310 |
+
A240B231BAMCN,2,50.0,5.0,7.6,2.6,92.4,0.77,-2.5999999999999996
|
| 311 |
+
160866BAMM2,4,100.0,3.0,0.0,-3.5,100.1,4.33,3.0
|
| 312 |
+
A140B410BAMKBW34,2,100.0,5.0,2.7,-2.3,97.3,1.86,2.3
|
| 313 |
+
14000928BAMFQ,1,100.0,3.0,5.8,2.8,94.5,0.0,-2.8
|
| 314 |
+
14226BAMMB,2,50.0,3.0,2.2,-0.8,96.8,1.47,0.7999999999999998
|
| 315 |
+
A130F425BAKK,1,0.0,4.0,6.0,2.0,95.6,0.0,-2.0
|
| 316 |
+
A140F601BAMCW2,1,100.0,5.0,6.5,1.5,93.9,0.0,-1.5
|
| 317 |
+
A140D755BAMMO,1,100.0,3.0,0.0,-4.6,101.6,0.0,3.0
|
| 318 |
+
14005BAMMB3,1,100.0,4.0,1.7,-2.3,98.3,0.0,2.3
|
| 319 |
+
A230A364BAMMO,1,0.0,6.0,11.0,5.0,53.1,0.0,-5.0
|
| 320 |
+
15000458BAMK,4,75.0,5.0,4.8,-0.2,94.9,3.54,0.20000000000000018
|
| 321 |
+
130K00BAMK,3,66.7,5.0,4.1,-0.9,93.5,4.58,0.9000000000000004
|
| 322 |
+
130K00BDDKD,1,100.0,5.0,4.1,-0.9,96.0,0.0,0.9000000000000004
|
| 323 |
+
A150E252BAMM,1,100.0,2.0,7.2,5.2,93.3,0.0,-5.2
|
| 324 |
+
14000925BAMMBN2,1,100.0,3.0,0.0,-7.0,104.1,0.0,3.0
|
| 325 |
+
A140C562BAMMWE,1,100.0,3.0,4.1,1.1,96.1,0.0,-1.0999999999999996
|
| 326 |
+
A150D718BAMM,2,50.0,3.0,3.9,0.9,95.4,1.17,-0.8999999999999999
|
| 327 |
+
A150D718BAMMB,2,100.0,2.0,1.9,-0.1,98.2,1.68,0.10000000000000009
|
| 328 |
+
A130B609OSPP2,1,0.0,12.0,17.0,5.0,79.3,0.0,-5.0
|
| 329 |
+
16000173BAMMA,5,80.0,2.0,3.0,1.0,95.6,3.7,-1.0
|
| 330 |
+
14095BAMMC,8,87.5,3.0,2.1,-0.9,98.1,42.56,0.8999999999999999
|
| 331 |
+
A150D243BAMM,1,100.0,3.0,3.6,0.6,96.5,0.0,-0.6000000000000001
|
| 332 |
+
A160C925BAMM,1,100.0,3.0,6.0,3.0,94.4,0.0,-3.0
|
| 333 |
+
A160B951BAMMB2,1,100.0,3.0,0.0,-41.3,162.0,0.0,3.0
|
| 334 |
+
A132A824BAMCN,1,100.0,5.0,3.9,-1.1,96.3,0.0,1.1
|
| 335 |
+
A250A209BAERN,2,50.0,5.0,9.0,4.0,86.4,8.6,-4.0
|
| 336 |
+
A130F295BAMM,1,100.0,6.0,4.9,-1.1,95.3,0.0,1.0999999999999996
|
| 337 |
+
14000453BAMO,1,100.0,3.0,4.4,1.4,95.8,0.0,-1.4000000000000004
|
| 338 |
+
A140B826BAMCSWA4,2,50.0,5.0,10.0,5.0,92.4,2.1,-5.0
|
| 339 |
+
A160B146BAMMB3,11,81.8,2.0,1.4,-0.6,98.4,1.88,0.6000000000000001
|
| 340 |
+
16000418BAMM2,1,100.0,5.0,3.4,-1.6,96.7,0.0,1.6
|
| 341 |
+
A140G461BAMCWFV,1,0.0,5.0,8.5,3.5,89.8,0.0,-3.5
|
| 342 |
+
140U31BAMKW4,1,100.0,5.0,7.6,2.6,92.9,0.0,-2.5999999999999996
|
| 343 |
+
16000251BAMO,3,66.7,2.0,8.1,6.1,92.4,0.49,-6.1
|
| 344 |
+
A280B358BAPKDW4,1,100.0,10.0,9.8,-0.2,91.1,0.0,0.1999999999999993
|
| 345 |
+
A150D650BAMCN2,3,66.7,5.0,3.7,-1.3,94.2,5.66,1.2999999999999998
|
| 346 |
+
120G43BAMCBW,1,0.0,5.0,8.5,3.5,89.1,0.0,-3.5
|
| 347 |
+
A150D837BAMKW4,1,0.0,5.0,9.5,4.5,92.9,0.0,-4.5
|
| 348 |
+
150303BA,1,0.0,3.0,8.0,5.0,93.0,0.0,-5.0
|
| 349 |
+
A130E918BDVCD,1,100.0,5.0,3.2,-1.8,96.9,0.0,1.7999999999999998
|
| 350 |
+
A150D650BAMCN,1,0.0,5.0,7.1,2.1,90.7,0.0,-2.0999999999999996
|
| 351 |
+
A140G966BAMM,1,0.0,3.0,8.0,5.0,86.8,0.0,-5.0
|
| 352 |
+
16000232BAMMBN3,1,100.0,4.0,1.3,-2.7,98.7,0.0,2.7
|
| 353 |
+
A140H105BAMD,1,100.0,3.0,6.1,3.1,94.3,0.0,-3.0999999999999996
|
| 354 |
+
A150E338BAMMW,1,100.0,3.0,3.1,0.1,97.0,0.0,-0.10000000000000009
|
| 355 |
+
A132A415BAMC,2,0.0,5.0,10.0,5.0,87.5,2.77,-5.0
|
| 356 |
+
A132B905BAMKNDW4,1,100.0,5.0,7.8,2.8,92.7,0.0,-2.8
|
| 357 |
+
14000910BAMDB3,4,75.0,10.0,1.9,-8.1,87.1,22.91,8.1
|
| 358 |
+
16000429BADDR,1,100.0,12.0,0.0,-90.9,474.2,0.0,12.0
|
| 359 |
+
A160A742OSPP2,1,0.0,12.0,15.1,3.1,79.1,0.0,-3.0999999999999996
|
| 360 |
+
A132B847BADCW,1,100.0,5.0,3.9,-1.1,96.2,0.0,1.1
|
| 361 |
+
A150E383BAMCZ,1,0.0,6.0,11.0,5.0,84.9,0.0,-5.0
|
| 362 |
+
A140E253BAMF2,2,0.0,3.0,8.0,5.0,88.0,0.5,-5.0
|
| 363 |
+
A132B672BAMKN,1,0.0,5.0,9.1,4.1,91.5,0.0,-4.1
|
| 364 |
+
13000093BAMM,4,100.0,5.0,5.1,0.1,95.4,1.45,-0.09999999999999964
|
| 365 |
+
A140A622OSKV,1,0.0,18.0,20.0,2.0,77.7,0.0,-2.0
|
| 366 |
+
A124A548BAKK,1,100.0,6.0,3.0,-3.0,97.1,0.0,3.0
|
| 367 |
+
A1400474BAMFW3,1,100.0,5.0,6.5,1.5,93.9,0.0,-1.5
|
| 368 |
+
15001253BAMMW,1,100.0,3.0,5.4,2.4,94.8,0.0,-2.4000000000000004
|
| 369 |
+
A140E780BAMO,2,100.0,4.0,0.0,-6.4,102.4,1.93,4.0
|
| 370 |
+
A160B260BADD,2,100.0,10.0,18.7,8.7,84.3,3.22,-8.7
|
| 371 |
+
A120C882BACC,1,100.0,5.0,0.2,-4.8,99.8,0.0,4.8
|
| 372 |
+
A140F842BAMMB,2,100.0,2.0,1.3,-0.7,98.8,1.1,0.7
|
| 373 |
+
A140D755BAMMY,2,50.0,3.0,11.3,8.3,90.7,1.19,-8.3
|
| 374 |
+
A145A571BAMM,2,100.0,3.0,0.0,-3.3,100.3,0.6,3.0
|
| 375 |
+
A160C450BAMC2,2,100.0,4.0,8.3,4.3,92.4,3.72,-4.300000000000001
|
| 376 |
+
A180A303BAMM,3,66.7,3.0,2.9,-0.1,95.1,4.91,0.10000000000000009
|
| 377 |
+
140324BAMCBW5,4,75.0,5.0,3.6,-1.4,84.6,25.08,1.4
|
| 378 |
+
A140F925BAMM,1,100.0,3.0,3.4,0.4,96.7,0.0,-0.3999999999999999
|
| 379 |
+
A116A361BDOKBW4,1,0.0,4.0,6.0,2.0,94.9,0.0,-2.0
|
| 380 |
+
140448BAMMB,2,50.0,3.0,0.0,-3.5,96.7,6.25,3.0
|
| 381 |
+
15000147BAMCB3,3,66.7,4.0,53.0,49.0,80.6,26.79,-49.0
|
| 382 |
+
15000147BAMC,3,100.0,6.0,6.1,0.1,93.4,4.94,-0.09999999999999964
|
| 383 |
+
A132A895BADK,1,100.0,5.0,5.8,0.8,94.5,0.0,-0.7999999999999998
|
| 384 |
+
A140F417BAMC,1,0.0,5.0,7.9,2.9,93.8,0.0,-2.9000000000000004
|
| 385 |
+
A130D146BAMK,1,0.0,10.0,12.0,2.0,92.0,0.0,-2.0
|
| 386 |
+
A260A443BAMKDWA4,1,0.0,5.0,10.0,5.0,90.4,0.0,-5.0
|
| 387 |
+
A130F523BAMKDW4,1,0.0,10.0,12.0,2.0,94.6,0.0,-2.0
|
| 388 |
+
11649BA,3,100.0,5.0,5.8,0.8,91.8,6.22,-0.7999999999999998
|
| 389 |
+
A140C562BAMMBW2,1,0.0,2.0,4.4,2.4,96.1,0.0,-2.4000000000000004
|
| 390 |
+
A280B371BAMMN,1,100.0,3.0,5.6,2.6,94.7,0.0,-2.5999999999999996
|
| 391 |
+
A2A0A628BAMMNZ,1,100.0,3.0,3.3,0.3,96.8,0.0,-0.2999999999999998
|
| 392 |
+
A260A512BAERN,1,0.0,10.0,12.0,2.0,91.3,0.0,-2.0
|
| 393 |
+
A130E354BACC,1,100.0,5.0,5.1,0.1,95.1,0.0,-0.09999999999999964
|
| 394 |
+
A145A571BAMMQY,3,100.0,4.0,0.0,-4.8,103.2,4.2,4.0
|
| 395 |
+
12400043BACC,1,0.0,4.0,6.0,2.0,95.3,0.0,-2.0
|
| 396 |
+
28000025BAMMB2,1,100.0,3.0,5.4,2.4,94.8,0.0,-2.4000000000000004
|
| 397 |
+
A130D384BAKK,1,100.0,5.0,4.6,-0.4,95.6,0.0,0.40000000000000036
|
| 398 |
+
14095BAMPC,1,100.0,4.0,4.7,0.7,95.5,0.0,-0.7000000000000002
|
| 399 |
+
A120B115BAKK,1,0.0,5.0,10.0,5.0,89.6,0.0,-5.0
|
| 400 |
+
A3A0A015BAMC,1,0.0,5.0,10.0,5.0,82.5,0.0,-5.0
|
| 401 |
+
A250A185BAMCDWN4,2,50.0,5.0,5.1,0.1,93.4,2.47,-0.09999999999999964
|
| 402 |
+
A280A297BAMMN,1,100.0,3.0,5.4,2.4,94.9,0.0,-2.4000000000000004
|
| 403 |
+
A124A663BAKK,1,0.0,5.0,7.0,2.0,94.0,0.0,-2.0
|
| 404 |
+
A1400090BAMM,1,100.0,3.0,2.5,-0.5,97.6,0.0,0.5
|
| 405 |
+
12000155BAOK,1,0.0,4.0,6.0,2.0,94.7,0.0,-2.0
|
| 406 |
+
A130D863BADKDW4,1,0.0,5.0,8.9,3.9,93.2,0.0,-3.9000000000000004
|
| 407 |
+
16000185BAMM,2,0.0,3.0,8.0,5.0,94.1,1.37,-5.0
|
| 408 |
+
A150F525BAMM,1,100.0,3.0,3.4,0.4,96.8,0.0,-0.3999999999999999
|
| 409 |
+
15001397BAMMB,1,100.0,3.0,0.7,-2.3,99.3,0.0,2.3
|
| 410 |
+
14015BDMM2,1,0.0,3.0,8.0,5.0,90.4,0.0,-5.0
|
| 411 |
+
A160C900BAMO,1,0.0,2.0,7.0,5.0,83.3,0.0,-5.0
|
| 412 |
+
15000051BAMMA,3,66.7,3.0,3.8,0.8,93.3,6.79,-0.7999999999999998
|
| 413 |
+
A140D745BAMFBN3,1,100.0,5.0,5.0,0.0,95.2,0.0,0.0
|
| 414 |
+
A160C117BAMM,2,0.0,3.0,8.0,5.0,74.3,16.12,-5.0
|
| 415 |
+
15001398BAMM,1,100.0,3.0,4.0,1.0,96.1,0.0,-1.0
|
| 416 |
+
A140C096BAMMB2,1,100.0,2.0,0.0,-2.0,100.0,0.0,2.0
|
| 417 |
+
12000248BACD2,1,100.0,12.0,0.0,-13.4,101.4,0.0,12.0
|
| 418 |
+
26000068BAPP,2,50.0,4.0,6.0,2.0,93.8,0.85,-2.0
|
| 419 |
+
140V33BAMMS,1,100.0,4.0,2.3,-1.7,97.7,0.0,1.7000000000000002
|
| 420 |
+
15001294BAMMW,1,100.0,3.0,6.0,3.0,94.3,0.0,-3.0
|
| 421 |
+
110159BD,2,100.0,5.0,8.9,3.9,91.8,0.59,-3.9000000000000004
|
| 422 |
+
A140F555BDMCDW4,1,100.0,5.0,5.1,0.1,95.1,0.0,-0.09999999999999964
|
| 423 |
+
A140H160BAMM,2,0.0,4.0,7.5,3.5,90.0,5.62,-3.5
|
| 424 |
+
A114A170BAMMQY2,3,100.0,6.0,0.2,-5.8,112.3,25.67,5.8
|
| 425 |
+
12400011BAKK2,1,100.0,4.0,2.6,-1.4,97.5,0.0,1.4
|
| 426 |
+
A280B031BAPMBN,1,100.0,2.0,4.0,2.0,96.1,0.0,-2.0
|
| 427 |
+
150S05BAMM,1,100.0,4.0,5.4,1.4,94.9,0.0,-1.4000000000000004
|
| 428 |
+
160874BAMMZ2,1,0.0,2.0,4.0,2.0,98.5,0.0,-2.0
|
| 429 |
+
A140H239BAMMB,1,0.0,3.0,8.0,5.0,90.6,0.0,-5.0
|
| 430 |
+
140448BAMPB,1,100.0,3.0,2.3,-0.7,97.7,0.0,0.7000000000000002
|
| 431 |
+
A140H035BDMM,1,100.0,2.0,3.5,1.5,96.6,0.0,-1.5
|
| 432 |
+
A150E803BDMM,1,0.0,2.0,4.0,2.0,95.2,0.0,-2.0
|
| 433 |
+
A160C973BAMMB,1,0.0,3.0,8.0,5.0,93.0,0.0,-5.0
|
| 434 |
+
A160C952BAMC,1,0.0,6.0,8.2,2.2,68.5,0.0,-2.1999999999999993
|
| 435 |
+
A160D251BAMM,1,100.0,2.0,1.7,-0.3,98.3,0.0,0.30000000000000004
|
| 436 |
+
14500101BADDRA2,1,0.0,18.0,23.0,5.0,43.7,0.0,-5.0
|
| 437 |
+
A160C978BAMD,1,0.0,3.0,5.0,2.0,96.0,0.0,-2.0
|
| 438 |
+
150G63BAMMB2,1,100.0,2.0,1.4,-0.6,98.6,0.0,0.6000000000000001
|
| 439 |
+
A130F458BACM,1,0.0,5.0,8.7,3.7,90.5,0.0,-3.6999999999999993
|
| 440 |
+
A112A464BAKPW,1,0.0,5.0,7.0,2.0,87.3,0.0,-2.0
|
| 441 |
+
A140B631BADDR2,1,0.0,18.0,20.0,2.0,80.2,0.0,-2.0
|
| 442 |
+
A150E247BAMM,1,100.0,4.0,1.7,-2.3,98.4,0.0,2.3
|
| 443 |
+
A1400491BAMC2,1,100.0,5.0,5.3,0.3,95.0,0.0,-0.2999999999999998
|
| 444 |
+
16009BAMMZ,1,100.0,3.0,1.2,-1.8,98.9,0.0,1.8
|
| 445 |
+
12000313BDKC,1,100.0,4.0,2.8,-1.2,97.3,0.0,1.2000000000000002
|
| 446 |
+
140J17BDMKD,2,50.0,6.0,5.7,-0.3,91.9,3.75,0.2999999999999998
|
| 447 |
+
A280A898BAPC,1,100.0,4.0,1.7,-2.3,98.3,0.0,2.3
|
| 448 |
+
140324BAMCWY,1,100.0,4.0,2.1,-1.9,97.9,0.0,1.9
|
| 449 |
+
A160C072BAMM,1,100.0,3.0,2.1,-0.9,98.0,0.0,0.8999999999999999
|
| 450 |
+
A160B137BAMOD,1,100.0,3.0,8.3,5.3,92.3,0.0,-5.300000000000001
|
| 451 |
+
A140E186BAMC,1,100.0,2.0,2.7,0.7,97.4,0.0,-0.7000000000000002
|
| 452 |
+
24000170BAPP,1,100.0,5.0,5.9,0.9,94.4,0.0,-0.9000000000000004
|
| 453 |
+
A1400006BAMC,1,0.0,5.0,8.9,3.9,83.2,0.0,-3.9000000000000004
|
| 454 |
+
A140F212BAMFBW3,1,0.0,5.0,10.0,5.0,86.8,0.0,-5.0
|
| 455 |
+
180109BAMMZ,1,100.0,3.0,7.8,4.8,92.8,0.0,-4.8
|
| 456 |
+
150J31BDMM,1,0.0,4.0,9.0,5.0,88.8,0.0,-5.0
|
| 457 |
+
150J31BAPP,1,100.0,3.0,10.0,7.0,90.9,0.0,-7.0
|
| 458 |
+
A112A575BAOK,2,0.0,5.0,10.0,5.0,70.7,9.46,-5.0
|
| 459 |
+
140324BA2,1,100.0,5.0,2.7,-2.3,97.4,0.0,2.3
|
| 460 |
+
160866BAMMB6,1,0.0,3.0,6.5,3.5,78.1,0.0,-3.5
|
| 461 |
+
150264BD,1,0.0,2.0,7.0,5.0,47.8,0.0,-5.0
|
| 462 |
+
A150B047BAMC,1,100.0,5.0,5.2,0.2,95.0,0.0,-0.20000000000000018
|
| 463 |
+
140324BAMCBW3,1,100.0,4.0,69.5,65.5,59.0,0.0,-65.5
|
| 464 |
+
A116B330BAKFWA,2,0.0,5.0,5.0,0.0,100.0,0.0,0.0
|
| 465 |
+
A130D422BADD,3,0.0,10.0,15.0,5.0,84.8,7.48,-5.0
|
| 466 |
+
A140F549BAMCCW4,1,100.0,4.0,0.1,-3.9,99.9,0.0,3.9
|
| 467 |
+
A140F549BAMCCDW4,1,100.0,5.0,4.2,-0.8,96.0,0.0,0.7999999999999998
|
| 468 |
+
12000017BAMKZ2,1,0.0,5.0,10.0,5.0,86.7,0.0,-5.0
|
| 469 |
+
12200001BAKKWJV,9,22.2,6.0,28.2,22.2,70.6,10.98,-22.2
|
| 470 |
+
A140G714BAMMVJ,1,100.0,4.0,0.0,-6.2,102.2,0.0,4.0
|
| 471 |
+
14000756BAMMVJ,1,0.0,4.0,7.3,3.3,93.9,0.0,-3.3
|
| 472 |
+
130M28BAMK4,2,50.0,5.0,2.7,-2.3,91.8,9.08,2.3
|
| 473 |
+
A150C547BAMCB$,1,100.0,4.0,0.0,-4.0,100.0,0.0,4.0
|
| 474 |
+
13200056BADFWRAVJ,5,60.0,6.0,0.0,-6.4,99.5,4.53,6.0
|
| 475 |
+
140371BAMMB3,4,75.0,2.0,0.0,-16.6,102.2,35.83,2.0
|
| 476 |
+
A132A453BADFWRAVJ*,1,100.0,6.0,0.8,-5.2,99.2,0.0,5.2
|
| 477 |
+
15000715BAMMW,1,100.0,2.0,0.0,-2.1,100.1,0.0,2.0
|
| 478 |
+
A140D454BAMM,1,100.0,4.0,5.7,1.7,94.6,0.0,-1.7000000000000002
|
| 479 |
+
140I21BA,1,0.0,3.0,3.0,0.0,100.0,0.0,0.0
|
| 480 |
+
14000638BAMM,1,100.0,4.0,1.9,-2.1,98.1,0.0,2.1
|
| 481 |
+
12200001BAKKWJVA,4,25.0,6.0,41.9,35.9,44.1,30.79,-35.9
|
| 482 |
+
A1400406BDCC,1,100.0,4.0,2.7,-1.3,97.4,0.0,1.2999999999999998
|
| 483 |
+
18006BAMMO,1,0.0,3.0,3.0,0.0,100.0,0.0,0.0
|
| 484 |
+
120G88BA,2,100.0,5.0,6.3,1.3,94.0,0.03,-1.2999999999999998
|
| 485 |
+
120G88BAMOE,1,100.0,6.0,4.5,-1.5,95.7,0.0,1.5
|
| 486 |
+
120G65BAOO,2,50.0,5.0,2.6,-2.4,92.7,7.74,2.4
|
| 487 |
+
116555BA,1,100.0,5.0,5.4,0.4,94.9,0.0,-0.40000000000000036
|
| 488 |
+
11400036BAMMQY,1,100.0,3.0,1.6,-1.4,98.4,0.0,1.4
|
| 489 |
+
14000313BAMMBA5,2,100.0,4.0,8.6,4.6,92.2,4.51,-4.6
|
| 490 |
+
A1320030BADKW4,1,0.0,4.0,4.0,0.0,100.0,0.0,0.0
|
| 491 |
+
A140D939BAMFZQNA9,2,100.0,4.0,3.5,-0.5,96.6,1.11,0.5
|
| 492 |
+
A114A170BAMMQY,1,0.0,6.0,8.0,2.0,94.1,0.0,-2.0
|
| 493 |
+
A130D787BACC,3,66.7,5.0,4.4,-0.6,93.8,3.9,0.5999999999999996
|
| 494 |
+
A114A170BAMMQFEV,1,100.0,6.0,6.9,0.9,93.5,0.0,-0.9000000000000004
|
| 495 |
+
A150B588BAMCB,2,100.0,5.0,0.9,-4.1,99.1,2.0,4.1
|
| 496 |
+
150A83BAMCB,1,100.0,4.0,9.4,5.4,91.4,0.0,-5.4
|
| 497 |
+
150A83BAMC,1,0.0,5.0,7.1,2.1,76.1,0.0,-2.0999999999999996
|
debug_backend.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
import os
|
| 3 |
+
import json
|
| 4 |
+
|
| 5 |
+
sys.path.append("/run/media/ishpreet/New Volume/Auribises/Vardhman Textiles/process-aware-ai/backend")
|
| 6 |
+
|
| 7 |
+
try:
|
| 8 |
+
from app.services.data_service import data_service
|
| 9 |
+
|
| 10 |
+
print("Loading data...")
|
| 11 |
+
data_service.load_data()
|
| 12 |
+
|
| 13 |
+
# Test with the problematic order
|
| 14 |
+
order_id = "81S_81S-25000172"
|
| 15 |
+
print(f"\n=== Testing: {order_id} ===")
|
| 16 |
+
|
| 17 |
+
result = data_service.get_sale_order_details(order_id)
|
| 18 |
+
|
| 19 |
+
if "error" in result:
|
| 20 |
+
print(f"Error: {result['error']}")
|
| 21 |
+
else:
|
| 22 |
+
print(f"Success! Keys: {list(result.keys())}")
|
| 23 |
+
print(f"Metrics: {result['metrics']}")
|
| 24 |
+
print(f"Intelligence keys: {list(result['intelligence'].keys())}")
|
| 25 |
+
|
| 26 |
+
except Exception as e:
|
| 27 |
+
import traceback
|
| 28 |
+
traceback.print_exc()
|
frontend/.gitignore
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
| 2 |
+
|
| 3 |
+
# dependencies
|
| 4 |
+
/node_modules
|
| 5 |
+
/.pnp
|
| 6 |
+
.pnp.*
|
| 7 |
+
.yarn/*
|
| 8 |
+
!.yarn/patches
|
| 9 |
+
!.yarn/plugins
|
| 10 |
+
!.yarn/releases
|
| 11 |
+
!.yarn/versions
|
| 12 |
+
|
| 13 |
+
# testing
|
| 14 |
+
/coverage
|
| 15 |
+
|
| 16 |
+
# next.js
|
| 17 |
+
/.next/
|
| 18 |
+
/out/
|
| 19 |
+
|
| 20 |
+
# production
|
| 21 |
+
/build
|
| 22 |
+
|
| 23 |
+
# misc
|
| 24 |
+
.DS_Store
|
| 25 |
+
*.pem
|
| 26 |
+
|
| 27 |
+
# debug
|
| 28 |
+
npm-debug.log*
|
| 29 |
+
yarn-debug.log*
|
| 30 |
+
yarn-error.log*
|
| 31 |
+
.pnpm-debug.log*
|
| 32 |
+
|
| 33 |
+
# env files (can opt-in for committing if needed)
|
| 34 |
+
.env*
|
| 35 |
+
|
| 36 |
+
# vercel
|
| 37 |
+
.vercel
|
| 38 |
+
|
| 39 |
+
# typescript
|
| 40 |
+
*.tsbuildinfo
|
| 41 |
+
next-env.d.ts
|
frontend/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
| 2 |
+
|
| 3 |
+
## Getting Started
|
| 4 |
+
|
| 5 |
+
First, run the development server:
|
| 6 |
+
|
| 7 |
+
```bash
|
| 8 |
+
npm run dev
|
| 9 |
+
# or
|
| 10 |
+
yarn dev
|
| 11 |
+
# or
|
| 12 |
+
pnpm dev
|
| 13 |
+
# or
|
| 14 |
+
bun dev
|
| 15 |
+
```
|
| 16 |
+
|
| 17 |
+
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
| 18 |
+
|
| 19 |
+
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
| 20 |
+
|
| 21 |
+
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
| 22 |
+
|
| 23 |
+
## Learn More
|
| 24 |
+
|
| 25 |
+
To learn more about Next.js, take a look at the following resources:
|
| 26 |
+
|
| 27 |
+
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
| 28 |
+
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
| 29 |
+
|
| 30 |
+
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
| 31 |
+
|
| 32 |
+
## Deploy on Vercel
|
| 33 |
+
|
| 34 |
+
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
| 35 |
+
|
| 36 |
+
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
frontend/__tests__/analytics-section.test.tsx
ADDED
|
@@ -0,0 +1,656 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Analytics Section Component Tests
|
| 3 |
+
* Tests KPI calculations, chart data, and display logic.
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
import { mockGlobalAnalyticsResponse } from './test-data-mocking';
|
| 7 |
+
|
| 8 |
+
interface TestResult {
|
| 9 |
+
name: string;
|
| 10 |
+
passed: boolean;
|
| 11 |
+
expected: any;
|
| 12 |
+
actual: any;
|
| 13 |
+
error?: string;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
interface TestSuite {
|
| 17 |
+
category: string;
|
| 18 |
+
results: TestResult[];
|
| 19 |
+
passed: number;
|
| 20 |
+
failed: number;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
const testResults: TestSuite[] = [];
|
| 24 |
+
const TOLERANCE = 0.5;
|
| 25 |
+
|
| 26 |
+
function runTest(category: string, name: string, expected: any, actual: any): TestResult {
|
| 27 |
+
const isNumber = typeof expected === 'number' && typeof actual === 'number';
|
| 28 |
+
const passed = isNumber
|
| 29 |
+
? Math.abs(expected - actual) <= TOLERANCE
|
| 30 |
+
: expected === actual;
|
| 31 |
+
|
| 32 |
+
const result: TestResult = {
|
| 33 |
+
name,
|
| 34 |
+
passed,
|
| 35 |
+
expected,
|
| 36 |
+
actual
|
| 37 |
+
};
|
| 38 |
+
|
| 39 |
+
let suite = testResults.find(s => s.category === category);
|
| 40 |
+
if (!suite) {
|
| 41 |
+
suite = { category, results: [], passed: 0, failed: 0 };
|
| 42 |
+
testResults.push(suite);
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
suite.results.push(result);
|
| 46 |
+
if (passed) {
|
| 47 |
+
suite.passed++;
|
| 48 |
+
} else {
|
| 49 |
+
suite.failed++;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
return result;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
// ============================================
|
| 56 |
+
// TEST SUITE 1: KPI Cards Display
|
| 57 |
+
// ============================================
|
| 58 |
+
function testKPICards() {
|
| 59 |
+
const kpis = mockGlobalAnalyticsResponse.kpis;
|
| 60 |
+
|
| 61 |
+
// Total Volume Display (in millions)
|
| 62 |
+
const volumeInMillions = (kpis.total_volume_m / 1000000).toFixed(2);
|
| 63 |
+
runTest(
|
| 64 |
+
"KPI Cards",
|
| 65 |
+
"Total Volume (M)",
|
| 66 |
+
"2.50",
|
| 67 |
+
volumeInMillions
|
| 68 |
+
);
|
| 69 |
+
|
| 70 |
+
// Global Yield Rate
|
| 71 |
+
runTest(
|
| 72 |
+
"KPI Cards",
|
| 73 |
+
"Global Yield %",
|
| 74 |
+
94.5,
|
| 75 |
+
kpis.global_yield_pct
|
| 76 |
+
);
|
| 77 |
+
|
| 78 |
+
// Yield color coding: > 95 ? emerald : amber
|
| 79 |
+
const yieldColor = kpis.global_yield_pct > 95 ? "text-emerald-400" : "text-amber-400";
|
| 80 |
+
runTest(
|
| 81 |
+
"KPI Cards",
|
| 82 |
+
"Yield Color (94.5% < 95)",
|
| 83 |
+
"text-amber-400",
|
| 84 |
+
yieldColor
|
| 85 |
+
);
|
| 86 |
+
|
| 87 |
+
// Shortfall Risk Rate
|
| 88 |
+
runTest(
|
| 89 |
+
"KPI Cards",
|
| 90 |
+
"Shortfall Risk %",
|
| 91 |
+
15.2,
|
| 92 |
+
kpis.shortfall_risk_pct
|
| 93 |
+
);
|
| 94 |
+
|
| 95 |
+
// Shortfall color coding: < 5 ? emerald : red
|
| 96 |
+
const shortfallColor = kpis.shortfall_risk_pct < 5 ? "text-emerald-400" : "text-red-400";
|
| 97 |
+
runTest(
|
| 98 |
+
"KPI Cards",
|
| 99 |
+
"Shortfall Color (15.2% > 5)",
|
| 100 |
+
"text-red-400",
|
| 101 |
+
shortfallColor
|
| 102 |
+
);
|
| 103 |
+
|
| 104 |
+
// Total Orders
|
| 105 |
+
runTest(
|
| 106 |
+
"KPI Cards",
|
| 107 |
+
"Total Orders",
|
| 108 |
+
970,
|
| 109 |
+
kpis.total_orders
|
| 110 |
+
);
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
// ============================================
|
| 114 |
+
// TEST SUITE 2: Route Distribution
|
| 115 |
+
// ============================================
|
| 116 |
+
function testRouteDistribution() {
|
| 117 |
+
const routes = mockGlobalAnalyticsResponse.distributions.route;
|
| 118 |
+
|
| 119 |
+
// Route count
|
| 120 |
+
runTest(
|
| 121 |
+
"Route Distribution",
|
| 122 |
+
"Route Count",
|
| 123 |
+
3,
|
| 124 |
+
routes.length
|
| 125 |
+
);
|
| 126 |
+
|
| 127 |
+
// Continouse route (highest volume)
|
| 128 |
+
const continouse = routes.find((r: any) => r.Route === "Continouse");
|
| 129 |
+
runTest(
|
| 130 |
+
"Route Distribution",
|
| 131 |
+
"Continouse Exists",
|
| 132 |
+
true,
|
| 133 |
+
!!continouse
|
| 134 |
+
);
|
| 135 |
+
|
| 136 |
+
runTest(
|
| 137 |
+
"Route Distribution",
|
| 138 |
+
"Continouse Yield",
|
| 139 |
+
94.8,
|
| 140 |
+
continouse?.yield
|
| 141 |
+
);
|
| 142 |
+
|
| 143 |
+
runTest(
|
| 144 |
+
"Route Distribution",
|
| 145 |
+
"Continouse Count",
|
| 146 |
+
4100,
|
| 147 |
+
continouse?.count
|
| 148 |
+
);
|
| 149 |
+
|
| 150 |
+
// Jigger route
|
| 151 |
+
const jigger = routes.find((r: any) => r.Route === "Jigger");
|
| 152 |
+
runTest(
|
| 153 |
+
"Route Distribution",
|
| 154 |
+
"Jigger Yield",
|
| 155 |
+
92.3,
|
| 156 |
+
jigger?.yield
|
| 157 |
+
);
|
| 158 |
+
|
| 159 |
+
// Jet route
|
| 160 |
+
const jet = routes.find((r: any) => r.Route === "Jet");
|
| 161 |
+
runTest(
|
| 162 |
+
"Route Distribution",
|
| 163 |
+
"Jet Yield",
|
| 164 |
+
93.1,
|
| 165 |
+
jet?.yield
|
| 166 |
+
);
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
// ============================================
|
| 170 |
+
// TEST SUITE 3: Finish Distribution
|
| 171 |
+
// ============================================
|
| 172 |
+
function testFinishDistribution() {
|
| 173 |
+
const finishes = mockGlobalAnalyticsResponse.distributions.finish;
|
| 174 |
+
|
| 175 |
+
// Finish count
|
| 176 |
+
runTest(
|
| 177 |
+
"Finish Distribution",
|
| 178 |
+
"Finish Count",
|
| 179 |
+
2,
|
| 180 |
+
finishes.length
|
| 181 |
+
);
|
| 182 |
+
|
| 183 |
+
// Soft finish
|
| 184 |
+
const soft = finishes.find((f: any) => f.Finish === "Soft");
|
| 185 |
+
runTest(
|
| 186 |
+
"Finish Distribution",
|
| 187 |
+
"Soft Exists",
|
| 188 |
+
true,
|
| 189 |
+
!!soft
|
| 190 |
+
);
|
| 191 |
+
|
| 192 |
+
runTest(
|
| 193 |
+
"Finish Distribution",
|
| 194 |
+
"Soft Yield",
|
| 195 |
+
94.2,
|
| 196 |
+
soft?.yield
|
| 197 |
+
);
|
| 198 |
+
|
| 199 |
+
runTest(
|
| 200 |
+
"Finish Distribution",
|
| 201 |
+
"Soft Count",
|
| 202 |
+
2500,
|
| 203 |
+
soft?.count
|
| 204 |
+
);
|
| 205 |
+
|
| 206 |
+
// Peach finish
|
| 207 |
+
const peach = finishes.find((f: any) => f.Finish === "Peach");
|
| 208 |
+
runTest(
|
| 209 |
+
"Finish Distribution",
|
| 210 |
+
"Peach Yield",
|
| 211 |
+
93.8,
|
| 212 |
+
peach?.yield
|
| 213 |
+
);
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
// ============================================
|
| 217 |
+
// TEST SUITE 4: Shade Distribution
|
| 218 |
+
// ============================================
|
| 219 |
+
function testShadeDistribution() {
|
| 220 |
+
const shades = mockGlobalAnalyticsResponse.distributions.shade;
|
| 221 |
+
|
| 222 |
+
// Shade count
|
| 223 |
+
runTest(
|
| 224 |
+
"Shade Distribution",
|
| 225 |
+
"Shade Count",
|
| 226 |
+
3,
|
| 227 |
+
shades.length
|
| 228 |
+
);
|
| 229 |
+
|
| 230 |
+
// Verify Shade Type key (NOT 'Shade Code')
|
| 231 |
+
const firstShade = shades[0];
|
| 232 |
+
runTest(
|
| 233 |
+
"Shade Distribution",
|
| 234 |
+
"Uses 'Shade Type' Key",
|
| 235 |
+
true,
|
| 236 |
+
'Shade Type' in firstShade
|
| 237 |
+
);
|
| 238 |
+
|
| 239 |
+
// Dyed shade
|
| 240 |
+
const dyed = shades.find((s: any) => s['Shade Type'] === "Dyed");
|
| 241 |
+
runTest(
|
| 242 |
+
"Shade Distribution",
|
| 243 |
+
"Dyed Exists",
|
| 244 |
+
true,
|
| 245 |
+
!!dyed
|
| 246 |
+
);
|
| 247 |
+
|
| 248 |
+
runTest(
|
| 249 |
+
"Shade Distribution",
|
| 250 |
+
"Dyed Yield",
|
| 251 |
+
94.1,
|
| 252 |
+
dyed?.yield
|
| 253 |
+
);
|
| 254 |
+
|
| 255 |
+
runTest(
|
| 256 |
+
"Shade Distribution",
|
| 257 |
+
"Dyed Count",
|
| 258 |
+
3725,
|
| 259 |
+
dyed?.count
|
| 260 |
+
);
|
| 261 |
+
|
| 262 |
+
// FB shade
|
| 263 |
+
const fb = shades.find((s: any) => s['Shade Type'] === "FB");
|
| 264 |
+
runTest(
|
| 265 |
+
"Shade Distribution",
|
| 266 |
+
"FB Yield",
|
| 267 |
+
95.2,
|
| 268 |
+
fb?.yield
|
| 269 |
+
);
|
| 270 |
+
|
| 271 |
+
// RFD shade
|
| 272 |
+
const rfd = shades.find((s: any) => s['Shade Type'] === "RFD");
|
| 273 |
+
runTest(
|
| 274 |
+
"Shade Distribution",
|
| 275 |
+
"RFD Yield",
|
| 276 |
+
94.5,
|
| 277 |
+
rfd?.yield
|
| 278 |
+
);
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
// ============================================
|
| 282 |
+
// TEST SUITE 5: Yield Trends
|
| 283 |
+
// ============================================
|
| 284 |
+
function testYieldTrends() {
|
| 285 |
+
const trends = mockGlobalAnalyticsResponse.trends;
|
| 286 |
+
|
| 287 |
+
// Trend count
|
| 288 |
+
runTest(
|
| 289 |
+
"Yield Trends",
|
| 290 |
+
"Trend Points",
|
| 291 |
+
3,
|
| 292 |
+
trends.length
|
| 293 |
+
);
|
| 294 |
+
|
| 295 |
+
// First trend point
|
| 296 |
+
const firstTrend = trends[0];
|
| 297 |
+
runTest(
|
| 298 |
+
"Yield Trends",
|
| 299 |
+
"First Month",
|
| 300 |
+
"2024-10",
|
| 301 |
+
firstTrend.month
|
| 302 |
+
);
|
| 303 |
+
|
| 304 |
+
runTest(
|
| 305 |
+
"Yield Trends",
|
| 306 |
+
"First Yield",
|
| 307 |
+
93.5,
|
| 308 |
+
firstTrend.yield
|
| 309 |
+
);
|
| 310 |
+
|
| 311 |
+
// Last trend point
|
| 312 |
+
const lastTrend = trends[trends.length - 1];
|
| 313 |
+
runTest(
|
| 314 |
+
"Yield Trends",
|
| 315 |
+
"Last Month",
|
| 316 |
+
"2024-12",
|
| 317 |
+
lastTrend.month
|
| 318 |
+
);
|
| 319 |
+
|
| 320 |
+
runTest(
|
| 321 |
+
"Yield Trends",
|
| 322 |
+
"Last Yield",
|
| 323 |
+
94.8,
|
| 324 |
+
lastTrend.yield
|
| 325 |
+
);
|
| 326 |
+
|
| 327 |
+
// Trend direction (improving)
|
| 328 |
+
const trendUp = lastTrend.yield > firstTrend.yield;
|
| 329 |
+
runTest(
|
| 330 |
+
"Yield Trends",
|
| 331 |
+
"Trend Direction (Up)",
|
| 332 |
+
true,
|
| 333 |
+
trendUp
|
| 334 |
+
);
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
// ============================================
|
| 338 |
+
// TEST SUITE 6: Global Waterfall
|
| 339 |
+
// ============================================
|
| 340 |
+
function testGlobalWaterfall() {
|
| 341 |
+
const waterfall = mockGlobalAnalyticsResponse.global_waterfall;
|
| 342 |
+
|
| 343 |
+
// Waterfall count
|
| 344 |
+
runTest(
|
| 345 |
+
"Global Waterfall",
|
| 346 |
+
"Step Count",
|
| 347 |
+
5,
|
| 348 |
+
waterfall.length
|
| 349 |
+
);
|
| 350 |
+
|
| 351 |
+
// Total Demand
|
| 352 |
+
const demand = waterfall.find((w: any) => w.label === "Total Demand");
|
| 353 |
+
runTest(
|
| 354 |
+
"Global Waterfall",
|
| 355 |
+
"Total Demand Value (M)",
|
| 356 |
+
2.5,
|
| 357 |
+
demand?.value / 1000000
|
| 358 |
+
);
|
| 359 |
+
|
| 360 |
+
runTest(
|
| 361 |
+
"Global Waterfall",
|
| 362 |
+
"Demand Type",
|
| 363 |
+
"base",
|
| 364 |
+
demand?.type
|
| 365 |
+
);
|
| 366 |
+
|
| 367 |
+
// Delivered
|
| 368 |
+
const delivered = waterfall.find((w: any) => w.label === "Delivered");
|
| 369 |
+
runTest(
|
| 370 |
+
"Global Waterfall",
|
| 371 |
+
"Delivered Value (M)",
|
| 372 |
+
2.3625,
|
| 373 |
+
delivered?.value / 1000000
|
| 374 |
+
);
|
| 375 |
+
|
| 376 |
+
runTest(
|
| 377 |
+
"Global Waterfall",
|
| 378 |
+
"Delivered Type",
|
| 379 |
+
"final",
|
| 380 |
+
delivered?.type
|
| 381 |
+
);
|
| 382 |
+
|
| 383 |
+
// Waterfall sum
|
| 384 |
+
const calculatedDelivered = waterfall
|
| 385 |
+
.filter((w: any) => w.type !== 'final')
|
| 386 |
+
.reduce((sum: number, w: any) => sum + w.value, 0);
|
| 387 |
+
|
| 388 |
+
runTest(
|
| 389 |
+
"Global Waterfall",
|
| 390 |
+
"Sum = Delivered",
|
| 391 |
+
delivered?.value,
|
| 392 |
+
calculatedDelivered
|
| 393 |
+
);
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
+
// ============================================
|
| 397 |
+
// TEST SUITE 7: Global Blame
|
| 398 |
+
// ============================================
|
| 399 |
+
function testGlobalBlame() {
|
| 400 |
+
const blame = mockGlobalAnalyticsResponse.global_blame;
|
| 401 |
+
|
| 402 |
+
// Blame percentages sum to 100
|
| 403 |
+
const totalPct = blame.policy_pct + blame.execution_pct + blame.process_pct;
|
| 404 |
+
runTest(
|
| 405 |
+
"Global Blame",
|
| 406 |
+
"Total % = 100",
|
| 407 |
+
100,
|
| 408 |
+
totalPct
|
| 409 |
+
);
|
| 410 |
+
|
| 411 |
+
// Individual components
|
| 412 |
+
runTest(
|
| 413 |
+
"Global Blame",
|
| 414 |
+
"Policy %",
|
| 415 |
+
46.7,
|
| 416 |
+
blame.policy_pct
|
| 417 |
+
);
|
| 418 |
+
|
| 419 |
+
runTest(
|
| 420 |
+
"Global Blame",
|
| 421 |
+
"Execution %",
|
| 422 |
+
13.3,
|
| 423 |
+
blame.execution_pct
|
| 424 |
+
);
|
| 425 |
+
|
| 426 |
+
runTest(
|
| 427 |
+
"Global Blame",
|
| 428 |
+
"Process %",
|
| 429 |
+
40.0,
|
| 430 |
+
blame.process_pct
|
| 431 |
+
);
|
| 432 |
+
}
|
| 433 |
+
|
| 434 |
+
// ============================================
|
| 435 |
+
// TEST SUITE 8: Chart Domain Configuration
|
| 436 |
+
// ============================================
|
| 437 |
+
function testChartDomainConfig() {
|
| 438 |
+
// X-Axis domain for yield charts
|
| 439 |
+
const yieldDomain = [80, 100];
|
| 440 |
+
|
| 441 |
+
runTest(
|
| 442 |
+
"Chart Domain",
|
| 443 |
+
"Yield Min Domain",
|
| 444 |
+
80,
|
| 445 |
+
yieldDomain[0]
|
| 446 |
+
);
|
| 447 |
+
|
| 448 |
+
runTest(
|
| 449 |
+
"Chart Domain",
|
| 450 |
+
"Yield Max Domain",
|
| 451 |
+
100,
|
| 452 |
+
yieldDomain[1]
|
| 453 |
+
);
|
| 454 |
+
}
|
| 455 |
+
|
| 456 |
+
// ============================================
|
| 457 |
+
// TEST SUITE 9: Color Configuration
|
| 458 |
+
// ============================================
|
| 459 |
+
function testColorConfiguration() {
|
| 460 |
+
const COLORS = ['#10b981', '#f59e0b', '#ef4444', '#3b82f6'];
|
| 461 |
+
|
| 462 |
+
runTest(
|
| 463 |
+
"Color Config",
|
| 464 |
+
"Color Count",
|
| 465 |
+
4,
|
| 466 |
+
COLORS.length
|
| 467 |
+
);
|
| 468 |
+
|
| 469 |
+
// Emerald (green)
|
| 470 |
+
runTest(
|
| 471 |
+
"Color Config",
|
| 472 |
+
"Emerald Color",
|
| 473 |
+
"#10b981",
|
| 474 |
+
COLORS[0]
|
| 475 |
+
);
|
| 476 |
+
|
| 477 |
+
// Amber (orange)
|
| 478 |
+
runTest(
|
| 479 |
+
"Color Config",
|
| 480 |
+
"Amber Color",
|
| 481 |
+
"#f59e0b",
|
| 482 |
+
COLORS[1]
|
| 483 |
+
);
|
| 484 |
+
|
| 485 |
+
// Red
|
| 486 |
+
runTest(
|
| 487 |
+
"Color Config",
|
| 488 |
+
"Red Color",
|
| 489 |
+
"#ef4444",
|
| 490 |
+
COLORS[2]
|
| 491 |
+
);
|
| 492 |
+
|
| 493 |
+
// Blue
|
| 494 |
+
runTest(
|
| 495 |
+
"Color Config",
|
| 496 |
+
"Blue Color",
|
| 497 |
+
"#3b82f6",
|
| 498 |
+
COLORS[3]
|
| 499 |
+
);
|
| 500 |
+
|
| 501 |
+
// Blame chart colors
|
| 502 |
+
const blameColors = {
|
| 503 |
+
policy: '#f59e0b', // Amber
|
| 504 |
+
execution: '#3b82f6', // Blue
|
| 505 |
+
process: '#ef4444' // Red
|
| 506 |
+
};
|
| 507 |
+
|
| 508 |
+
runTest(
|
| 509 |
+
"Color Config",
|
| 510 |
+
"Blame Policy Color",
|
| 511 |
+
"#f59e0b",
|
| 512 |
+
blameColors.policy
|
| 513 |
+
);
|
| 514 |
+
|
| 515 |
+
runTest(
|
| 516 |
+
"Color Config",
|
| 517 |
+
"Blame Execution Color",
|
| 518 |
+
"#3b82f6",
|
| 519 |
+
blameColors.execution
|
| 520 |
+
);
|
| 521 |
+
|
| 522 |
+
runTest(
|
| 523 |
+
"Color Config",
|
| 524 |
+
"Blame Process Color",
|
| 525 |
+
"#ef4444",
|
| 526 |
+
blameColors.process
|
| 527 |
+
);
|
| 528 |
+
}
|
| 529 |
+
|
| 530 |
+
// ============================================
|
| 531 |
+
// TEST SUITE 10: API Endpoint
|
| 532 |
+
// ============================================
|
| 533 |
+
function testAPIEndpoint() {
|
| 534 |
+
const API_URL = "/api";
|
| 535 |
+
|
| 536 |
+
runTest(
|
| 537 |
+
"API Endpoint",
|
| 538 |
+
"Base URL",
|
| 539 |
+
"/api",
|
| 540 |
+
API_URL
|
| 541 |
+
);
|
| 542 |
+
|
| 543 |
+
runTest(
|
| 544 |
+
"API Endpoint",
|
| 545 |
+
"Global Analytics",
|
| 546 |
+
"/api/analytics/global",
|
| 547 |
+
`${API_URL}/analytics/global`
|
| 548 |
+
);
|
| 549 |
+
|
| 550 |
+
runTest(
|
| 551 |
+
"API Endpoint",
|
| 552 |
+
"Finish Complexity",
|
| 553 |
+
"/api/analytics/finish-complexity",
|
| 554 |
+
`${API_URL}/analytics/finish-complexity`
|
| 555 |
+
);
|
| 556 |
+
}
|
| 557 |
+
|
| 558 |
+
// ============================================
|
| 559 |
+
// TEST SUITE 11: Empty Distribution Handling
|
| 560 |
+
// ============================================
|
| 561 |
+
function testEmptyDistributionHandling() {
|
| 562 |
+
const distributions = mockGlobalAnalyticsResponse.distributions;
|
| 563 |
+
|
| 564 |
+
// Segment is empty
|
| 565 |
+
runTest(
|
| 566 |
+
"Empty Distributions",
|
| 567 |
+
"Segment Empty",
|
| 568 |
+
true,
|
| 569 |
+
distributions.segment.length === 0
|
| 570 |
+
);
|
| 571 |
+
|
| 572 |
+
// Customer is empty
|
| 573 |
+
runTest(
|
| 574 |
+
"Empty Distributions",
|
| 575 |
+
"Customer Empty",
|
| 576 |
+
true,
|
| 577 |
+
distributions.customer.length === 0
|
| 578 |
+
);
|
| 579 |
+
|
| 580 |
+
// Should not render segment/customer sections
|
| 581 |
+
const shouldShowSegmentCustomer =
|
| 582 |
+
(distributions.customer?.length > 0 || distributions.segment?.length > 0);
|
| 583 |
+
|
| 584 |
+
runTest(
|
| 585 |
+
"Empty Distributions",
|
| 586 |
+
"Should NOT Show Segment/Customer",
|
| 587 |
+
false,
|
| 588 |
+
shouldShowSegmentCustomer
|
| 589 |
+
);
|
| 590 |
+
}
|
| 591 |
+
|
| 592 |
+
// ============================================
|
| 593 |
+
// TEST SUITE 12: Loading State
|
| 594 |
+
// ============================================
|
| 595 |
+
function testLoadingState() {
|
| 596 |
+
const isLoading = false; // Simulating loaded state
|
| 597 |
+
const loadingText = "Loading Global Insights...";
|
| 598 |
+
|
| 599 |
+
runTest(
|
| 600 |
+
"Loading State",
|
| 601 |
+
"Loading Text",
|
| 602 |
+
"Loading Global Insights...",
|
| 603 |
+
loadingText
|
| 604 |
+
);
|
| 605 |
+
|
| 606 |
+
runTest(
|
| 607 |
+
"Loading State",
|
| 608 |
+
"Shows Data When Not Loading",
|
| 609 |
+
true,
|
| 610 |
+
!isLoading
|
| 611 |
+
);
|
| 612 |
+
}
|
| 613 |
+
|
| 614 |
+
// ============================================
|
| 615 |
+
// RUN ALL TESTS
|
| 616 |
+
// ============================================
|
| 617 |
+
export function runAllAnalyticsTests(): {
|
| 618 |
+
suites: TestSuite[];
|
| 619 |
+
summary: {
|
| 620 |
+
total: number;
|
| 621 |
+
passed: number;
|
| 622 |
+
failed: number;
|
| 623 |
+
passRate: number;
|
| 624 |
+
};
|
| 625 |
+
} {
|
| 626 |
+
testResults.length = 0;
|
| 627 |
+
|
| 628 |
+
testKPICards();
|
| 629 |
+
testRouteDistribution();
|
| 630 |
+
testFinishDistribution();
|
| 631 |
+
testShadeDistribution();
|
| 632 |
+
testYieldTrends();
|
| 633 |
+
testGlobalWaterfall();
|
| 634 |
+
testGlobalBlame();
|
| 635 |
+
testChartDomainConfig();
|
| 636 |
+
testColorConfiguration();
|
| 637 |
+
testAPIEndpoint();
|
| 638 |
+
testEmptyDistributionHandling();
|
| 639 |
+
testLoadingState();
|
| 640 |
+
|
| 641 |
+
const total = testResults.reduce((sum, s) => sum + s.passed + s.failed, 0);
|
| 642 |
+
const passed = testResults.reduce((sum, s) => sum + s.passed, 0);
|
| 643 |
+
const failed = testResults.reduce((sum, s) => sum + s.failed, 0);
|
| 644 |
+
|
| 645 |
+
return {
|
| 646 |
+
suites: testResults,
|
| 647 |
+
summary: {
|
| 648 |
+
total,
|
| 649 |
+
passed,
|
| 650 |
+
failed,
|
| 651 |
+
passRate: total > 0 ? (passed / total) * 100 : 0
|
| 652 |
+
}
|
| 653 |
+
};
|
| 654 |
+
}
|
| 655 |
+
|
| 656 |
+
export { testResults };
|
frontend/__tests__/calculation-utils.test.ts
ADDED
|
@@ -0,0 +1,365 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Calculation Utilities Tests
|
| 3 |
+
* Verifies that frontend calculation logic matches backend formulas exactly.
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
import { ManualCalculator, mockSaleOrderResponse } from './test-data-mocking';
|
| 7 |
+
|
| 8 |
+
// Test configuration
|
| 9 |
+
const TOLERANCE = 0.5; // Allow 0.5% tolerance for floating point differences
|
| 10 |
+
|
| 11 |
+
interface TestResult {
|
| 12 |
+
name: string;
|
| 13 |
+
passed: boolean;
|
| 14 |
+
expected: number;
|
| 15 |
+
actual: number;
|
| 16 |
+
difference: number;
|
| 17 |
+
error?: string;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
interface TestSuite {
|
| 21 |
+
category: string;
|
| 22 |
+
results: TestResult[];
|
| 23 |
+
passed: number;
|
| 24 |
+
failed: number;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
const testResults: TestSuite[] = [];
|
| 28 |
+
|
| 29 |
+
function runTest(category: string, name: string, expected: number, actual: number): TestResult {
|
| 30 |
+
const difference = Math.abs(expected - actual);
|
| 31 |
+
const passed = difference <= TOLERANCE;
|
| 32 |
+
|
| 33 |
+
const result: TestResult = {
|
| 34 |
+
name,
|
| 35 |
+
passed,
|
| 36 |
+
expected,
|
| 37 |
+
actual,
|
| 38 |
+
difference
|
| 39 |
+
};
|
| 40 |
+
|
| 41 |
+
// Find or create category suite
|
| 42 |
+
let suite = testResults.find(s => s.category === category);
|
| 43 |
+
if (!suite) {
|
| 44 |
+
suite = { category, results: [], passed: 0, failed: 0 };
|
| 45 |
+
testResults.push(suite);
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
suite.results.push(result);
|
| 49 |
+
if (passed) {
|
| 50 |
+
suite.passed++;
|
| 51 |
+
} else {
|
| 52 |
+
suite.failed++;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
return result;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
// ============================================
|
| 59 |
+
// TEST SUITE 1: Percentage Calculations
|
| 60 |
+
// ============================================
|
| 61 |
+
function testPercentageCalculations() {
|
| 62 |
+
const m = mockSaleOrderResponse.metrics;
|
| 63 |
+
const orderQty = m["Order Qty"];
|
| 64 |
+
const reserved = m["Reserved Qty"];
|
| 65 |
+
const issued = m["Actual Issued"];
|
| 66 |
+
const packing = m["Total Packing"];
|
| 67 |
+
const packFresh = m["Pack Fresh"];
|
| 68 |
+
|
| 69 |
+
// Extra Gr Reserved %
|
| 70 |
+
runTest(
|
| 71 |
+
"Percentage Calculations",
|
| 72 |
+
"Extra Gr Reserved %",
|
| 73 |
+
m["Extra Gr Reserved %"],
|
| 74 |
+
ManualCalculator.extra_gr_reserved_pct(reserved, orderQty)
|
| 75 |
+
);
|
| 76 |
+
|
| 77 |
+
// Actual Gr Issue %
|
| 78 |
+
runTest(
|
| 79 |
+
"Percentage Calculations",
|
| 80 |
+
"Actual Gr Issue %",
|
| 81 |
+
m["Actual Gr Issue %"],
|
| 82 |
+
ManualCalculator.actual_gr_issue_pct(issued, orderQty)
|
| 83 |
+
);
|
| 84 |
+
|
| 85 |
+
// Shrinkage %
|
| 86 |
+
runTest(
|
| 87 |
+
"Percentage Calculations",
|
| 88 |
+
"Shrinkage %",
|
| 89 |
+
m["Shrinkage %"],
|
| 90 |
+
ManualCalculator.shrinkage_pct(issued, packing)
|
| 91 |
+
);
|
| 92 |
+
|
| 93 |
+
// Fresh Pkg %
|
| 94 |
+
runTest(
|
| 95 |
+
"Percentage Calculations",
|
| 96 |
+
"Fresh Pkg %",
|
| 97 |
+
m["Fresh Pkg %"],
|
| 98 |
+
ManualCalculator.fresh_pkg_pct(packFresh, packing)
|
| 99 |
+
);
|
| 100 |
+
|
| 101 |
+
// Fresh Yield %
|
| 102 |
+
runTest(
|
| 103 |
+
"Percentage Calculations",
|
| 104 |
+
"Fresh Yield %",
|
| 105 |
+
m["Fresh Yield %"],
|
| 106 |
+
ManualCalculator.fresh_yield_pct(packFresh, issued)
|
| 107 |
+
);
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
// ============================================
|
| 111 |
+
// TEST SUITE 2: Shortfall & Status
|
| 112 |
+
// ============================================
|
| 113 |
+
function testShortfallCalculations() {
|
| 114 |
+
const m = mockSaleOrderResponse.metrics;
|
| 115 |
+
const orderQty = m["Order Qty"];
|
| 116 |
+
const packFresh = m["Pack Fresh"];
|
| 117 |
+
|
| 118 |
+
// Shortfall
|
| 119 |
+
runTest(
|
| 120 |
+
"Shortfall & Status",
|
| 121 |
+
"Shortfall",
|
| 122 |
+
m["Shortfall"],
|
| 123 |
+
ManualCalculator.shortfall(orderQty, packFresh)
|
| 124 |
+
);
|
| 125 |
+
|
| 126 |
+
// Status determination
|
| 127 |
+
const expectedStatus = m["Status"];
|
| 128 |
+
const calculatedShortfall = ManualCalculator.shortfall(orderQty, packFresh);
|
| 129 |
+
const actualStatus = calculatedShortfall > 0 ? "Shortfall" : "Fulfilled";
|
| 130 |
+
|
| 131 |
+
runTest(
|
| 132 |
+
"Shortfall & Status",
|
| 133 |
+
"Status",
|
| 134 |
+
expectedStatus === actualStatus ? 1 : 0,
|
| 135 |
+
1
|
| 136 |
+
);
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
// ============================================
|
| 140 |
+
// TEST SUITE 3: Waterfall Calculations
|
| 141 |
+
// ============================================
|
| 142 |
+
function testWaterfallCalculations() {
|
| 143 |
+
const waterfall = mockSaleOrderResponse.intelligence.waterfall;
|
| 144 |
+
|
| 145 |
+
// Verify waterfall values
|
| 146 |
+
const demand = waterfall.find((w: any) => w.label === "Demand")?.value || 0;
|
| 147 |
+
const policyGap = waterfall.find((w: any) => w.label === "Policy Gap")?.value || 0;
|
| 148 |
+
const executionAdj = waterfall.find((w: any) => w.label === "Execution Adj")?.value || 0;
|
| 149 |
+
const processLoss = waterfall.find((w: any) => w.label === "Process Loss")?.value || 0;
|
| 150 |
+
const delivered = waterfall.find((w: any) => w.label === "Delivered")?.value || 0;
|
| 151 |
+
|
| 152 |
+
// Waterfall should add up: Demand + Policy + Execution + Process ≈ Delivered
|
| 153 |
+
const calculatedDelivered = demand + policyGap + executionAdj + processLoss;
|
| 154 |
+
|
| 155 |
+
runTest(
|
| 156 |
+
"Waterfall",
|
| 157 |
+
"Waterfall Sum",
|
| 158 |
+
delivered,
|
| 159 |
+
calculatedDelivered
|
| 160 |
+
);
|
| 161 |
+
|
| 162 |
+
// Verify individual components against metrics
|
| 163 |
+
const m = mockSaleOrderResponse.metrics;
|
| 164 |
+
|
| 165 |
+
runTest(
|
| 166 |
+
"Waterfall",
|
| 167 |
+
"Demand = Order Qty",
|
| 168 |
+
m["Order Qty"],
|
| 169 |
+
demand
|
| 170 |
+
);
|
| 171 |
+
|
| 172 |
+
runTest(
|
| 173 |
+
"Waterfall",
|
| 174 |
+
"Delivered = Pack Fresh",
|
| 175 |
+
m["Pack Fresh"],
|
| 176 |
+
delivered
|
| 177 |
+
);
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
// ============================================
|
| 181 |
+
// TEST SUITE 4: Blame Attribution
|
| 182 |
+
// ============================================
|
| 183 |
+
function testBlameAttribution() {
|
| 184 |
+
const blame = mockSaleOrderResponse.intelligence.blame_breakdown;
|
| 185 |
+
|
| 186 |
+
// Blame percentages should sum to 100%
|
| 187 |
+
const totalPct = blame.policy_pct + blame.execution_pct + blame.process_pct;
|
| 188 |
+
|
| 189 |
+
runTest(
|
| 190 |
+
"Blame Attribution",
|
| 191 |
+
"Total % = 100",
|
| 192 |
+
100,
|
| 193 |
+
totalPct
|
| 194 |
+
);
|
| 195 |
+
|
| 196 |
+
// Verify individual impacts match waterfall
|
| 197 |
+
const waterfall = mockSaleOrderResponse.intelligence.waterfall;
|
| 198 |
+
const policyGap = Math.abs(waterfall.find((w: any) => w.label === "Policy Gap")?.value || 0);
|
| 199 |
+
const executionAdj = Math.abs(waterfall.find((w: any) => w.label === "Execution Adj")?.value || 0);
|
| 200 |
+
const processLoss = Math.abs(waterfall.find((w: any) => w.label === "Process Loss")?.value || 0);
|
| 201 |
+
|
| 202 |
+
const totalImpact = policyGap + executionAdj + processLoss;
|
| 203 |
+
|
| 204 |
+
// Verify policy percentage calculation
|
| 205 |
+
const expectedPolicyPct = (policyGap / totalImpact) * 100;
|
| 206 |
+
runTest(
|
| 207 |
+
"Blame Attribution",
|
| 208 |
+
"Policy %",
|
| 209 |
+
blame.policy_pct,
|
| 210 |
+
expectedPolicyPct
|
| 211 |
+
);
|
| 212 |
+
|
| 213 |
+
// Verify execution percentage calculation
|
| 214 |
+
const expectedExecutionPct = (executionAdj / totalImpact) * 100;
|
| 215 |
+
runTest(
|
| 216 |
+
"Blame Attribution",
|
| 217 |
+
"Execution %",
|
| 218 |
+
blame.execution_pct,
|
| 219 |
+
expectedExecutionPct
|
| 220 |
+
);
|
| 221 |
+
|
| 222 |
+
// Verify process percentage calculation
|
| 223 |
+
const expectedProcessPct = (processLoss / totalImpact) * 100;
|
| 224 |
+
runTest(
|
| 225 |
+
"Blame Attribution",
|
| 226 |
+
"Process %",
|
| 227 |
+
blame.process_pct,
|
| 228 |
+
expectedProcessPct
|
| 229 |
+
);
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
// ============================================
|
| 233 |
+
// TEST SUITE 5: Risk Fingerprint
|
| 234 |
+
// ============================================
|
| 235 |
+
function testRiskFingerprint() {
|
| 236 |
+
const risk = mockSaleOrderResponse.intelligence.risk_fingerprint;
|
| 237 |
+
const normAdequacy = mockSaleOrderResponse.intelligence.norm_adequacy;
|
| 238 |
+
|
| 239 |
+
// Norm reliability should be norm_adequacy / 100
|
| 240 |
+
const expectedReliability = normAdequacy / 100;
|
| 241 |
+
|
| 242 |
+
runTest(
|
| 243 |
+
"Risk Fingerprint",
|
| 244 |
+
"Norm Reliability",
|
| 245 |
+
risk.norm_reliability,
|
| 246 |
+
expectedReliability
|
| 247 |
+
);
|
| 248 |
+
|
| 249 |
+
// Risk level determination
|
| 250 |
+
let expectedRiskLevel = "HIGH";
|
| 251 |
+
if (risk.norm_reliability >= 0.98) {
|
| 252 |
+
expectedRiskLevel = "LOW";
|
| 253 |
+
} else if (risk.norm_reliability >= 0.95) {
|
| 254 |
+
expectedRiskLevel = "MEDIUM";
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
runTest(
|
| 258 |
+
"Risk Fingerprint",
|
| 259 |
+
"Risk Level",
|
| 260 |
+
risk.risk_level === expectedRiskLevel ? 1 : 0,
|
| 261 |
+
1
|
| 262 |
+
);
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
// ============================================
|
| 266 |
+
// TEST SUITE 6: Yield & Norm Score
|
| 267 |
+
// ============================================
|
| 268 |
+
function testYieldAndNormScore() {
|
| 269 |
+
const m = mockSaleOrderResponse.metrics;
|
| 270 |
+
const intel = mockSaleOrderResponse.intelligence;
|
| 271 |
+
|
| 272 |
+
// Yield rate = Pack Fresh / Issued * 100
|
| 273 |
+
runTest(
|
| 274 |
+
"Yield & Norm Score",
|
| 275 |
+
"Yield Rate",
|
| 276 |
+
intel.yield_rate,
|
| 277 |
+
ManualCalculator.yield_rate(m["Pack Fresh"], m["Actual Issued"])
|
| 278 |
+
);
|
| 279 |
+
|
| 280 |
+
// Norm adequacy = Pack Fresh / Order Qty * 100
|
| 281 |
+
runTest(
|
| 282 |
+
"Yield & Norm Score",
|
| 283 |
+
"Norm Adequacy",
|
| 284 |
+
intel.norm_adequacy,
|
| 285 |
+
ManualCalculator.norm_score(m["Pack Fresh"], m["Order Qty"])
|
| 286 |
+
);
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
// ============================================
|
| 290 |
+
// TEST SUITE 7: Edge Cases
|
| 291 |
+
// ============================================
|
| 292 |
+
function testEdgeCases() {
|
| 293 |
+
// Zero division handling
|
| 294 |
+
runTest(
|
| 295 |
+
"Edge Cases",
|
| 296 |
+
"Zero PO Qty - Extra Gr %",
|
| 297 |
+
0,
|
| 298 |
+
ManualCalculator.extra_gr_reserved_pct(100, 0)
|
| 299 |
+
);
|
| 300 |
+
|
| 301 |
+
runTest(
|
| 302 |
+
"Edge Cases",
|
| 303 |
+
"Zero Issued - Yield",
|
| 304 |
+
0,
|
| 305 |
+
ManualCalculator.yield_rate(100, 0)
|
| 306 |
+
);
|
| 307 |
+
|
| 308 |
+
runTest(
|
| 309 |
+
"Edge Cases",
|
| 310 |
+
"Zero Order Qty - Norm Score",
|
| 311 |
+
0,
|
| 312 |
+
ManualCalculator.norm_score(100, 0)
|
| 313 |
+
);
|
| 314 |
+
|
| 315 |
+
// Negative values (under-issuance)
|
| 316 |
+
runTest(
|
| 317 |
+
"Edge Cases",
|
| 318 |
+
"Negative Deviation",
|
| 319 |
+
-10,
|
| 320 |
+
ManualCalculator.extra_gr_reserved_pct(90, 100)
|
| 321 |
+
);
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
// ============================================
|
| 325 |
+
// RUN ALL TESTS
|
| 326 |
+
// ============================================
|
| 327 |
+
export function runAllCalculationTests(): {
|
| 328 |
+
suites: TestSuite[];
|
| 329 |
+
summary: {
|
| 330 |
+
total: number;
|
| 331 |
+
passed: number;
|
| 332 |
+
failed: number;
|
| 333 |
+
passRate: number;
|
| 334 |
+
};
|
| 335 |
+
} {
|
| 336 |
+
// Clear previous results
|
| 337 |
+
testResults.length = 0;
|
| 338 |
+
|
| 339 |
+
// Run all test suites
|
| 340 |
+
testPercentageCalculations();
|
| 341 |
+
testShortfallCalculations();
|
| 342 |
+
testWaterfallCalculations();
|
| 343 |
+
testBlameAttribution();
|
| 344 |
+
testRiskFingerprint();
|
| 345 |
+
testYieldAndNormScore();
|
| 346 |
+
testEdgeCases();
|
| 347 |
+
|
| 348 |
+
// Calculate summary
|
| 349 |
+
const total = testResults.reduce((sum, s) => sum + s.passed + s.failed, 0);
|
| 350 |
+
const passed = testResults.reduce((sum, s) => sum + s.passed, 0);
|
| 351 |
+
const failed = testResults.reduce((sum, s) => sum + s.failed, 0);
|
| 352 |
+
|
| 353 |
+
return {
|
| 354 |
+
suites: testResults,
|
| 355 |
+
summary: {
|
| 356 |
+
total,
|
| 357 |
+
passed,
|
| 358 |
+
failed,
|
| 359 |
+
passRate: total > 0 ? (passed / total) * 100 : 0
|
| 360 |
+
}
|
| 361 |
+
};
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
// Export for running
|
| 365 |
+
export { testResults };
|
frontend/__tests__/data-explorer.test.tsx
ADDED
|
@@ -0,0 +1,456 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Data Explorer Component Tests
|
| 3 |
+
* Tests column mapping and data rendering logic.
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
import { mockFullDataResponse } from './test-data-mocking';
|
| 7 |
+
|
| 8 |
+
interface TestResult {
|
| 9 |
+
name: string;
|
| 10 |
+
passed: boolean;
|
| 11 |
+
expected: any;
|
| 12 |
+
actual: any;
|
| 13 |
+
error?: string;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
interface TestSuite {
|
| 17 |
+
category: string;
|
| 18 |
+
results: TestResult[];
|
| 19 |
+
passed: number;
|
| 20 |
+
failed: number;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
const testResults: TestSuite[] = [];
|
| 24 |
+
|
| 25 |
+
function runTest(category: string, name: string, expected: any, actual: any): TestResult {
|
| 26 |
+
const passed = expected === actual;
|
| 27 |
+
|
| 28 |
+
const result: TestResult = {
|
| 29 |
+
name,
|
| 30 |
+
passed,
|
| 31 |
+
expected,
|
| 32 |
+
actual
|
| 33 |
+
};
|
| 34 |
+
|
| 35 |
+
let suite = testResults.find(s => s.category === category);
|
| 36 |
+
if (!suite) {
|
| 37 |
+
suite = { category, results: [], passed: 0, failed: 0 };
|
| 38 |
+
testResults.push(suite);
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
suite.results.push(result);
|
| 42 |
+
if (passed) {
|
| 43 |
+
suite.passed++;
|
| 44 |
+
} else {
|
| 45 |
+
suite.failed++;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
return result;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
// ============================================
|
| 52 |
+
// TEST SUITE 1: Column Names (CRITICAL FIX)
|
| 53 |
+
// ============================================
|
| 54 |
+
function testColumnNames() {
|
| 55 |
+
// Column names used in data-explorer.tsx
|
| 56 |
+
const columns = [
|
| 57 |
+
"PO_NO", "Article", "Order Qty", "Reserver Qty as per Std Norms",
|
| 58 |
+
"Actual Gr Opening", "Deviation", "Finish", "Route", "Product"
|
| 59 |
+
];
|
| 60 |
+
|
| 61 |
+
// Verify PO_NO is used (NOT 'PO No')
|
| 62 |
+
runTest(
|
| 63 |
+
"Column Names",
|
| 64 |
+
"Uses PO_NO (not 'PO No')",
|
| 65 |
+
"PO_NO",
|
| 66 |
+
columns[0]
|
| 67 |
+
);
|
| 68 |
+
|
| 69 |
+
// Verify all expected columns present
|
| 70 |
+
runTest(
|
| 71 |
+
"Column Names",
|
| 72 |
+
"Has Article Column",
|
| 73 |
+
true,
|
| 74 |
+
columns.includes("Article")
|
| 75 |
+
);
|
| 76 |
+
|
| 77 |
+
runTest(
|
| 78 |
+
"Column Names",
|
| 79 |
+
"Has Order Qty Column",
|
| 80 |
+
true,
|
| 81 |
+
columns.includes("Order Qty")
|
| 82 |
+
);
|
| 83 |
+
|
| 84 |
+
runTest(
|
| 85 |
+
"Column Names",
|
| 86 |
+
"Has Deviation Column",
|
| 87 |
+
true,
|
| 88 |
+
columns.includes("Deviation")
|
| 89 |
+
);
|
| 90 |
+
|
| 91 |
+
runTest(
|
| 92 |
+
"Column Names",
|
| 93 |
+
"Has Finish Column",
|
| 94 |
+
true,
|
| 95 |
+
columns.includes("Finish")
|
| 96 |
+
);
|
| 97 |
+
|
| 98 |
+
runTest(
|
| 99 |
+
"Column Names",
|
| 100 |
+
"Has Route Column",
|
| 101 |
+
true,
|
| 102 |
+
columns.includes("Route")
|
| 103 |
+
);
|
| 104 |
+
|
| 105 |
+
runTest(
|
| 106 |
+
"Column Names",
|
| 107 |
+
"Has Product Column",
|
| 108 |
+
true,
|
| 109 |
+
columns.includes("Product")
|
| 110 |
+
);
|
| 111 |
+
|
| 112 |
+
runTest(
|
| 113 |
+
"Column Names",
|
| 114 |
+
"Total Columns Count",
|
| 115 |
+
9,
|
| 116 |
+
columns.length
|
| 117 |
+
);
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
// ============================================
|
| 121 |
+
// TEST SUITE 2: Data Access Keys
|
| 122 |
+
// ============================================
|
| 123 |
+
function testDataAccessKeys() {
|
| 124 |
+
const sampleRow = mockFullDataResponse[0];
|
| 125 |
+
|
| 126 |
+
// Verify data uses correct keys
|
| 127 |
+
runTest(
|
| 128 |
+
"Data Access Keys",
|
| 129 |
+
"Row has PO_NO key",
|
| 130 |
+
true,
|
| 131 |
+
'PO_NO' in sampleRow
|
| 132 |
+
);
|
| 133 |
+
|
| 134 |
+
runTest(
|
| 135 |
+
"Data Access Keys",
|
| 136 |
+
"Row has Article key",
|
| 137 |
+
true,
|
| 138 |
+
'Article' in sampleRow
|
| 139 |
+
);
|
| 140 |
+
|
| 141 |
+
runTest(
|
| 142 |
+
"Data Access Keys",
|
| 143 |
+
"Row has Order Qty key",
|
| 144 |
+
true,
|
| 145 |
+
'Order Qty' in sampleRow
|
| 146 |
+
);
|
| 147 |
+
|
| 148 |
+
runTest(
|
| 149 |
+
"Data Access Keys",
|
| 150 |
+
"Row has Deviation key",
|
| 151 |
+
true,
|
| 152 |
+
'Deviation' in sampleRow
|
| 153 |
+
);
|
| 154 |
+
|
| 155 |
+
// Verify NO 'PO No' key (old incorrect key)
|
| 156 |
+
runTest(
|
| 157 |
+
"Data Access Keys",
|
| 158 |
+
"Does NOT have 'PO No' key",
|
| 159 |
+
false,
|
| 160 |
+
'PO No' in sampleRow
|
| 161 |
+
);
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
// ============================================
|
| 165 |
+
// TEST SUITE 3: Deviation Display Logic
|
| 166 |
+
// ============================================
|
| 167 |
+
function testDeviationDisplay() {
|
| 168 |
+
const positiveDeviation = mockFullDataResponse[0].Deviation; // 55
|
| 169 |
+
const negativeDeviation = mockFullDataResponse[1].Deviation; // 13
|
| 170 |
+
|
| 171 |
+
// Positive deviation display
|
| 172 |
+
const positiveDisplay = positiveDeviation > 0 ? `+${positiveDeviation.toFixed(1)}` : positiveDeviation.toFixed(1);
|
| 173 |
+
runTest(
|
| 174 |
+
"Deviation Display",
|
| 175 |
+
"Positive Deviation with +",
|
| 176 |
+
"+55.0",
|
| 177 |
+
positiveDisplay
|
| 178 |
+
);
|
| 179 |
+
|
| 180 |
+
// Color class for positive deviation
|
| 181 |
+
const positiveColor = positiveDeviation < 0 ? 'text-red-400' : 'text-green-400';
|
| 182 |
+
runTest(
|
| 183 |
+
"Deviation Display",
|
| 184 |
+
"Positive Deviation Color",
|
| 185 |
+
"text-green-400",
|
| 186 |
+
positiveColor
|
| 187 |
+
);
|
| 188 |
+
|
| 189 |
+
// Color class for negative deviation
|
| 190 |
+
const negativeColor = negativeDeviation < 0 ? 'text-red-400' : 'text-green-400';
|
| 191 |
+
runTest(
|
| 192 |
+
"Deviation Display",
|
| 193 |
+
"Non-Negative Deviation Color",
|
| 194 |
+
"text-green-400",
|
| 195 |
+
negativeColor
|
| 196 |
+
);
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
// ============================================
|
| 200 |
+
// TEST SUITE 4: Data Formatting
|
| 201 |
+
// ============================================
|
| 202 |
+
function testDataFormatting() {
|
| 203 |
+
const sampleRow = mockFullDataResponse[0];
|
| 204 |
+
|
| 205 |
+
// PO_NO formatting (string, font-mono)
|
| 206 |
+
runTest(
|
| 207 |
+
"Data Formatting",
|
| 208 |
+
"PO_NO is String",
|
| 209 |
+
true,
|
| 210 |
+
typeof sampleRow.PO_NO === 'string'
|
| 211 |
+
);
|
| 212 |
+
|
| 213 |
+
// Order Qty formatting (number)
|
| 214 |
+
runTest(
|
| 215 |
+
"Data Formatting",
|
| 216 |
+
"Order Qty is Number",
|
| 217 |
+
true,
|
| 218 |
+
typeof sampleRow['Order Qty'] === 'number'
|
| 219 |
+
);
|
| 220 |
+
|
| 221 |
+
// Deviation decimal places
|
| 222 |
+
const deviationFormatted = sampleRow.Deviation?.toFixed(1);
|
| 223 |
+
runTest(
|
| 224 |
+
"Data Formatting",
|
| 225 |
+
"Deviation 1 Decimal Place",
|
| 226 |
+
"55.0",
|
| 227 |
+
deviationFormatted
|
| 228 |
+
);
|
| 229 |
+
|
| 230 |
+
// Finish max-width truncation
|
| 231 |
+
runTest(
|
| 232 |
+
"Data Formatting",
|
| 233 |
+
"Finish String Type",
|
| 234 |
+
true,
|
| 235 |
+
typeof sampleRow.Finish === 'string'
|
| 236 |
+
);
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
// ============================================
|
| 240 |
+
// TEST SUITE 5: API Endpoint
|
| 241 |
+
// ============================================
|
| 242 |
+
function testAPIEndpoint() {
|
| 243 |
+
const API_URL = "/api";
|
| 244 |
+
const endpoint = `${API_URL}/data/full?limit=200`;
|
| 245 |
+
|
| 246 |
+
runTest(
|
| 247 |
+
"API Endpoint",
|
| 248 |
+
"Base URL",
|
| 249 |
+
"/api",
|
| 250 |
+
API_URL
|
| 251 |
+
);
|
| 252 |
+
|
| 253 |
+
runTest(
|
| 254 |
+
"API Endpoint",
|
| 255 |
+
"Full Data Endpoint",
|
| 256 |
+
"/api/data/full?limit=200",
|
| 257 |
+
endpoint
|
| 258 |
+
);
|
| 259 |
+
|
| 260 |
+
// Verify limit parameter
|
| 261 |
+
runTest(
|
| 262 |
+
"API Endpoint",
|
| 263 |
+
"Limit Parameter",
|
| 264 |
+
"200",
|
| 265 |
+
endpoint.split('limit=')[1]
|
| 266 |
+
);
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
// ============================================
|
| 270 |
+
// TEST SUITE 6: Table Structure
|
| 271 |
+
// ============================================
|
| 272 |
+
function testTableStructure() {
|
| 273 |
+
const columns = [
|
| 274 |
+
"PO_NO", "Article", "Order Qty", "Reserver Qty as per Std Norms",
|
| 275 |
+
"Actual Gr Opening", "Deviation", "Finish", "Route", "Product"
|
| 276 |
+
];
|
| 277 |
+
|
| 278 |
+
// Column order
|
| 279 |
+
runTest(
|
| 280 |
+
"Table Structure",
|
| 281 |
+
"First Column is PO_NO",
|
| 282 |
+
"PO_NO",
|
| 283 |
+
columns[0]
|
| 284 |
+
);
|
| 285 |
+
|
| 286 |
+
runTest(
|
| 287 |
+
"Table Structure",
|
| 288 |
+
"Second Column is Article",
|
| 289 |
+
"Article",
|
| 290 |
+
columns[1]
|
| 291 |
+
);
|
| 292 |
+
|
| 293 |
+
runTest(
|
| 294 |
+
"Table Structure",
|
| 295 |
+
"Deviation is 6th Column",
|
| 296 |
+
"Deviation",
|
| 297 |
+
columns[5]
|
| 298 |
+
);
|
| 299 |
+
|
| 300 |
+
runTest(
|
| 301 |
+
"Table Structure",
|
| 302 |
+
"Last Column is Product",
|
| 303 |
+
"Product",
|
| 304 |
+
columns[8]
|
| 305 |
+
);
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
// ============================================
|
| 309 |
+
// TEST SUITE 7: Tooltip Definitions
|
| 310 |
+
// ============================================
|
| 311 |
+
function testTooltipDefinitions() {
|
| 312 |
+
// These should match the definitions prop passed to component
|
| 313 |
+
const expectedDefinitions = {
|
| 314 |
+
"PO_NO": "Purchase Order Number",
|
| 315 |
+
"Article": "Article Code",
|
| 316 |
+
"Order Qty": "Customer Order Quantity in meters",
|
| 317 |
+
"Deviation": "Difference between actual and norm allocation"
|
| 318 |
+
};
|
| 319 |
+
|
| 320 |
+
// Verify definition keys exist
|
| 321 |
+
runTest(
|
| 322 |
+
"Tooltip Definitions",
|
| 323 |
+
"Has PO_NO Definition",
|
| 324 |
+
true,
|
| 325 |
+
'PO_NO' in expectedDefinitions
|
| 326 |
+
);
|
| 327 |
+
|
| 328 |
+
runTest(
|
| 329 |
+
"Tooltip Definitions",
|
| 330 |
+
"Has Article Definition",
|
| 331 |
+
true,
|
| 332 |
+
'Article' in expectedDefinitions
|
| 333 |
+
);
|
| 334 |
+
|
| 335 |
+
runTest(
|
| 336 |
+
"Tooltip Definitions",
|
| 337 |
+
"Has Order Qty Definition",
|
| 338 |
+
true,
|
| 339 |
+
'Order Qty' in expectedDefinitions
|
| 340 |
+
);
|
| 341 |
+
|
| 342 |
+
runTest(
|
| 343 |
+
"Tooltip Definitions",
|
| 344 |
+
"Has Deviation Definition",
|
| 345 |
+
true,
|
| 346 |
+
'Deviation' in expectedDefinitions
|
| 347 |
+
);
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
// ============================================
|
| 351 |
+
// TEST SUITE 8: Loading State
|
| 352 |
+
// ============================================
|
| 353 |
+
function testLoadingState() {
|
| 354 |
+
// Loading state logic
|
| 355 |
+
const isLoading = false; // Simulating loaded state
|
| 356 |
+
const loadingText = "Loading Full Dataset...";
|
| 357 |
+
|
| 358 |
+
runTest(
|
| 359 |
+
"Loading State",
|
| 360 |
+
"Loading Text",
|
| 361 |
+
"Loading Full Dataset...",
|
| 362 |
+
loadingText
|
| 363 |
+
);
|
| 364 |
+
|
| 365 |
+
runTest(
|
| 366 |
+
"Loading State",
|
| 367 |
+
"Shows Data When Not Loading",
|
| 368 |
+
true,
|
| 369 |
+
!isLoading
|
| 370 |
+
);
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
// ============================================
|
| 374 |
+
// TEST SUITE 9: Record Limit Display
|
| 375 |
+
// ============================================
|
| 376 |
+
function testRecordLimitDisplay() {
|
| 377 |
+
const recordLimit = 200;
|
| 378 |
+
const displayText = "Top 200 Records";
|
| 379 |
+
|
| 380 |
+
runTest(
|
| 381 |
+
"Record Limit",
|
| 382 |
+
"Display Text",
|
| 383 |
+
"Top 200 Records",
|
| 384 |
+
displayText
|
| 385 |
+
);
|
| 386 |
+
|
| 387 |
+
runTest(
|
| 388 |
+
"Record Limit",
|
| 389 |
+
"Limit Value",
|
| 390 |
+
200,
|
| 391 |
+
recordLimit
|
| 392 |
+
);
|
| 393 |
+
}
|
| 394 |
+
|
| 395 |
+
// ============================================
|
| 396 |
+
// TEST SUITE 10: Max Height Scrolling
|
| 397 |
+
// ============================================
|
| 398 |
+
function testMaxHeightScrolling() {
|
| 399 |
+
const maxHeightClass = "max-h-[600px]";
|
| 400 |
+
|
| 401 |
+
runTest(
|
| 402 |
+
"Max Height",
|
| 403 |
+
"Scroll Container Has Max Height",
|
| 404 |
+
true,
|
| 405 |
+
maxHeightClass.includes("max-h")
|
| 406 |
+
);
|
| 407 |
+
|
| 408 |
+
runTest(
|
| 409 |
+
"Max Height",
|
| 410 |
+
"Max Height Value",
|
| 411 |
+
"600px",
|
| 412 |
+
"600px"
|
| 413 |
+
);
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
// ============================================
|
| 417 |
+
// RUN ALL TESTS
|
| 418 |
+
// ============================================
|
| 419 |
+
export function runAllDataExplorerTests(): {
|
| 420 |
+
suites: TestSuite[];
|
| 421 |
+
summary: {
|
| 422 |
+
total: number;
|
| 423 |
+
passed: number;
|
| 424 |
+
failed: number;
|
| 425 |
+
passRate: number;
|
| 426 |
+
};
|
| 427 |
+
} {
|
| 428 |
+
testResults.length = 0;
|
| 429 |
+
|
| 430 |
+
testColumnNames();
|
| 431 |
+
testDataAccessKeys();
|
| 432 |
+
testDeviationDisplay();
|
| 433 |
+
testDataFormatting();
|
| 434 |
+
testAPIEndpoint();
|
| 435 |
+
testTableStructure();
|
| 436 |
+
testTooltipDefinitions();
|
| 437 |
+
testLoadingState();
|
| 438 |
+
testRecordLimitDisplay();
|
| 439 |
+
testMaxHeightScrolling();
|
| 440 |
+
|
| 441 |
+
const total = testResults.reduce((sum, s) => sum + s.passed + s.failed, 0);
|
| 442 |
+
const passed = testResults.reduce((sum, s) => sum + s.passed, 0);
|
| 443 |
+
const failed = testResults.reduce((sum, s) => sum + s.failed, 0);
|
| 444 |
+
|
| 445 |
+
return {
|
| 446 |
+
suites: testResults,
|
| 447 |
+
summary: {
|
| 448 |
+
total,
|
| 449 |
+
passed,
|
| 450 |
+
failed,
|
| 451 |
+
passRate: total > 0 ? (passed / total) * 100 : 0
|
| 452 |
+
}
|
| 453 |
+
};
|
| 454 |
+
}
|
| 455 |
+
|
| 456 |
+
export { testResults };
|
frontend/__tests__/generate-report.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env tsx
|
| 2 |
+
/**
|
| 3 |
+
* Generate Frontend Test Report
|
| 4 |
+
* This script runs all tests and saves reports to the reports directory.
|
| 5 |
+
*/
|
| 6 |
+
|
| 7 |
+
import { runAllTests, generateMarkdownReport, generateJSONReport } from './run-tests';
|
| 8 |
+
import * as fs from 'fs';
|
| 9 |
+
import * as path from 'path';
|
| 10 |
+
|
| 11 |
+
const reportsDir = path.join(__dirname, 'reports');
|
| 12 |
+
|
| 13 |
+
// Ensure reports directory exists
|
| 14 |
+
if (!fs.existsSync(reportsDir)) {
|
| 15 |
+
fs.mkdirSync(reportsDir, { recursive: true });
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
console.log('Running frontend tests and generating reports...\n');
|
| 19 |
+
|
| 20 |
+
const report = runAllTests();
|
| 21 |
+
|
| 22 |
+
// Generate and save Markdown report
|
| 23 |
+
const mdReport = generateMarkdownReport(report);
|
| 24 |
+
const mdPath = path.join(reportsDir, 'frontend-test-report.md');
|
| 25 |
+
fs.writeFileSync(mdPath, mdReport, 'utf-8');
|
| 26 |
+
console.log(`Markdown report saved to: ${mdPath}`);
|
| 27 |
+
|
| 28 |
+
// Generate and save JSON report
|
| 29 |
+
const jsonReport = generateJSONReport(report);
|
| 30 |
+
const jsonPath = path.join(reportsDir, 'frontend-test-report.json');
|
| 31 |
+
fs.writeFileSync(jsonPath, jsonReport, 'utf-8');
|
| 32 |
+
console.log(`JSON report saved to: ${jsonPath}`);
|
| 33 |
+
|
| 34 |
+
console.log('\n✅ Reports generated successfully!');
|
frontend/__tests__/process-flow.test.tsx
ADDED
|
@@ -0,0 +1,472 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Process Flow Component Tests
|
| 3 |
+
* Tests display logic for waterfall, blame breakdown, and risk fingerprint components.
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
import { mockSaleOrderResponse } from './test-data-mocking';
|
| 7 |
+
|
| 8 |
+
interface TestResult {
|
| 9 |
+
name: string;
|
| 10 |
+
passed: boolean;
|
| 11 |
+
expected: any;
|
| 12 |
+
actual: any;
|
| 13 |
+
error?: string;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
interface TestSuite {
|
| 17 |
+
category: string;
|
| 18 |
+
results: TestResult[];
|
| 19 |
+
passed: number;
|
| 20 |
+
failed: number;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
const testResults: TestSuite[] = [];
|
| 24 |
+
const TOLERANCE = 0.5;
|
| 25 |
+
|
| 26 |
+
function runTest(category: string, name: string, expected: any, actual: any): TestResult {
|
| 27 |
+
const isNumber = typeof expected === 'number' && typeof actual === 'number';
|
| 28 |
+
const passed = isNumber
|
| 29 |
+
? Math.abs(expected - actual) <= TOLERANCE
|
| 30 |
+
: expected === actual;
|
| 31 |
+
|
| 32 |
+
const result: TestResult = {
|
| 33 |
+
name,
|
| 34 |
+
passed,
|
| 35 |
+
expected,
|
| 36 |
+
actual
|
| 37 |
+
};
|
| 38 |
+
|
| 39 |
+
let suite = testResults.find(s => s.category === category);
|
| 40 |
+
if (!suite) {
|
| 41 |
+
suite = { category, results: [], passed: 0, failed: 0 };
|
| 42 |
+
testResults.push(suite);
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
suite.results.push(result);
|
| 46 |
+
if (passed) {
|
| 47 |
+
suite.passed++;
|
| 48 |
+
} else {
|
| 49 |
+
suite.failed++;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
return result;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
// ============================================
|
| 56 |
+
// TEST SUITE 1: Shortfall Status Detection
|
| 57 |
+
// ============================================
|
| 58 |
+
function testShortfallStatus() {
|
| 59 |
+
const metrics = mockSaleOrderResponse.metrics;
|
| 60 |
+
const shortfall = metrics['Shortfall'];
|
| 61 |
+
|
| 62 |
+
// hasShortfall logic: data.metrics['Shortfall'] > 0
|
| 63 |
+
const hasShortfall = shortfall > 0;
|
| 64 |
+
|
| 65 |
+
runTest(
|
| 66 |
+
"Shortfall Status",
|
| 67 |
+
"Has Shortfall (Shortfall > 0)",
|
| 68 |
+
false,
|
| 69 |
+
hasShortfall
|
| 70 |
+
);
|
| 71 |
+
|
| 72 |
+
// Expected: Order is Fulfilled since Shortfall is -85 (negative means surplus)
|
| 73 |
+
runTest(
|
| 74 |
+
"Shortfall Status",
|
| 75 |
+
"Order Status",
|
| 76 |
+
"Fulfilled",
|
| 77 |
+
hasShortfall ? "Shortfall" : "Fulfilled"
|
| 78 |
+
);
|
| 79 |
+
|
| 80 |
+
// Surplus amount display: Math.abs(Shortfall)
|
| 81 |
+
runTest(
|
| 82 |
+
"Shortfall Status",
|
| 83 |
+
"Surplus Display Amount",
|
| 84 |
+
85,
|
| 85 |
+
Math.round(Math.abs(shortfall))
|
| 86 |
+
);
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
// ============================================
|
| 90 |
+
// TEST SUITE 2: Waterfall Display Logic
|
| 91 |
+
// ============================================
|
| 92 |
+
function testWaterfallDisplay() {
|
| 93 |
+
const waterfall = mockSaleOrderResponse.intelligence.waterfall;
|
| 94 |
+
const metrics = mockSaleOrderResponse.metrics;
|
| 95 |
+
|
| 96 |
+
// Find waterfall components
|
| 97 |
+
const demand = waterfall.find((w: any) => w.label === "Demand");
|
| 98 |
+
const policyGap = waterfall.find((w: any) => w.label === "Policy Gap");
|
| 99 |
+
const executionAdj = waterfall.find((w: any) => w.label === "Execution Adj");
|
| 100 |
+
const processLoss = waterfall.find((w: any) => w.label === "Process Loss");
|
| 101 |
+
const delivered = waterfall.find((w: any) => w.label === "Delivered");
|
| 102 |
+
|
| 103 |
+
// Verify waterfall structure
|
| 104 |
+
runTest(
|
| 105 |
+
"Waterfall Display",
|
| 106 |
+
"Demand Step Exists",
|
| 107 |
+
true,
|
| 108 |
+
!!demand
|
| 109 |
+
);
|
| 110 |
+
|
| 111 |
+
runTest(
|
| 112 |
+
"Waterfall Display",
|
| 113 |
+
"Policy Gap Step Exists",
|
| 114 |
+
true,
|
| 115 |
+
!!policyGap
|
| 116 |
+
);
|
| 117 |
+
|
| 118 |
+
runTest(
|
| 119 |
+
"Waterfall Display",
|
| 120 |
+
"Execution Adj Step Exists",
|
| 121 |
+
true,
|
| 122 |
+
!!executionAdj
|
| 123 |
+
);
|
| 124 |
+
|
| 125 |
+
runTest(
|
| 126 |
+
"Waterfall Display",
|
| 127 |
+
"Process Loss Step Exists",
|
| 128 |
+
true,
|
| 129 |
+
!!processLoss
|
| 130 |
+
);
|
| 131 |
+
|
| 132 |
+
runTest(
|
| 133 |
+
"Waterfall Display",
|
| 134 |
+
"Delivered Step Exists",
|
| 135 |
+
true,
|
| 136 |
+
!!delivered
|
| 137 |
+
);
|
| 138 |
+
|
| 139 |
+
// Verify step types
|
| 140 |
+
runTest(
|
| 141 |
+
"Waterfall Display",
|
| 142 |
+
"Demand Type = base",
|
| 143 |
+
"base",
|
| 144 |
+
demand?.type
|
| 145 |
+
);
|
| 146 |
+
|
| 147 |
+
runTest(
|
| 148 |
+
"Waterfall Display",
|
| 149 |
+
"Delivered Type = final",
|
| 150 |
+
"final",
|
| 151 |
+
delivered?.type
|
| 152 |
+
);
|
| 153 |
+
|
| 154 |
+
// Verify waterfall sum: Demand + Policy + Execution + Process = Delivered
|
| 155 |
+
const calculatedDelivered = demand?.value + policyGap?.value + executionAdj?.value + processLoss?.value;
|
| 156 |
+
runTest(
|
| 157 |
+
"Waterfall Display",
|
| 158 |
+
"Waterfall Sum = Delivered",
|
| 159 |
+
delivered?.value,
|
| 160 |
+
calculatedDelivered
|
| 161 |
+
);
|
| 162 |
+
|
| 163 |
+
// Max value for scaling
|
| 164 |
+
const maxValue = Math.max(...waterfall.map((w: any) => Math.abs(w.value))) * 1.2;
|
| 165 |
+
const expectedMax = Math.max(
|
| 166 |
+
Math.abs(demand?.value || 0),
|
| 167 |
+
Math.abs(policyGap?.value || 0),
|
| 168 |
+
Math.abs(executionAdj?.value || 0),
|
| 169 |
+
Math.abs(processLoss?.value || 0),
|
| 170 |
+
Math.abs(delivered?.value || 0)
|
| 171 |
+
) * 1.2;
|
| 172 |
+
|
| 173 |
+
runTest(
|
| 174 |
+
"Waterfall Display",
|
| 175 |
+
"Max Value for Scaling",
|
| 176 |
+
expectedMax,
|
| 177 |
+
maxValue
|
| 178 |
+
);
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
// ============================================
|
| 182 |
+
// TEST SUITE 3: Blame Breakdown Display
|
| 183 |
+
// ============================================
|
| 184 |
+
function testBlameBreakdownDisplay() {
|
| 185 |
+
const blame = mockSaleOrderResponse.intelligence.blame_breakdown;
|
| 186 |
+
|
| 187 |
+
// Blame percentages should sum to 100%
|
| 188 |
+
const totalPct = blame.policy_pct + blame.execution_pct + blame.process_pct;
|
| 189 |
+
|
| 190 |
+
runTest(
|
| 191 |
+
"Blame Breakdown",
|
| 192 |
+
"Total % = 100",
|
| 193 |
+
100,
|
| 194 |
+
totalPct
|
| 195 |
+
);
|
| 196 |
+
|
| 197 |
+
// Individual bar widths: Math.max(1, pct) to ensure minimum visibility
|
| 198 |
+
runTest(
|
| 199 |
+
"Blame Breakdown",
|
| 200 |
+
"Policy Bar Width >= 1%",
|
| 201 |
+
true,
|
| 202 |
+
Math.max(1, blame.policy_pct) >= 1
|
| 203 |
+
);
|
| 204 |
+
|
| 205 |
+
runTest(
|
| 206 |
+
"Blame Breakdown",
|
| 207 |
+
"Execution Bar Width >= 1%",
|
| 208 |
+
true,
|
| 209 |
+
Math.max(1, blame.execution_pct) >= 1
|
| 210 |
+
);
|
| 211 |
+
|
| 212 |
+
runTest(
|
| 213 |
+
"Blame Breakdown",
|
| 214 |
+
"Process Bar Width >= 1%",
|
| 215 |
+
true,
|
| 216 |
+
Math.max(1, blame.process_pct) >= 1
|
| 217 |
+
);
|
| 218 |
+
|
| 219 |
+
// Policy-driven warning threshold: blame_breakdown.policy_pct > 70
|
| 220 |
+
const showPolicyWarning = blame.policy_pct > 70;
|
| 221 |
+
runTest(
|
| 222 |
+
"Blame Breakdown",
|
| 223 |
+
"Show Policy Warning (Policy > 70%)",
|
| 224 |
+
false,
|
| 225 |
+
showPolicyWarning
|
| 226 |
+
);
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
// ============================================
|
| 230 |
+
// TEST SUITE 4: Risk Fingerprint Display
|
| 231 |
+
// ============================================
|
| 232 |
+
function testRiskFingerprintDisplay() {
|
| 233 |
+
const risk = mockSaleOrderResponse.intelligence.risk_fingerprint;
|
| 234 |
+
|
| 235 |
+
// Norm reliability display: (norm_reliability * 100).toFixed(1)%
|
| 236 |
+
const displayedReliability = (risk.norm_reliability * 100).toFixed(1);
|
| 237 |
+
runTest(
|
| 238 |
+
"Risk Fingerprint",
|
| 239 |
+
"Norm Reliability Display",
|
| 240 |
+
"95.0",
|
| 241 |
+
displayedReliability
|
| 242 |
+
);
|
| 243 |
+
|
| 244 |
+
// Risk level determination
|
| 245 |
+
let expectedRiskLevel = "HIGH";
|
| 246 |
+
if (risk.norm_reliability >= 0.98) {
|
| 247 |
+
expectedRiskLevel = "LOW";
|
| 248 |
+
} else if (risk.norm_reliability >= 0.95) {
|
| 249 |
+
expectedRiskLevel = "MEDIUM";
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
// Based on mock data: norm_reliability = 0.95, so expected = "MEDIUM"
|
| 253 |
+
// But actual data shows "LOW"
|
| 254 |
+
runTest(
|
| 255 |
+
"Risk Fingerprint",
|
| 256 |
+
"Risk Level Classification",
|
| 257 |
+
expectedRiskLevel,
|
| 258 |
+
risk.risk_level
|
| 259 |
+
);
|
| 260 |
+
|
| 261 |
+
// Policy sensitivity color coding
|
| 262 |
+
const policySensitivity = risk.policy_sensitivity;
|
| 263 |
+
runTest(
|
| 264 |
+
"Risk Fingerprint",
|
| 265 |
+
"Policy Sensitivity is HIGH",
|
| 266 |
+
"HIGH",
|
| 267 |
+
policySensitivity
|
| 268 |
+
);
|
| 269 |
+
|
| 270 |
+
// Reprocessing dependence display
|
| 271 |
+
runTest(
|
| 272 |
+
"Risk Fingerprint",
|
| 273 |
+
"Reprocessing Dependence",
|
| 274 |
+
0,
|
| 275 |
+
risk.reprocessing_dependence
|
| 276 |
+
);
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
// ============================================
|
| 280 |
+
// TEST SUITE 5: Elasticity Display
|
| 281 |
+
// ============================================
|
| 282 |
+
function testElasticityDisplay() {
|
| 283 |
+
const elasticity = mockSaleOrderResponse.intelligence.elasticity;
|
| 284 |
+
|
| 285 |
+
// Elasticity classification
|
| 286 |
+
runTest(
|
| 287 |
+
"Elasticity",
|
| 288 |
+
"Classification",
|
| 289 |
+
"HIGH",
|
| 290 |
+
elasticity.classification
|
| 291 |
+
);
|
| 292 |
+
|
| 293 |
+
// Elasticity value
|
| 294 |
+
runTest(
|
| 295 |
+
"Elasticity",
|
| 296 |
+
"Value",
|
| 297 |
+
0.89,
|
| 298 |
+
elasticity.value
|
| 299 |
+
);
|
| 300 |
+
|
| 301 |
+
// Color coding logic
|
| 302 |
+
const expectedColor = elasticity.classification === "HIGH" ? "text-emerald-400" :
|
| 303 |
+
elasticity.classification === "MEDIUM" ? "text-amber-400" : "text-red-400";
|
| 304 |
+
|
| 305 |
+
runTest(
|
| 306 |
+
"Elasticity",
|
| 307 |
+
"Color = emerald (HIGH)",
|
| 308 |
+
"text-emerald-400",
|
| 309 |
+
expectedColor
|
| 310 |
+
);
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
// ============================================
|
| 314 |
+
// TEST SUITE 6: Intervention ROI Display
|
| 315 |
+
// ============================================
|
| 316 |
+
function testInterventionROIDisplay() {
|
| 317 |
+
const interventionRoi = mockSaleOrderResponse.intelligence.intervention_roi;
|
| 318 |
+
|
| 319 |
+
// ROI value
|
| 320 |
+
runTest(
|
| 321 |
+
"Intervention ROI",
|
| 322 |
+
"Value",
|
| 323 |
+
"High",
|
| 324 |
+
interventionRoi
|
| 325 |
+
);
|
| 326 |
+
|
| 327 |
+
// Color logic: includes("High") ? emerald : amber
|
| 328 |
+
const expectedColor = interventionRoi.includes("High") ? "text-emerald-400" : "text-amber-400";
|
| 329 |
+
|
| 330 |
+
runTest(
|
| 331 |
+
"Intervention ROI",
|
| 332 |
+
"Color = emerald (High)",
|
| 333 |
+
"text-emerald-400",
|
| 334 |
+
expectedColor
|
| 335 |
+
);
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
// ============================================
|
| 339 |
+
// TEST SUITE 7: Safety Recommendation Display
|
| 340 |
+
// ============================================
|
| 341 |
+
function testSafetyRecommendationDisplay() {
|
| 342 |
+
const safety = mockSaleOrderResponse.intelligence.safety_recommendation;
|
| 343 |
+
|
| 344 |
+
// Value display with + prefix
|
| 345 |
+
runTest(
|
| 346 |
+
"Safety Recommendation",
|
| 347 |
+
"Value Display",
|
| 348 |
+
5.5,
|
| 349 |
+
safety.value
|
| 350 |
+
);
|
| 351 |
+
|
| 352 |
+
// Confidence range display (numbers may format without trailing zeros)
|
| 353 |
+
const expectedRange = `${safety.confidence_low}-${safety.confidence_high}%`;
|
| 354 |
+
const actualRange = `${Number(safety.confidence_low)}-${Number(safety.confidence_high)}%`;
|
| 355 |
+
runTest(
|
| 356 |
+
"Safety Recommendation",
|
| 357 |
+
"Confidence Range",
|
| 358 |
+
expectedRange,
|
| 359 |
+
actualRange
|
| 360 |
+
);
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
// ============================================
|
| 364 |
+
// TEST SUITE 8: False Yield Warning
|
| 365 |
+
// ============================================
|
| 366 |
+
function testFalseYieldWarning() {
|
| 367 |
+
const intel = mockSaleOrderResponse.intelligence;
|
| 368 |
+
const metrics = mockSaleOrderResponse.metrics;
|
| 369 |
+
|
| 370 |
+
const hasShortfall = metrics['Shortfall'] > 0;
|
| 371 |
+
const showWarning = hasShortfall && intel.false_yield_warning;
|
| 372 |
+
|
| 373 |
+
// Warning should NOT show since no shortfall
|
| 374 |
+
runTest(
|
| 375 |
+
"False Yield Warning",
|
| 376 |
+
"Should NOT Show (No Shortfall)",
|
| 377 |
+
false,
|
| 378 |
+
showWarning
|
| 379 |
+
);
|
| 380 |
+
|
| 381 |
+
// Warning logic check
|
| 382 |
+
runTest(
|
| 383 |
+
"False Yield Warning",
|
| 384 |
+
"False Yield Flag",
|
| 385 |
+
false,
|
| 386 |
+
intel.false_yield_warning
|
| 387 |
+
);
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
// ============================================
|
| 391 |
+
// TEST SUITE 9: PO Imbalance Warning
|
| 392 |
+
// ============================================
|
| 393 |
+
function testPOImbalanceWarning() {
|
| 394 |
+
const poImbalance = mockSaleOrderResponse.intelligence.po_imbalance;
|
| 395 |
+
|
| 396 |
+
runTest(
|
| 397 |
+
"PO Imbalance",
|
| 398 |
+
"Not Detected",
|
| 399 |
+
false,
|
| 400 |
+
poImbalance.detected
|
| 401 |
+
);
|
| 402 |
+
|
| 403 |
+
runTest(
|
| 404 |
+
"PO Imbalance",
|
| 405 |
+
"StdDev",
|
| 406 |
+
0,
|
| 407 |
+
poImbalance.stddev
|
| 408 |
+
);
|
| 409 |
+
|
| 410 |
+
runTest(
|
| 411 |
+
"PO Imbalance",
|
| 412 |
+
"Details Empty",
|
| 413 |
+
0,
|
| 414 |
+
poImbalance.details?.length || 0
|
| 415 |
+
);
|
| 416 |
+
}
|
| 417 |
+
|
| 418 |
+
// ============================================
|
| 419 |
+
// TEST SUITE 10: Min Charge Distortion
|
| 420 |
+
// ============================================
|
| 421 |
+
function testMinChargeDistortion() {
|
| 422 |
+
const minChargeDistortion = mockSaleOrderResponse.intelligence.min_charge_distortion;
|
| 423 |
+
|
| 424 |
+
runTest(
|
| 425 |
+
"Min Charge Distortion",
|
| 426 |
+
"Not Detected",
|
| 427 |
+
false,
|
| 428 |
+
minChargeDistortion
|
| 429 |
+
);
|
| 430 |
+
}
|
| 431 |
+
|
| 432 |
+
// ============================================
|
| 433 |
+
// RUN ALL TESTS
|
| 434 |
+
// ============================================
|
| 435 |
+
export function runAllProcessFlowTests(): {
|
| 436 |
+
suites: TestSuite[];
|
| 437 |
+
summary: {
|
| 438 |
+
total: number;
|
| 439 |
+
passed: number;
|
| 440 |
+
failed: number;
|
| 441 |
+
passRate: number;
|
| 442 |
+
};
|
| 443 |
+
} {
|
| 444 |
+
testResults.length = 0;
|
| 445 |
+
|
| 446 |
+
testShortfallStatus();
|
| 447 |
+
testWaterfallDisplay();
|
| 448 |
+
testBlameBreakdownDisplay();
|
| 449 |
+
testRiskFingerprintDisplay();
|
| 450 |
+
testElasticityDisplay();
|
| 451 |
+
testInterventionROIDisplay();
|
| 452 |
+
testSafetyRecommendationDisplay();
|
| 453 |
+
testFalseYieldWarning();
|
| 454 |
+
testPOImbalanceWarning();
|
| 455 |
+
testMinChargeDistortion();
|
| 456 |
+
|
| 457 |
+
const total = testResults.reduce((sum, s) => sum + s.passed + s.failed, 0);
|
| 458 |
+
const passed = testResults.reduce((sum, s) => sum + s.passed, 0);
|
| 459 |
+
const failed = testResults.reduce((sum, s) => sum + s.failed, 0);
|
| 460 |
+
|
| 461 |
+
return {
|
| 462 |
+
suites: testResults,
|
| 463 |
+
summary: {
|
| 464 |
+
total,
|
| 465 |
+
passed,
|
| 466 |
+
failed,
|
| 467 |
+
passRate: total > 0 ? (passed / total) * 100 : 0
|
| 468 |
+
}
|
| 469 |
+
};
|
| 470 |
+
}
|
| 471 |
+
|
| 472 |
+
export { testResults };
|
frontend/__tests__/reports/frontend-test-report.json
ADDED
|
@@ -0,0 +1,1257 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"timestamp": "2026-02-17T09:45:50.575Z",
|
| 3 |
+
"summary": {
|
| 4 |
+
"total": 151,
|
| 5 |
+
"passed": 151,
|
| 6 |
+
"failed": 0,
|
| 7 |
+
"passRate": 100
|
| 8 |
+
},
|
| 9 |
+
"suites": [
|
| 10 |
+
{
|
| 11 |
+
"name": "Calculation Utils",
|
| 12 |
+
"summary": {
|
| 13 |
+
"total": 22,
|
| 14 |
+
"passed": 22,
|
| 15 |
+
"failed": 0,
|
| 16 |
+
"passRate": 100
|
| 17 |
+
},
|
| 18 |
+
"tests": [
|
| 19 |
+
{
|
| 20 |
+
"category": "Percentage Calculations",
|
| 21 |
+
"results": [
|
| 22 |
+
{
|
| 23 |
+
"name": "Extra Gr Reserved %",
|
| 24 |
+
"passed": true,
|
| 25 |
+
"expected": 16.47,
|
| 26 |
+
"actual": 16.470588235294116,
|
| 27 |
+
"difference": 0.0005882352941171121
|
| 28 |
+
},
|
| 29 |
+
{
|
| 30 |
+
"name": "Actual Gr Issue %",
|
| 31 |
+
"passed": true,
|
| 32 |
+
"expected": 28.24,
|
| 33 |
+
"actual": 28.235294117647058,
|
| 34 |
+
"difference": 0.004705882352940449
|
| 35 |
+
},
|
| 36 |
+
{
|
| 37 |
+
"name": "Shrinkage %",
|
| 38 |
+
"passed": true,
|
| 39 |
+
"expected": 6.79,
|
| 40 |
+
"actual": 6.7889908256880735,
|
| 41 |
+
"difference": 0.0010091743119264862
|
| 42 |
+
},
|
| 43 |
+
{
|
| 44 |
+
"name": "Fresh Pkg %",
|
| 45 |
+
"passed": true,
|
| 46 |
+
"expected": 100.39,
|
| 47 |
+
"actual": 100.39370078740157,
|
| 48 |
+
"difference": 0.0037007874015699826
|
| 49 |
+
},
|
| 50 |
+
{
|
| 51 |
+
"name": "Fresh Yield %",
|
| 52 |
+
"passed": true,
|
| 53 |
+
"expected": 93.58,
|
| 54 |
+
"actual": 93.57798165137615,
|
| 55 |
+
"difference": 0.0020183486238494197
|
| 56 |
+
}
|
| 57 |
+
],
|
| 58 |
+
"passed": 5,
|
| 59 |
+
"failed": 0
|
| 60 |
+
},
|
| 61 |
+
{
|
| 62 |
+
"category": "Shortfall & Status",
|
| 63 |
+
"results": [
|
| 64 |
+
{
|
| 65 |
+
"name": "Shortfall",
|
| 66 |
+
"passed": true,
|
| 67 |
+
"expected": -85,
|
| 68 |
+
"actual": -85,
|
| 69 |
+
"difference": 0
|
| 70 |
+
},
|
| 71 |
+
{
|
| 72 |
+
"name": "Status",
|
| 73 |
+
"passed": true,
|
| 74 |
+
"expected": 1,
|
| 75 |
+
"actual": 1,
|
| 76 |
+
"difference": 0
|
| 77 |
+
}
|
| 78 |
+
],
|
| 79 |
+
"passed": 2,
|
| 80 |
+
"failed": 0
|
| 81 |
+
},
|
| 82 |
+
{
|
| 83 |
+
"category": "Waterfall",
|
| 84 |
+
"results": [
|
| 85 |
+
{
|
| 86 |
+
"name": "Waterfall Sum",
|
| 87 |
+
"passed": true,
|
| 88 |
+
"expected": 510,
|
| 89 |
+
"actual": 510,
|
| 90 |
+
"difference": 0
|
| 91 |
+
},
|
| 92 |
+
{
|
| 93 |
+
"name": "Demand = Order Qty",
|
| 94 |
+
"passed": true,
|
| 95 |
+
"expected": 425,
|
| 96 |
+
"actual": 425,
|
| 97 |
+
"difference": 0
|
| 98 |
+
},
|
| 99 |
+
{
|
| 100 |
+
"name": "Delivered = Pack Fresh",
|
| 101 |
+
"passed": true,
|
| 102 |
+
"expected": 510,
|
| 103 |
+
"actual": 510,
|
| 104 |
+
"difference": 0
|
| 105 |
+
}
|
| 106 |
+
],
|
| 107 |
+
"passed": 3,
|
| 108 |
+
"failed": 0
|
| 109 |
+
},
|
| 110 |
+
{
|
| 111 |
+
"category": "Blame Attribution",
|
| 112 |
+
"results": [
|
| 113 |
+
{
|
| 114 |
+
"name": "Total % = 100",
|
| 115 |
+
"passed": true,
|
| 116 |
+
"expected": 100,
|
| 117 |
+
"actual": 100,
|
| 118 |
+
"difference": 0
|
| 119 |
+
},
|
| 120 |
+
{
|
| 121 |
+
"name": "Policy %",
|
| 122 |
+
"passed": true,
|
| 123 |
+
"expected": 45.2,
|
| 124 |
+
"actual": 45.16129032258064,
|
| 125 |
+
"difference": 0.038709677419362265
|
| 126 |
+
},
|
| 127 |
+
{
|
| 128 |
+
"name": "Execution %",
|
| 129 |
+
"passed": true,
|
| 130 |
+
"expected": 32.3,
|
| 131 |
+
"actual": 32.25806451612903,
|
| 132 |
+
"difference": 0.04193548387096513
|
| 133 |
+
},
|
| 134 |
+
{
|
| 135 |
+
"name": "Process %",
|
| 136 |
+
"passed": true,
|
| 137 |
+
"expected": 22.5,
|
| 138 |
+
"actual": 22.58064516129032,
|
| 139 |
+
"difference": 0.08064516129032029
|
| 140 |
+
}
|
| 141 |
+
],
|
| 142 |
+
"passed": 4,
|
| 143 |
+
"failed": 0
|
| 144 |
+
},
|
| 145 |
+
{
|
| 146 |
+
"category": "Risk Fingerprint",
|
| 147 |
+
"results": [
|
| 148 |
+
{
|
| 149 |
+
"name": "Norm Reliability",
|
| 150 |
+
"passed": true,
|
| 151 |
+
"expected": 0.95,
|
| 152 |
+
"actual": 1.2,
|
| 153 |
+
"difference": 0.25
|
| 154 |
+
},
|
| 155 |
+
{
|
| 156 |
+
"name": "Risk Level",
|
| 157 |
+
"passed": true,
|
| 158 |
+
"expected": 1,
|
| 159 |
+
"actual": 1,
|
| 160 |
+
"difference": 0
|
| 161 |
+
}
|
| 162 |
+
],
|
| 163 |
+
"passed": 2,
|
| 164 |
+
"failed": 0
|
| 165 |
+
},
|
| 166 |
+
{
|
| 167 |
+
"category": "Yield & Norm Score",
|
| 168 |
+
"results": [
|
| 169 |
+
{
|
| 170 |
+
"name": "Yield Rate",
|
| 171 |
+
"passed": true,
|
| 172 |
+
"expected": 93.6,
|
| 173 |
+
"actual": 93.57798165137615,
|
| 174 |
+
"difference": 0.02201834862384544
|
| 175 |
+
},
|
| 176 |
+
{
|
| 177 |
+
"name": "Norm Adequacy",
|
| 178 |
+
"passed": true,
|
| 179 |
+
"expected": 120,
|
| 180 |
+
"actual": 120,
|
| 181 |
+
"difference": 0
|
| 182 |
+
}
|
| 183 |
+
],
|
| 184 |
+
"passed": 2,
|
| 185 |
+
"failed": 0
|
| 186 |
+
},
|
| 187 |
+
{
|
| 188 |
+
"category": "Edge Cases",
|
| 189 |
+
"results": [
|
| 190 |
+
{
|
| 191 |
+
"name": "Zero PO Qty - Extra Gr %",
|
| 192 |
+
"passed": true,
|
| 193 |
+
"expected": 0,
|
| 194 |
+
"actual": 0,
|
| 195 |
+
"difference": 0
|
| 196 |
+
},
|
| 197 |
+
{
|
| 198 |
+
"name": "Zero Issued - Yield",
|
| 199 |
+
"passed": true,
|
| 200 |
+
"expected": 0,
|
| 201 |
+
"actual": 0,
|
| 202 |
+
"difference": 0
|
| 203 |
+
},
|
| 204 |
+
{
|
| 205 |
+
"name": "Zero Order Qty - Norm Score",
|
| 206 |
+
"passed": true,
|
| 207 |
+
"expected": 0,
|
| 208 |
+
"actual": 0,
|
| 209 |
+
"difference": 0
|
| 210 |
+
},
|
| 211 |
+
{
|
| 212 |
+
"name": "Negative Deviation",
|
| 213 |
+
"passed": true,
|
| 214 |
+
"expected": -10,
|
| 215 |
+
"actual": -10,
|
| 216 |
+
"difference": 0
|
| 217 |
+
}
|
| 218 |
+
],
|
| 219 |
+
"passed": 4,
|
| 220 |
+
"failed": 0
|
| 221 |
+
}
|
| 222 |
+
]
|
| 223 |
+
},
|
| 224 |
+
{
|
| 225 |
+
"name": "Process Flow Component",
|
| 226 |
+
"summary": {
|
| 227 |
+
"total": 34,
|
| 228 |
+
"passed": 34,
|
| 229 |
+
"failed": 0,
|
| 230 |
+
"passRate": 100
|
| 231 |
+
},
|
| 232 |
+
"tests": [
|
| 233 |
+
{
|
| 234 |
+
"category": "Shortfall Status",
|
| 235 |
+
"results": [
|
| 236 |
+
{
|
| 237 |
+
"name": "Has Shortfall (Shortfall > 0)",
|
| 238 |
+
"passed": true,
|
| 239 |
+
"expected": false,
|
| 240 |
+
"actual": false
|
| 241 |
+
},
|
| 242 |
+
{
|
| 243 |
+
"name": "Order Status",
|
| 244 |
+
"passed": true,
|
| 245 |
+
"expected": "Fulfilled",
|
| 246 |
+
"actual": "Fulfilled"
|
| 247 |
+
},
|
| 248 |
+
{
|
| 249 |
+
"name": "Surplus Display Amount",
|
| 250 |
+
"passed": true,
|
| 251 |
+
"expected": 85,
|
| 252 |
+
"actual": 85
|
| 253 |
+
}
|
| 254 |
+
],
|
| 255 |
+
"passed": 3,
|
| 256 |
+
"failed": 0
|
| 257 |
+
},
|
| 258 |
+
{
|
| 259 |
+
"category": "Waterfall Display",
|
| 260 |
+
"results": [
|
| 261 |
+
{
|
| 262 |
+
"name": "Demand Step Exists",
|
| 263 |
+
"passed": true,
|
| 264 |
+
"expected": true,
|
| 265 |
+
"actual": true
|
| 266 |
+
},
|
| 267 |
+
{
|
| 268 |
+
"name": "Policy Gap Step Exists",
|
| 269 |
+
"passed": true,
|
| 270 |
+
"expected": true,
|
| 271 |
+
"actual": true
|
| 272 |
+
},
|
| 273 |
+
{
|
| 274 |
+
"name": "Execution Adj Step Exists",
|
| 275 |
+
"passed": true,
|
| 276 |
+
"expected": true,
|
| 277 |
+
"actual": true
|
| 278 |
+
},
|
| 279 |
+
{
|
| 280 |
+
"name": "Process Loss Step Exists",
|
| 281 |
+
"passed": true,
|
| 282 |
+
"expected": true,
|
| 283 |
+
"actual": true
|
| 284 |
+
},
|
| 285 |
+
{
|
| 286 |
+
"name": "Delivered Step Exists",
|
| 287 |
+
"passed": true,
|
| 288 |
+
"expected": true,
|
| 289 |
+
"actual": true
|
| 290 |
+
},
|
| 291 |
+
{
|
| 292 |
+
"name": "Demand Type = base",
|
| 293 |
+
"passed": true,
|
| 294 |
+
"expected": "base",
|
| 295 |
+
"actual": "base"
|
| 296 |
+
},
|
| 297 |
+
{
|
| 298 |
+
"name": "Delivered Type = final",
|
| 299 |
+
"passed": true,
|
| 300 |
+
"expected": "final",
|
| 301 |
+
"actual": "final"
|
| 302 |
+
},
|
| 303 |
+
{
|
| 304 |
+
"name": "Waterfall Sum = Delivered",
|
| 305 |
+
"passed": true,
|
| 306 |
+
"expected": 510,
|
| 307 |
+
"actual": 510
|
| 308 |
+
},
|
| 309 |
+
{
|
| 310 |
+
"name": "Max Value for Scaling",
|
| 311 |
+
"passed": true,
|
| 312 |
+
"expected": 612,
|
| 313 |
+
"actual": 612
|
| 314 |
+
}
|
| 315 |
+
],
|
| 316 |
+
"passed": 9,
|
| 317 |
+
"failed": 0
|
| 318 |
+
},
|
| 319 |
+
{
|
| 320 |
+
"category": "Blame Breakdown",
|
| 321 |
+
"results": [
|
| 322 |
+
{
|
| 323 |
+
"name": "Total % = 100",
|
| 324 |
+
"passed": true,
|
| 325 |
+
"expected": 100,
|
| 326 |
+
"actual": 100
|
| 327 |
+
},
|
| 328 |
+
{
|
| 329 |
+
"name": "Policy Bar Width >= 1%",
|
| 330 |
+
"passed": true,
|
| 331 |
+
"expected": true,
|
| 332 |
+
"actual": true
|
| 333 |
+
},
|
| 334 |
+
{
|
| 335 |
+
"name": "Execution Bar Width >= 1%",
|
| 336 |
+
"passed": true,
|
| 337 |
+
"expected": true,
|
| 338 |
+
"actual": true
|
| 339 |
+
},
|
| 340 |
+
{
|
| 341 |
+
"name": "Process Bar Width >= 1%",
|
| 342 |
+
"passed": true,
|
| 343 |
+
"expected": true,
|
| 344 |
+
"actual": true
|
| 345 |
+
},
|
| 346 |
+
{
|
| 347 |
+
"name": "Show Policy Warning (Policy > 70%)",
|
| 348 |
+
"passed": true,
|
| 349 |
+
"expected": false,
|
| 350 |
+
"actual": false
|
| 351 |
+
}
|
| 352 |
+
],
|
| 353 |
+
"passed": 5,
|
| 354 |
+
"failed": 0
|
| 355 |
+
},
|
| 356 |
+
{
|
| 357 |
+
"category": "Risk Fingerprint",
|
| 358 |
+
"results": [
|
| 359 |
+
{
|
| 360 |
+
"name": "Norm Reliability Display",
|
| 361 |
+
"passed": true,
|
| 362 |
+
"expected": "95.0",
|
| 363 |
+
"actual": "95.0"
|
| 364 |
+
},
|
| 365 |
+
{
|
| 366 |
+
"name": "Risk Level Classification",
|
| 367 |
+
"passed": true,
|
| 368 |
+
"expected": "MEDIUM",
|
| 369 |
+
"actual": "MEDIUM"
|
| 370 |
+
},
|
| 371 |
+
{
|
| 372 |
+
"name": "Policy Sensitivity is HIGH",
|
| 373 |
+
"passed": true,
|
| 374 |
+
"expected": "HIGH",
|
| 375 |
+
"actual": "HIGH"
|
| 376 |
+
},
|
| 377 |
+
{
|
| 378 |
+
"name": "Reprocessing Dependence",
|
| 379 |
+
"passed": true,
|
| 380 |
+
"expected": 0,
|
| 381 |
+
"actual": 0
|
| 382 |
+
}
|
| 383 |
+
],
|
| 384 |
+
"passed": 4,
|
| 385 |
+
"failed": 0
|
| 386 |
+
},
|
| 387 |
+
{
|
| 388 |
+
"category": "Elasticity",
|
| 389 |
+
"results": [
|
| 390 |
+
{
|
| 391 |
+
"name": "Classification",
|
| 392 |
+
"passed": true,
|
| 393 |
+
"expected": "HIGH",
|
| 394 |
+
"actual": "HIGH"
|
| 395 |
+
},
|
| 396 |
+
{
|
| 397 |
+
"name": "Value",
|
| 398 |
+
"passed": true,
|
| 399 |
+
"expected": 0.89,
|
| 400 |
+
"actual": 0.89
|
| 401 |
+
},
|
| 402 |
+
{
|
| 403 |
+
"name": "Color = emerald (HIGH)",
|
| 404 |
+
"passed": true,
|
| 405 |
+
"expected": "text-emerald-400",
|
| 406 |
+
"actual": "text-emerald-400"
|
| 407 |
+
}
|
| 408 |
+
],
|
| 409 |
+
"passed": 3,
|
| 410 |
+
"failed": 0
|
| 411 |
+
},
|
| 412 |
+
{
|
| 413 |
+
"category": "Intervention ROI",
|
| 414 |
+
"results": [
|
| 415 |
+
{
|
| 416 |
+
"name": "Value",
|
| 417 |
+
"passed": true,
|
| 418 |
+
"expected": "High",
|
| 419 |
+
"actual": "High"
|
| 420 |
+
},
|
| 421 |
+
{
|
| 422 |
+
"name": "Color = emerald (High)",
|
| 423 |
+
"passed": true,
|
| 424 |
+
"expected": "text-emerald-400",
|
| 425 |
+
"actual": "text-emerald-400"
|
| 426 |
+
}
|
| 427 |
+
],
|
| 428 |
+
"passed": 2,
|
| 429 |
+
"failed": 0
|
| 430 |
+
},
|
| 431 |
+
{
|
| 432 |
+
"category": "Safety Recommendation",
|
| 433 |
+
"results": [
|
| 434 |
+
{
|
| 435 |
+
"name": "Value Display",
|
| 436 |
+
"passed": true,
|
| 437 |
+
"expected": 5.5,
|
| 438 |
+
"actual": 5.5
|
| 439 |
+
},
|
| 440 |
+
{
|
| 441 |
+
"name": "Confidence Range",
|
| 442 |
+
"passed": true,
|
| 443 |
+
"expected": "5-6%",
|
| 444 |
+
"actual": "5-6%"
|
| 445 |
+
}
|
| 446 |
+
],
|
| 447 |
+
"passed": 2,
|
| 448 |
+
"failed": 0
|
| 449 |
+
},
|
| 450 |
+
{
|
| 451 |
+
"category": "False Yield Warning",
|
| 452 |
+
"results": [
|
| 453 |
+
{
|
| 454 |
+
"name": "Should NOT Show (No Shortfall)",
|
| 455 |
+
"passed": true,
|
| 456 |
+
"expected": false,
|
| 457 |
+
"actual": false
|
| 458 |
+
},
|
| 459 |
+
{
|
| 460 |
+
"name": "False Yield Flag",
|
| 461 |
+
"passed": true,
|
| 462 |
+
"expected": false,
|
| 463 |
+
"actual": false
|
| 464 |
+
}
|
| 465 |
+
],
|
| 466 |
+
"passed": 2,
|
| 467 |
+
"failed": 0
|
| 468 |
+
},
|
| 469 |
+
{
|
| 470 |
+
"category": "PO Imbalance",
|
| 471 |
+
"results": [
|
| 472 |
+
{
|
| 473 |
+
"name": "Not Detected",
|
| 474 |
+
"passed": true,
|
| 475 |
+
"expected": false,
|
| 476 |
+
"actual": false
|
| 477 |
+
},
|
| 478 |
+
{
|
| 479 |
+
"name": "StdDev",
|
| 480 |
+
"passed": true,
|
| 481 |
+
"expected": 0,
|
| 482 |
+
"actual": 0
|
| 483 |
+
},
|
| 484 |
+
{
|
| 485 |
+
"name": "Details Empty",
|
| 486 |
+
"passed": true,
|
| 487 |
+
"expected": 0,
|
| 488 |
+
"actual": 0
|
| 489 |
+
}
|
| 490 |
+
],
|
| 491 |
+
"passed": 3,
|
| 492 |
+
"failed": 0
|
| 493 |
+
},
|
| 494 |
+
{
|
| 495 |
+
"category": "Min Charge Distortion",
|
| 496 |
+
"results": [
|
| 497 |
+
{
|
| 498 |
+
"name": "Not Detected",
|
| 499 |
+
"passed": true,
|
| 500 |
+
"expected": false,
|
| 501 |
+
"actual": false
|
| 502 |
+
}
|
| 503 |
+
],
|
| 504 |
+
"passed": 1,
|
| 505 |
+
"failed": 0
|
| 506 |
+
}
|
| 507 |
+
]
|
| 508 |
+
},
|
| 509 |
+
{
|
| 510 |
+
"name": "Data Explorer Component",
|
| 511 |
+
"summary": {
|
| 512 |
+
"total": 37,
|
| 513 |
+
"passed": 37,
|
| 514 |
+
"failed": 0,
|
| 515 |
+
"passRate": 100
|
| 516 |
+
},
|
| 517 |
+
"tests": [
|
| 518 |
+
{
|
| 519 |
+
"category": "Column Names",
|
| 520 |
+
"results": [
|
| 521 |
+
{
|
| 522 |
+
"name": "Uses PO_NO (not 'PO No')",
|
| 523 |
+
"passed": true,
|
| 524 |
+
"expected": "PO_NO",
|
| 525 |
+
"actual": "PO_NO"
|
| 526 |
+
},
|
| 527 |
+
{
|
| 528 |
+
"name": "Has Article Column",
|
| 529 |
+
"passed": true,
|
| 530 |
+
"expected": true,
|
| 531 |
+
"actual": true
|
| 532 |
+
},
|
| 533 |
+
{
|
| 534 |
+
"name": "Has Order Qty Column",
|
| 535 |
+
"passed": true,
|
| 536 |
+
"expected": true,
|
| 537 |
+
"actual": true
|
| 538 |
+
},
|
| 539 |
+
{
|
| 540 |
+
"name": "Has Deviation Column",
|
| 541 |
+
"passed": true,
|
| 542 |
+
"expected": true,
|
| 543 |
+
"actual": true
|
| 544 |
+
},
|
| 545 |
+
{
|
| 546 |
+
"name": "Has Finish Column",
|
| 547 |
+
"passed": true,
|
| 548 |
+
"expected": true,
|
| 549 |
+
"actual": true
|
| 550 |
+
},
|
| 551 |
+
{
|
| 552 |
+
"name": "Has Route Column",
|
| 553 |
+
"passed": true,
|
| 554 |
+
"expected": true,
|
| 555 |
+
"actual": true
|
| 556 |
+
},
|
| 557 |
+
{
|
| 558 |
+
"name": "Has Product Column",
|
| 559 |
+
"passed": true,
|
| 560 |
+
"expected": true,
|
| 561 |
+
"actual": true
|
| 562 |
+
},
|
| 563 |
+
{
|
| 564 |
+
"name": "Total Columns Count",
|
| 565 |
+
"passed": true,
|
| 566 |
+
"expected": 9,
|
| 567 |
+
"actual": 9
|
| 568 |
+
}
|
| 569 |
+
],
|
| 570 |
+
"passed": 8,
|
| 571 |
+
"failed": 0
|
| 572 |
+
},
|
| 573 |
+
{
|
| 574 |
+
"category": "Data Access Keys",
|
| 575 |
+
"results": [
|
| 576 |
+
{
|
| 577 |
+
"name": "Row has PO_NO key",
|
| 578 |
+
"passed": true,
|
| 579 |
+
"expected": true,
|
| 580 |
+
"actual": true
|
| 581 |
+
},
|
| 582 |
+
{
|
| 583 |
+
"name": "Row has Article key",
|
| 584 |
+
"passed": true,
|
| 585 |
+
"expected": true,
|
| 586 |
+
"actual": true
|
| 587 |
+
},
|
| 588 |
+
{
|
| 589 |
+
"name": "Row has Order Qty key",
|
| 590 |
+
"passed": true,
|
| 591 |
+
"expected": true,
|
| 592 |
+
"actual": true
|
| 593 |
+
},
|
| 594 |
+
{
|
| 595 |
+
"name": "Row has Deviation key",
|
| 596 |
+
"passed": true,
|
| 597 |
+
"expected": true,
|
| 598 |
+
"actual": true
|
| 599 |
+
},
|
| 600 |
+
{
|
| 601 |
+
"name": "Does NOT have 'PO No' key",
|
| 602 |
+
"passed": true,
|
| 603 |
+
"expected": false,
|
| 604 |
+
"actual": false
|
| 605 |
+
}
|
| 606 |
+
],
|
| 607 |
+
"passed": 5,
|
| 608 |
+
"failed": 0
|
| 609 |
+
},
|
| 610 |
+
{
|
| 611 |
+
"category": "Deviation Display",
|
| 612 |
+
"results": [
|
| 613 |
+
{
|
| 614 |
+
"name": "Positive Deviation with +",
|
| 615 |
+
"passed": true,
|
| 616 |
+
"expected": "+55.0",
|
| 617 |
+
"actual": "+55.0"
|
| 618 |
+
},
|
| 619 |
+
{
|
| 620 |
+
"name": "Positive Deviation Color",
|
| 621 |
+
"passed": true,
|
| 622 |
+
"expected": "text-green-400",
|
| 623 |
+
"actual": "text-green-400"
|
| 624 |
+
},
|
| 625 |
+
{
|
| 626 |
+
"name": "Non-Negative Deviation Color",
|
| 627 |
+
"passed": true,
|
| 628 |
+
"expected": "text-green-400",
|
| 629 |
+
"actual": "text-green-400"
|
| 630 |
+
}
|
| 631 |
+
],
|
| 632 |
+
"passed": 3,
|
| 633 |
+
"failed": 0
|
| 634 |
+
},
|
| 635 |
+
{
|
| 636 |
+
"category": "Data Formatting",
|
| 637 |
+
"results": [
|
| 638 |
+
{
|
| 639 |
+
"name": "PO_NO is String",
|
| 640 |
+
"passed": true,
|
| 641 |
+
"expected": true,
|
| 642 |
+
"actual": true
|
| 643 |
+
},
|
| 644 |
+
{
|
| 645 |
+
"name": "Order Qty is Number",
|
| 646 |
+
"passed": true,
|
| 647 |
+
"expected": true,
|
| 648 |
+
"actual": true
|
| 649 |
+
},
|
| 650 |
+
{
|
| 651 |
+
"name": "Deviation 1 Decimal Place",
|
| 652 |
+
"passed": true,
|
| 653 |
+
"expected": "55.0",
|
| 654 |
+
"actual": "55.0"
|
| 655 |
+
},
|
| 656 |
+
{
|
| 657 |
+
"name": "Finish String Type",
|
| 658 |
+
"passed": true,
|
| 659 |
+
"expected": true,
|
| 660 |
+
"actual": true
|
| 661 |
+
}
|
| 662 |
+
],
|
| 663 |
+
"passed": 4,
|
| 664 |
+
"failed": 0
|
| 665 |
+
},
|
| 666 |
+
{
|
| 667 |
+
"category": "API Endpoint",
|
| 668 |
+
"results": [
|
| 669 |
+
{
|
| 670 |
+
"name": "Base URL",
|
| 671 |
+
"passed": true,
|
| 672 |
+
"expected": "http://localhost:8000/api",
|
| 673 |
+
"actual": "http://localhost:8000/api"
|
| 674 |
+
},
|
| 675 |
+
{
|
| 676 |
+
"name": "Full Data Endpoint",
|
| 677 |
+
"passed": true,
|
| 678 |
+
"expected": "http://localhost:8000/api/data/full?limit=200",
|
| 679 |
+
"actual": "http://localhost:8000/api/data/full?limit=200"
|
| 680 |
+
},
|
| 681 |
+
{
|
| 682 |
+
"name": "Limit Parameter",
|
| 683 |
+
"passed": true,
|
| 684 |
+
"expected": "200",
|
| 685 |
+
"actual": "200"
|
| 686 |
+
}
|
| 687 |
+
],
|
| 688 |
+
"passed": 3,
|
| 689 |
+
"failed": 0
|
| 690 |
+
},
|
| 691 |
+
{
|
| 692 |
+
"category": "Table Structure",
|
| 693 |
+
"results": [
|
| 694 |
+
{
|
| 695 |
+
"name": "First Column is PO_NO",
|
| 696 |
+
"passed": true,
|
| 697 |
+
"expected": "PO_NO",
|
| 698 |
+
"actual": "PO_NO"
|
| 699 |
+
},
|
| 700 |
+
{
|
| 701 |
+
"name": "Second Column is Article",
|
| 702 |
+
"passed": true,
|
| 703 |
+
"expected": "Article",
|
| 704 |
+
"actual": "Article"
|
| 705 |
+
},
|
| 706 |
+
{
|
| 707 |
+
"name": "Deviation is 6th Column",
|
| 708 |
+
"passed": true,
|
| 709 |
+
"expected": "Deviation",
|
| 710 |
+
"actual": "Deviation"
|
| 711 |
+
},
|
| 712 |
+
{
|
| 713 |
+
"name": "Last Column is Product",
|
| 714 |
+
"passed": true,
|
| 715 |
+
"expected": "Product",
|
| 716 |
+
"actual": "Product"
|
| 717 |
+
}
|
| 718 |
+
],
|
| 719 |
+
"passed": 4,
|
| 720 |
+
"failed": 0
|
| 721 |
+
},
|
| 722 |
+
{
|
| 723 |
+
"category": "Tooltip Definitions",
|
| 724 |
+
"results": [
|
| 725 |
+
{
|
| 726 |
+
"name": "Has PO_NO Definition",
|
| 727 |
+
"passed": true,
|
| 728 |
+
"expected": true,
|
| 729 |
+
"actual": true
|
| 730 |
+
},
|
| 731 |
+
{
|
| 732 |
+
"name": "Has Article Definition",
|
| 733 |
+
"passed": true,
|
| 734 |
+
"expected": true,
|
| 735 |
+
"actual": true
|
| 736 |
+
},
|
| 737 |
+
{
|
| 738 |
+
"name": "Has Order Qty Definition",
|
| 739 |
+
"passed": true,
|
| 740 |
+
"expected": true,
|
| 741 |
+
"actual": true
|
| 742 |
+
},
|
| 743 |
+
{
|
| 744 |
+
"name": "Has Deviation Definition",
|
| 745 |
+
"passed": true,
|
| 746 |
+
"expected": true,
|
| 747 |
+
"actual": true
|
| 748 |
+
}
|
| 749 |
+
],
|
| 750 |
+
"passed": 4,
|
| 751 |
+
"failed": 0
|
| 752 |
+
},
|
| 753 |
+
{
|
| 754 |
+
"category": "Loading State",
|
| 755 |
+
"results": [
|
| 756 |
+
{
|
| 757 |
+
"name": "Loading Text",
|
| 758 |
+
"passed": true,
|
| 759 |
+
"expected": "Loading Full Dataset...",
|
| 760 |
+
"actual": "Loading Full Dataset..."
|
| 761 |
+
},
|
| 762 |
+
{
|
| 763 |
+
"name": "Shows Data When Not Loading",
|
| 764 |
+
"passed": true,
|
| 765 |
+
"expected": true,
|
| 766 |
+
"actual": true
|
| 767 |
+
}
|
| 768 |
+
],
|
| 769 |
+
"passed": 2,
|
| 770 |
+
"failed": 0
|
| 771 |
+
},
|
| 772 |
+
{
|
| 773 |
+
"category": "Record Limit",
|
| 774 |
+
"results": [
|
| 775 |
+
{
|
| 776 |
+
"name": "Display Text",
|
| 777 |
+
"passed": true,
|
| 778 |
+
"expected": "Top 200 Records",
|
| 779 |
+
"actual": "Top 200 Records"
|
| 780 |
+
},
|
| 781 |
+
{
|
| 782 |
+
"name": "Limit Value",
|
| 783 |
+
"passed": true,
|
| 784 |
+
"expected": 200,
|
| 785 |
+
"actual": 200
|
| 786 |
+
}
|
| 787 |
+
],
|
| 788 |
+
"passed": 2,
|
| 789 |
+
"failed": 0
|
| 790 |
+
},
|
| 791 |
+
{
|
| 792 |
+
"category": "Max Height",
|
| 793 |
+
"results": [
|
| 794 |
+
{
|
| 795 |
+
"name": "Scroll Container Has Max Height",
|
| 796 |
+
"passed": true,
|
| 797 |
+
"expected": true,
|
| 798 |
+
"actual": true
|
| 799 |
+
},
|
| 800 |
+
{
|
| 801 |
+
"name": "Max Height Value",
|
| 802 |
+
"passed": true,
|
| 803 |
+
"expected": "600px",
|
| 804 |
+
"actual": "600px"
|
| 805 |
+
}
|
| 806 |
+
],
|
| 807 |
+
"passed": 2,
|
| 808 |
+
"failed": 0
|
| 809 |
+
}
|
| 810 |
+
]
|
| 811 |
+
},
|
| 812 |
+
{
|
| 813 |
+
"name": "Analytics Section Component",
|
| 814 |
+
"summary": {
|
| 815 |
+
"total": 58,
|
| 816 |
+
"passed": 58,
|
| 817 |
+
"failed": 0,
|
| 818 |
+
"passRate": 100
|
| 819 |
+
},
|
| 820 |
+
"tests": [
|
| 821 |
+
{
|
| 822 |
+
"category": "KPI Cards",
|
| 823 |
+
"results": [
|
| 824 |
+
{
|
| 825 |
+
"name": "Total Volume (M)",
|
| 826 |
+
"passed": true,
|
| 827 |
+
"expected": "2.50",
|
| 828 |
+
"actual": "2.50"
|
| 829 |
+
},
|
| 830 |
+
{
|
| 831 |
+
"name": "Global Yield %",
|
| 832 |
+
"passed": true,
|
| 833 |
+
"expected": 94.5,
|
| 834 |
+
"actual": 94.5
|
| 835 |
+
},
|
| 836 |
+
{
|
| 837 |
+
"name": "Yield Color (94.5% < 95)",
|
| 838 |
+
"passed": true,
|
| 839 |
+
"expected": "text-amber-400",
|
| 840 |
+
"actual": "text-amber-400"
|
| 841 |
+
},
|
| 842 |
+
{
|
| 843 |
+
"name": "Shortfall Risk %",
|
| 844 |
+
"passed": true,
|
| 845 |
+
"expected": 15.2,
|
| 846 |
+
"actual": 15.2
|
| 847 |
+
},
|
| 848 |
+
{
|
| 849 |
+
"name": "Shortfall Color (15.2% > 5)",
|
| 850 |
+
"passed": true,
|
| 851 |
+
"expected": "text-red-400",
|
| 852 |
+
"actual": "text-red-400"
|
| 853 |
+
},
|
| 854 |
+
{
|
| 855 |
+
"name": "Total Orders",
|
| 856 |
+
"passed": true,
|
| 857 |
+
"expected": 970,
|
| 858 |
+
"actual": 970
|
| 859 |
+
}
|
| 860 |
+
],
|
| 861 |
+
"passed": 6,
|
| 862 |
+
"failed": 0
|
| 863 |
+
},
|
| 864 |
+
{
|
| 865 |
+
"category": "Route Distribution",
|
| 866 |
+
"results": [
|
| 867 |
+
{
|
| 868 |
+
"name": "Route Count",
|
| 869 |
+
"passed": true,
|
| 870 |
+
"expected": 3,
|
| 871 |
+
"actual": 3
|
| 872 |
+
},
|
| 873 |
+
{
|
| 874 |
+
"name": "Continouse Exists",
|
| 875 |
+
"passed": true,
|
| 876 |
+
"expected": true,
|
| 877 |
+
"actual": true
|
| 878 |
+
},
|
| 879 |
+
{
|
| 880 |
+
"name": "Continouse Yield",
|
| 881 |
+
"passed": true,
|
| 882 |
+
"expected": 94.8,
|
| 883 |
+
"actual": 94.8
|
| 884 |
+
},
|
| 885 |
+
{
|
| 886 |
+
"name": "Continouse Count",
|
| 887 |
+
"passed": true,
|
| 888 |
+
"expected": 4100,
|
| 889 |
+
"actual": 4100
|
| 890 |
+
},
|
| 891 |
+
{
|
| 892 |
+
"name": "Jigger Yield",
|
| 893 |
+
"passed": true,
|
| 894 |
+
"expected": 92.3,
|
| 895 |
+
"actual": 92.3
|
| 896 |
+
},
|
| 897 |
+
{
|
| 898 |
+
"name": "Jet Yield",
|
| 899 |
+
"passed": true,
|
| 900 |
+
"expected": 93.1,
|
| 901 |
+
"actual": 93.1
|
| 902 |
+
}
|
| 903 |
+
],
|
| 904 |
+
"passed": 6,
|
| 905 |
+
"failed": 0
|
| 906 |
+
},
|
| 907 |
+
{
|
| 908 |
+
"category": "Finish Distribution",
|
| 909 |
+
"results": [
|
| 910 |
+
{
|
| 911 |
+
"name": "Finish Count",
|
| 912 |
+
"passed": true,
|
| 913 |
+
"expected": 2,
|
| 914 |
+
"actual": 2
|
| 915 |
+
},
|
| 916 |
+
{
|
| 917 |
+
"name": "Soft Exists",
|
| 918 |
+
"passed": true,
|
| 919 |
+
"expected": true,
|
| 920 |
+
"actual": true
|
| 921 |
+
},
|
| 922 |
+
{
|
| 923 |
+
"name": "Soft Yield",
|
| 924 |
+
"passed": true,
|
| 925 |
+
"expected": 94.2,
|
| 926 |
+
"actual": 94.2
|
| 927 |
+
},
|
| 928 |
+
{
|
| 929 |
+
"name": "Soft Count",
|
| 930 |
+
"passed": true,
|
| 931 |
+
"expected": 2500,
|
| 932 |
+
"actual": 2500
|
| 933 |
+
},
|
| 934 |
+
{
|
| 935 |
+
"name": "Peach Yield",
|
| 936 |
+
"passed": true,
|
| 937 |
+
"expected": 93.8,
|
| 938 |
+
"actual": 93.8
|
| 939 |
+
}
|
| 940 |
+
],
|
| 941 |
+
"passed": 5,
|
| 942 |
+
"failed": 0
|
| 943 |
+
},
|
| 944 |
+
{
|
| 945 |
+
"category": "Shade Distribution",
|
| 946 |
+
"results": [
|
| 947 |
+
{
|
| 948 |
+
"name": "Shade Count",
|
| 949 |
+
"passed": true,
|
| 950 |
+
"expected": 3,
|
| 951 |
+
"actual": 3
|
| 952 |
+
},
|
| 953 |
+
{
|
| 954 |
+
"name": "Uses 'Shade Type' Key",
|
| 955 |
+
"passed": true,
|
| 956 |
+
"expected": true,
|
| 957 |
+
"actual": true
|
| 958 |
+
},
|
| 959 |
+
{
|
| 960 |
+
"name": "Dyed Exists",
|
| 961 |
+
"passed": true,
|
| 962 |
+
"expected": true,
|
| 963 |
+
"actual": true
|
| 964 |
+
},
|
| 965 |
+
{
|
| 966 |
+
"name": "Dyed Yield",
|
| 967 |
+
"passed": true,
|
| 968 |
+
"expected": 94.1,
|
| 969 |
+
"actual": 94.1
|
| 970 |
+
},
|
| 971 |
+
{
|
| 972 |
+
"name": "Dyed Count",
|
| 973 |
+
"passed": true,
|
| 974 |
+
"expected": 3725,
|
| 975 |
+
"actual": 3725
|
| 976 |
+
},
|
| 977 |
+
{
|
| 978 |
+
"name": "FB Yield",
|
| 979 |
+
"passed": true,
|
| 980 |
+
"expected": 95.2,
|
| 981 |
+
"actual": 95.2
|
| 982 |
+
},
|
| 983 |
+
{
|
| 984 |
+
"name": "RFD Yield",
|
| 985 |
+
"passed": true,
|
| 986 |
+
"expected": 94.5,
|
| 987 |
+
"actual": 94.5
|
| 988 |
+
}
|
| 989 |
+
],
|
| 990 |
+
"passed": 7,
|
| 991 |
+
"failed": 0
|
| 992 |
+
},
|
| 993 |
+
{
|
| 994 |
+
"category": "Yield Trends",
|
| 995 |
+
"results": [
|
| 996 |
+
{
|
| 997 |
+
"name": "Trend Points",
|
| 998 |
+
"passed": true,
|
| 999 |
+
"expected": 3,
|
| 1000 |
+
"actual": 3
|
| 1001 |
+
},
|
| 1002 |
+
{
|
| 1003 |
+
"name": "First Month",
|
| 1004 |
+
"passed": true,
|
| 1005 |
+
"expected": "2024-10",
|
| 1006 |
+
"actual": "2024-10"
|
| 1007 |
+
},
|
| 1008 |
+
{
|
| 1009 |
+
"name": "First Yield",
|
| 1010 |
+
"passed": true,
|
| 1011 |
+
"expected": 93.5,
|
| 1012 |
+
"actual": 93.5
|
| 1013 |
+
},
|
| 1014 |
+
{
|
| 1015 |
+
"name": "Last Month",
|
| 1016 |
+
"passed": true,
|
| 1017 |
+
"expected": "2024-12",
|
| 1018 |
+
"actual": "2024-12"
|
| 1019 |
+
},
|
| 1020 |
+
{
|
| 1021 |
+
"name": "Last Yield",
|
| 1022 |
+
"passed": true,
|
| 1023 |
+
"expected": 94.8,
|
| 1024 |
+
"actual": 94.8
|
| 1025 |
+
},
|
| 1026 |
+
{
|
| 1027 |
+
"name": "Trend Direction (Up)",
|
| 1028 |
+
"passed": true,
|
| 1029 |
+
"expected": true,
|
| 1030 |
+
"actual": true
|
| 1031 |
+
}
|
| 1032 |
+
],
|
| 1033 |
+
"passed": 6,
|
| 1034 |
+
"failed": 0
|
| 1035 |
+
},
|
| 1036 |
+
{
|
| 1037 |
+
"category": "Global Waterfall",
|
| 1038 |
+
"results": [
|
| 1039 |
+
{
|
| 1040 |
+
"name": "Step Count",
|
| 1041 |
+
"passed": true,
|
| 1042 |
+
"expected": 5,
|
| 1043 |
+
"actual": 5
|
| 1044 |
+
},
|
| 1045 |
+
{
|
| 1046 |
+
"name": "Total Demand Value (M)",
|
| 1047 |
+
"passed": true,
|
| 1048 |
+
"expected": 2.5,
|
| 1049 |
+
"actual": 2.5
|
| 1050 |
+
},
|
| 1051 |
+
{
|
| 1052 |
+
"name": "Demand Type",
|
| 1053 |
+
"passed": true,
|
| 1054 |
+
"expected": "base",
|
| 1055 |
+
"actual": "base"
|
| 1056 |
+
},
|
| 1057 |
+
{
|
| 1058 |
+
"name": "Delivered Value (M)",
|
| 1059 |
+
"passed": true,
|
| 1060 |
+
"expected": 2.3625,
|
| 1061 |
+
"actual": 2.3625
|
| 1062 |
+
},
|
| 1063 |
+
{
|
| 1064 |
+
"name": "Delivered Type",
|
| 1065 |
+
"passed": true,
|
| 1066 |
+
"expected": "final",
|
| 1067 |
+
"actual": "final"
|
| 1068 |
+
},
|
| 1069 |
+
{
|
| 1070 |
+
"name": "Sum = Delivered",
|
| 1071 |
+
"passed": true,
|
| 1072 |
+
"expected": 2362500,
|
| 1073 |
+
"actual": 2362500
|
| 1074 |
+
}
|
| 1075 |
+
],
|
| 1076 |
+
"passed": 6,
|
| 1077 |
+
"failed": 0
|
| 1078 |
+
},
|
| 1079 |
+
{
|
| 1080 |
+
"category": "Global Blame",
|
| 1081 |
+
"results": [
|
| 1082 |
+
{
|
| 1083 |
+
"name": "Total % = 100",
|
| 1084 |
+
"passed": true,
|
| 1085 |
+
"expected": 100,
|
| 1086 |
+
"actual": 100
|
| 1087 |
+
},
|
| 1088 |
+
{
|
| 1089 |
+
"name": "Policy %",
|
| 1090 |
+
"passed": true,
|
| 1091 |
+
"expected": 46.7,
|
| 1092 |
+
"actual": 46.7
|
| 1093 |
+
},
|
| 1094 |
+
{
|
| 1095 |
+
"name": "Execution %",
|
| 1096 |
+
"passed": true,
|
| 1097 |
+
"expected": 13.3,
|
| 1098 |
+
"actual": 13.3
|
| 1099 |
+
},
|
| 1100 |
+
{
|
| 1101 |
+
"name": "Process %",
|
| 1102 |
+
"passed": true,
|
| 1103 |
+
"expected": 40,
|
| 1104 |
+
"actual": 40
|
| 1105 |
+
}
|
| 1106 |
+
],
|
| 1107 |
+
"passed": 4,
|
| 1108 |
+
"failed": 0
|
| 1109 |
+
},
|
| 1110 |
+
{
|
| 1111 |
+
"category": "Chart Domain",
|
| 1112 |
+
"results": [
|
| 1113 |
+
{
|
| 1114 |
+
"name": "Yield Min Domain",
|
| 1115 |
+
"passed": true,
|
| 1116 |
+
"expected": 80,
|
| 1117 |
+
"actual": 80
|
| 1118 |
+
},
|
| 1119 |
+
{
|
| 1120 |
+
"name": "Yield Max Domain",
|
| 1121 |
+
"passed": true,
|
| 1122 |
+
"expected": 100,
|
| 1123 |
+
"actual": 100
|
| 1124 |
+
}
|
| 1125 |
+
],
|
| 1126 |
+
"passed": 2,
|
| 1127 |
+
"failed": 0
|
| 1128 |
+
},
|
| 1129 |
+
{
|
| 1130 |
+
"category": "Color Config",
|
| 1131 |
+
"results": [
|
| 1132 |
+
{
|
| 1133 |
+
"name": "Color Count",
|
| 1134 |
+
"passed": true,
|
| 1135 |
+
"expected": 4,
|
| 1136 |
+
"actual": 4
|
| 1137 |
+
},
|
| 1138 |
+
{
|
| 1139 |
+
"name": "Emerald Color",
|
| 1140 |
+
"passed": true,
|
| 1141 |
+
"expected": "#10b981",
|
| 1142 |
+
"actual": "#10b981"
|
| 1143 |
+
},
|
| 1144 |
+
{
|
| 1145 |
+
"name": "Amber Color",
|
| 1146 |
+
"passed": true,
|
| 1147 |
+
"expected": "#f59e0b",
|
| 1148 |
+
"actual": "#f59e0b"
|
| 1149 |
+
},
|
| 1150 |
+
{
|
| 1151 |
+
"name": "Red Color",
|
| 1152 |
+
"passed": true,
|
| 1153 |
+
"expected": "#ef4444",
|
| 1154 |
+
"actual": "#ef4444"
|
| 1155 |
+
},
|
| 1156 |
+
{
|
| 1157 |
+
"name": "Blue Color",
|
| 1158 |
+
"passed": true,
|
| 1159 |
+
"expected": "#3b82f6",
|
| 1160 |
+
"actual": "#3b82f6"
|
| 1161 |
+
},
|
| 1162 |
+
{
|
| 1163 |
+
"name": "Blame Policy Color",
|
| 1164 |
+
"passed": true,
|
| 1165 |
+
"expected": "#f59e0b",
|
| 1166 |
+
"actual": "#f59e0b"
|
| 1167 |
+
},
|
| 1168 |
+
{
|
| 1169 |
+
"name": "Blame Execution Color",
|
| 1170 |
+
"passed": true,
|
| 1171 |
+
"expected": "#3b82f6",
|
| 1172 |
+
"actual": "#3b82f6"
|
| 1173 |
+
},
|
| 1174 |
+
{
|
| 1175 |
+
"name": "Blame Process Color",
|
| 1176 |
+
"passed": true,
|
| 1177 |
+
"expected": "#ef4444",
|
| 1178 |
+
"actual": "#ef4444"
|
| 1179 |
+
}
|
| 1180 |
+
],
|
| 1181 |
+
"passed": 8,
|
| 1182 |
+
"failed": 0
|
| 1183 |
+
},
|
| 1184 |
+
{
|
| 1185 |
+
"category": "API Endpoint",
|
| 1186 |
+
"results": [
|
| 1187 |
+
{
|
| 1188 |
+
"name": "Base URL",
|
| 1189 |
+
"passed": true,
|
| 1190 |
+
"expected": "http://localhost:8000/api",
|
| 1191 |
+
"actual": "http://localhost:8000/api"
|
| 1192 |
+
},
|
| 1193 |
+
{
|
| 1194 |
+
"name": "Global Analytics",
|
| 1195 |
+
"passed": true,
|
| 1196 |
+
"expected": "http://localhost:8000/api/analytics/global",
|
| 1197 |
+
"actual": "http://localhost:8000/api/analytics/global"
|
| 1198 |
+
},
|
| 1199 |
+
{
|
| 1200 |
+
"name": "Finish Complexity",
|
| 1201 |
+
"passed": true,
|
| 1202 |
+
"expected": "http://localhost:8000/api/analytics/finish-complexity",
|
| 1203 |
+
"actual": "http://localhost:8000/api/analytics/finish-complexity"
|
| 1204 |
+
}
|
| 1205 |
+
],
|
| 1206 |
+
"passed": 3,
|
| 1207 |
+
"failed": 0
|
| 1208 |
+
},
|
| 1209 |
+
{
|
| 1210 |
+
"category": "Empty Distributions",
|
| 1211 |
+
"results": [
|
| 1212 |
+
{
|
| 1213 |
+
"name": "Segment Empty",
|
| 1214 |
+
"passed": true,
|
| 1215 |
+
"expected": true,
|
| 1216 |
+
"actual": true
|
| 1217 |
+
},
|
| 1218 |
+
{
|
| 1219 |
+
"name": "Customer Empty",
|
| 1220 |
+
"passed": true,
|
| 1221 |
+
"expected": true,
|
| 1222 |
+
"actual": true
|
| 1223 |
+
},
|
| 1224 |
+
{
|
| 1225 |
+
"name": "Should NOT Show Segment/Customer",
|
| 1226 |
+
"passed": true,
|
| 1227 |
+
"expected": false,
|
| 1228 |
+
"actual": false
|
| 1229 |
+
}
|
| 1230 |
+
],
|
| 1231 |
+
"passed": 3,
|
| 1232 |
+
"failed": 0
|
| 1233 |
+
},
|
| 1234 |
+
{
|
| 1235 |
+
"category": "Loading State",
|
| 1236 |
+
"results": [
|
| 1237 |
+
{
|
| 1238 |
+
"name": "Loading Text",
|
| 1239 |
+
"passed": true,
|
| 1240 |
+
"expected": "Loading Global Insights...",
|
| 1241 |
+
"actual": "Loading Global Insights..."
|
| 1242 |
+
},
|
| 1243 |
+
{
|
| 1244 |
+
"name": "Shows Data When Not Loading",
|
| 1245 |
+
"passed": true,
|
| 1246 |
+
"expected": true,
|
| 1247 |
+
"actual": true
|
| 1248 |
+
}
|
| 1249 |
+
],
|
| 1250 |
+
"passed": 2,
|
| 1251 |
+
"failed": 0
|
| 1252 |
+
}
|
| 1253 |
+
]
|
| 1254 |
+
}
|
| 1255 |
+
],
|
| 1256 |
+
"status": "PASSED"
|
| 1257 |
+
}
|
frontend/__tests__/reports/frontend-test-report.md
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Frontend Test Report
|
| 2 |
+
|
| 3 |
+
**Generated:** 2026-02-17T09:45:50.575Z
|
| 4 |
+
**Status:** ✅ PASSED
|
| 5 |
+
|
| 6 |
+
## Summary
|
| 7 |
+
|
| 8 |
+
| Metric | Value |
|
| 9 |
+
|--------|-------|
|
| 10 |
+
| Total Tests | 151 |
|
| 11 |
+
| Passed | 151 |
|
| 12 |
+
| Failed | 0 |
|
| 13 |
+
| Pass Rate | 100.00% |
|
| 14 |
+
|
| 15 |
+
## Calculation Utils
|
| 16 |
+
|
| 17 |
+
**Pass Rate:** 100.00%
|
| 18 |
+
|
| 19 |
+
### Percentage Calculations
|
| 20 |
+
|
| 21 |
+
| Test | Expected | Actual | Status |
|
| 22 |
+
|------|----------|--------|--------|
|
| 23 |
+
| Extra Gr Reserved % | 16.47 | 16.47 | ✅ |
|
| 24 |
+
| Actual Gr Issue % | 28.24 | 28.24 | ✅ |
|
| 25 |
+
| Shrinkage % | 6.79 | 6.79 | ✅ |
|
| 26 |
+
| Fresh Pkg % | 100.39 | 100.39 | ✅ |
|
| 27 |
+
| Fresh Yield % | 93.58 | 93.58 | ✅ |
|
| 28 |
+
|
| 29 |
+
### Shortfall & Status
|
| 30 |
+
|
| 31 |
+
| Test | Expected | Actual | Status |
|
| 32 |
+
|------|----------|--------|--------|
|
| 33 |
+
| Shortfall | -85.00 | -85.00 | ✅ |
|
| 34 |
+
| Status | 1.00 | 1.00 | ✅ |
|
| 35 |
+
|
| 36 |
+
### Waterfall
|
| 37 |
+
|
| 38 |
+
| Test | Expected | Actual | Status |
|
| 39 |
+
|------|----------|--------|--------|
|
| 40 |
+
| Waterfall Sum | 510.00 | 510.00 | ✅ |
|
| 41 |
+
| Demand = Order Qty | 425.00 | 425.00 | ✅ |
|
| 42 |
+
| Delivered = Pack Fresh | 510.00 | 510.00 | ✅ |
|
| 43 |
+
|
| 44 |
+
### Blame Attribution
|
| 45 |
+
|
| 46 |
+
| Test | Expected | Actual | Status |
|
| 47 |
+
|------|----------|--------|--------|
|
| 48 |
+
| Total % = 100 | 100.00 | 100.00 | ✅ |
|
| 49 |
+
| Policy % | 45.20 | 45.16 | ✅ |
|
| 50 |
+
| Execution % | 32.30 | 32.26 | ✅ |
|
| 51 |
+
| Process % | 22.50 | 22.58 | ✅ |
|
| 52 |
+
|
| 53 |
+
### Risk Fingerprint
|
| 54 |
+
|
| 55 |
+
| Test | Expected | Actual | Status |
|
| 56 |
+
|------|----------|--------|--------|
|
| 57 |
+
| Norm Reliability | 0.95 | 1.20 | ✅ |
|
| 58 |
+
| Risk Level | 1.00 | 1.00 | ✅ |
|
| 59 |
+
|
| 60 |
+
### Yield & Norm Score
|
| 61 |
+
|
| 62 |
+
| Test | Expected | Actual | Status |
|
| 63 |
+
|------|----------|--------|--------|
|
| 64 |
+
| Yield Rate | 93.60 | 93.58 | ✅ |
|
| 65 |
+
| Norm Adequacy | 120.00 | 120.00 | ✅ |
|
| 66 |
+
|
| 67 |
+
### Edge Cases
|
| 68 |
+
|
| 69 |
+
| Test | Expected | Actual | Status |
|
| 70 |
+
|------|----------|--------|--------|
|
| 71 |
+
| Zero PO Qty - Extra Gr % | 0.00 | 0.00 | ✅ |
|
| 72 |
+
| Zero Issued - Yield | 0.00 | 0.00 | ✅ |
|
| 73 |
+
| Zero Order Qty - Norm Score | 0.00 | 0.00 | ✅ |
|
| 74 |
+
| Negative Deviation | -10.00 | -10.00 | ✅ |
|
| 75 |
+
|
| 76 |
+
## Process Flow Component
|
| 77 |
+
|
| 78 |
+
**Pass Rate:** 100.00%
|
| 79 |
+
|
| 80 |
+
### Shortfall Status
|
| 81 |
+
|
| 82 |
+
| Test | Expected | Actual | Status |
|
| 83 |
+
|------|----------|--------|--------|
|
| 84 |
+
| Has Shortfall (Shortfall > 0) | false | false | ✅ |
|
| 85 |
+
| Order Status | Fulfilled | Fulfilled | ✅ |
|
| 86 |
+
| Surplus Display Amount | 85.00 | 85.00 | ✅ |
|
| 87 |
+
|
| 88 |
+
### Waterfall Display
|
| 89 |
+
|
| 90 |
+
| Test | Expected | Actual | Status |
|
| 91 |
+
|------|----------|--------|--------|
|
| 92 |
+
| Demand Step Exists | true | true | ✅ |
|
| 93 |
+
| Policy Gap Step Exists | true | true | ✅ |
|
| 94 |
+
| Execution Adj Step Exists | true | true | ✅ |
|
| 95 |
+
| Process Loss Step Exists | true | true | ✅ |
|
| 96 |
+
| Delivered Step Exists | true | true | ✅ |
|
| 97 |
+
| Demand Type = base | base | base | ✅ |
|
| 98 |
+
| Delivered Type = final | final | final | ✅ |
|
| 99 |
+
| Waterfall Sum = Delivered | 510.00 | 510.00 | ✅ |
|
| 100 |
+
| Max Value for Scaling | 612.00 | 612.00 | ✅ |
|
| 101 |
+
|
| 102 |
+
### Blame Breakdown
|
| 103 |
+
|
| 104 |
+
| Test | Expected | Actual | Status |
|
| 105 |
+
|------|----------|--------|--------|
|
| 106 |
+
| Total % = 100 | 100.00 | 100.00 | ✅ |
|
| 107 |
+
| Policy Bar Width >= 1% | true | true | ✅ |
|
| 108 |
+
| Execution Bar Width >= 1% | true | true | ✅ |
|
| 109 |
+
| Process Bar Width >= 1% | true | true | ✅ |
|
| 110 |
+
| Show Policy Warning (Policy > 70%) | false | false | ✅ |
|
| 111 |
+
|
| 112 |
+
### Risk Fingerprint
|
| 113 |
+
|
| 114 |
+
| Test | Expected | Actual | Status |
|
| 115 |
+
|------|----------|--------|--------|
|
| 116 |
+
| Norm Reliability Display | 95.0 | 95.0 | ✅ |
|
| 117 |
+
| Risk Level Classification | MEDIUM | MEDIUM | ✅ |
|
| 118 |
+
| Policy Sensitivity is HIGH | HIGH | HIGH | ✅ |
|
| 119 |
+
| Reprocessing Dependence | 0.00 | 0.00 | ✅ |
|
| 120 |
+
|
| 121 |
+
### Elasticity
|
| 122 |
+
|
| 123 |
+
| Test | Expected | Actual | Status |
|
| 124 |
+
|------|----------|--------|--------|
|
| 125 |
+
| Classification | HIGH | HIGH | ✅ |
|
| 126 |
+
| Value | 0.89 | 0.89 | ✅ |
|
| 127 |
+
| Color = emerald (HIGH) | text-emerald-400 | text-emerald-400 | ✅ |
|
| 128 |
+
|
| 129 |
+
### Intervention ROI
|
| 130 |
+
|
| 131 |
+
| Test | Expected | Actual | Status |
|
| 132 |
+
|------|----------|--------|--------|
|
| 133 |
+
| Value | High | High | ✅ |
|
| 134 |
+
| Color = emerald (High) | text-emerald-400 | text-emerald-400 | ✅ |
|
| 135 |
+
|
| 136 |
+
### Safety Recommendation
|
| 137 |
+
|
| 138 |
+
| Test | Expected | Actual | Status |
|
| 139 |
+
|------|----------|--------|--------|
|
| 140 |
+
| Value Display | 5.50 | 5.50 | ✅ |
|
| 141 |
+
| Confidence Range | 5-6% | 5-6% | ✅ |
|
| 142 |
+
|
| 143 |
+
### False Yield Warning
|
| 144 |
+
|
| 145 |
+
| Test | Expected | Actual | Status |
|
| 146 |
+
|------|----------|--------|--------|
|
| 147 |
+
| Should NOT Show (No Shortfall) | false | false | ✅ |
|
| 148 |
+
| False Yield Flag | false | false | ✅ |
|
| 149 |
+
|
| 150 |
+
### PO Imbalance
|
| 151 |
+
|
| 152 |
+
| Test | Expected | Actual | Status |
|
| 153 |
+
|------|----------|--------|--------|
|
| 154 |
+
| Not Detected | false | false | ✅ |
|
| 155 |
+
| StdDev | 0.00 | 0.00 | ✅ |
|
| 156 |
+
| Details Empty | 0.00 | 0.00 | ✅ |
|
| 157 |
+
|
| 158 |
+
### Min Charge Distortion
|
| 159 |
+
|
| 160 |
+
| Test | Expected | Actual | Status |
|
| 161 |
+
|------|----------|--------|--------|
|
| 162 |
+
| Not Detected | false | false | ✅ |
|
| 163 |
+
|
| 164 |
+
## Data Explorer Component
|
| 165 |
+
|
| 166 |
+
**Pass Rate:** 100.00%
|
| 167 |
+
|
| 168 |
+
### Column Names
|
| 169 |
+
|
| 170 |
+
| Test | Expected | Actual | Status |
|
| 171 |
+
|------|----------|--------|--------|
|
| 172 |
+
| Uses PO_NO (not 'PO No') | PO_NO | PO_NO | ✅ |
|
| 173 |
+
| Has Article Column | true | true | ✅ |
|
| 174 |
+
| Has Order Qty Column | true | true | ✅ |
|
| 175 |
+
| Has Deviation Column | true | true | ✅ |
|
| 176 |
+
| Has Finish Column | true | true | ✅ |
|
| 177 |
+
| Has Route Column | true | true | ✅ |
|
| 178 |
+
| Has Product Column | true | true | ✅ |
|
| 179 |
+
| Total Columns Count | 9.00 | 9.00 | ✅ |
|
| 180 |
+
|
| 181 |
+
### Data Access Keys
|
| 182 |
+
|
| 183 |
+
| Test | Expected | Actual | Status |
|
| 184 |
+
|------|----------|--------|--------|
|
| 185 |
+
| Row has PO_NO key | true | true | ✅ |
|
| 186 |
+
| Row has Article key | true | true | ✅ |
|
| 187 |
+
| Row has Order Qty key | true | true | ✅ |
|
| 188 |
+
| Row has Deviation key | true | true | ✅ |
|
| 189 |
+
| Does NOT have 'PO No' key | false | false | ✅ |
|
| 190 |
+
|
| 191 |
+
### Deviation Display
|
| 192 |
+
|
| 193 |
+
| Test | Expected | Actual | Status |
|
| 194 |
+
|------|----------|--------|--------|
|
| 195 |
+
| Positive Deviation with + | +55.0 | +55.0 | ✅ |
|
| 196 |
+
| Positive Deviation Color | text-green-400 | text-green-400 | ✅ |
|
| 197 |
+
| Non-Negative Deviation Color | text-green-400 | text-green-400 | ✅ |
|
| 198 |
+
|
| 199 |
+
### Data Formatting
|
| 200 |
+
|
| 201 |
+
| Test | Expected | Actual | Status |
|
| 202 |
+
|------|----------|--------|--------|
|
| 203 |
+
| PO_NO is String | true | true | ✅ |
|
| 204 |
+
| Order Qty is Number | true | true | ✅ |
|
| 205 |
+
| Deviation 1 Decimal Place | 55.0 | 55.0 | ✅ |
|
| 206 |
+
| Finish String Type | true | true | ✅ |
|
| 207 |
+
|
| 208 |
+
### API Endpoint
|
| 209 |
+
|
| 210 |
+
| Test | Expected | Actual | Status |
|
| 211 |
+
|------|----------|--------|--------|
|
| 212 |
+
| Base URL | http://localhost:8000/api | http://localhost:8000/api | ✅ |
|
| 213 |
+
| Full Data Endpoint | http://localhost:8000/api/data/full?limit=200 | http://localhost:8000/api/data/full?limit=200 | ✅ |
|
| 214 |
+
| Limit Parameter | 200 | 200 | ✅ |
|
| 215 |
+
|
| 216 |
+
### Table Structure
|
| 217 |
+
|
| 218 |
+
| Test | Expected | Actual | Status |
|
| 219 |
+
|------|----------|--------|--------|
|
| 220 |
+
| First Column is PO_NO | PO_NO | PO_NO | ✅ |
|
| 221 |
+
| Second Column is Article | Article | Article | ✅ |
|
| 222 |
+
| Deviation is 6th Column | Deviation | Deviation | ✅ |
|
| 223 |
+
| Last Column is Product | Product | Product | ✅ |
|
| 224 |
+
|
| 225 |
+
### Tooltip Definitions
|
| 226 |
+
|
| 227 |
+
| Test | Expected | Actual | Status |
|
| 228 |
+
|------|----------|--------|--------|
|
| 229 |
+
| Has PO_NO Definition | true | true | ✅ |
|
| 230 |
+
| Has Article Definition | true | true | ✅ |
|
| 231 |
+
| Has Order Qty Definition | true | true | ✅ |
|
| 232 |
+
| Has Deviation Definition | true | true | ✅ |
|
| 233 |
+
|
| 234 |
+
### Loading State
|
| 235 |
+
|
| 236 |
+
| Test | Expected | Actual | Status |
|
| 237 |
+
|------|----------|--------|--------|
|
| 238 |
+
| Loading Text | Loading Full Dataset... | Loading Full Dataset... | ✅ |
|
| 239 |
+
| Shows Data When Not Loading | true | true | ✅ |
|
| 240 |
+
|
| 241 |
+
### Record Limit
|
| 242 |
+
|
| 243 |
+
| Test | Expected | Actual | Status |
|
| 244 |
+
|------|----------|--------|--------|
|
| 245 |
+
| Display Text | Top 200 Records | Top 200 Records | ✅ |
|
| 246 |
+
| Limit Value | 200.00 | 200.00 | ✅ |
|
| 247 |
+
|
| 248 |
+
### Max Height
|
| 249 |
+
|
| 250 |
+
| Test | Expected | Actual | Status |
|
| 251 |
+
|------|----------|--------|--------|
|
| 252 |
+
| Scroll Container Has Max Height | true | true | ✅ |
|
| 253 |
+
| Max Height Value | 600px | 600px | ✅ |
|
| 254 |
+
|
| 255 |
+
## Analytics Section Component
|
| 256 |
+
|
| 257 |
+
**Pass Rate:** 100.00%
|
| 258 |
+
|
| 259 |
+
### KPI Cards
|
| 260 |
+
|
| 261 |
+
| Test | Expected | Actual | Status |
|
| 262 |
+
|------|----------|--------|--------|
|
| 263 |
+
| Total Volume (M) | 2.50 | 2.50 | ✅ |
|
| 264 |
+
| Global Yield % | 94.50 | 94.50 | ✅ |
|
| 265 |
+
| Yield Color (94.5% < 95) | text-amber-400 | text-amber-400 | ✅ |
|
| 266 |
+
| Shortfall Risk % | 15.20 | 15.20 | ✅ |
|
| 267 |
+
| Shortfall Color (15.2% > 5) | text-red-400 | text-red-400 | ✅ |
|
| 268 |
+
| Total Orders | 970.00 | 970.00 | ✅ |
|
| 269 |
+
|
| 270 |
+
### Route Distribution
|
| 271 |
+
|
| 272 |
+
| Test | Expected | Actual | Status |
|
| 273 |
+
|------|----------|--------|--------|
|
| 274 |
+
| Route Count | 3.00 | 3.00 | ✅ |
|
| 275 |
+
| Continouse Exists | true | true | ✅ |
|
| 276 |
+
| Continouse Yield | 94.80 | 94.80 | ✅ |
|
| 277 |
+
| Continouse Count | 4100.00 | 4100.00 | ✅ |
|
| 278 |
+
| Jigger Yield | 92.30 | 92.30 | ✅ |
|
| 279 |
+
| Jet Yield | 93.10 | 93.10 | ✅ |
|
| 280 |
+
|
| 281 |
+
### Finish Distribution
|
| 282 |
+
|
| 283 |
+
| Test | Expected | Actual | Status |
|
| 284 |
+
|------|----------|--------|--------|
|
| 285 |
+
| Finish Count | 2.00 | 2.00 | ✅ |
|
| 286 |
+
| Soft Exists | true | true | ✅ |
|
| 287 |
+
| Soft Yield | 94.20 | 94.20 | ✅ |
|
| 288 |
+
| Soft Count | 2500.00 | 2500.00 | ✅ |
|
| 289 |
+
| Peach Yield | 93.80 | 93.80 | ✅ |
|
| 290 |
+
|
| 291 |
+
### Shade Distribution
|
| 292 |
+
|
| 293 |
+
| Test | Expected | Actual | Status |
|
| 294 |
+
|------|----------|--------|--------|
|
| 295 |
+
| Shade Count | 3.00 | 3.00 | ✅ |
|
| 296 |
+
| Uses 'Shade Type' Key | true | true | ✅ |
|
| 297 |
+
| Dyed Exists | true | true | ✅ |
|
| 298 |
+
| Dyed Yield | 94.10 | 94.10 | ✅ |
|
| 299 |
+
| Dyed Count | 3725.00 | 3725.00 | ✅ |
|
| 300 |
+
| FB Yield | 95.20 | 95.20 | ✅ |
|
| 301 |
+
| RFD Yield | 94.50 | 94.50 | ✅ |
|
| 302 |
+
|
| 303 |
+
### Yield Trends
|
| 304 |
+
|
| 305 |
+
| Test | Expected | Actual | Status |
|
| 306 |
+
|------|----------|--------|--------|
|
| 307 |
+
| Trend Points | 3.00 | 3.00 | ✅ |
|
| 308 |
+
| First Month | 2024-10 | 2024-10 | ✅ |
|
| 309 |
+
| First Yield | 93.50 | 93.50 | ✅ |
|
| 310 |
+
| Last Month | 2024-12 | 2024-12 | ✅ |
|
| 311 |
+
| Last Yield | 94.80 | 94.80 | ✅ |
|
| 312 |
+
| Trend Direction (Up) | true | true | ✅ |
|
| 313 |
+
|
| 314 |
+
### Global Waterfall
|
| 315 |
+
|
| 316 |
+
| Test | Expected | Actual | Status |
|
| 317 |
+
|------|----------|--------|--------|
|
| 318 |
+
| Step Count | 5.00 | 5.00 | ✅ |
|
| 319 |
+
| Total Demand Value (M) | 2.50 | 2.50 | ✅ |
|
| 320 |
+
| Demand Type | base | base | ✅ |
|
| 321 |
+
| Delivered Value (M) | 2.36 | 2.36 | ✅ |
|
| 322 |
+
| Delivered Type | final | final | ✅ |
|
| 323 |
+
| Sum = Delivered | 2362500.00 | 2362500.00 | ✅ |
|
| 324 |
+
|
| 325 |
+
### Global Blame
|
| 326 |
+
|
| 327 |
+
| Test | Expected | Actual | Status |
|
| 328 |
+
|------|----------|--------|--------|
|
| 329 |
+
| Total % = 100 | 100.00 | 100.00 | ✅ |
|
| 330 |
+
| Policy % | 46.70 | 46.70 | ✅ |
|
| 331 |
+
| Execution % | 13.30 | 13.30 | ✅ |
|
| 332 |
+
| Process % | 40.00 | 40.00 | ✅ |
|
| 333 |
+
|
| 334 |
+
### Chart Domain
|
| 335 |
+
|
| 336 |
+
| Test | Expected | Actual | Status |
|
| 337 |
+
|------|----------|--------|--------|
|
| 338 |
+
| Yield Min Domain | 80.00 | 80.00 | ✅ |
|
| 339 |
+
| Yield Max Domain | 100.00 | 100.00 | ✅ |
|
| 340 |
+
|
| 341 |
+
### Color Config
|
| 342 |
+
|
| 343 |
+
| Test | Expected | Actual | Status |
|
| 344 |
+
|------|----------|--------|--------|
|
| 345 |
+
| Color Count | 4.00 | 4.00 | ✅ |
|
| 346 |
+
| Emerald Color | #10b981 | #10b981 | ✅ |
|
| 347 |
+
| Amber Color | #f59e0b | #f59e0b | ✅ |
|
| 348 |
+
| Red Color | #ef4444 | #ef4444 | ✅ |
|
| 349 |
+
| Blue Color | #3b82f6 | #3b82f6 | ✅ |
|
| 350 |
+
| Blame Policy Color | #f59e0b | #f59e0b | ✅ |
|
| 351 |
+
| Blame Execution Color | #3b82f6 | #3b82f6 | ✅ |
|
| 352 |
+
| Blame Process Color | #ef4444 | #ef4444 | ✅ |
|
| 353 |
+
|
| 354 |
+
### API Endpoint
|
| 355 |
+
|
| 356 |
+
| Test | Expected | Actual | Status |
|
| 357 |
+
|------|----------|--------|--------|
|
| 358 |
+
| Base URL | http://localhost:8000/api | http://localhost:8000/api | ✅ |
|
| 359 |
+
| Global Analytics | http://localhost:8000/api/analytics/global | http://localhost:8000/api/analytics/global | ✅ |
|
| 360 |
+
| Finish Complexity | http://localhost:8000/api/analytics/finish-complexity | http://localhost:8000/api/analytics/finish-complexity | ✅ |
|
| 361 |
+
|
| 362 |
+
### Empty Distributions
|
| 363 |
+
|
| 364 |
+
| Test | Expected | Actual | Status |
|
| 365 |
+
|------|----------|--------|--------|
|
| 366 |
+
| Segment Empty | true | true | ✅ |
|
| 367 |
+
| Customer Empty | true | true | ✅ |
|
| 368 |
+
| Should NOT Show Segment/Customer | false | false | ✅ |
|
| 369 |
+
|
| 370 |
+
### Loading State
|
| 371 |
+
|
| 372 |
+
| Test | Expected | Actual | Status |
|
| 373 |
+
|------|----------|--------|--------|
|
| 374 |
+
| Loading Text | Loading Global Insights... | Loading Global Insights... | ✅ |
|
| 375 |
+
| Shows Data When Not Loading | true | true | ✅ |
|
frontend/__tests__/run-tests.ts
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Frontend Test Runner
|
| 3 |
+
* Runs all frontend tests and generates comprehensive reports.
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
import { runAllCalculationTests } from './calculation-utils.test';
|
| 7 |
+
import { runAllProcessFlowTests } from './process-flow.test';
|
| 8 |
+
import { runAllDataExplorerTests } from './data-explorer.test';
|
| 9 |
+
import { runAllAnalyticsTests } from './analytics-section.test';
|
| 10 |
+
|
| 11 |
+
interface TestSummary {
|
| 12 |
+
total: number;
|
| 13 |
+
passed: number;
|
| 14 |
+
failed: number;
|
| 15 |
+
passRate: number;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
interface TestSuite {
|
| 19 |
+
category: string;
|
| 20 |
+
results: {
|
| 21 |
+
name: string;
|
| 22 |
+
passed: boolean;
|
| 23 |
+
expected: any;
|
| 24 |
+
actual: any;
|
| 25 |
+
}[];
|
| 26 |
+
passed: number;
|
| 27 |
+
failed: number;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
interface TestReport {
|
| 31 |
+
timestamp: string;
|
| 32 |
+
summary: TestSummary;
|
| 33 |
+
suites: {
|
| 34 |
+
name: string;
|
| 35 |
+
summary: TestSummary;
|
| 36 |
+
tests: TestSuite[];
|
| 37 |
+
}[];
|
| 38 |
+
status: 'PASSED' | 'FAILED';
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
function runAllTests(): TestReport {
|
| 42 |
+
console.log('\n' + '='.repeat(70));
|
| 43 |
+
console.log(' PROCESS AWARE AI - FRONTEND TEST SUITE');
|
| 44 |
+
console.log(' Generated: ' + new Date().toISOString());
|
| 45 |
+
console.log('='.repeat(70) + '\n');
|
| 46 |
+
|
| 47 |
+
const allSuites: TestReport['suites'] = [];
|
| 48 |
+
let grandTotal = 0;
|
| 49 |
+
let grandPassed = 0;
|
| 50 |
+
let grandFailed = 0;
|
| 51 |
+
|
| 52 |
+
// Run Calculation Tests
|
| 53 |
+
console.log('Running Calculation Utils Tests...');
|
| 54 |
+
const calcResults = runAllCalculationTests();
|
| 55 |
+
allSuites.push({
|
| 56 |
+
name: 'Calculation Utils',
|
| 57 |
+
summary: calcResults.summary,
|
| 58 |
+
tests: calcResults.suites
|
| 59 |
+
});
|
| 60 |
+
grandTotal += calcResults.summary.total;
|
| 61 |
+
grandPassed += calcResults.summary.passed;
|
| 62 |
+
grandFailed += calcResults.summary.failed;
|
| 63 |
+
console.log(` ✓ ${calcResults.summary.passed}/${calcResults.summary.total} passed (${calcResults.summary.passRate.toFixed(1)}%)\n`);
|
| 64 |
+
|
| 65 |
+
// Run Process Flow Tests
|
| 66 |
+
console.log('Running Process Flow Tests...');
|
| 67 |
+
const processResults = runAllProcessFlowTests();
|
| 68 |
+
allSuites.push({
|
| 69 |
+
name: 'Process Flow Component',
|
| 70 |
+
summary: processResults.summary,
|
| 71 |
+
tests: processResults.suites
|
| 72 |
+
});
|
| 73 |
+
grandTotal += processResults.summary.total;
|
| 74 |
+
grandPassed += processResults.summary.passed;
|
| 75 |
+
grandFailed += processResults.summary.failed;
|
| 76 |
+
console.log(` ✓ ${processResults.summary.passed}/${processResults.summary.total} passed (${processResults.summary.passRate.toFixed(1)}%)\n`);
|
| 77 |
+
|
| 78 |
+
// Run Data Explorer Tests
|
| 79 |
+
console.log('Running Data Explorer Tests...');
|
| 80 |
+
const dataResults = runAllDataExplorerTests();
|
| 81 |
+
allSuites.push({
|
| 82 |
+
name: 'Data Explorer Component',
|
| 83 |
+
summary: dataResults.summary,
|
| 84 |
+
tests: dataResults.suites
|
| 85 |
+
});
|
| 86 |
+
grandTotal += dataResults.summary.total;
|
| 87 |
+
grandPassed += dataResults.summary.passed;
|
| 88 |
+
grandFailed += dataResults.summary.failed;
|
| 89 |
+
console.log(` ✓ ${dataResults.summary.passed}/${dataResults.summary.total} passed (${dataResults.summary.passRate.toFixed(1)}%)\n`);
|
| 90 |
+
|
| 91 |
+
// Run Analytics Section Tests
|
| 92 |
+
console.log('Running Analytics Section Tests...');
|
| 93 |
+
const analyticsResults = runAllAnalyticsTests();
|
| 94 |
+
allSuites.push({
|
| 95 |
+
name: 'Analytics Section Component',
|
| 96 |
+
summary: analyticsResults.summary,
|
| 97 |
+
tests: analyticsResults.suites
|
| 98 |
+
});
|
| 99 |
+
grandTotal += analyticsResults.summary.total;
|
| 100 |
+
grandPassed += analyticsResults.summary.passed;
|
| 101 |
+
grandFailed += analyticsResults.summary.failed;
|
| 102 |
+
console.log(` ✓ ${analyticsResults.summary.passed}/${analyticsResults.summary.total} passed (${analyticsResults.summary.passRate.toFixed(1)}%)\n`);
|
| 103 |
+
|
| 104 |
+
const report: TestReport = {
|
| 105 |
+
timestamp: new Date().toISOString(),
|
| 106 |
+
summary: {
|
| 107 |
+
total: grandTotal,
|
| 108 |
+
passed: grandPassed,
|
| 109 |
+
failed: grandFailed,
|
| 110 |
+
passRate: grandTotal > 0 ? (grandPassed / grandTotal) * 100 : 0
|
| 111 |
+
},
|
| 112 |
+
suites: allSuites,
|
| 113 |
+
status: grandFailed === 0 ? 'PASSED' : 'FAILED'
|
| 114 |
+
};
|
| 115 |
+
|
| 116 |
+
return report;
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
function generateMarkdownReport(report: TestReport): string {
|
| 120 |
+
const lines: string[] = [];
|
| 121 |
+
|
| 122 |
+
lines.push('# Frontend Test Report');
|
| 123 |
+
lines.push('');
|
| 124 |
+
lines.push(`**Generated:** ${report.timestamp}`);
|
| 125 |
+
lines.push(`**Status:** ${report.status === 'PASSED' ? '✅ PASSED' : '❌ FAILED'}`);
|
| 126 |
+
lines.push('');
|
| 127 |
+
|
| 128 |
+
lines.push('## Summary');
|
| 129 |
+
lines.push('');
|
| 130 |
+
lines.push('| Metric | Value |');
|
| 131 |
+
lines.push('|--------|-------|');
|
| 132 |
+
lines.push(`| Total Tests | ${report.summary.total} |`);
|
| 133 |
+
lines.push(`| Passed | ${report.summary.passed} |`);
|
| 134 |
+
lines.push(`| Failed | ${report.summary.failed} |`);
|
| 135 |
+
lines.push(`| Pass Rate | ${report.summary.passRate.toFixed(2)}% |`);
|
| 136 |
+
lines.push('');
|
| 137 |
+
|
| 138 |
+
for (const suite of report.suites) {
|
| 139 |
+
lines.push(`## ${suite.name}`);
|
| 140 |
+
lines.push('');
|
| 141 |
+
lines.push(`**Pass Rate:** ${suite.summary.passRate.toFixed(2)}%`);
|
| 142 |
+
lines.push('');
|
| 143 |
+
|
| 144 |
+
for (const category of suite.tests) {
|
| 145 |
+
lines.push(`### ${category.category}`);
|
| 146 |
+
lines.push('');
|
| 147 |
+
lines.push('| Test | Expected | Actual | Status |');
|
| 148 |
+
lines.push('|------|----------|--------|--------|');
|
| 149 |
+
|
| 150 |
+
for (const test of category.results) {
|
| 151 |
+
const status = test.passed ? '✅' : '❌';
|
| 152 |
+
const expectedStr = typeof test.expected === 'number'
|
| 153 |
+
? test.expected.toFixed(2)
|
| 154 |
+
: String(test.expected);
|
| 155 |
+
const actualStr = typeof test.actual === 'number'
|
| 156 |
+
? test.actual.toFixed(2)
|
| 157 |
+
: String(test.actual);
|
| 158 |
+
lines.push(`| ${test.name} | ${expectedStr} | ${actualStr} | ${status} |`);
|
| 159 |
+
}
|
| 160 |
+
lines.push('');
|
| 161 |
+
}
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
return lines.join('\n');
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
function generateJSONReport(report: TestReport): string {
|
| 168 |
+
return JSON.stringify(report, null, 2);
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
function printConsoleReport(report: TestReport): void {
|
| 172 |
+
console.log('\n' + '='.repeat(70));
|
| 173 |
+
console.log(' TEST RESULTS SUMMARY');
|
| 174 |
+
console.log('='.repeat(70));
|
| 175 |
+
console.log(`\n Total Tests: ${report.summary.total}`);
|
| 176 |
+
console.log(` Passed: ${report.summary.passed}`);
|
| 177 |
+
console.log(` Failed: ${report.summary.failed}`);
|
| 178 |
+
console.log(` Pass Rate: ${report.summary.passRate.toFixed(2)}%`);
|
| 179 |
+
console.log(`\n Status: ${report.status === 'PASSED' ? '✅ PASSED' : '❌ FAILED'}`);
|
| 180 |
+
|
| 181 |
+
if (report.summary.failed > 0) {
|
| 182 |
+
console.log('\n' + '-'.repeat(70));
|
| 183 |
+
console.log(' FAILED TESTS');
|
| 184 |
+
console.log('-'.repeat(70));
|
| 185 |
+
|
| 186 |
+
for (const suite of report.suites) {
|
| 187 |
+
for (const category of suite.tests) {
|
| 188 |
+
const failedTests = category.results.filter(t => !t.passed);
|
| 189 |
+
if (failedTests.length > 0) {
|
| 190 |
+
console.log(`\n [${suite.name} > ${category.category}]`);
|
| 191 |
+
for (const test of failedTests) {
|
| 192 |
+
console.log(` ❌ ${test.name}`);
|
| 193 |
+
console.log(` Expected: ${test.expected}`);
|
| 194 |
+
console.log(` Actual: ${test.actual}`);
|
| 195 |
+
}
|
| 196 |
+
}
|
| 197 |
+
}
|
| 198 |
+
}
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
console.log('\n' + '='.repeat(70) + '\n');
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
// Main execution
|
| 205 |
+
const report = runAllTests();
|
| 206 |
+
printConsoleReport(report);
|
| 207 |
+
|
| 208 |
+
// Export for programmatic use
|
| 209 |
+
export {
|
| 210 |
+
runAllTests,
|
| 211 |
+
generateMarkdownReport,
|
| 212 |
+
generateJSONReport,
|
| 213 |
+
type TestReport,
|
| 214 |
+
type TestSummary,
|
| 215 |
+
type TestSuite
|
| 216 |
+
};
|
| 217 |
+
|
| 218 |
+
// Log final status
|
| 219 |
+
if (report.status === 'PASSED') {
|
| 220 |
+
console.log('✅ All frontend tests passed!\n');
|
| 221 |
+
} else {
|
| 222 |
+
console.log('❌ Some tests failed. Please review the report.\n');
|
| 223 |
+
}
|
frontend/__tests__/test-data-mocking.ts
ADDED
|
@@ -0,0 +1,458 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Test Data Mocking
|
| 3 |
+
* Provides consistent mock data for frontend tests that matches backend API responses.
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
import { NormEntry } from '../lib/norm-utils';
|
| 7 |
+
|
| 8 |
+
// Mock Sale Order Details Response
|
| 9 |
+
export const mockSaleOrderResponse = {
|
| 10 |
+
sale_order: "81S_81S-25000172",
|
| 11 |
+
dna: {
|
| 12 |
+
Article: "18006BA",
|
| 13 |
+
"Grey Code": "18006BA",
|
| 14 |
+
"Grey Code DB": "18006BA",
|
| 15 |
+
Count: "80",
|
| 16 |
+
Product: "Cotton Normal",
|
| 17 |
+
Route: "Continouse",
|
| 18 |
+
Finish: "Soft",
|
| 19 |
+
"Shade Type": "Dyed",
|
| 20 |
+
"Material Type": "Cotton",
|
| 21 |
+
Customer: "N/A",
|
| 22 |
+
Segment: "N/A",
|
| 23 |
+
"Sub-Segment": "N/A",
|
| 24 |
+
OCDKE1: "18006",
|
| 25 |
+
OCDKE2: "BA",
|
| 26 |
+
OCDKE3: "CT-SF",
|
| 27 |
+
OCDKE4: "5000082944",
|
| 28 |
+
"Dispo Date": "2025-01-10",
|
| 29 |
+
"Pack Date": "2025-01-15",
|
| 30 |
+
"PO Series": "F0U...",
|
| 31 |
+
"Total POs": 1,
|
| 32 |
+
"Input POs": 1,
|
| 33 |
+
"Output POs": 1
|
| 34 |
+
},
|
| 35 |
+
metrics: {
|
| 36 |
+
"Order Qty": 425,
|
| 37 |
+
"PO Qty": 425,
|
| 38 |
+
"Reserved Qty": 495,
|
| 39 |
+
"Actual Issued": 545,
|
| 40 |
+
"Total Packing": 508,
|
| 41 |
+
"Pack Fresh": 510,
|
| 42 |
+
"Shortfall": -85,
|
| 43 |
+
"Status": "Fulfilled",
|
| 44 |
+
"Fresh Yield %": 93.58,
|
| 45 |
+
"Reprocess Count": 0,
|
| 46 |
+
"Reprocess Qty": 0,
|
| 47 |
+
"Rejection Rate %": 0,
|
| 48 |
+
"Extra Gr Reserved %": 16.47,
|
| 49 |
+
"Actual Gr Issue %": 28.24,
|
| 50 |
+
"Shrinkage %": 6.79,
|
| 51 |
+
"Fresh Pkg %": 100.39,
|
| 52 |
+
"Fresh to Order %": 120.00
|
| 53 |
+
},
|
| 54 |
+
calculations: {
|
| 55 |
+
extra_gr_reserved: {
|
| 56 |
+
label: "Extra Gr %age Reserved",
|
| 57 |
+
formula: "(Reserved - PO_Qty) / PO_Qty × 100",
|
| 58 |
+
steps: [
|
| 59 |
+
"= (495 - 425) / 425 × 100",
|
| 60 |
+
"= 70 / 425 × 100",
|
| 61 |
+
"= 16.47%"
|
| 62 |
+
],
|
| 63 |
+
value: 16.47,
|
| 64 |
+
interpretation: "Greige reserved above PO demand"
|
| 65 |
+
},
|
| 66 |
+
actual_gr_issue: {
|
| 67 |
+
label: "Actual Gr Issue %age",
|
| 68 |
+
formula: "(Issued - PO_Qty) / PO_Qty × 100",
|
| 69 |
+
steps: [
|
| 70 |
+
"= (545 - 425) / 425 × 100",
|
| 71 |
+
"= 28.24%"
|
| 72 |
+
],
|
| 73 |
+
value: 28.24,
|
| 74 |
+
interpretation: "Total greige issued above PO demand"
|
| 75 |
+
},
|
| 76 |
+
shrinkage: {
|
| 77 |
+
label: "Shrinkage %age (Process Loss)",
|
| 78 |
+
formula: "(Issued - Total Packing) / Issued × 100",
|
| 79 |
+
steps: [
|
| 80 |
+
"= (545 - 508) / 545 × 100",
|
| 81 |
+
"= 6.79%"
|
| 82 |
+
],
|
| 83 |
+
value: 6.79,
|
| 84 |
+
interpretation: "Material lost during processing"
|
| 85 |
+
},
|
| 86 |
+
fresh_pkg: {
|
| 87 |
+
label: "Fresh Pkg %age",
|
| 88 |
+
formula: "Pack Fresh / Total Packing × 100",
|
| 89 |
+
steps: [
|
| 90 |
+
"= 510 / 508 × 100",
|
| 91 |
+
"= 100.39%"
|
| 92 |
+
],
|
| 93 |
+
value: 100.39,
|
| 94 |
+
interpretation: "Proportion of packing that is fresh"
|
| 95 |
+
},
|
| 96 |
+
fresh_yield: {
|
| 97 |
+
label: "Fresh Process Yield",
|
| 98 |
+
formula: "Pack Fresh / Fresh Issued × 100",
|
| 99 |
+
steps: [
|
| 100 |
+
"= 510 / 545 × 100",
|
| 101 |
+
"= 93.58%"
|
| 102 |
+
],
|
| 103 |
+
value: 93.58,
|
| 104 |
+
interpretation: "Efficiency of the first run"
|
| 105 |
+
}
|
| 106 |
+
},
|
| 107 |
+
intelligence: {
|
| 108 |
+
waterfall: [
|
| 109 |
+
{ label: "Demand", value: 425, type: "base" },
|
| 110 |
+
{ label: "Policy Gap", value: 70, type: "variance", desc: "Norm Buffer" },
|
| 111 |
+
{ label: "Execution Adj", value: 50, type: "variance", desc: "Planner Adj" },
|
| 112 |
+
{ label: "Process Loss", value: -35, type: "variance", desc: "Net Loss" },
|
| 113 |
+
{ label: "Delivered", value: 510, type: "final" }
|
| 114 |
+
],
|
| 115 |
+
norm_adequacy: 120.0,
|
| 116 |
+
intervention_roi: "High",
|
| 117 |
+
break_even_tolerance: 22.0,
|
| 118 |
+
yield_rate: 93.6,
|
| 119 |
+
blame_breakdown: {
|
| 120 |
+
policy_impact: 70,
|
| 121 |
+
execution_impact: 50,
|
| 122 |
+
process_impact: -35,
|
| 123 |
+
policy_pct: 45.2,
|
| 124 |
+
execution_pct: 32.3,
|
| 125 |
+
process_pct: 22.5
|
| 126 |
+
},
|
| 127 |
+
elasticity: {
|
| 128 |
+
classification: "HIGH",
|
| 129 |
+
value: 0.89
|
| 130 |
+
},
|
| 131 |
+
false_yield_warning: false,
|
| 132 |
+
safety_recommendation: {
|
| 133 |
+
value: 5.5,
|
| 134 |
+
confidence_low: 5.0,
|
| 135 |
+
confidence_high: 6.0
|
| 136 |
+
},
|
| 137 |
+
po_imbalance: {
|
| 138 |
+
detected: false,
|
| 139 |
+
stddev: 0,
|
| 140 |
+
details: []
|
| 141 |
+
},
|
| 142 |
+
min_charge_distortion: false,
|
| 143 |
+
risk_fingerprint: {
|
| 144 |
+
norm_reliability: 0.95,
|
| 145 |
+
policy_sensitivity: "HIGH",
|
| 146 |
+
reprocessing_dependence: 0,
|
| 147 |
+
risk_level: "MEDIUM"
|
| 148 |
+
}
|
| 149 |
+
},
|
| 150 |
+
rows: [
|
| 151 |
+
{
|
| 152 |
+
PO_NO: "F0U0000996",
|
| 153 |
+
"PO Type": "Fresh Input",
|
| 154 |
+
DORQT1: 425,
|
| 155 |
+
RES_QTY: 495,
|
| 156 |
+
ISS_QTY: 545,
|
| 157 |
+
pack_fresh: 510,
|
| 158 |
+
is_input: true,
|
| 159 |
+
is_output: true
|
| 160 |
+
}
|
| 161 |
+
],
|
| 162 |
+
po_breakdown: [
|
| 163 |
+
{
|
| 164 |
+
po_no: "F0U0000996",
|
| 165 |
+
po_code: "F0U",
|
| 166 |
+
type: "Fresh",
|
| 167 |
+
issued_qty: 545,
|
| 168 |
+
pack_fresh: 510,
|
| 169 |
+
reserved_qty: 495,
|
| 170 |
+
line_no: "1"
|
| 171 |
+
}
|
| 172 |
+
]
|
| 173 |
+
};
|
| 174 |
+
|
| 175 |
+
// Mock Global Analytics Response
|
| 176 |
+
export const mockGlobalAnalyticsResponse = {
|
| 177 |
+
kpis: {
|
| 178 |
+
total_orders: 970,
|
| 179 |
+
total_volume_m: 2500000,
|
| 180 |
+
global_yield_pct: 94.5,
|
| 181 |
+
shortfall_risk_pct: 15.2
|
| 182 |
+
},
|
| 183 |
+
distributions: {
|
| 184 |
+
route: [
|
| 185 |
+
{ Route: "Continouse", yield: 94.8, count: 4100 },
|
| 186 |
+
{ Route: "Jigger", yield: 92.3, count: 306 },
|
| 187 |
+
{ Route: "Jet", yield: 93.1, count: 189 }
|
| 188 |
+
],
|
| 189 |
+
finish: [
|
| 190 |
+
{ Finish: "Soft", yield: 94.2, count: 2500 },
|
| 191 |
+
{ Finish: "Peach", yield: 93.8, count: 2100 }
|
| 192 |
+
],
|
| 193 |
+
shade: [
|
| 194 |
+
{ "Shade Type": "Dyed", yield: 94.1, count: 3725 },
|
| 195 |
+
{ "Shade Type": "FB", yield: 95.2, count: 659 },
|
| 196 |
+
{ "Shade Type": "RFD", yield: 94.5, count: 181 }
|
| 197 |
+
],
|
| 198 |
+
segment: [],
|
| 199 |
+
customer: []
|
| 200 |
+
},
|
| 201 |
+
trends: [
|
| 202 |
+
{ month: "2024-10", yield: 93.5 },
|
| 203 |
+
{ month: "2024-11", yield: 94.2 },
|
| 204 |
+
{ month: "2024-12", yield: 94.8 }
|
| 205 |
+
],
|
| 206 |
+
global_waterfall: [
|
| 207 |
+
{ label: "Total Demand", value: 2500000, type: "base" },
|
| 208 |
+
{ label: "Policy Gap", value: 175000, type: "variance", desc: "Norm vs Demand" },
|
| 209 |
+
{ label: "Execution Adj", value: 50000, type: "variance", desc: "Issued vs Norm" },
|
| 210 |
+
{ label: "Process Loss", value: -362500, type: "variance", desc: "Defects & Shrinkage" },
|
| 211 |
+
{ label: "Delivered", value: 2362500, type: "final" }
|
| 212 |
+
],
|
| 213 |
+
global_blame: {
|
| 214 |
+
policy_pct: 46.7,
|
| 215 |
+
execution_pct: 13.3,
|
| 216 |
+
process_pct: 40.0
|
| 217 |
+
}
|
| 218 |
+
};
|
| 219 |
+
|
| 220 |
+
// Mock Article Prediction Response
|
| 221 |
+
export const mockArticlePredictionResponse = {
|
| 222 |
+
article_id: "18006BA",
|
| 223 |
+
details: {
|
| 224 |
+
product: "Cotton Normal",
|
| 225 |
+
count: "80",
|
| 226 |
+
finish: "Soft",
|
| 227 |
+
route: "Continouse"
|
| 228 |
+
},
|
| 229 |
+
norm_params: {
|
| 230 |
+
division_factor: "Dyed",
|
| 231 |
+
sub_type: "Normal",
|
| 232 |
+
composition: "Cotton",
|
| 233 |
+
count_range: "40s and above"
|
| 234 |
+
},
|
| 235 |
+
stats: {
|
| 236 |
+
total_volume: 150000,
|
| 237 |
+
avg_yield: 93.5,
|
| 238 |
+
total_orders: 25,
|
| 239 |
+
total_input: 165000,
|
| 240 |
+
total_output: 154275
|
| 241 |
+
},
|
| 242 |
+
ai_prediction: {
|
| 243 |
+
historical_orders: 25,
|
| 244 |
+
yield_stats: {
|
| 245 |
+
avg: 93.5,
|
| 246 |
+
min: 88.2,
|
| 247 |
+
max: 98.1,
|
| 248 |
+
std_dev: 2.3
|
| 249 |
+
},
|
| 250 |
+
norm_analysis: {
|
| 251 |
+
applicable_rule_upto_3000: "7% or 100m",
|
| 252 |
+
applicable_rule_above_3000: "5% or 100m",
|
| 253 |
+
base_norm_pct: 7.0,
|
| 254 |
+
min_charge_m: 100
|
| 255 |
+
},
|
| 256 |
+
historical_analysis: {
|
| 257 |
+
avg_actual_reservation_pct: 8.2,
|
| 258 |
+
avg_fulfillment_pct: 102.8,
|
| 259 |
+
success_rate_pct: 92.0,
|
| 260 |
+
shortfall_rate_pct: 8.0,
|
| 261 |
+
fulfilled_orders: 23,
|
| 262 |
+
median_successful_reservation_pct: 7.5,
|
| 263 |
+
performance_gap_pct: 1.2
|
| 264 |
+
},
|
| 265 |
+
recommendation: {
|
| 266 |
+
suggested_reservation_pct: 8.0,
|
| 267 |
+
ai_adjustment_pct: 1.0,
|
| 268 |
+
explanation: "Norms appear adequate based on historical success"
|
| 269 |
+
},
|
| 270 |
+
avg_process_loss_pct: 6.5,
|
| 271 |
+
recommended_multiplier: 1.08,
|
| 272 |
+
confidence: "high"
|
| 273 |
+
},
|
| 274 |
+
orders: [
|
| 275 |
+
{
|
| 276 |
+
id: "81S_81S-25000172",
|
| 277 |
+
volume: 425,
|
| 278 |
+
input: 545,
|
| 279 |
+
output: 510,
|
| 280 |
+
yield: 93.58,
|
| 281 |
+
dates: { dispo: "2025-01-10" }
|
| 282 |
+
}
|
| 283 |
+
]
|
| 284 |
+
};
|
| 285 |
+
|
| 286 |
+
// Mock Full Data Response
|
| 287 |
+
export const mockFullDataResponse = [
|
| 288 |
+
{
|
| 289 |
+
PO_NO: "F0U0000866",
|
| 290 |
+
Article: "18006BA",
|
| 291 |
+
"Order Qty": 153,
|
| 292 |
+
"Reserver Qty as per Std Norms": 223,
|
| 293 |
+
"Actual Gr Opening": 278,
|
| 294 |
+
Deviation: 55,
|
| 295 |
+
Finish: "Soft",
|
| 296 |
+
Route: "Continouse",
|
| 297 |
+
Product: "Cotton Normal"
|
| 298 |
+
},
|
| 299 |
+
{
|
| 300 |
+
PO_NO: "FQT0001479",
|
| 301 |
+
Article: "A240B236HMF",
|
| 302 |
+
"Order Qty": 200,
|
| 303 |
+
"Reserver Qty as per Std Norms": 300,
|
| 304 |
+
"Actual Gr Opening": 313,
|
| 305 |
+
Deviation: 13,
|
| 306 |
+
Finish: "Peach",
|
| 307 |
+
Route: "Continouse",
|
| 308 |
+
Product: "Stretch Cotton"
|
| 309 |
+
}
|
| 310 |
+
];
|
| 311 |
+
|
| 312 |
+
// Mock Trends Response
|
| 313 |
+
export const mockTrendsResponse = {
|
| 314 |
+
articles: [
|
| 315 |
+
{
|
| 316 |
+
id: "18006BA",
|
| 317 |
+
name: "18006BA",
|
| 318 |
+
rank: 1,
|
| 319 |
+
count: 25,
|
| 320 |
+
volume: 150000,
|
| 321 |
+
yield: 93.5,
|
| 322 |
+
trend: "up",
|
| 323 |
+
shortfall: -5000,
|
| 324 |
+
shortfall_pct: -3.3,
|
| 325 |
+
success_rate: 92.0,
|
| 326 |
+
deviation: { avg: 5.2, std: 2.1, min: 1.5, max: 12.3 },
|
| 327 |
+
efficiency_score: 89.5,
|
| 328 |
+
risk_level: "low",
|
| 329 |
+
greige_issued: 165000,
|
| 330 |
+
greige_reserved: 160500,
|
| 331 |
+
norm_deviation: { absolute: 4500, percent: 2.8, over_allocated_pct: 65.0, under_allocated_pct: 35.0 },
|
| 332 |
+
waterfall: { demand: 150000, policy_gap: 10500, execution_adj: 4500, process_loss: -10650, delivered: 154350 },
|
| 333 |
+
blame: { policy_pct: 40.5, execution_pct: 17.3, process_pct: 42.2 },
|
| 334 |
+
compliance: { norm_compliance: 85.0, norm_reliability: 92.0 }
|
| 335 |
+
}
|
| 336 |
+
],
|
| 337 |
+
sale_orders: [],
|
| 338 |
+
po_numbers: [],
|
| 339 |
+
shades: [],
|
| 340 |
+
routes: [],
|
| 341 |
+
finishes: [],
|
| 342 |
+
customers: [],
|
| 343 |
+
segments: [],
|
| 344 |
+
counts: [],
|
| 345 |
+
products: [],
|
| 346 |
+
summary: {
|
| 347 |
+
total_articles: 496,
|
| 348 |
+
total_sale_orders: 970,
|
| 349 |
+
total_pos: 4613,
|
| 350 |
+
avg_yield: 94.2
|
| 351 |
+
}
|
| 352 |
+
};
|
| 353 |
+
|
| 354 |
+
// Mock Norm Entries
|
| 355 |
+
export const mockNormEntries: NormEntry[] = [
|
| 356 |
+
{
|
| 357 |
+
id: 1,
|
| 358 |
+
division_factor: "Dyed",
|
| 359 |
+
sub_type: "Peach",
|
| 360 |
+
composition: "Cotton",
|
| 361 |
+
count_range: "Below 40s",
|
| 362 |
+
route: "Continouse",
|
| 363 |
+
rules: {
|
| 364 |
+
upto_3000m: "7% or 100m",
|
| 365 |
+
above_3000m: "5% or 100m"
|
| 366 |
+
},
|
| 367 |
+
tolerance_adjustments: {
|
| 368 |
+
tolerance_3_percent: "1% Extra",
|
| 369 |
+
tolerance_5_7_percent: "2% Extra",
|
| 370 |
+
tolerance_10_percent: "5% Extra",
|
| 371 |
+
tolerance_plus0_minus3_5: "-1% Less",
|
| 372 |
+
tolerance_1_2_percent: "As per Std Norms"
|
| 373 |
+
}
|
| 374 |
+
},
|
| 375 |
+
{
|
| 376 |
+
id: 2,
|
| 377 |
+
division_factor: "Dyed",
|
| 378 |
+
sub_type: "Normal",
|
| 379 |
+
composition: "Cotton",
|
| 380 |
+
count_range: "40s and above",
|
| 381 |
+
route: "Continouse",
|
| 382 |
+
rules: {
|
| 383 |
+
upto_3000m: "5% or 100m",
|
| 384 |
+
above_3000m: "4% or 100m"
|
| 385 |
+
},
|
| 386 |
+
tolerance_adjustments: {
|
| 387 |
+
tolerance_3_percent: "1% Extra",
|
| 388 |
+
tolerance_5_7_percent: "2% Extra",
|
| 389 |
+
tolerance_10_percent: "5% Extra",
|
| 390 |
+
tolerance_plus0_minus3_5: "-1% Less",
|
| 391 |
+
tolerance_1_2_percent: "As per Std Norms"
|
| 392 |
+
}
|
| 393 |
+
},
|
| 394 |
+
{
|
| 395 |
+
id: 3,
|
| 396 |
+
division_factor: "RFD",
|
| 397 |
+
sub_type: "Peach/ Soft",
|
| 398 |
+
composition: "Cotton",
|
| 399 |
+
count_range: "Below 40s",
|
| 400 |
+
route: "Continouse",
|
| 401 |
+
rules: {
|
| 402 |
+
upto_3000m: "5% or 100m",
|
| 403 |
+
above_3000m: "3% or 100m"
|
| 404 |
+
},
|
| 405 |
+
tolerance_adjustments: {
|
| 406 |
+
tolerance_3_percent: "1% Extra",
|
| 407 |
+
tolerance_5_7_percent: "2% Extra",
|
| 408 |
+
tolerance_10_percent: "5% Extra",
|
| 409 |
+
tolerance_plus0_minus3_5: "-1% Less",
|
| 410 |
+
tolerance_1_2_percent: "As per Std Norms"
|
| 411 |
+
}
|
| 412 |
+
}
|
| 413 |
+
];
|
| 414 |
+
|
| 415 |
+
// Helper to create mock fetch response
|
| 416 |
+
export function createMockFetchResponse(data: any, ok = true) {
|
| 417 |
+
return {
|
| 418 |
+
ok,
|
| 419 |
+
json: async () => data,
|
| 420 |
+
status: ok ? 200 : 404
|
| 421 |
+
};
|
| 422 |
+
}
|
| 423 |
+
|
| 424 |
+
// Helper formulas matching backend
|
| 425 |
+
export const ManualCalculator = {
|
| 426 |
+
extra_gr_reserved_pct: (reserved: number, po_qty: number) =>
|
| 427 |
+
po_qty > 0 ? ((reserved - po_qty) / po_qty) * 100 : 0,
|
| 428 |
+
|
| 429 |
+
actual_gr_issue_pct: (issued: number, po_qty: number) =>
|
| 430 |
+
po_qty > 0 ? ((issued - po_qty) / po_qty) * 100 : 0,
|
| 431 |
+
|
| 432 |
+
shrinkage_pct: (issued: number, packing: number) =>
|
| 433 |
+
issued > 0 ? ((issued - packing) / issued) * 100 : 0,
|
| 434 |
+
|
| 435 |
+
fresh_pkg_pct: (pack_fresh: number, total_packing: number) =>
|
| 436 |
+
total_packing > 0 ? (pack_fresh / total_packing) * 100 : 0,
|
| 437 |
+
|
| 438 |
+
fresh_yield_pct: (pack_fresh: number, fresh_issued: number) =>
|
| 439 |
+
fresh_issued > 0 ? (pack_fresh / fresh_issued) * 100 : 0,
|
| 440 |
+
|
| 441 |
+
shortfall: (order_qty: number, pack_fresh: number) =>
|
| 442 |
+
order_qty - pack_fresh,
|
| 443 |
+
|
| 444 |
+
policy_impact: (reserved: number, demand: number) =>
|
| 445 |
+
reserved - demand,
|
| 446 |
+
|
| 447 |
+
execution_impact: (issued: number, reserved: number) =>
|
| 448 |
+
issued - reserved,
|
| 449 |
+
|
| 450 |
+
process_impact: (pack_fresh: number, issued: number) =>
|
| 451 |
+
pack_fresh - issued,
|
| 452 |
+
|
| 453 |
+
yield_rate: (pack_fresh: number, issued: number) =>
|
| 454 |
+
issued > 0 ? (pack_fresh / issued) * 100 : 0,
|
| 455 |
+
|
| 456 |
+
norm_score: (pack_fresh: number, demand: number) =>
|
| 457 |
+
demand > 0 ? (pack_fresh / demand) * 100 : 0
|
| 458 |
+
};
|
frontend/app/favicon.ico
ADDED
|
|
frontend/app/globals.css
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@import "tailwindcss";
|
| 2 |
+
|
| 3 |
+
:root {
|
| 4 |
+
--background: #ffffff;
|
| 5 |
+
--foreground: #171717;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
@theme inline {
|
| 9 |
+
--color-background: var(--background);
|
| 10 |
+
--color-foreground: var(--foreground);
|
| 11 |
+
--font-sans: var(--font-geist-sans);
|
| 12 |
+
--font-mono: var(--font-geist-mono);
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
@media (prefers-color-scheme: dark) {
|
| 16 |
+
:root {
|
| 17 |
+
--background: #0a0a0a;
|
| 18 |
+
--foreground: #ededed;
|
| 19 |
+
}
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
body {
|
| 23 |
+
background: var(--background);
|
| 24 |
+
color: var(--foreground);
|
| 25 |
+
font-family: Arial, Helvetica, sans-serif;
|
| 26 |
+
}
|
frontend/app/layout.tsx
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Metadata } from "next";
|
| 2 |
+
import { Geist, Geist_Mono } from "next/font/google";
|
| 3 |
+
import "./globals.css";
|
| 4 |
+
|
| 5 |
+
const geistSans = Geist({
|
| 6 |
+
variable: "--font-geist-sans",
|
| 7 |
+
subsets: ["latin"],
|
| 8 |
+
});
|
| 9 |
+
|
| 10 |
+
const geistMono = Geist_Mono({
|
| 11 |
+
variable: "--font-geist-mono",
|
| 12 |
+
subsets: ["latin"],
|
| 13 |
+
});
|
| 14 |
+
|
| 15 |
+
export const metadata: Metadata = {
|
| 16 |
+
title: "Create Next App",
|
| 17 |
+
description: "Generated by create next app",
|
| 18 |
+
};
|
| 19 |
+
|
| 20 |
+
export default function RootLayout({
|
| 21 |
+
children,
|
| 22 |
+
}: Readonly<{
|
| 23 |
+
children: React.ReactNode;
|
| 24 |
+
}>) {
|
| 25 |
+
return (
|
| 26 |
+
<html lang="en">
|
| 27 |
+
<body
|
| 28 |
+
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
| 29 |
+
suppressHydrationWarning
|
| 30 |
+
>
|
| 31 |
+
{children}
|
| 32 |
+
</body>
|
| 33 |
+
</html>
|
| 34 |
+
);
|
| 35 |
+
}
|