Implement approved/rejected quantity triggers and warehouse inventory

Database Triggers Implementation:
- Added automatic quantity calculation triggers for scanfg_orders
- Added automatic quantity calculation triggers for scan1_orders (T1 phase)
- Triggers calculate based on CP_base_code grouping (8 digits)
- Quality code: 0 = approved, != 0 = rejected
- Quantities set at insertion time (BEFORE INSERT trigger)
- Added create_triggers() function to initialize_db.py

Warehouse Inventory Enhancement:
- Analyzed old app database quantity calculation logic
- Created comprehensive trigger implementation guide
- Added trigger verification and testing procedures
- Documented data migration strategy

Documentation Added:
- APPROVED_REJECTED_QUANTITIES_ANALYSIS.md - Old app logic analysis
- DATABASE_TRIGGERS_IMPLEMENTATION.md - v2 implementation guide
- WAREHOUSE_INVENTORY_IMPLEMENTATION.md - Inventory view feature

Files Modified:
- initialize_db.py: Added create_triggers() function and call in main()
- Documentation: 3 comprehensive guides for database and inventory management

Quality Metrics:
- Triggers maintain legacy compatibility
- Automatic calculation ensures data consistency
- Performance optimized at database level
- Comprehensive testing documented
This commit is contained in:
Quality App Developer
2026-01-30 12:30:56 +02:00
parent b15cc93b9d
commit 07f77603eb
7 changed files with 2246 additions and 42 deletions

View File

@@ -0,0 +1,456 @@
# 📊 Approved & Rejected Quantities - Database Trigger Logic
**Date:** January 30, 2026
**Source:** Old Application Analysis
**Status:** ✅ Analysis Complete
**Critical for Migration:** Yes - This is automatic calculation logic
---
## 🎯 Overview
In the original application, **approved and rejected quantities are NOT user-entered values**. They are **automatically calculated and maintained by database triggers** that execute whenever a scan record is inserted.
This is a critical distinction for the migration - we need to replicate this logic in the v2 application.
---
## 🔑 Key Concepts
### Quality Code Values
```
quality_code = 0 → APPROVED ✅
quality_code = 1+ → REJECTED ❌ (any non-zero value)
```
### What Quantities Track
- **approved_quantity:** Count of approved scans for this CP_base_code (same CP base, quality_code = 0)
- **rejected_quantity:** Count of rejected scans for this CP_base_code (same CP base, quality_code != 0)
### Important Note
These are **counters aggregated by CP_base_code (8 digits)**, NOT by the full 15-character code!
---
## 🗄️ Database Schema (Old App)
### scan1_orders & scanfg_orders Tables
```sql
CREATE TABLE scan1_orders (
Id INT AUTO_INCREMENT PRIMARY KEY,
operator_code VARCHAR(4) NOT NULL, -- Who scanned (e.g., "OP01")
CP_full_code VARCHAR(15) NOT NULL UNIQUE, -- Full code (e.g., "CP00000001-0001")
OC1_code VARCHAR(4) NOT NULL, -- OC1 code (e.g., "OC01")
OC2_code VARCHAR(4) NOT NULL, -- OC2 code (e.g., "OC02")
CP_base_code VARCHAR(10) GENERATED ALWAYS AS (LEFT(CP_full_code, 10)) STORED, -- Auto-generated from CP_full_code
quality_code INT(3) NOT NULL, -- 0=Approved, 1+=Rejected
date DATE NOT NULL,
time TIME NOT NULL,
approved_quantity INT DEFAULT 0, -- Auto-calculated by trigger
rejected_quantity INT DEFAULT 0 -- Auto-calculated by trigger
);
CREATE TABLE scanfg_orders (
-- Same structure as scan1_orders
Id INT AUTO_INCREMENT PRIMARY KEY,
operator_code VARCHAR(4) NOT NULL,
CP_full_code VARCHAR(15) NOT NULL UNIQUE,
OC1_code VARCHAR(4) NOT NULL,
OC2_code VARCHAR(4) NOT NULL,
CP_base_code VARCHAR(10) GENERATED ALWAYS AS (LEFT(CP_full_code, 10)) STORED,
quality_code INT(3) NOT NULL,
date DATE NOT NULL,
time TIME NOT NULL,
approved_quantity INT DEFAULT 0,
rejected_quantity INT DEFAULT 0
);
```
### Important Detail: CP_base_code
**Generated Column:** `CP_base_code` is automatically extracted from the first 10 characters of `CP_full_code`
This means:
- When you insert: `CP00000001-0001`
- Automatically stored: `CP_base_code = CP00000001`
- Used in trigger: for grouping and counting
---
## 🔄 Trigger Logic (Old App)
### Trigger: `set_quantities_scan1` (for scan1_orders)
Executes **BEFORE INSERT** on each new row:
```sql
CREATE TRIGGER set_quantities_scan1
BEFORE INSERT ON scan1_orders
FOR EACH ROW
BEGIN
-- Step 1: Count how many APPROVED entries already exist for this CP_base_code
SET @approved = (SELECT COUNT(*) FROM scan1_orders
WHERE CP_base_code = LEFT(NEW.CP_full_code, 10)
AND quality_code = 0);
-- Step 2: Count how many REJECTED entries already exist for this CP_base_code
SET @rejected = (SELECT COUNT(*) FROM scan1_orders
WHERE CP_base_code = LEFT(NEW.CP_full_code, 10)
AND quality_code != 0);
-- Step 3: Add 1 to appropriate counter based on this new row's quality_code
IF NEW.quality_code = 0 THEN
-- This is an APPROVED scan
SET NEW.approved_quantity = @approved + 1;
SET NEW.rejected_quantity = @rejected;
ELSE
-- This is a REJECTED scan
SET NEW.approved_quantity = @approved;
SET NEW.rejected_quantity = @rejected + 1;
END IF;
END;
```
### Trigger: `set_quantities_fg` (for scanfg_orders)
**Identical logic** as `set_quantities_scan1` but for scanfg_orders table:
```sql
CREATE TRIGGER set_quantities_fg
BEFORE INSERT ON scanfg_orders
FOR EACH ROW
BEGIN
-- Count existing approved for this CP_base_code
SET @approved = (SELECT COUNT(*) FROM scanfg_orders
WHERE CP_base_code = LEFT(NEW.CP_full_code, 10)
AND quality_code = 0);
-- Count existing rejected for this CP_base_code
SET @rejected = (SELECT COUNT(*) FROM scanfg_orders
WHERE CP_base_code = LEFT(NEW.CP_full_code, 10)
AND quality_code != 0);
-- Add 1 to appropriate counter for this new row
IF NEW.quality_code = 0 THEN
SET NEW.approved_quantity = @approved + 1;
SET NEW.rejected_quantity = @rejected;
ELSE
SET NEW.approved_quantity = @approved;
SET NEW.rejected_quantity = @rejected + 1;
END IF;
END;
```
---
## 📊 Example Walkthrough
### Scenario: Scanning CP00000001 with Different Quality Codes
#### Initial State
```
scanfg_orders table is empty
```
#### Scan 1: CP00000001-0001, quality_code = 0 (APPROVED)
```
BEFORE INSERT trigger executes:
@approved = COUNT(*) WHERE CP_base_code = "CP00000001" AND quality_code = 0
= 0 (no existing records)
@rejected = COUNT(*) WHERE CP_base_code = "CP00000001" AND quality_code != 0
= 0 (no existing records)
NEW.quality_code = 0 (APPROVED)
→ Set NEW.approved_quantity = 0 + 1 = 1
→ Set NEW.rejected_quantity = 0
Record inserted:
Id | operator_code | CP_full_code | quality_code | approved_qty | rejected_qty
1 | OP01 | CP00000001-0001 | 0 | 1 | 0
```
#### Scan 2: CP00000001-0002, quality_code = 0 (APPROVED)
```
BEFORE INSERT trigger executes:
@approved = COUNT(*) WHERE CP_base_code = "CP00000001" AND quality_code = 0
= 1 (found Scan 1)
@rejected = COUNT(*) WHERE CP_base_code = "CP00000001" AND quality_code != 0
= 0
NEW.quality_code = 0 (APPROVED)
→ Set NEW.approved_quantity = 1 + 1 = 2
→ Set NEW.rejected_quantity = 0
Record inserted:
Id | operator_code | CP_full_code | quality_code | approved_qty | rejected_qty
2 | OP02 | CP00000001-0002 | 0 | 2 | 0
```
#### Scan 3: CP00000001-0003, quality_code = 2 (REJECTED)
```
BEFORE INSERT trigger executes:
@approved = COUNT(*) WHERE CP_base_code = "CP00000001" AND quality_code = 0
= 2 (found Scans 1 & 2)
@rejected = COUNT(*) WHERE CP_base_code = "CP00000001" AND quality_code != 0
= 0
NEW.quality_code = 2 (REJECTED, non-zero)
→ Set NEW.approved_quantity = 2
→ Set NEW.rejected_quantity = 0 + 1 = 1
Record inserted:
Id | operator_code | CP_full_code | quality_code | approved_qty | rejected_qty
3 | OP01 | CP00000001-0003 | 2 | 2 | 1
```
#### Scan 4: CP00000002-0001, quality_code = 0 (APPROVED)
```
BEFORE INSERT trigger executes:
@approved = COUNT(*) WHERE CP_base_code = "CP00000002" AND quality_code = 0
= 0 (different CP base code!)
@rejected = COUNT(*) WHERE CP_base_code = "CP00000002" AND quality_code != 0
= 0
NEW.quality_code = 0 (APPROVED)
→ Set NEW.approved_quantity = 0 + 1 = 1
→ Set NEW.rejected_quantity = 0
Record inserted:
Id | operator_code | CP_full_code | quality_code | approved_qty | rejected_qty
4 | OP03 | CP00000002-0001 | 0 | 1 | 0
```
#### Final Table State
```
CP00000001 group:
CP00000001-0001 (Approved, 0) → approved_qty=1, rejected_qty=0
CP00000001-0002 (Approved, 0) → approved_qty=2, rejected_qty=0
CP00000001-0003 (Rejected, 2) → approved_qty=2, rejected_qty=1
CP00000002 group:
CP00000002-0001 (Approved, 0) → approved_qty=1, rejected_qty=0
```
---
## 🔑 Critical Points
### 1. **Aggregation by CP_base_code (8 digits)**
Each record shows:
- How many approved scans exist for its CP base code
- How many rejected scans exist for its CP base code
It's **NOT** the count of just that specific full code!
### 2. **Trigger Runs on INSERT ONLY**
- Quantities are set when record is inserted
- They are **NOT** updated if other records are inserted later
- Each record's quantities represent the state AT THE TIME OF INSERTION
### 3. **Example Impact**
If you insert records in different order, quantities will differ:
**Order 1:** Insert Approved, then Rejected
```
Approved record: approved_qty=1, rejected_qty=0
Rejected record: approved_qty=1, rejected_qty=1 ← Includes the approved!
```
**Order 2:** Insert Rejected, then Approved
```
Rejected record: approved_qty=0, rejected_qty=1
Approved record: approved_qty=1, rejected_qty=1 ← Updated count
```
### 4. **Quality Code Interpretation**
- `quality_code = 0` → Approved ✅
- `quality_code != 0` → Rejected ❌ (could be 1, 2, 3, etc.)
The trigger counts ANY non-zero value as rejected.
---
## 🚀 Migration Approach
### Option 1: Use Database Triggers (Recommended)
**Pros:**
- Exact replica of old system behavior
- Automatic calculation
- Consistent with legacy data
- Performance optimized at DB level
**Cons:**
- Complex trigger logic
- Hard to debug
- Must match old behavior exactly
### Option 2: Calculate in Python
**Pros:**
- Easy to understand and debug
- Flexible logic
- Can add validation
**Cons:**
- Performance impact for high volume
- Must call calculation function on every insert
- Must ensure consistency
### Option 3: Store Pre-calculated Values (Batch)
**Pros:**
- Can cache results
- Fast queries
- Good for reporting
**Cons:**
- Data can become stale
- Requires batch update process
- Extra complexity
---
## 📋 Implementation Steps for v2
### Step 1: Create Generated Column
```sql
ALTER TABLE scanfg_orders ADD COLUMN
cp_base_code VARCHAR(10) GENERATED ALWAYS AS (SUBSTRING(CP_full_code, 1, 10)) STORED;
```
### Step 2: Create Trigger
Copy the `set_quantities_fg` trigger from old app, adjusted for new table structure
### Step 3: Test
Insert test records and verify quantities calculate correctly
### Step 4: Update Routes
Update FG Scan route to use quality_code properly:
- User selects "Approved" or "Rejected"
- System sets quality_code = 0 (approved) or quality_code = 1 (rejected)
- Trigger automatically sets quantities
---
## 🔍 Current v2 Status
### What We Have Now
- scanfg_orders table with box_id and location_id
- Manual quantity input (NOT automatic!)
### What We Need to Add
1. quality_code field interpretation (0 vs 1+)
2. Database triggers for automatic calculation
3. Update FG Scan form to capture quality status properly
4. Remove manual quantity entry from forms
---
## 📝 Database Differences: Old vs New
| Aspect | Old App | New v2 | Notes |
|--------|---------|--------|-------|
| CP_base_code | GENERATED ALWAYS | Manual? | Should also be GENERATED |
| Quantities | AUTO (trigger) | Manual | **NEEDS UPDATE** |
| Quality Code | 0/1+ system | Storing in DB | **GOOD** |
| Trigger Logic | Complex | N/A yet | Needs implementation |
| Multiple Suffixes | Yes (-0001, -0002) | Yes | Same structure |
---
## 🎯 Recommendation
**Implement database triggers** to automatically calculate approved/rejected quantities. This ensures:
1. ✅ Consistency with legacy data
2. ✅ Automatic calculation (no user entry needed)
3. ✅ Data integrity at database level
4. ✅ Performance (calculated once on insert)
5. ✅ Easy to audit (SQL-based logic)
---
## 📚 Related Tables
### Dependencies
- **scanfg_orders** ← Contains quality_code
- **scan1_orders** ← T1 phase (has same trigger)
- **boxes_crates** ← FK relationship
- **warehouse_locations** ← FK relationship
### Query Examples
**Get all scans with their aggregated quantities:**
```sql
SELECT
CP_full_code,
SUBSTRING(CP_full_code, 1, 10) as cp_base,
operator_code,
quality_code,
approved_quantity,
rejected_quantity,
date,
time
FROM scanfg_orders
ORDER BY created_at DESC;
```
**Verify trigger working correctly:**
```sql
-- All scans for CP base "CP00000001"
SELECT
CP_full_code,
quality_code,
approved_quantity,
rejected_quantity
FROM scanfg_orders
WHERE SUBSTRING(CP_full_code, 1, 10) = 'CP00000001'
ORDER BY created_at;
-- Should show:
-- - All rows with same approved_qty and rejected_qty for same CP_base
-- - Each new scan increments quantities correctly
```
---
## ✅ Checklist for v2 Implementation
- [ ] Add cp_base_code as GENERATED ALWAYS column
- [ ] Create set_quantities_fg trigger in v2
- [ ] Test trigger with sample inserts
- [ ] Update FG Scan form to capture quality status
- [ ] Update routes.py to set quality_code properly
- [ ] Remove manual quantity entry from frontend
- [ ] Verify migration data (recalculate quantities for existing records)
- [ ] Create documentation for team
- [ ] Test bulk imports
---
## 📞 Migration Notes
When migrating existing data from old app:
1. Old app quantities are CALCULATED and IMMUTABLE (set at insert time)
2. V2 should use same trigger logic
3. Existing records need trigger applied during migration
4. Test thoroughly with production data sample
---
**Status:** ✅ Analysis Complete
**Next Step:** Implement triggers in v2 application
**Priority:** HIGH - Affects data accuracy and reports

View File

@@ -0,0 +1,354 @@
# 🔄 Database Triggers Implementation for v2
**Date:** January 30, 2026
**Status:** ✅ Ready for Implementation
**Priority:** HIGH
---
## 📋 SQL Triggers for v2 scanfg_orders
### Current Situation
The v2 application has the scanfg_orders table but:
- ❌ No database triggers for automatic calculation
- ❌ CP_base_code not extracted automatically
- ❌ Quantities may be entered manually or not calculated
### Required Implementation
#### 1. Add Generated Column (if not present)
```sql
-- Check if cp_base_code column exists
SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'scanfg_orders' AND COLUMN_NAME = 'cp_base_code';
-- If not exists, add it:
ALTER TABLE scanfg_orders
ADD COLUMN cp_base_code VARCHAR(10)
GENERATED ALWAYS AS (SUBSTRING(CP_full_code, 1, 10)) STORED;
-- Add index for performance
CREATE INDEX idx_cp_base_code ON scanfg_orders(cp_base_code);
```
#### 2. Create Trigger for Automatic Quantity Calculation
```sql
-- Drop existing trigger if present
DROP TRIGGER IF EXISTS set_quantities_fg;
-- Create new trigger
CREATE TRIGGER set_quantities_fg
BEFORE INSERT ON scanfg_orders
FOR EACH ROW
BEGIN
-- Count how many APPROVED entries exist for this CP_base_code
SET @approved = (
SELECT COUNT(*) FROM scanfg_orders
WHERE SUBSTRING(CP_full_code, 1, 10) = SUBSTRING(NEW.CP_full_code, 1, 10)
AND quality_code = 0
);
-- Count how many REJECTED entries exist for this CP_base_code
SET @rejected = (
SELECT COUNT(*) FROM scanfg_orders
WHERE SUBSTRING(CP_full_code, 1, 10) = SUBSTRING(NEW.CP_full_code, 1, 10)
AND quality_code != 0
);
-- Set quantities based on this new row's quality_code
IF NEW.quality_code = 0 THEN
-- Approved scan: increment approved count
SET NEW.approved_quantity = @approved + 1;
SET NEW.rejected_quantity = @rejected;
ELSE
-- Rejected scan: increment rejected count
SET NEW.approved_quantity = @approved;
SET NEW.rejected_quantity = @rejected + 1;
END IF;
END;
```
#### 3. Same for scan1_orders (T1 Phase)
```sql
DROP TRIGGER IF EXISTS set_quantities_scan1;
CREATE TRIGGER set_quantities_scan1
BEFORE INSERT ON scan1_orders
FOR EACH ROW
BEGIN
SET @approved = (
SELECT COUNT(*) FROM scan1_orders
WHERE SUBSTRING(CP_full_code, 1, 10) = SUBSTRING(NEW.CP_full_code, 1, 10)
AND quality_code = 0
);
SET @rejected = (
SELECT COUNT(*) FROM scan1_orders
WHERE SUBSTRING(CP_full_code, 1, 10) = SUBSTRING(NEW.CP_full_code, 1, 10)
AND quality_code != 0
);
IF NEW.quality_code = 0 THEN
SET NEW.approved_quantity = @approved + 1;
SET NEW.rejected_quantity = @rejected;
ELSE
SET NEW.approved_quantity = @approved;
SET NEW.rejected_quantity = @rejected + 1;
END IF;
END;
```
---
## 🔍 Verification Queries
### Check if Triggers Exist
```sql
SELECT TRIGGER_NAME, TRIGGER_SCHEMA, TRIGGER_TABLE, ACTION_STATEMENT
FROM INFORMATION_SCHEMA.TRIGGERS
WHERE TRIGGER_SCHEMA = 'quality_app_v2'
AND TRIGGER_TABLE IN ('scanfg_orders', 'scan1_orders');
```
### Verify Trigger is Working
```sql
-- Insert test record
INSERT INTO scanfg_orders (operator_code, CP_full_code, OC1_code, OC2_code, quality_code, date, time)
VALUES ('OP01', 'CP00000001-0001', 'OC01', 'OC02', 0, CURDATE(), CURTIME());
-- Check if quantities were set automatically
SELECT Id, CP_full_code, quality_code, approved_quantity, rejected_quantity
FROM scanfg_orders
WHERE CP_full_code = 'CP00000001-0001';
-- Should show: approved_quantity = 1, rejected_quantity = 0
```
### Full Test Scenario
```sql
-- Step 1: Insert approved record
INSERT INTO scanfg_orders (operator_code, CP_full_code, OC1_code, OC2_code, quality_code, date, time)
VALUES ('OP01', 'CP00000001-0001', 'OC01', 'OC02', 0, CURDATE(), CURTIME());
-- Expected: approved_qty=1, rejected_qty=0
-- Step 2: Insert another approved record
INSERT INTO scanfg_orders (operator_code, CP_full_code, OC1_code, OC2_code, quality_code, date, time)
VALUES ('OP02', 'CP00000001-0002', 'OC01', 'OC02', 0, CURDATE(), CURTIME());
-- Expected: approved_qty=2, rejected_qty=0
-- Step 3: Insert rejected record
INSERT INTO scanfg_orders (operator_code, CP_full_code, OC1_code, OC2_code, quality_code, date, time)
VALUES ('OP01', 'CP00000001-0003', 'OC01', 'OC02', 2, CURDATE(), CURTIME());
-- Expected: approved_qty=2, rejected_qty=1
-- Verify all records
SELECT CP_full_code, quality_code, approved_quantity, rejected_quantity
FROM scanfg_orders
WHERE SUBSTRING(CP_full_code, 1, 10) = 'CP00000001'
ORDER BY Id;
```
---
## 🔧 Integration Points in Python Code
### 1. Database Initialization (initialize_db.py)
Add trigger creation to the database setup:
```python
def create_scan_triggers():
"""Create triggers for automatic quantity calculation"""
try:
conn = get_db()
cursor = conn.cursor()
# Drop existing triggers
cursor.execute("DROP TRIGGER IF EXISTS set_quantities_fg")
cursor.execute("DROP TRIGGER IF EXISTS set_quantities_scan1")
# Create scanfg_orders trigger
cursor.execute("""
CREATE TRIGGER set_quantities_fg
BEFORE INSERT ON scanfg_orders
FOR EACH ROW
BEGIN
SET @approved = (SELECT COUNT(*) FROM scanfg_orders
WHERE SUBSTRING(CP_full_code, 1, 10) = SUBSTRING(NEW.CP_full_code, 1, 10)
AND quality_code = 0);
SET @rejected = (SELECT COUNT(*) FROM scanfg_orders
WHERE SUBSTRING(CP_full_code, 1, 10) = SUBSTRING(NEW.CP_full_code, 1, 10)
AND quality_code != 0);
IF NEW.quality_code = 0 THEN
SET NEW.approved_quantity = @approved + 1;
SET NEW.rejected_quantity = @rejected;
ELSE
SET NEW.approved_quantity = @approved;
SET NEW.rejected_quantity = @rejected + 1;
END IF;
END
""")
logger.info("✓ Trigger 'set_quantities_fg' created")
# Create scan1_orders trigger (similar)
cursor.execute("""
CREATE TRIGGER set_quantities_scan1
BEFORE INSERT ON scan1_orders
FOR EACH ROW
BEGIN
SET @approved = (SELECT COUNT(*) FROM scan1_orders
WHERE SUBSTRING(CP_full_code, 1, 10) = SUBSTRING(NEW.CP_full_code, 1, 10)
AND quality_code = 0);
SET @rejected = (SELECT COUNT(*) FROM scan1_orders
WHERE SUBSTRING(CP_full_code, 1, 10) = SUBSTRING(NEW.CP_full_code, 1, 10)
AND quality_code != 0);
IF NEW.quality_code = 0 THEN
SET NEW.approved_quantity = @approved + 1;
SET NEW.rejected_quantity = @rejected;
ELSE
SET NEW.approved_quantity = @approved;
SET NEW.rejected_quantity = @rejected + 1;
END IF;
END
""")
logger.info("✓ Trigger 'set_quantities_scan1' created")
conn.commit()
cursor.close()
return True
except Exception as e:
logger.error(f"✗ Error creating triggers: {e}")
return False
```
### 2. FG Scan Form (fg_scan.html)
Ensure quality_code is set correctly:
```python
# In routes.py fg_scan endpoint
quality_status = request.form.get('quality_code', '0') # From form
# Map user input to quality_code
if quality_status.lower() in ['approved', '0']:
quality_code = 0 # Approved
else:
quality_code = 1 # Rejected (or 2, 3, etc.)
# Insert record (trigger will auto-calculate quantities)
cursor.execute("""
INSERT INTO scanfg_orders
(operator_code, CP_full_code, OC1_code, OC2_code, quality_code, date, time, box_id, location_id)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
""", (
operator_code, cp_full_code, oc1_code, oc2_code, quality_code,
date, time, box_id, location_id
))
# quantities are automatically set by trigger!
```
### 3. Warehouse Inventory Display
The quantities are now automatically available:
```sql
-- In warehouse.py get_cp_inventory_list()
SELECT
s.CP_full_code,
SUBSTRING(s.CP_full_code, 1, 10) as cp_base,
SUM(s.approved_quantity) as total_approved, -- Auto-calculated
SUM(s.rejected_quantity) as total_rejected, -- Auto-calculated
...
FROM scanfg_orders s
...
```
---
## 📊 Data Migration
For existing records in the database:
```sql
-- Recalculate quantities for all existing records
UPDATE scanfg_orders s1
SET
approved_quantity = (
SELECT COUNT(*) FROM scanfg_orders s2
WHERE SUBSTRING(s2.CP_full_code, 1, 10) = SUBSTRING(s1.CP_full_code, 1, 10)
AND s2.quality_code = 0
AND s2.Id <= s1.Id -- Only count up to current record
),
rejected_quantity = (
SELECT COUNT(*) FROM scanfg_orders s2
WHERE SUBSTRING(s2.CP_full_code, 1, 10) = SUBSTRING(s1.CP_full_code, 1, 10)
AND s2.quality_code != 0
AND s2.Id <= s1.Id -- Only count up to current record
);
```
---
## ✅ Checklist
- [ ] Add cp_base_code generated column to scanfg_orders
- [ ] Add cp_base_code generated column to scan1_orders
- [ ] Create set_quantities_fg trigger
- [ ] Create set_quantities_scan1 trigger
- [ ] Test with sample inserts
- [ ] Verify trigger working correctly
- [ ] Update initialize_db.py to create triggers
- [ ] Update db_schema_verifier.py to verify triggers exist
- [ ] Test with production-like data volume
- [ ] Document for team
- [ ] Deploy to production
---
## 🚀 Execution Steps
### Step 1: Test Locally
```bash
# Connect to test database
mysql -h localhost -u root -p quality_app_v2
# Run verification query
SELECT TRIGGER_NAME FROM INFORMATION_SCHEMA.TRIGGERS
WHERE TRIGGER_TABLE IN ('scanfg_orders', 'scan1_orders');
```
### Step 2: Add to Schema Verifier
Update `db_schema_verifier.py` to check and recreate triggers if missing
### Step 3: Update initialize_db.py
Add trigger creation to database initialization sequence
### Step 4: Deploy
- Restart application
- Verify triggers created in database
- Test with new FG scan entries
---
## 📝 Notes
- Triggers execute **BEFORE INSERT** (before record is written to DB)
- Quantities are **immutable** after insertion (set once)
- Grouping is by **CP_base_code** (8 digits), not full code
- Compatible with existing data and warehouse features
- Maintains consistency with legacy application behavior
---
**Priority:** HIGH
**Effort:** MEDIUM
**Impact:** Data Accuracy, Report Correctness

View File

@@ -0,0 +1,499 @@
# 🏭 Warehouse Inventory - CP Articles View Implementation
**Date:** January 30, 2026
**Status:** ✅ Implemented and Deployed
**Feature:** CP Article Inventory with Box & Location Tracking
---
## 📋 Overview
Implemented a comprehensive warehouse inventory view that allows users to:
- View all CP articles scanned in the FG Scan module
- Track which boxes contain specific CP codes
- View warehouse locations for each box
- Search by CP code (8 digits or full 15-character code)
- Search by box number
- View detailed information for each CP code variation
- Filter and sort entries with latest entries displayed first
---
## 🗄️ Database Structure Understanding
### scanfg_orders Table (Extended Schema)
```sql
CREATE TABLE scanfg_orders (
id INT AUTO_INCREMENT PRIMARY KEY,
operator_code VARCHAR(50), -- Person who scanned (e.g., "OP01")
CP_full_code VARCHAR(50), -- Full CP code (e.g., "CP00000001-0001")
OC1_code VARCHAR(50), -- OC1 code
OC2_code VARCHAR(50), -- OC2 code
quality_code VARCHAR(10), -- Quality status (0=Rejected, 1=Approved)
date DATE, -- Scan date
time TIME, -- Scan time
approved_quantity INT, -- Approved count
rejected_quantity INT, -- Rejected count
box_id BIGINT, -- FK: Reference to boxes_crates table
location_id BIGINT, -- FK: Reference to warehouse_locations table
created_at TIMESTAMP,
-- Indexes for fast querying
INDEX idx_cp_code (CP_full_code),
INDEX idx_operator (operator_code),
INDEX idx_date (date),
INDEX idx_box_id (box_id),
INDEX idx_location_id (location_id),
-- Foreign Keys
FOREIGN KEY (box_id) REFERENCES boxes_crates(id) ON DELETE SET NULL,
FOREIGN KEY (location_id) REFERENCES warehouse_locations(id) ON DELETE SET NULL
);
```
### CP Code Structure
From the old application and current implementation:
- **CP Base (8 digits):** `CP00000001`
- Used to group related entries
- Represents the core product/order reference
- **CP Full Code (15 characters):** `CP00000001-0001`
- Includes 4-digit suffix after hyphen
- Can have multiple entries (e.g., `-0001`, `-0002`, `-0003`)
- Different suffixes can be in different boxes and locations
### Relationships
```
scanfg_orders (many)
├─→ boxes_crates (one)
│ └─→ warehouse_locations (one)
└─→ warehouse_locations (one) [Direct location assignment]
```
---
## ✨ Features Implemented
### 1. Backend Functions (warehouse.py)
#### `get_cp_inventory_list(limit=100, offset=0)`
- Returns all CP articles with aggregated data
- Groups by CP base code and box
- Shows latest entries first
- Returns: box_number, location_code, total_entries, total_approved, total_rejected, latest_date, latest_time
#### `search_cp_code(cp_code_search)`
- Searches by full CP code or CP base (8 digits)
- Handles hyphen-separated format
- Returns all matching entries with box and location info
#### `search_by_box_number(box_number_search)`
- Finds all CP codes in a specific box
- Shows operator, quality code, quantities
- Returns full details for each entry
#### `get_cp_details(cp_code)`
- Gets all variations of a CP code (all suffixes)
- Shows each entry's operator, quality status, box, location
- Useful for traceability and detailed audit trail
### 2. API Endpoints (routes.py)
#### `GET /warehouse/api/cp-inventory`
- List all CP inventory items
- Pagination support (limit, offset)
- Response includes count and metadata
**Example Response:**
```json
{
"success": true,
"inventory": [
{
"id": 1,
"CP_full_code": "CP00000001-0001",
"cp_base": "CP00000001",
"total_entries": 1,
"box_id": 5,
"box_number": "BOX001",
"location_code": "FG_INCOMING",
"total_approved": 10,
"total_rejected": 0,
"latest_date": "2026-01-30",
"latest_time": "14:30:15"
}
],
"count": 25,
"limit": 500,
"offset": 0
}
```
#### `POST /warehouse/api/search-cp`
- Search by CP code
- Accepts: `{ "cp_code": "CP00000001" }`
- Returns matching entries grouped by box
#### `POST /warehouse/api/search-cp-box`
- Search by box number
- Accepts: `{ "box_number": "BOX001" }`
- Returns all CP entries in that specific box
#### `GET /warehouse/api/cp-details/<cp_code>`
- Get detailed info for a CP code (8 digits)
- Shows all variations with different suffixes
- Returns all scans, locations, and box assignments
### 3. Frontend Interface (inventory.html)
#### Search Section
- **CP Code Search:** Enter full code or base code
- Example: "CP00000001" or "CP00000001-0001"
- Wildcard search for partial matches
- **Box Number Search:** Find all CP codes in a box
- Example: "BOX001"
- Shows all related entries
#### Results Table
Columns displayed (sorted by latest first):
- **CP Code (Base):** Base 8-digit CP code (badge)
- **CP Full Code:** Complete 15-character code with suffix
- **Box Number:** Which box contains this item
- **Location:** Warehouse location code
- **Total Entries:** How many variations exist
- **Approved Qty:** Total approved quantity
- **Rejected Qty:** Total rejected quantity
- **Latest Date:** Most recent scan date
- **Latest Time:** Most recent scan time
- **Actions:** View details button
#### Detail Modal
- Displays all variations of a CP code
- Shows detailed table with:
- CP Full Code
- Operator Code
- Quality status (Approved/Rejected)
- Box assignment
- Location
- Scan date and time
---
## 🔄 Data Flow
### 1. CP Code Entry Flow
```
FG Scan Module (/quality/fg-scan)
User scans CP code (CP + 8 digits + hyphen + 4 digits)
Scan saved to scanfg_orders table
Optional: Assign to box (box_id)
Optional: Update location (location_id)
```
### 2. Inventory Query Flow
```
User visits /warehouse/inventory
loadInventory() called (JavaScript)
Fetch /warehouse/api/cp-inventory
Database aggregates all scanfg_orders entries
Groups by CP base + box
Returns sorted by latest date (DESC)
Render in table with all details
```
### 3. Search Flow
```
User searches for CP code "CP00000001"
JavaScript sends POST to /warehouse/api/search-cp
Backend searches for REPLACE(CP_full_code, '-', '') LIKE 'CP00000001%'
Returns all matching entries
Frontend renders results table
User can click "View Details" for each entry
```
---
## 📊 Query Examples
### Get all CP inventory grouped by base code and box
```sql
SELECT
s.CP_full_code,
SUBSTRING(s.CP_full_code, 1, 10) as cp_base,
COUNT(*) as total_entries,
s.box_id,
bc.box_number,
wl.location_code,
MAX(s.date) as latest_date,
MAX(s.time) as latest_time,
SUM(s.approved_quantity) as total_approved,
SUM(s.rejected_quantity) as total_rejected
FROM scanfg_orders s
LEFT JOIN boxes_crates bc ON s.box_id = bc.id
LEFT JOIN warehouse_locations wl ON s.location_id = wl.id
GROUP BY SUBSTRING(s.CP_full_code, 1, 10), s.box_id
ORDER BY MAX(s.created_at) DESC;
```
### Search for specific CP code
```sql
SELECT * FROM scanfg_orders
WHERE REPLACE(CP_full_code, '-', '') LIKE 'CP00000001%'
ORDER BY created_at DESC;
```
### Find all CP codes in a box
```sql
SELECT s.*, bc.box_number, wl.location_code
FROM scanfg_orders s
LEFT JOIN boxes_crates bc ON s.box_id = bc.id
LEFT JOIN warehouse_locations wl ON s.location_id = wl.id
WHERE bc.box_number LIKE '%BOX001%'
ORDER BY s.created_at DESC;
```
---
## 🚀 Usage Guide
### Accessing the Inventory View
1. Navigate to Warehouse Module
2. Click "Inventory" option
3. Page loads with all CP articles (latest first)
### Searching by CP Code
1. Enter CP code in "Search by CP Code" field
- Can enter: "CP00000001" or "CP00000001-0001"
- Can enter partial: "CP000" for wildcard search
2. Click "Search CP" button
3. Results update in real-time
### Searching by Box Number
1. Enter box number in "Search by Box Number" field
- Example: "BOX001"
2. Click "Search Box" button
3. View all CP codes in that box
### Viewing CP Details
1. Click the "eye" icon in the Actions column
2. Modal opens showing:
- All variations of that CP code
- Operator who scanned each one
- Quality status for each
- Box and location assignments
- Detailed timestamp info
### Clearing Searches
1. Click "Clear" button next to search field
2. Returns to full inventory view
3. Shows all latest entries
---
## 📈 Performance Considerations
### Indexes Used
- `idx_cp_code` on CP_full_code
- `idx_box_id` for box lookups
- `idx_location_id` for location filtering
- `idx_date` for date-range queries
- `idx_operator` for operator tracking
### Query Optimization
- Group aggregation reduces result set size
- Indexes on foreign keys for fast joins
- Limit 500 default to prevent large transfers
- Latest entries first using created_at DESC
### Scalability
For large datasets (100,000+ records):
- Consider table partitioning by date
- Archive old scans to separate table
- Materialized views for common reports
- Consider caching for frequently searched CPs
---
## 🔐 Security & Permissions
### Access Control
- User must be logged in (session check)
- Returns 401 Unauthorized if not authenticated
- All routes check `if 'user_id' not in session`
### Data Validation
- CP code search minimum 2 characters
- Box number search requires non-empty string
- Pagination limits (max 1000 records)
- SQL injection prevented via parameterized queries
---
## 📋 Implementation Details
### Files Modified
1. **app/modules/warehouse/warehouse.py**
- Added 4 new functions for CP inventory operations
- Added pymysql import for cursor operations
2. **app/modules/warehouse/routes.py**
- Updated imports to include new warehouse functions
- Added 4 new API endpoints
- Integrated authentication checks
3. **app/templates/modules/warehouse/inventory.html**
- Complete rewrite with interactive interface
- Added JavaScript for real-time search
- Added detail modal for viewing CP variations
- Responsive design with Bootstrap styling
### Code Statistics
- **Backend Functions:** 4 new functions
- **API Endpoints:** 4 new routes
- **Frontend Lines:** 600+ lines (HTML + CSS + JS)
- **Total Code Added:** ~800 lines
---
## ✅ Testing Checklist
- [x] Backend functions execute without errors
- [x] API endpoints return proper JSON responses
- [x] Search by CP code works with full and partial codes
- [x] Search by box number finds all related CP entries
- [x] Latest entries display first (ORDER BY DESC)
- [x] CP details modal shows all variations
- [x] Pagination works with limit/offset
- [x] Error messages display properly
- [x] Loading indicators appear during API calls
- [x] Authentication checks work
- [x] Database joins with boxes_crates and warehouse_locations
- [x] Status messages show search results count
---
## 🔄 Integration Points
### With FG Scan Module
- Reads scanfg_orders entries created by FG Scan
- Shows CP codes as they are scanned
- Tracks box assignments from assign_cp_to_box feature
### With Warehouse Module
- Uses warehouse_locations for location display
- Uses boxes_crates for box information
- Part of warehouse management workflow
### With Quality Module
- Shows quality_code status (approved/rejected)
- Tracks operator who scanned each item
- Provides quality metrics
---
## 🚀 Future Enhancements
1. **Export/Download**
- Export search results to CSV/Excel
- Print inventory reports
2. **Advanced Filtering**
- Filter by date range
- Filter by quality status (approved/rejected)
- Filter by operator code
- Filter by location
3. **Analytics**
- Generate warehouse occupancy reports
- CP code aging (how long in warehouse)
- Location utilization statistics
4. **Real-Time Updates**
- WebSocket for live inventory updates
- Automatic refresh when new scans added
- Real-time location change notifications
5. **Barcode Generation**
- Generate barcodes for CP codes
- Generate warehouse location labels
- Print bulk labels for organization
6. **Mobile Interface**
- Responsive inventory lookup
- Mobile-optimized search
- QR code scanning support
---
## 📞 Support & Troubleshooting
### Common Issues
**Issue:** No results when searching
- **Solution:** Check CP code format (must include "CP")
- Ensure items exist in scanfg_orders table
- Try searching for partial CP code
**Issue:** Box shows as "No Box"
- **Solution:** Item may not be assigned to box yet
- Check box_id field in scanfg_orders
- Assign to box through FG Scan assign feature
**Issue:** Location shows as "No Location"
- **Solution:** Box may not have location assigned
- Assign location in warehouse locations module
- Update box location through inventory interface
**Issue:** Database errors
- **Solution:** Ensure boxes_crates table exists
- Ensure warehouse_locations table exists
- Check database connection parameters
---
## 📝 Documentation Files
Related documentation:
- [SCANFG_ORDERS_BOX_TRACKING.md](SCANFG_ORDERS_BOX_TRACKING.md) - Box tracking details
- [BOXES_IMPLEMENTATION_DETAILS.md](BOXES_IMPLEMENTATION_DETAILS.md) - Box feature docs
- [TRACEABILITY_WORKFLOW_ANALYSIS.md](TRACEABILITY_WORKFLOW_ANALYSIS.md) - CP traceability flow
---
## ✨ Summary
The Warehouse Inventory CP Articles view provides a complete solution for:
- **Viewing:** All CP articles scanned in FG module
- **Tracking:** Which boxes and locations contain each CP
- **Searching:** Quick lookup by CP code or box number
- **Analyzing:** Detailed information for traceability
- **Managing:** Latest entries displayed for efficient warehouse operations
This feature bridges the gap between FG Scanning and warehouse operations, enabling complete product traceability from scan to storage location.
---
**Last Updated:** January 30, 2026
**Version:** 1.0
**Status:** Production Ready ✅