Spaces:
Sleeping
Sleeping
Implemented improved inventory requirements system with API endpoints, validation, and comprehensive documentation.
Browse files- docs/examples/inventory_requirements_setup.py +346 -0
- docs/features/INVENTORY_REQUIREMENTS.md +263 -0
- docs/features/INVENTORY_REQUIREMENTS_CHANGELOG.md +166 -0
- docs/features/INVENTORY_REQUIREMENTS_FLOW.md +0 -0
- docs/features/INVENTORY_REQUIREMENTS_QUICK_REF.md +181 -0
- src/app/api/v1/projects.py +185 -0
- src/app/models/project.py +25 -2
- src/app/schemas/project.py +27 -6
- src/app/services/inventory_requirements_service.py +209 -0
- src/app/services/inventory_service.py +41 -2
- src/app/services/ticket_completion_service.py +31 -3
docs/examples/inventory_requirements_setup.py
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Example: Setting up Inventory Requirements for a Project
|
| 3 |
+
|
| 4 |
+
This script demonstrates how to configure inventory requirements
|
| 5 |
+
for a typical FTTH (Fiber to the Home) installation project.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
# Example 1: FTTH Installation Project
|
| 9 |
+
ftth_inventory_requirements = {
|
| 10 |
+
# Equipment - Installed at customer sites
|
| 11 |
+
"ONT-ZTE-F670L": {
|
| 12 |
+
"code": "ONT-ZTE-F670L",
|
| 13 |
+
"name": "ZTE F670L ONT Device",
|
| 14 |
+
"description": "Fiber optic network terminal with 4 GE ports and WiFi",
|
| 15 |
+
"usage_type": "installed",
|
| 16 |
+
"unit": "pieces",
|
| 17 |
+
"requires_serial_number": True,
|
| 18 |
+
"category": "Equipment",
|
| 19 |
+
"include_in_completion": True,
|
| 20 |
+
"completion_field_label": "ONT Serial Number",
|
| 21 |
+
"completion_required": True
|
| 22 |
+
},
|
| 23 |
+
"ONT-HUAWEI-HG8546M": {
|
| 24 |
+
"code": "ONT-HUAWEI-HG8546M",
|
| 25 |
+
"name": "Huawei HG8546M ONT",
|
| 26 |
+
"description": "GPON ONT with 4 GE ports, 2 POTS, WiFi",
|
| 27 |
+
"usage_type": "installed",
|
| 28 |
+
"unit": "pieces",
|
| 29 |
+
"requires_serial_number": True,
|
| 30 |
+
"category": "Equipment",
|
| 31 |
+
"include_in_completion": True,
|
| 32 |
+
"completion_field_label": "ONT Serial Number",
|
| 33 |
+
"completion_required": True
|
| 34 |
+
},
|
| 35 |
+
"ROUTER-TP-C6": {
|
| 36 |
+
"code": "ROUTER-TP-C6",
|
| 37 |
+
"name": "TP-Link Archer C6 Router",
|
| 38 |
+
"description": "AC1200 Dual-band wireless router",
|
| 39 |
+
"usage_type": "installed",
|
| 40 |
+
"unit": "pieces",
|
| 41 |
+
"requires_serial_number": True,
|
| 42 |
+
"category": "Equipment",
|
| 43 |
+
"include_in_completion": True,
|
| 44 |
+
"completion_field_label": "Router Serial Number",
|
| 45 |
+
"completion_required": False # Optional if ONT has WiFi
|
| 46 |
+
},
|
| 47 |
+
|
| 48 |
+
# Cables - Consumed during installation
|
| 49 |
+
"CABLE-FIBER-SM-DROP": {
|
| 50 |
+
"code": "CABLE-FIBER-SM-DROP",
|
| 51 |
+
"name": "Single Mode Fiber Drop Cable",
|
| 52 |
+
"description": "Outdoor drop cable from pole to customer premises",
|
| 53 |
+
"usage_type": "consumed",
|
| 54 |
+
"unit": "meters",
|
| 55 |
+
"requires_serial_number": False,
|
| 56 |
+
"category": "Cable",
|
| 57 |
+
"include_in_completion": False
|
| 58 |
+
},
|
| 59 |
+
"CABLE-FIBER-SM-INDOOR": {
|
| 60 |
+
"code": "CABLE-FIBER-SM-INDOOR",
|
| 61 |
+
"name": "Single Mode Indoor Fiber Cable",
|
| 62 |
+
"description": "Indoor fiber cable for in-home routing",
|
| 63 |
+
"usage_type": "consumed",
|
| 64 |
+
"unit": "meters",
|
| 65 |
+
"requires_serial_number": False,
|
| 66 |
+
"category": "Cable",
|
| 67 |
+
"include_in_completion": False
|
| 68 |
+
},
|
| 69 |
+
"CABLE-CAT6": {
|
| 70 |
+
"code": "CABLE-CAT6",
|
| 71 |
+
"name": "Cat6 Ethernet Cable",
|
| 72 |
+
"description": "Category 6 UTP cable for LAN connections",
|
| 73 |
+
"usage_type": "consumed",
|
| 74 |
+
"unit": "meters",
|
| 75 |
+
"requires_serial_number": False,
|
| 76 |
+
"category": "Cable",
|
| 77 |
+
"include_in_completion": False
|
| 78 |
+
},
|
| 79 |
+
|
| 80 |
+
# Connectors & Consumables
|
| 81 |
+
"CONNECTOR-SC-APC": {
|
| 82 |
+
"code": "CONNECTOR-SC-APC",
|
| 83 |
+
"name": "SC/APC Fiber Connector",
|
| 84 |
+
"description": "Single mode SC/APC connector for termination",
|
| 85 |
+
"usage_type": "consumed",
|
| 86 |
+
"unit": "pieces",
|
| 87 |
+
"requires_serial_number": False,
|
| 88 |
+
"category": "Consumables",
|
| 89 |
+
"include_in_completion": False
|
| 90 |
+
},
|
| 91 |
+
"CONNECTOR-LC-UPC": {
|
| 92 |
+
"code": "CONNECTOR-LC-UPC",
|
| 93 |
+
"name": "LC/UPC Fiber Connector",
|
| 94 |
+
"description": "Single mode LC/UPC connector",
|
| 95 |
+
"usage_type": "consumed",
|
| 96 |
+
"unit": "pieces",
|
| 97 |
+
"requires_serial_number": False,
|
| 98 |
+
"category": "Consumables",
|
| 99 |
+
"include_in_completion": False
|
| 100 |
+
},
|
| 101 |
+
"CONNECTOR-RJ45": {
|
| 102 |
+
"code": "CONNECTOR-RJ45",
|
| 103 |
+
"name": "RJ45 Ethernet Connector",
|
| 104 |
+
"description": "Cat6 RJ45 connector for termination",
|
| 105 |
+
"usage_type": "consumed",
|
| 106 |
+
"unit": "pieces",
|
| 107 |
+
"requires_serial_number": False,
|
| 108 |
+
"category": "Consumables",
|
| 109 |
+
"include_in_completion": False
|
| 110 |
+
},
|
| 111 |
+
|
| 112 |
+
# Installation Materials
|
| 113 |
+
"FASTENER-CABLE-CLIP": {
|
| 114 |
+
"code": "FASTENER-CABLE-CLIP",
|
| 115 |
+
"name": "Cable Clips",
|
| 116 |
+
"description": "Plastic clips for securing drop cable",
|
| 117 |
+
"usage_type": "consumed",
|
| 118 |
+
"unit": "pieces",
|
| 119 |
+
"requires_serial_number": False,
|
| 120 |
+
"category": "Installation Materials",
|
| 121 |
+
"include_in_completion": False
|
| 122 |
+
},
|
| 123 |
+
"FASTENER-WALL-ANCHOR": {
|
| 124 |
+
"code": "FASTENER-WALL-ANCHOR",
|
| 125 |
+
"name": "Wall Anchors",
|
| 126 |
+
"description": "Plastic wall anchors for mounting",
|
| 127 |
+
"usage_type": "consumed",
|
| 128 |
+
"unit": "pieces",
|
| 129 |
+
"requires_serial_number": False,
|
| 130 |
+
"category": "Installation Materials",
|
| 131 |
+
"include_in_completion": False
|
| 132 |
+
},
|
| 133 |
+
"TAPE-DUCT": {
|
| 134 |
+
"code": "TAPE-DUCT",
|
| 135 |
+
"name": "Duct Tape",
|
| 136 |
+
"description": "Heavy-duty duct tape for cable management",
|
| 137 |
+
"usage_type": "consumed",
|
| 138 |
+
"unit": "rolls",
|
| 139 |
+
"requires_serial_number": False,
|
| 140 |
+
"category": "Installation Materials",
|
| 141 |
+
"include_in_completion": False
|
| 142 |
+
},
|
| 143 |
+
|
| 144 |
+
# Power & Protection
|
| 145 |
+
"ADAPTER-POWER-12V": {
|
| 146 |
+
"code": "ADAPTER-POWER-12V",
|
| 147 |
+
"name": "12V Power Adapter",
|
| 148 |
+
"description": "12V DC power adapter for ONT",
|
| 149 |
+
"usage_type": "installed",
|
| 150 |
+
"unit": "pieces",
|
| 151 |
+
"requires_serial_number": False,
|
| 152 |
+
"category": "Power",
|
| 153 |
+
"include_in_completion": False
|
| 154 |
+
},
|
| 155 |
+
"PROTECTOR-SURGE": {
|
| 156 |
+
"code": "PROTECTOR-SURGE",
|
| 157 |
+
"name": "Surge Protector",
|
| 158 |
+
"description": "Electrical surge protector",
|
| 159 |
+
"usage_type": "installed",
|
| 160 |
+
"unit": "pieces",
|
| 161 |
+
"requires_serial_number": False,
|
| 162 |
+
"category": "Protection",
|
| 163 |
+
"include_in_completion": False
|
| 164 |
+
}
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
# Example 2: Fixed Wireless Project
|
| 169 |
+
fixed_wireless_inventory_requirements = {
|
| 170 |
+
"CPE-UBIQUITI-LITEBEAM": {
|
| 171 |
+
"code": "CPE-UBIQUITI-LITEBEAM",
|
| 172 |
+
"name": "Ubiquiti LiteBeam AC Gen2",
|
| 173 |
+
"description": "5GHz airMAX CPE with integrated antenna",
|
| 174 |
+
"usage_type": "installed",
|
| 175 |
+
"unit": "pieces",
|
| 176 |
+
"requires_serial_number": True,
|
| 177 |
+
"category": "Equipment",
|
| 178 |
+
"include_in_completion": True,
|
| 179 |
+
"completion_field_label": "CPE Serial Number",
|
| 180 |
+
"completion_required": True
|
| 181 |
+
},
|
| 182 |
+
"ROUTER-MIKROTIK-RB750": {
|
| 183 |
+
"code": "ROUTER-MIKROTIK-RB750",
|
| 184 |
+
"name": "MikroTik hEX RB750Gr3",
|
| 185 |
+
"description": "5-port Gigabit router",
|
| 186 |
+
"usage_type": "installed",
|
| 187 |
+
"unit": "pieces",
|
| 188 |
+
"requires_serial_number": True,
|
| 189 |
+
"category": "Equipment",
|
| 190 |
+
"include_in_completion": True,
|
| 191 |
+
"completion_field_label": "Router Serial Number",
|
| 192 |
+
"completion_required": True
|
| 193 |
+
},
|
| 194 |
+
"CABLE-CAT6-OUTDOOR": {
|
| 195 |
+
"code": "CABLE-CAT6-OUTDOOR",
|
| 196 |
+
"name": "Cat6 Outdoor Cable",
|
| 197 |
+
"description": "Outdoor-rated Cat6 cable for CPE connection",
|
| 198 |
+
"usage_type": "consumed",
|
| 199 |
+
"unit": "meters",
|
| 200 |
+
"requires_serial_number": False,
|
| 201 |
+
"category": "Cable",
|
| 202 |
+
"include_in_completion": False
|
| 203 |
+
},
|
| 204 |
+
"POE-INJECTOR-24V": {
|
| 205 |
+
"code": "POE-INJECTOR-24V",
|
| 206 |
+
"name": "24V PoE Injector",
|
| 207 |
+
"description": "Power over Ethernet injector for CPE",
|
| 208 |
+
"usage_type": "installed",
|
| 209 |
+
"unit": "pieces",
|
| 210 |
+
"requires_serial_number": False,
|
| 211 |
+
"category": "Power",
|
| 212 |
+
"include_in_completion": False
|
| 213 |
+
}
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
# Example 3: API Request to Create Project with Inventory Requirements
|
| 218 |
+
import requests
|
| 219 |
+
|
| 220 |
+
def create_project_with_inventory_requirements():
|
| 221 |
+
"""
|
| 222 |
+
Example API call to create a project with inventory requirements
|
| 223 |
+
"""
|
| 224 |
+
api_url = "https://api.example.com/api/v1/projects"
|
| 225 |
+
headers = {
|
| 226 |
+
"Authorization": "Bearer YOUR_TOKEN_HERE",
|
| 227 |
+
"Content-Type": "application/json"
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
project_data = {
|
| 231 |
+
"title": "Nairobi FTTH Rollout Phase 1",
|
| 232 |
+
"description": "Fiber to the home installations in Nairobi suburbs",
|
| 233 |
+
"project_type": "customer_service",
|
| 234 |
+
"service_type": "ftth",
|
| 235 |
+
"client_id": "client-uuid-here",
|
| 236 |
+
"contractor_id": "contractor-uuid-here",
|
| 237 |
+
"planned_start_date": "2025-01-01",
|
| 238 |
+
"planned_end_date": "2025-12-31",
|
| 239 |
+
|
| 240 |
+
# Inventory requirements
|
| 241 |
+
"inventory_requirements": ftth_inventory_requirements,
|
| 242 |
+
|
| 243 |
+
# Photo requirements
|
| 244 |
+
"photo_requirements": [
|
| 245 |
+
{
|
| 246 |
+
"type": "before_installation",
|
| 247 |
+
"required": True,
|
| 248 |
+
"min_photos": 1,
|
| 249 |
+
"max_photos": 3,
|
| 250 |
+
"description": "Photo of installation site before work"
|
| 251 |
+
},
|
| 252 |
+
{
|
| 253 |
+
"type": "ont_installation",
|
| 254 |
+
"required": True,
|
| 255 |
+
"min_photos": 1,
|
| 256 |
+
"max_photos": 2,
|
| 257 |
+
"description": "Photo of installed ONT device"
|
| 258 |
+
},
|
| 259 |
+
{
|
| 260 |
+
"type": "speedtest",
|
| 261 |
+
"required": True,
|
| 262 |
+
"min_photos": 1,
|
| 263 |
+
"max_photos": 1,
|
| 264 |
+
"description": "Screenshot of speed test results"
|
| 265 |
+
}
|
| 266 |
+
],
|
| 267 |
+
|
| 268 |
+
# Activation requirements
|
| 269 |
+
"activation_requirements": [
|
| 270 |
+
{
|
| 271 |
+
"field": "customer_phone",
|
| 272 |
+
"label": "Customer Phone Number",
|
| 273 |
+
"type": "text",
|
| 274 |
+
"required": True,
|
| 275 |
+
"placeholder": "+254..."
|
| 276 |
+
},
|
| 277 |
+
{
|
| 278 |
+
"field": "speed_test_download_mbps",
|
| 279 |
+
"label": "Download Speed (Mbps)",
|
| 280 |
+
"type": "number",
|
| 281 |
+
"required": True
|
| 282 |
+
},
|
| 283 |
+
{
|
| 284 |
+
"field": "speed_test_upload_mbps",
|
| 285 |
+
"label": "Upload Speed (Mbps)",
|
| 286 |
+
"type": "number",
|
| 287 |
+
"required": True
|
| 288 |
+
}
|
| 289 |
+
]
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
response = requests.post(api_url, json=project_data, headers=headers)
|
| 293 |
+
return response.json()
|
| 294 |
+
|
| 295 |
+
|
| 296 |
+
# Example 4: Receiving Inventory Batch
|
| 297 |
+
def receive_inventory_batch():
|
| 298 |
+
"""
|
| 299 |
+
Example API call to receive inventory batch
|
| 300 |
+
The equipment_type must match a code from inventory_requirements
|
| 301 |
+
"""
|
| 302 |
+
api_url = "https://api.example.com/api/v1/inventory"
|
| 303 |
+
headers = {
|
| 304 |
+
"Authorization": "Bearer YOUR_TOKEN_HERE",
|
| 305 |
+
"Content-Type": "application/json"
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
inventory_data = {
|
| 309 |
+
"project_id": "project-uuid-here",
|
| 310 |
+
"equipment_type": "ONT-ZTE-F670L", # Must match inventory requirement code
|
| 311 |
+
"equipment_name": "ZTE F670L ONT Device", # Auto-populated from requirement
|
| 312 |
+
"description": "Batch received from supplier",
|
| 313 |
+
"quantity_received": 100,
|
| 314 |
+
"unit": "pieces",
|
| 315 |
+
"item_type": "equipment",
|
| 316 |
+
"has_serial_numbers": True,
|
| 317 |
+
"serial_numbers": [
|
| 318 |
+
{"serial": "HW12345001", "status": "available"},
|
| 319 |
+
{"serial": "HW12345002", "status": "available"},
|
| 320 |
+
# ... more serials
|
| 321 |
+
],
|
| 322 |
+
"received_date": "2025-01-15",
|
| 323 |
+
"unit_cost": 2500.00,
|
| 324 |
+
"total_cost": 250000.00,
|
| 325 |
+
"currency": "KES"
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
response = requests.post(api_url, json=inventory_data, headers=headers)
|
| 329 |
+
return response.json()
|
| 330 |
+
|
| 331 |
+
|
| 332 |
+
if __name__ == "__main__":
|
| 333 |
+
print("FTTH Inventory Requirements:")
|
| 334 |
+
print(f"Total items: {len(ftth_inventory_requirements)}")
|
| 335 |
+
|
| 336 |
+
installed_items = [k for k, v in ftth_inventory_requirements.items() if v['usage_type'] == 'installed']
|
| 337 |
+
consumed_items = [k for k, v in ftth_inventory_requirements.items() if v['usage_type'] == 'consumed']
|
| 338 |
+
|
| 339 |
+
print(f"Installed items: {len(installed_items)}")
|
| 340 |
+
print(f"Consumed items: {len(consumed_items)}")
|
| 341 |
+
|
| 342 |
+
print("\nInstalled items (will appear in ticket completion):")
|
| 343 |
+
for code in installed_items:
|
| 344 |
+
item = ftth_inventory_requirements[code]
|
| 345 |
+
if item.get('include_in_completion'):
|
| 346 |
+
print(f" - {item['name']} ({code})")
|
docs/features/INVENTORY_REQUIREMENTS.md
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Inventory Requirements System
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
|
| 5 |
+
The Inventory Requirements system provides a flexible way to define and manage inventory types for projects. It controls what inventory can be received, tracked, and used in ticket completion forms.
|
| 6 |
+
|
| 7 |
+
## Key Concepts
|
| 8 |
+
|
| 9 |
+
### 1. Project-Level Configuration
|
| 10 |
+
|
| 11 |
+
Inventory requirements are defined at the project level in `project.inventory_requirements` (JSONB dictionary). Each requirement defines:
|
| 12 |
+
|
| 13 |
+
- **code**: Unique identifier (e.g., `ONT-ZTE-F670L`, `CABLE-FIBER-SM`)
|
| 14 |
+
- **name**: Display name (e.g., "ZTE F670L ONT Device")
|
| 15 |
+
- **description**: Detailed description
|
| 16 |
+
- **usage_type**: How the inventory is used
|
| 17 |
+
- `installed`: Equipment installed at customer sites (appears in ticket completion)
|
| 18 |
+
- `consumed`: Materials used up during work (tracked but not in completion forms)
|
| 19 |
+
- **unit**: Unit of measurement (pieces, meters, boxes, sets, etc.)
|
| 20 |
+
- **requires_serial_number**: Whether serial number tracking is required
|
| 21 |
+
- **category**: Grouping category (Equipment, Cable, Tools, Consumables, etc.)
|
| 22 |
+
|
| 23 |
+
### 2. Ticket Completion Integration
|
| 24 |
+
|
| 25 |
+
For items with `usage_type: "installed"`:
|
| 26 |
+
|
| 27 |
+
- **include_in_completion**: Whether to include in ticket completion forms (default: true)
|
| 28 |
+
- **completion_field_label**: Custom label for the completion form field
|
| 29 |
+
- **completion_required**: Whether the field is mandatory in ticket completion
|
| 30 |
+
|
| 31 |
+
## Example Configuration
|
| 32 |
+
|
| 33 |
+
```json
|
| 34 |
+
{
|
| 35 |
+
"ONT-ZTE-F670L": {
|
| 36 |
+
"code": "ONT-ZTE-F670L",
|
| 37 |
+
"name": "ZTE F670L ONT Device",
|
| 38 |
+
"description": "Fiber optic network terminal for FTTH installations",
|
| 39 |
+
"usage_type": "installed",
|
| 40 |
+
"unit": "pieces",
|
| 41 |
+
"requires_serial_number": true,
|
| 42 |
+
"category": "Equipment",
|
| 43 |
+
"include_in_completion": true,
|
| 44 |
+
"completion_field_label": "ONT Serial Number",
|
| 45 |
+
"completion_required": true
|
| 46 |
+
},
|
| 47 |
+
"ROUTER-TP-C6": {
|
| 48 |
+
"code": "ROUTER-TP-C6",
|
| 49 |
+
"name": "TP-Link Archer C6 Router",
|
| 50 |
+
"description": "Dual-band wireless router",
|
| 51 |
+
"usage_type": "installed",
|
| 52 |
+
"unit": "pieces",
|
| 53 |
+
"requires_serial_number": true,
|
| 54 |
+
"category": "Equipment",
|
| 55 |
+
"include_in_completion": true,
|
| 56 |
+
"completion_field_label": "Router Serial Number",
|
| 57 |
+
"completion_required": true
|
| 58 |
+
},
|
| 59 |
+
"CABLE-FIBER-SM": {
|
| 60 |
+
"code": "CABLE-FIBER-SM",
|
| 61 |
+
"name": "Single Mode Fiber Cable",
|
| 62 |
+
"description": "Outdoor fiber optic cable for drop installations",
|
| 63 |
+
"usage_type": "consumed",
|
| 64 |
+
"unit": "meters",
|
| 65 |
+
"requires_serial_number": false,
|
| 66 |
+
"category": "Cable",
|
| 67 |
+
"include_in_completion": false
|
| 68 |
+
},
|
| 69 |
+
"CONNECTOR-SC-APC": {
|
| 70 |
+
"code": "CONNECTOR-SC-APC",
|
| 71 |
+
"name": "SC/APC Fiber Connector",
|
| 72 |
+
"description": "Single mode fiber connector",
|
| 73 |
+
"usage_type": "consumed",
|
| 74 |
+
"unit": "pieces",
|
| 75 |
+
"requires_serial_number": false,
|
| 76 |
+
"category": "Consumables",
|
| 77 |
+
"include_in_completion": false
|
| 78 |
+
}
|
| 79 |
+
}
|
| 80 |
+
```
|
| 81 |
+
|
| 82 |
+
## Workflow
|
| 83 |
+
|
| 84 |
+
### 1. Project Setup
|
| 85 |
+
|
| 86 |
+
When creating or updating a project, define inventory requirements:
|
| 87 |
+
|
| 88 |
+
```http
|
| 89 |
+
POST /api/v1/projects
|
| 90 |
+
{
|
| 91 |
+
"title": "Nairobi FTTH Rollout",
|
| 92 |
+
"inventory_requirements": {
|
| 93 |
+
"ONT-ZTE-F670L": { ... },
|
| 94 |
+
"ROUTER-TP-C6": { ... },
|
| 95 |
+
"CABLE-FIBER-SM": { ... }
|
| 96 |
+
}
|
| 97 |
+
}
|
| 98 |
+
```
|
| 99 |
+
|
| 100 |
+
### 2. Receiving Inventory
|
| 101 |
+
|
| 102 |
+
When receiving inventory batches, the system validates against requirements:
|
| 103 |
+
|
| 104 |
+
```http
|
| 105 |
+
GET /api/v1/projects/{project_id}/inventory-requirements/dropdown
|
| 106 |
+
```
|
| 107 |
+
|
| 108 |
+
Returns dropdown options for inventory receiving:
|
| 109 |
+
|
| 110 |
+
```json
|
| 111 |
+
{
|
| 112 |
+
"options": [
|
| 113 |
+
{
|
| 114 |
+
"code": "ONT-ZTE-F670L",
|
| 115 |
+
"name": "ZTE F670L ONT Device",
|
| 116 |
+
"description": "Fiber optic network terminal",
|
| 117 |
+
"category": "Equipment",
|
| 118 |
+
"unit": "pieces",
|
| 119 |
+
"requires_serial_number": true,
|
| 120 |
+
"usage_type": "installed"
|
| 121 |
+
},
|
| 122 |
+
...
|
| 123 |
+
]
|
| 124 |
+
}
|
| 125 |
+
```
|
| 126 |
+
|
| 127 |
+
When creating inventory batch:
|
| 128 |
+
|
| 129 |
+
```http
|
| 130 |
+
POST /api/v1/inventory
|
| 131 |
+
{
|
| 132 |
+
"project_id": "...",
|
| 133 |
+
"equipment_type": "ONT-ZTE-F670L", // Must match a code from requirements
|
| 134 |
+
"equipment_name": "ZTE F670L ONT Device", // Auto-populated from requirement
|
| 135 |
+
"quantity_received": 100,
|
| 136 |
+
"item_type": "equipment"
|
| 137 |
+
}
|
| 138 |
+
```
|
| 139 |
+
|
| 140 |
+
The system validates that `equipment_type` matches a code in `project.inventory_requirements`.
|
| 141 |
+
|
| 142 |
+
### 3. Ticket Completion
|
| 143 |
+
|
| 144 |
+
When a ticket is being completed, the system dynamically generates completion fields:
|
| 145 |
+
|
| 146 |
+
```http
|
| 147 |
+
GET /api/v1/tickets/{ticket_id}/completion/checklist
|
| 148 |
+
```
|
| 149 |
+
|
| 150 |
+
Returns checklist including inventory fields:
|
| 151 |
+
|
| 152 |
+
```json
|
| 153 |
+
{
|
| 154 |
+
"field_items": [
|
| 155 |
+
{
|
| 156 |
+
"id": "inventory_ONT-ZTE-F670L",
|
| 157 |
+
"type": "field",
|
| 158 |
+
"field_name": "inventory_ONT-ZTE-F670L",
|
| 159 |
+
"label": "ONT Serial Number",
|
| 160 |
+
"data_type": "text",
|
| 161 |
+
"required": true,
|
| 162 |
+
"validation_regex": "^[A-Z0-9\\-]{4,}$",
|
| 163 |
+
"inventory_code": "ONT-ZTE-F670L",
|
| 164 |
+
"source": "inventory",
|
| 165 |
+
"status": "pending"
|
| 166 |
+
},
|
| 167 |
+
{
|
| 168 |
+
"id": "inventory_ROUTER-TP-C6",
|
| 169 |
+
"type": "field",
|
| 170 |
+
"field_name": "inventory_ROUTER-TP-C6",
|
| 171 |
+
"label": "Router Serial Number",
|
| 172 |
+
"data_type": "text",
|
| 173 |
+
"required": true,
|
| 174 |
+
"validation_regex": "^[A-Z0-9\\-]{4,}$",
|
| 175 |
+
"inventory_code": "ROUTER-TP-C6",
|
| 176 |
+
"source": "inventory",
|
| 177 |
+
"status": "pending"
|
| 178 |
+
}
|
| 179 |
+
]
|
| 180 |
+
}
|
| 181 |
+
```
|
| 182 |
+
|
| 183 |
+
Field agents must provide values for installed inventory items:
|
| 184 |
+
|
| 185 |
+
```http
|
| 186 |
+
POST /api/v1/tickets/{ticket_id}/completion/data
|
| 187 |
+
{
|
| 188 |
+
"completion_data": {
|
| 189 |
+
"inventory_ONT-ZTE-F670L": "HW12345678",
|
| 190 |
+
"inventory_ROUTER-TP-C6": "TP98765432",
|
| 191 |
+
"speed_test_download_mbps": 95.5
|
| 192 |
+
}
|
| 193 |
+
}
|
| 194 |
+
```
|
| 195 |
+
|
| 196 |
+
## API Endpoints
|
| 197 |
+
|
| 198 |
+
### Get Inventory Requirements
|
| 199 |
+
|
| 200 |
+
```http
|
| 201 |
+
GET /api/v1/projects/{project_id}/inventory-requirements
|
| 202 |
+
```
|
| 203 |
+
|
| 204 |
+
Query parameters:
|
| 205 |
+
- `usage_type`: Filter by 'installed' or 'consumed'
|
| 206 |
+
- `category`: Filter by category
|
| 207 |
+
|
| 208 |
+
Returns full inventory requirements dictionary.
|
| 209 |
+
|
| 210 |
+
### Get Dropdown Options
|
| 211 |
+
|
| 212 |
+
```http
|
| 213 |
+
GET /api/v1/projects/{project_id}/inventory-requirements/dropdown
|
| 214 |
+
```
|
| 215 |
+
|
| 216 |
+
Query parameters:
|
| 217 |
+
- `usage_type`: Filter by 'installed' or 'consumed'
|
| 218 |
+
|
| 219 |
+
Returns formatted options for dropdown selection (sorted by category and name).
|
| 220 |
+
|
| 221 |
+
### Get Completion Items
|
| 222 |
+
|
| 223 |
+
```http
|
| 224 |
+
GET /api/v1/projects/{project_id}/inventory-requirements/completion-items
|
| 225 |
+
```
|
| 226 |
+
|
| 227 |
+
Returns only installed items that appear in ticket completion forms.
|
| 228 |
+
|
| 229 |
+
### Get Categories
|
| 230 |
+
|
| 231 |
+
```http
|
| 232 |
+
GET /api/v1/projects/{project_id}/inventory-requirements/categories
|
| 233 |
+
```
|
| 234 |
+
|
| 235 |
+
Returns list of unique categories for filtering.
|
| 236 |
+
|
| 237 |
+
## Benefits
|
| 238 |
+
|
| 239 |
+
1. **Controlled Inventory Types**: Only pre-defined inventory types can be received
|
| 240 |
+
2. **Consistent Naming**: Inventory names are standardized across the project
|
| 241 |
+
3. **Automatic Validation**: System validates inventory codes at receiving time
|
| 242 |
+
4. **Dynamic Completion Forms**: Ticket completion forms automatically include installed items
|
| 243 |
+
5. **Flexible Configuration**: Project managers can define any inventory types needed
|
| 244 |
+
6. **Serial Number Tracking**: Automatic validation for items requiring serial numbers
|
| 245 |
+
7. **Usage Tracking**: Distinguish between installed equipment and consumed materials
|
| 246 |
+
|
| 247 |
+
## Migration Notes
|
| 248 |
+
|
| 249 |
+
For existing projects without inventory requirements:
|
| 250 |
+
|
| 251 |
+
1. Inventory can still be received (backward compatible)
|
| 252 |
+
2. Validation is only enforced if `inventory_requirements` is populated
|
| 253 |
+
3. Recommend defining requirements for new projects
|
| 254 |
+
4. Existing projects can be updated to add requirements
|
| 255 |
+
|
| 256 |
+
## Related Files
|
| 257 |
+
|
| 258 |
+
- `src/app/schemas/project.py` - InventoryRequirement schema
|
| 259 |
+
- `src/app/models/project.py` - Project model with inventory_requirements JSONB
|
| 260 |
+
- `src/app/services/inventory_requirements_service.py` - Helper service
|
| 261 |
+
- `src/app/services/inventory_service.py` - Inventory receiving with validation
|
| 262 |
+
- `src/app/services/ticket_completion_service.py` - Dynamic checklist generation
|
| 263 |
+
- `src/app/api/v1/projects.py` - API endpoints for inventory requirements
|
docs/features/INVENTORY_REQUIREMENTS_CHANGELOG.md
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Inventory Requirements System - Implementation Changelog
|
| 2 |
+
|
| 3 |
+
## Summary
|
| 4 |
+
|
| 5 |
+
Enhanced the inventory requirements system to be more realistic and integrated with the inventory receiving and ticket completion workflows.
|
| 6 |
+
|
| 7 |
+
## Changes Made
|
| 8 |
+
|
| 9 |
+
### 1. Updated InventoryRequirement Schema (`src/app/schemas/project.py`)
|
| 10 |
+
|
| 11 |
+
**Before:**
|
| 12 |
+
```python
|
| 13 |
+
class InventoryRequirement(BaseModel):
|
| 14 |
+
item_name: str
|
| 15 |
+
quantity: int
|
| 16 |
+
unit: Optional[str]
|
| 17 |
+
model: Optional[str]
|
| 18 |
+
notes: Optional[str]
|
| 19 |
+
```
|
| 20 |
+
|
| 21 |
+
**After:**
|
| 22 |
+
```python
|
| 23 |
+
class InventoryRequirement(BaseModel):
|
| 24 |
+
code: str # Unique inventory code (e.g., 'ONT-ZTE-F670L')
|
| 25 |
+
name: str # Display name
|
| 26 |
+
description: Optional[str] # Detailed description
|
| 27 |
+
usage_type: Literal["installed", "consumed"] # How inventory is used
|
| 28 |
+
unit: str # Unit of measurement
|
| 29 |
+
requires_serial_number: bool # Serial number tracking
|
| 30 |
+
category: Optional[str] # Grouping category
|
| 31 |
+
|
| 32 |
+
# Ticket completion integration
|
| 33 |
+
include_in_completion: bool # Include in completion forms
|
| 34 |
+
completion_field_label: Optional[str] # Custom label
|
| 35 |
+
completion_required: bool # Required in completion
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
**Key Improvements:**
|
| 39 |
+
- Added `code` field as unique identifier
|
| 40 |
+
- Added `usage_type` to distinguish installed vs consumed items
|
| 41 |
+
- Added `requires_serial_number` for tracking
|
| 42 |
+
- Added `category` for grouping
|
| 43 |
+
- Added ticket completion integration fields
|
| 44 |
+
|
| 45 |
+
### 2. Updated Project Model Comments (`src/app/models/project.py`)
|
| 46 |
+
|
| 47 |
+
Added comprehensive documentation showing the new JSONB structure with examples.
|
| 48 |
+
|
| 49 |
+
### 3. Enhanced Ticket Completion Service (`src/app/services/ticket_completion_service.py`)
|
| 50 |
+
|
| 51 |
+
**Changes:**
|
| 52 |
+
- Modified `_get_project_requirements()` to extract installed inventory items
|
| 53 |
+
- Converts inventory requirements to field requirements dynamically
|
| 54 |
+
- Only includes items with `usage_type='installed'` and `include_in_completion=True`
|
| 55 |
+
- Generates field names like `inventory_ONT-ZTE-F670L`
|
| 56 |
+
- Adds validation regex for items requiring serial numbers
|
| 57 |
+
|
| 58 |
+
**Result:**
|
| 59 |
+
Installed inventory items now automatically appear in ticket completion checklists.
|
| 60 |
+
|
| 61 |
+
### 4. Created Inventory Requirements Service (`src/app/services/inventory_requirements_service.py`)
|
| 62 |
+
|
| 63 |
+
New helper service with methods:
|
| 64 |
+
- `get_project_inventory_requirements()` - Get requirements with filters
|
| 65 |
+
- `get_inventory_requirement_by_code()` - Get specific requirement
|
| 66 |
+
- `validate_inventory_code()` - Validate code exists
|
| 67 |
+
- `get_inventory_dropdown_options()` - Format for dropdowns
|
| 68 |
+
- `get_installed_inventory_for_completion()` - Get completion items
|
| 69 |
+
- `get_inventory_categories()` - Get unique categories
|
| 70 |
+
|
| 71 |
+
### 5. Enhanced Inventory Service (`src/app/services/inventory_service.py`)
|
| 72 |
+
|
| 73 |
+
**Changes to `create_inventory()`:**
|
| 74 |
+
- Validates `equipment_type` against `project.inventory_requirements`
|
| 75 |
+
- Only allows receiving inventory types defined in project requirements
|
| 76 |
+
- Auto-populates `equipment_name` from requirement if not provided
|
| 77 |
+
- Provides helpful error messages with available codes
|
| 78 |
+
|
| 79 |
+
**Result:**
|
| 80 |
+
Controlled inventory receiving - only pre-defined types can be received.
|
| 81 |
+
|
| 82 |
+
### 6. Added API Endpoints (`src/app/api/v1/projects.py`)
|
| 83 |
+
|
| 84 |
+
New endpoints:
|
| 85 |
+
- `GET /api/v1/projects/{project_id}/inventory-requirements`
|
| 86 |
+
- Get all inventory requirements with optional filters
|
| 87 |
+
|
| 88 |
+
- `GET /api/v1/projects/{project_id}/inventory-requirements/dropdown`
|
| 89 |
+
- Get formatted options for dropdown selection
|
| 90 |
+
- Used when receiving inventory batches
|
| 91 |
+
|
| 92 |
+
- `GET /api/v1/projects/{project_id}/inventory-requirements/completion-items`
|
| 93 |
+
- Get items that appear in ticket completion forms
|
| 94 |
+
|
| 95 |
+
- `GET /api/v1/projects/{project_id}/inventory-requirements/categories`
|
| 96 |
+
- Get list of unique categories
|
| 97 |
+
|
| 98 |
+
### 7. Documentation
|
| 99 |
+
|
| 100 |
+
Created comprehensive documentation:
|
| 101 |
+
- `docs/features/INVENTORY_REQUIREMENTS.md` - Full system documentation
|
| 102 |
+
- `docs/examples/inventory_requirements_setup.py` - Example configurations
|
| 103 |
+
|
| 104 |
+
## Workflow Integration
|
| 105 |
+
|
| 106 |
+
### 1. Project Setup
|
| 107 |
+
Project managers define inventory requirements when creating/updating projects.
|
| 108 |
+
|
| 109 |
+
### 2. Inventory Receiving
|
| 110 |
+
When receiving inventory:
|
| 111 |
+
1. Call `GET /inventory-requirements/dropdown` to get valid options
|
| 112 |
+
2. Select inventory type from dropdown
|
| 113 |
+
3. System validates code exists in project requirements
|
| 114 |
+
4. Auto-populates name and metadata from requirements
|
| 115 |
+
|
| 116 |
+
### 3. Ticket Completion
|
| 117 |
+
When completing tickets:
|
| 118 |
+
1. System generates checklist from project requirements
|
| 119 |
+
2. Installed inventory items automatically included as fields
|
| 120 |
+
3. Field agents must provide values (e.g., serial numbers)
|
| 121 |
+
4. Validation ensures required fields are completed
|
| 122 |
+
|
| 123 |
+
## Benefits
|
| 124 |
+
|
| 125 |
+
1. **Controlled Inventory Types** - Only pre-defined types can be received
|
| 126 |
+
2. **Consistent Naming** - Standardized across project
|
| 127 |
+
3. **Automatic Validation** - At receiving and completion time
|
| 128 |
+
4. **Dynamic Forms** - Completion forms adapt to project needs
|
| 129 |
+
5. **Serial Number Tracking** - Automatic for items requiring it
|
| 130 |
+
6. **Usage Tracking** - Distinguish installed vs consumed
|
| 131 |
+
7. **Flexible Configuration** - Project managers control requirements
|
| 132 |
+
|
| 133 |
+
## Backward Compatibility
|
| 134 |
+
|
| 135 |
+
- Existing projects without `inventory_requirements` continue to work
|
| 136 |
+
- Validation only enforced when requirements are defined
|
| 137 |
+
- Recommend defining requirements for new projects
|
| 138 |
+
- Existing projects can be updated to add requirements
|
| 139 |
+
|
| 140 |
+
## Testing Recommendations
|
| 141 |
+
|
| 142 |
+
1. Create project with inventory requirements
|
| 143 |
+
2. Test receiving inventory with valid codes
|
| 144 |
+
3. Test receiving inventory with invalid codes (should fail)
|
| 145 |
+
4. Test ticket completion checklist generation
|
| 146 |
+
5. Test completing ticket with inventory fields
|
| 147 |
+
6. Test filtering and dropdown endpoints
|
| 148 |
+
|
| 149 |
+
## Migration Path
|
| 150 |
+
|
| 151 |
+
For existing projects:
|
| 152 |
+
1. Analyze current inventory types being used
|
| 153 |
+
2. Define inventory requirements based on actual usage
|
| 154 |
+
3. Update project with requirements
|
| 155 |
+
4. Future inventory receiving will be validated
|
| 156 |
+
|
| 157 |
+
## Related Files
|
| 158 |
+
|
| 159 |
+
- `src/app/schemas/project.py` - Schema definitions
|
| 160 |
+
- `src/app/models/project.py` - Database model
|
| 161 |
+
- `src/app/services/inventory_requirements_service.py` - Helper service
|
| 162 |
+
- `src/app/services/inventory_service.py` - Inventory receiving
|
| 163 |
+
- `src/app/services/ticket_completion_service.py` - Completion logic
|
| 164 |
+
- `src/app/api/v1/projects.py` - API endpoints
|
| 165 |
+
- `docs/features/INVENTORY_REQUIREMENTS.md` - Documentation
|
| 166 |
+
- `docs/examples/inventory_requirements_setup.py` - Examples
|
docs/features/INVENTORY_REQUIREMENTS_FLOW.md
ADDED
|
File without changes
|
docs/features/INVENTORY_REQUIREMENTS_QUICK_REF.md
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Inventory Requirements - Quick Reference
|
| 2 |
+
|
| 3 |
+
## TL;DR
|
| 4 |
+
|
| 5 |
+
Inventory requirements define what inventory types can be used in a project. Items marked as "installed" automatically appear in ticket completion forms.
|
| 6 |
+
|
| 7 |
+
## Quick Setup
|
| 8 |
+
|
| 9 |
+
### 1. Define Requirements in Project
|
| 10 |
+
|
| 11 |
+
```json
|
| 12 |
+
{
|
| 13 |
+
"inventory_requirements": {
|
| 14 |
+
"ONT-ZTE-F670L": {
|
| 15 |
+
"code": "ONT-ZTE-F670L",
|
| 16 |
+
"name": "ZTE F670L ONT Device",
|
| 17 |
+
"description": "Fiber optic network terminal",
|
| 18 |
+
"usage_type": "installed",
|
| 19 |
+
"unit": "pieces",
|
| 20 |
+
"requires_serial_number": true,
|
| 21 |
+
"category": "Equipment",
|
| 22 |
+
"include_in_completion": true,
|
| 23 |
+
"completion_field_label": "ONT Serial Number",
|
| 24 |
+
"completion_required": true
|
| 25 |
+
},
|
| 26 |
+
"CABLE-FIBER-SM": {
|
| 27 |
+
"code": "CABLE-FIBER-SM",
|
| 28 |
+
"name": "Single Mode Fiber Cable",
|
| 29 |
+
"description": "Outdoor fiber cable",
|
| 30 |
+
"usage_type": "consumed",
|
| 31 |
+
"unit": "meters",
|
| 32 |
+
"requires_serial_number": false,
|
| 33 |
+
"category": "Cable"
|
| 34 |
+
}
|
| 35 |
+
}
|
| 36 |
+
}
|
| 37 |
+
```
|
| 38 |
+
|
| 39 |
+
### 2. Get Dropdown Options for Receiving
|
| 40 |
+
|
| 41 |
+
```http
|
| 42 |
+
GET /api/v1/projects/{project_id}/inventory-requirements/dropdown
|
| 43 |
+
```
|
| 44 |
+
|
| 45 |
+
### 3. Receive Inventory
|
| 46 |
+
|
| 47 |
+
```http
|
| 48 |
+
POST /api/v1/inventory
|
| 49 |
+
{
|
| 50 |
+
"project_id": "...",
|
| 51 |
+
"equipment_type": "ONT-ZTE-F670L", // Must match a code
|
| 52 |
+
"quantity_received": 100
|
| 53 |
+
}
|
| 54 |
+
```
|
| 55 |
+
|
| 56 |
+
### 4. Ticket Completion (Automatic)
|
| 57 |
+
|
| 58 |
+
Installed items automatically appear in completion checklist:
|
| 59 |
+
|
| 60 |
+
```json
|
| 61 |
+
{
|
| 62 |
+
"field_items": [
|
| 63 |
+
{
|
| 64 |
+
"field_name": "inventory_ONT-ZTE-F670L",
|
| 65 |
+
"label": "ONT Serial Number",
|
| 66 |
+
"required": true
|
| 67 |
+
}
|
| 68 |
+
]
|
| 69 |
+
}
|
| 70 |
+
```
|
| 71 |
+
|
| 72 |
+
## Field Reference
|
| 73 |
+
|
| 74 |
+
| Field | Required | Description | Example |
|
| 75 |
+
|-------|----------|-------------|---------|
|
| 76 |
+
| `code` | Yes | Unique identifier | `"ONT-ZTE-F670L"` |
|
| 77 |
+
| `name` | Yes | Display name | `"ZTE F670L ONT Device"` |
|
| 78 |
+
| `description` | No | Detailed description | `"Fiber optic network terminal"` |
|
| 79 |
+
| `usage_type` | Yes | `"installed"` or `"consumed"` | `"installed"` |
|
| 80 |
+
| `unit` | Yes | Unit of measurement | `"pieces"`, `"meters"` |
|
| 81 |
+
| `requires_serial_number` | No | Track serial numbers | `true` |
|
| 82 |
+
| `category` | No | Grouping category | `"Equipment"` |
|
| 83 |
+
| `include_in_completion` | No | Show in completion form | `true` (default) |
|
| 84 |
+
| `completion_field_label` | No | Custom label | `"ONT Serial Number"` |
|
| 85 |
+
| `completion_required` | No | Required in completion | `true` (default) |
|
| 86 |
+
|
| 87 |
+
## Usage Types
|
| 88 |
+
|
| 89 |
+
### `installed`
|
| 90 |
+
- Equipment installed at customer sites
|
| 91 |
+
- Appears in ticket completion forms (if `include_in_completion=true`)
|
| 92 |
+
- Examples: ONT devices, routers, CPE equipment
|
| 93 |
+
|
| 94 |
+
### `consumed`
|
| 95 |
+
- Materials used up during work
|
| 96 |
+
- Does NOT appear in completion forms
|
| 97 |
+
- Examples: cables, connectors, fasteners
|
| 98 |
+
|
| 99 |
+
## API Endpoints
|
| 100 |
+
|
| 101 |
+
| Endpoint | Purpose |
|
| 102 |
+
|----------|---------|
|
| 103 |
+
| `GET /projects/{id}/inventory-requirements` | Get all requirements |
|
| 104 |
+
| `GET /projects/{id}/inventory-requirements/dropdown` | Get dropdown options |
|
| 105 |
+
| `GET /projects/{id}/inventory-requirements/completion-items` | Get completion items |
|
| 106 |
+
| `GET /projects/{id}/inventory-requirements/categories` | Get categories |
|
| 107 |
+
|
| 108 |
+
## Common Patterns
|
| 109 |
+
|
| 110 |
+
### Equipment with Serial Numbers
|
| 111 |
+
```json
|
| 112 |
+
{
|
| 113 |
+
"code": "ONT-ZTE-F670L",
|
| 114 |
+
"usage_type": "installed",
|
| 115 |
+
"requires_serial_number": true,
|
| 116 |
+
"include_in_completion": true,
|
| 117 |
+
"completion_field_label": "ONT Serial Number"
|
| 118 |
+
}
|
| 119 |
+
```
|
| 120 |
+
|
| 121 |
+
### Consumable Materials
|
| 122 |
+
```json
|
| 123 |
+
{
|
| 124 |
+
"code": "CABLE-FIBER-SM",
|
| 125 |
+
"usage_type": "consumed",
|
| 126 |
+
"requires_serial_number": false,
|
| 127 |
+
"include_in_completion": false
|
| 128 |
+
}
|
| 129 |
+
```
|
| 130 |
+
|
| 131 |
+
### Optional Equipment
|
| 132 |
+
```json
|
| 133 |
+
{
|
| 134 |
+
"code": "ROUTER-TP-C6",
|
| 135 |
+
"usage_type": "installed",
|
| 136 |
+
"include_in_completion": true,
|
| 137 |
+
"completion_required": false // Optional in completion
|
| 138 |
+
}
|
| 139 |
+
```
|
| 140 |
+
|
| 141 |
+
## Validation Rules
|
| 142 |
+
|
| 143 |
+
1. **Receiving Inventory**: `equipment_type` must match a `code` in requirements
|
| 144 |
+
2. **Ticket Completion**: Installed items with `completion_required=true` must be provided
|
| 145 |
+
3. **Serial Numbers**: Items with `requires_serial_number=true` get automatic validation regex
|
| 146 |
+
|
| 147 |
+
## Tips
|
| 148 |
+
|
| 149 |
+
- Use descriptive codes: `ONT-ZTE-F670L` not `ONT1`
|
| 150 |
+
- Group by category for better organization
|
| 151 |
+
- Mark optional items with `completion_required: false`
|
| 152 |
+
- Use `consumed` for materials that don't need completion tracking
|
| 153 |
+
- Set `include_in_completion: false` for items that don't need serial tracking
|
| 154 |
+
|
| 155 |
+
## Example Categories
|
| 156 |
+
|
| 157 |
+
- Equipment
|
| 158 |
+
- Cable
|
| 159 |
+
- Consumables
|
| 160 |
+
- Tools
|
| 161 |
+
- Installation Materials
|
| 162 |
+
- Power
|
| 163 |
+
- Protection
|
| 164 |
+
- Networking
|
| 165 |
+
|
| 166 |
+
## Error Messages
|
| 167 |
+
|
| 168 |
+
**Invalid inventory code:**
|
| 169 |
+
```json
|
| 170 |
+
{
|
| 171 |
+
"message": "Invalid equipment_type 'INVALID-CODE'. Must match a code from project inventory requirements.",
|
| 172 |
+
"available_codes": ["ONT-ZTE-F670L", "CABLE-FIBER-SM"],
|
| 173 |
+
"hint": "Use GET /api/v1/projects/{project_id}/inventory-requirements to see valid inventory types"
|
| 174 |
+
}
|
| 175 |
+
```
|
| 176 |
+
|
| 177 |
+
## See Also
|
| 178 |
+
|
| 179 |
+
- Full documentation: `docs/features/INVENTORY_REQUIREMENTS.md`
|
| 180 |
+
- Examples: `docs/examples/inventory_requirements_setup.py`
|
| 181 |
+
- Changelog: `docs/features/INVENTORY_REQUIREMENTS_CHANGELOG.md`
|
src/app/api/v1/projects.py
CHANGED
|
@@ -2187,3 +2187,188 @@ async def finalize_project_setup(
|
|
| 2187 |
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 2188 |
detail="An error occurred while finalizing project setup"
|
| 2189 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2187 |
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 2188 |
detail="An error occurred while finalizing project setup"
|
| 2189 |
)
|
| 2190 |
+
|
| 2191 |
+
|
| 2192 |
+
# ============================================
|
| 2193 |
+
# INVENTORY REQUIREMENTS
|
| 2194 |
+
# ============================================
|
| 2195 |
+
|
| 2196 |
+
@router.get("/{project_id}/inventory-requirements")
|
| 2197 |
+
async def get_project_inventory_requirements(
|
| 2198 |
+
project_id: UUID,
|
| 2199 |
+
usage_type: Optional[str] = Query(None, description="Filter by usage type: 'installed' or 'consumed'"),
|
| 2200 |
+
category: Optional[str] = Query(None, description="Filter by category"),
|
| 2201 |
+
current_user: User = Depends(get_current_active_user),
|
| 2202 |
+
db: Session = Depends(get_db)
|
| 2203 |
+
):
|
| 2204 |
+
"""
|
| 2205 |
+
Get inventory requirements for a project
|
| 2206 |
+
|
| 2207 |
+
Returns the inventory types that can be received and tracked for this project.
|
| 2208 |
+
Used when receiving inventory batches to populate dropdown options.
|
| 2209 |
+
|
| 2210 |
+
**Authorization:**
|
| 2211 |
+
- Any user on the project team can view
|
| 2212 |
+
|
| 2213 |
+
**Query Parameters:**
|
| 2214 |
+
- usage_type: Filter by 'installed' (equipment installed at customer sites) or 'consumed' (materials used up)
|
| 2215 |
+
- category: Filter by category (e.g., 'Equipment', 'Cable', 'Tools')
|
| 2216 |
+
|
| 2217 |
+
**Response:**
|
| 2218 |
+
- Dictionary of inventory requirements keyed by code
|
| 2219 |
+
- Each requirement includes: code, name, description, usage_type, unit, requires_serial_number, category
|
| 2220 |
+
"""
|
| 2221 |
+
from app.services.inventory_requirements_service import InventoryRequirementsService
|
| 2222 |
+
|
| 2223 |
+
try:
|
| 2224 |
+
# Get inventory requirements
|
| 2225 |
+
inventory_reqs = InventoryRequirementsService.get_project_inventory_requirements(
|
| 2226 |
+
project_id=project_id,
|
| 2227 |
+
db=db,
|
| 2228 |
+
usage_type=usage_type,
|
| 2229 |
+
category=category
|
| 2230 |
+
)
|
| 2231 |
+
|
| 2232 |
+
return {
|
| 2233 |
+
"project_id": str(project_id),
|
| 2234 |
+
"inventory_requirements": inventory_reqs,
|
| 2235 |
+
"total_count": len(inventory_reqs)
|
| 2236 |
+
}
|
| 2237 |
+
|
| 2238 |
+
except HTTPException:
|
| 2239 |
+
raise
|
| 2240 |
+
except Exception as e:
|
| 2241 |
+
logger.error(f"Error getting inventory requirements: {str(e)}")
|
| 2242 |
+
raise HTTPException(
|
| 2243 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 2244 |
+
detail="Failed to retrieve inventory requirements"
|
| 2245 |
+
)
|
| 2246 |
+
|
| 2247 |
+
|
| 2248 |
+
@router.get("/{project_id}/inventory-requirements/dropdown")
|
| 2249 |
+
async def get_inventory_dropdown_options(
|
| 2250 |
+
project_id: UUID,
|
| 2251 |
+
usage_type: Optional[str] = Query(None, description="Filter by usage type: 'installed' or 'consumed'"),
|
| 2252 |
+
current_user: User = Depends(get_current_active_user),
|
| 2253 |
+
db: Session = Depends(get_db)
|
| 2254 |
+
):
|
| 2255 |
+
"""
|
| 2256 |
+
Get inventory options formatted for dropdown selection
|
| 2257 |
+
|
| 2258 |
+
Used when receiving inventory batches - provides a clean list of options
|
| 2259 |
+
with code, name, description, and metadata.
|
| 2260 |
+
|
| 2261 |
+
**Authorization:**
|
| 2262 |
+
- Any user on the project team can view
|
| 2263 |
+
|
| 2264 |
+
**Response:**
|
| 2265 |
+
- Array of dropdown options sorted by category and name
|
| 2266 |
+
- Each option includes: code, name, description, category, unit, requires_serial_number, usage_type
|
| 2267 |
+
"""
|
| 2268 |
+
from app.services.inventory_requirements_service import InventoryRequirementsService
|
| 2269 |
+
|
| 2270 |
+
try:
|
| 2271 |
+
options = InventoryRequirementsService.get_inventory_dropdown_options(
|
| 2272 |
+
project_id=project_id,
|
| 2273 |
+
db=db,
|
| 2274 |
+
usage_type=usage_type
|
| 2275 |
+
)
|
| 2276 |
+
|
| 2277 |
+
return {
|
| 2278 |
+
"project_id": str(project_id),
|
| 2279 |
+
"options": options,
|
| 2280 |
+
"total_count": len(options)
|
| 2281 |
+
}
|
| 2282 |
+
|
| 2283 |
+
except HTTPException:
|
| 2284 |
+
raise
|
| 2285 |
+
except Exception as e:
|
| 2286 |
+
logger.error(f"Error getting inventory dropdown options: {str(e)}")
|
| 2287 |
+
raise HTTPException(
|
| 2288 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 2289 |
+
detail="Failed to retrieve inventory dropdown options"
|
| 2290 |
+
)
|
| 2291 |
+
|
| 2292 |
+
|
| 2293 |
+
@router.get("/{project_id}/inventory-requirements/completion-items")
|
| 2294 |
+
async def get_inventory_completion_items(
|
| 2295 |
+
project_id: UUID,
|
| 2296 |
+
current_user: User = Depends(get_current_active_user),
|
| 2297 |
+
db: Session = Depends(get_db)
|
| 2298 |
+
):
|
| 2299 |
+
"""
|
| 2300 |
+
Get inventory items that appear in ticket completion forms
|
| 2301 |
+
|
| 2302 |
+
Returns only 'installed' items with include_in_completion=True.
|
| 2303 |
+
These items will be dynamically added to ticket completion checklists.
|
| 2304 |
+
|
| 2305 |
+
**Authorization:**
|
| 2306 |
+
- Any user on the project team can view
|
| 2307 |
+
|
| 2308 |
+
**Response:**
|
| 2309 |
+
- Array of inventory items that require completion data
|
| 2310 |
+
- Each item includes: code, name, description, requires_serial_number, completion_field_label, completion_required
|
| 2311 |
+
"""
|
| 2312 |
+
from app.services.inventory_requirements_service import InventoryRequirementsService
|
| 2313 |
+
|
| 2314 |
+
try:
|
| 2315 |
+
completion_items = InventoryRequirementsService.get_installed_inventory_for_completion(
|
| 2316 |
+
project_id=project_id,
|
| 2317 |
+
db=db
|
| 2318 |
+
)
|
| 2319 |
+
|
| 2320 |
+
return {
|
| 2321 |
+
"project_id": str(project_id),
|
| 2322 |
+
"completion_items": completion_items,
|
| 2323 |
+
"total_count": len(completion_items)
|
| 2324 |
+
}
|
| 2325 |
+
|
| 2326 |
+
except HTTPException:
|
| 2327 |
+
raise
|
| 2328 |
+
except Exception as e:
|
| 2329 |
+
logger.error(f"Error getting inventory completion items: {str(e)}")
|
| 2330 |
+
raise HTTPException(
|
| 2331 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 2332 |
+
detail="Failed to retrieve inventory completion items"
|
| 2333 |
+
)
|
| 2334 |
+
|
| 2335 |
+
|
| 2336 |
+
@router.get("/{project_id}/inventory-requirements/categories")
|
| 2337 |
+
async def get_inventory_categories(
|
| 2338 |
+
project_id: UUID,
|
| 2339 |
+
current_user: User = Depends(get_current_active_user),
|
| 2340 |
+
db: Session = Depends(get_db)
|
| 2341 |
+
):
|
| 2342 |
+
"""
|
| 2343 |
+
Get list of unique inventory categories in project
|
| 2344 |
+
|
| 2345 |
+
Useful for filtering and grouping inventory items.
|
| 2346 |
+
|
| 2347 |
+
**Authorization:**
|
| 2348 |
+
- Any user on the project team can view
|
| 2349 |
+
|
| 2350 |
+
**Response:**
|
| 2351 |
+
- Array of category names (sorted alphabetically)
|
| 2352 |
+
"""
|
| 2353 |
+
from app.services.inventory_requirements_service import InventoryRequirementsService
|
| 2354 |
+
|
| 2355 |
+
try:
|
| 2356 |
+
categories = InventoryRequirementsService.get_inventory_categories(
|
| 2357 |
+
project_id=project_id,
|
| 2358 |
+
db=db
|
| 2359 |
+
)
|
| 2360 |
+
|
| 2361 |
+
return {
|
| 2362 |
+
"project_id": str(project_id),
|
| 2363 |
+
"categories": categories,
|
| 2364 |
+
"total_count": len(categories)
|
| 2365 |
+
}
|
| 2366 |
+
|
| 2367 |
+
except HTTPException:
|
| 2368 |
+
raise
|
| 2369 |
+
except Exception as e:
|
| 2370 |
+
logger.error(f"Error getting inventory categories: {str(e)}")
|
| 2371 |
+
raise HTTPException(
|
| 2372 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 2373 |
+
detail="Failed to retrieve inventory categories"
|
| 2374 |
+
)
|
src/app/models/project.py
CHANGED
|
@@ -55,8 +55,31 @@ class Project(BaseModel):
|
|
| 55 |
# Example: {"payroll": 500000, "equipment": 300000, "transport": 100000, "materials": 150000}
|
| 56 |
budget = Column(JSONB, default={}, nullable=False)
|
| 57 |
|
| 58 |
-
#
|
| 59 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
inventory_requirements = Column(JSONB, default={}, nullable=False)
|
| 61 |
|
| 62 |
# Photo Requirements (JSONB Array)
|
|
|
|
| 55 |
# Example: {"payroll": 500000, "equipment": 300000, "transport": 100000, "materials": 150000}
|
| 56 |
budget = Column(JSONB, default={}, nullable=False)
|
| 57 |
|
| 58 |
+
# Inventory Requirements (JSONB Dictionary) - Defines allowed inventory types for this project
|
| 59 |
+
# Key: inventory code, Value: inventory requirement object
|
| 60 |
+
# Example: {
|
| 61 |
+
# "ONT-ZTE-F670L": {
|
| 62 |
+
# "code": "ONT-ZTE-F670L",
|
| 63 |
+
# "name": "ZTE F670L ONT Device",
|
| 64 |
+
# "description": "Fiber optic network terminal for FTTH installations",
|
| 65 |
+
# "usage_type": "installed",
|
| 66 |
+
# "unit": "pieces",
|
| 67 |
+
# "requires_serial_number": true,
|
| 68 |
+
# "category": "Equipment",
|
| 69 |
+
# "include_in_completion": true,
|
| 70 |
+
# "completion_field_label": "ONT Serial Number",
|
| 71 |
+
# "completion_required": true
|
| 72 |
+
# },
|
| 73 |
+
# "CABLE-FIBER-SM": {
|
| 74 |
+
# "code": "CABLE-FIBER-SM",
|
| 75 |
+
# "name": "Single Mode Fiber Cable",
|
| 76 |
+
# "description": "Outdoor fiber optic cable",
|
| 77 |
+
# "usage_type": "consumed",
|
| 78 |
+
# "unit": "meters",
|
| 79 |
+
# "requires_serial_number": false,
|
| 80 |
+
# "category": "Cable"
|
| 81 |
+
# }
|
| 82 |
+
# }
|
| 83 |
inventory_requirements = Column(JSONB, default={}, nullable=False)
|
| 84 |
|
| 85 |
# Photo Requirements (JSONB Array)
|
src/app/schemas/project.py
CHANGED
|
@@ -78,12 +78,33 @@ class BudgetCategory(BaseModel):
|
|
| 78 |
|
| 79 |
|
| 80 |
class InventoryRequirement(BaseModel):
|
| 81 |
-
"""
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
|
| 88 |
|
| 89 |
# ============================================
|
|
|
|
| 78 |
|
| 79 |
|
| 80 |
class InventoryRequirement(BaseModel):
|
| 81 |
+
"""
|
| 82 |
+
Schema for inventory requirement (JSONB validation)
|
| 83 |
+
|
| 84 |
+
Defines inventory types that can be used in this project.
|
| 85 |
+
Controls what inventory types can be received and tracked.
|
| 86 |
+
If usage_type='installed', this item will appear in ticket completion forms.
|
| 87 |
+
"""
|
| 88 |
+
code: str = Field(..., min_length=1, max_length=50, description="Unique inventory code (e.g., 'ONT-ZTE-F670L', 'CABLE-FIBER-SM')")
|
| 89 |
+
name: str = Field(..., min_length=1, max_length=200, description="Inventory item name (e.g., 'ZTE F670L ONT', 'Single Mode Fiber Cable')")
|
| 90 |
+
description: Optional[str] = Field(None, max_length=500, description="Detailed description of the inventory item")
|
| 91 |
+
usage_type: Literal["installed", "consumed"] = Field(..., description="How this inventory is used: 'installed' (at customer site) or 'consumed' (used up)")
|
| 92 |
+
unit: str = Field(default="pieces", max_length=50, description="Unit of measurement (pieces, meters, boxes, sets, etc.)")
|
| 93 |
+
requires_serial_number: bool = Field(default=False, description="Whether this item requires serial number tracking")
|
| 94 |
+
category: Optional[str] = Field(None, max_length=100, description="Category for grouping (e.g., 'Equipment', 'Cable', 'Tools', 'Consumables')")
|
| 95 |
+
|
| 96 |
+
# Ticket completion integration (for installed items)
|
| 97 |
+
include_in_completion: bool = Field(default=True, description="Include in ticket completion form (only applies to installed items)")
|
| 98 |
+
completion_field_label: Optional[str] = Field(None, max_length=200, description="Custom label for completion form (defaults to name)")
|
| 99 |
+
completion_required: bool = Field(default=True, description="Whether this field is required in ticket completion")
|
| 100 |
+
|
| 101 |
+
@model_validator(mode='after')
|
| 102 |
+
def validate_completion_settings(self):
|
| 103 |
+
"""Ensure completion settings only apply to installed items"""
|
| 104 |
+
if self.usage_type == "consumed" and self.include_in_completion:
|
| 105 |
+
# Auto-correct: consumed items shouldn't be in completion forms
|
| 106 |
+
self.include_in_completion = False
|
| 107 |
+
return self
|
| 108 |
|
| 109 |
|
| 110 |
# ============================================
|
src/app/services/inventory_requirements_service.py
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Inventory Requirements Service
|
| 3 |
+
|
| 4 |
+
Helper service for working with project inventory requirements.
|
| 5 |
+
Used when receiving inventory batches to validate against project requirements.
|
| 6 |
+
"""
|
| 7 |
+
from sqlalchemy.orm import Session
|
| 8 |
+
from fastapi import HTTPException
|
| 9 |
+
from typing import List, Dict, Any, Optional
|
| 10 |
+
from uuid import UUID
|
| 11 |
+
import logging
|
| 12 |
+
|
| 13 |
+
from app.models.project import Project
|
| 14 |
+
|
| 15 |
+
logger = logging.getLogger(__name__)
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class InventoryRequirementsService:
|
| 19 |
+
"""Service for managing project inventory requirements"""
|
| 20 |
+
|
| 21 |
+
@staticmethod
|
| 22 |
+
def get_project_inventory_requirements(
|
| 23 |
+
project_id: UUID,
|
| 24 |
+
db: Session,
|
| 25 |
+
usage_type: Optional[str] = None,
|
| 26 |
+
category: Optional[str] = None
|
| 27 |
+
) -> Dict[str, Dict[str, Any]]:
|
| 28 |
+
"""
|
| 29 |
+
Get inventory requirements for a project
|
| 30 |
+
|
| 31 |
+
Args:
|
| 32 |
+
project_id: Project ID
|
| 33 |
+
db: Database session
|
| 34 |
+
usage_type: Filter by usage type ('installed' or 'consumed')
|
| 35 |
+
category: Filter by category
|
| 36 |
+
|
| 37 |
+
Returns:
|
| 38 |
+
Dictionary of inventory requirements keyed by code
|
| 39 |
+
"""
|
| 40 |
+
project = db.query(Project).filter(Project.id == project_id).first()
|
| 41 |
+
if not project:
|
| 42 |
+
raise HTTPException(status_code=404, detail="Project not found")
|
| 43 |
+
|
| 44 |
+
inventory_reqs = project.inventory_requirements or {}
|
| 45 |
+
|
| 46 |
+
# Apply filters if provided
|
| 47 |
+
if usage_type or category:
|
| 48 |
+
filtered_reqs = {}
|
| 49 |
+
for code, req in inventory_reqs.items():
|
| 50 |
+
if usage_type and req.get('usage_type') != usage_type:
|
| 51 |
+
continue
|
| 52 |
+
if category and req.get('category') != category:
|
| 53 |
+
continue
|
| 54 |
+
filtered_reqs[code] = req
|
| 55 |
+
return filtered_reqs
|
| 56 |
+
|
| 57 |
+
return inventory_reqs
|
| 58 |
+
|
| 59 |
+
@staticmethod
|
| 60 |
+
def get_inventory_requirement_by_code(
|
| 61 |
+
project_id: UUID,
|
| 62 |
+
inventory_code: str,
|
| 63 |
+
db: Session
|
| 64 |
+
) -> Optional[Dict[str, Any]]:
|
| 65 |
+
"""
|
| 66 |
+
Get a specific inventory requirement by code
|
| 67 |
+
|
| 68 |
+
Args:
|
| 69 |
+
project_id: Project ID
|
| 70 |
+
inventory_code: Inventory code to look up
|
| 71 |
+
db: Database session
|
| 72 |
+
|
| 73 |
+
Returns:
|
| 74 |
+
Inventory requirement dict or None if not found
|
| 75 |
+
"""
|
| 76 |
+
inventory_reqs = InventoryRequirementsService.get_project_inventory_requirements(
|
| 77 |
+
project_id, db
|
| 78 |
+
)
|
| 79 |
+
return inventory_reqs.get(inventory_code)
|
| 80 |
+
|
| 81 |
+
@staticmethod
|
| 82 |
+
def validate_inventory_code(
|
| 83 |
+
project_id: UUID,
|
| 84 |
+
inventory_code: str,
|
| 85 |
+
db: Session
|
| 86 |
+
) -> bool:
|
| 87 |
+
"""
|
| 88 |
+
Validate that an inventory code exists in project requirements
|
| 89 |
+
|
| 90 |
+
Args:
|
| 91 |
+
project_id: Project ID
|
| 92 |
+
inventory_code: Inventory code to validate
|
| 93 |
+
db: Database session
|
| 94 |
+
|
| 95 |
+
Returns:
|
| 96 |
+
True if valid, raises HTTPException if invalid
|
| 97 |
+
"""
|
| 98 |
+
req = InventoryRequirementsService.get_inventory_requirement_by_code(
|
| 99 |
+
project_id, inventory_code, db
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
if not req:
|
| 103 |
+
raise HTTPException(
|
| 104 |
+
status_code=400,
|
| 105 |
+
detail=f"Invalid inventory code '{inventory_code}'. Not defined in project requirements."
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
return True
|
| 109 |
+
|
| 110 |
+
@staticmethod
|
| 111 |
+
def get_inventory_dropdown_options(
|
| 112 |
+
project_id: UUID,
|
| 113 |
+
db: Session,
|
| 114 |
+
usage_type: Optional[str] = None
|
| 115 |
+
) -> List[Dict[str, Any]]:
|
| 116 |
+
"""
|
| 117 |
+
Get inventory options formatted for dropdown selection
|
| 118 |
+
Used when receiving inventory batches
|
| 119 |
+
|
| 120 |
+
Args:
|
| 121 |
+
project_id: Project ID
|
| 122 |
+
db: Database session
|
| 123 |
+
usage_type: Filter by usage type
|
| 124 |
+
|
| 125 |
+
Returns:
|
| 126 |
+
List of dropdown options with code, name, description
|
| 127 |
+
"""
|
| 128 |
+
inventory_reqs = InventoryRequirementsService.get_project_inventory_requirements(
|
| 129 |
+
project_id, db, usage_type=usage_type
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
options = []
|
| 133 |
+
for code, req in inventory_reqs.items():
|
| 134 |
+
options.append({
|
| 135 |
+
'code': code,
|
| 136 |
+
'name': req.get('name'),
|
| 137 |
+
'description': req.get('description'),
|
| 138 |
+
'category': req.get('category'),
|
| 139 |
+
'unit': req.get('unit', 'pieces'),
|
| 140 |
+
'requires_serial_number': req.get('requires_serial_number', False),
|
| 141 |
+
'usage_type': req.get('usage_type')
|
| 142 |
+
})
|
| 143 |
+
|
| 144 |
+
# Sort by category then name
|
| 145 |
+
options.sort(key=lambda x: (x.get('category') or '', x.get('name') or ''))
|
| 146 |
+
|
| 147 |
+
return options
|
| 148 |
+
|
| 149 |
+
@staticmethod
|
| 150 |
+
def get_installed_inventory_for_completion(
|
| 151 |
+
project_id: UUID,
|
| 152 |
+
db: Session
|
| 153 |
+
) -> List[Dict[str, Any]]:
|
| 154 |
+
"""
|
| 155 |
+
Get inventory items that should appear in ticket completion forms
|
| 156 |
+
Only returns 'installed' items with include_in_completion=True
|
| 157 |
+
|
| 158 |
+
Args:
|
| 159 |
+
project_id: Project ID
|
| 160 |
+
db: Database session
|
| 161 |
+
|
| 162 |
+
Returns:
|
| 163 |
+
List of inventory items for completion form
|
| 164 |
+
"""
|
| 165 |
+
inventory_reqs = InventoryRequirementsService.get_project_inventory_requirements(
|
| 166 |
+
project_id, db, usage_type='installed'
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
+
completion_items = []
|
| 170 |
+
for code, req in inventory_reqs.items():
|
| 171 |
+
if req.get('include_in_completion', True):
|
| 172 |
+
completion_items.append({
|
| 173 |
+
'code': code,
|
| 174 |
+
'name': req.get('name'),
|
| 175 |
+
'description': req.get('description'),
|
| 176 |
+
'requires_serial_number': req.get('requires_serial_number', False),
|
| 177 |
+
'completion_field_label': req.get('completion_field_label') or req.get('name'),
|
| 178 |
+
'completion_required': req.get('completion_required', True),
|
| 179 |
+
'category': req.get('category')
|
| 180 |
+
})
|
| 181 |
+
|
| 182 |
+
return completion_items
|
| 183 |
+
|
| 184 |
+
@staticmethod
|
| 185 |
+
def get_inventory_categories(
|
| 186 |
+
project_id: UUID,
|
| 187 |
+
db: Session
|
| 188 |
+
) -> List[str]:
|
| 189 |
+
"""
|
| 190 |
+
Get list of unique inventory categories in project
|
| 191 |
+
|
| 192 |
+
Args:
|
| 193 |
+
project_id: Project ID
|
| 194 |
+
db: Database session
|
| 195 |
+
|
| 196 |
+
Returns:
|
| 197 |
+
List of category names
|
| 198 |
+
"""
|
| 199 |
+
inventory_reqs = InventoryRequirementsService.get_project_inventory_requirements(
|
| 200 |
+
project_id, db
|
| 201 |
+
)
|
| 202 |
+
|
| 203 |
+
categories = set()
|
| 204 |
+
for req in inventory_reqs.values():
|
| 205 |
+
category = req.get('category')
|
| 206 |
+
if category:
|
| 207 |
+
categories.add(category)
|
| 208 |
+
|
| 209 |
+
return sorted(list(categories))
|
src/app/services/inventory_service.py
CHANGED
|
@@ -130,7 +130,12 @@ class InventoryService:
|
|
| 130 |
|
| 131 |
@staticmethod
|
| 132 |
def create_inventory(db: Session, data: ProjectInventoryCreate, current_user: User) -> ProjectInventory:
|
| 133 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
# Authorization
|
| 135 |
if not InventoryService.can_user_manage_inventory(current_user, data.project_id, db):
|
| 136 |
raise HTTPException(
|
|
@@ -146,6 +151,37 @@ class InventoryService:
|
|
| 146 |
detail=f"Project not found: {data.project_id}"
|
| 147 |
)
|
| 148 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
# Create inventory
|
| 150 |
inventory = ProjectInventory(
|
| 151 |
**data.model_dump(),
|
|
@@ -157,7 +193,10 @@ class InventoryService:
|
|
| 157 |
db.commit()
|
| 158 |
db.refresh(inventory)
|
| 159 |
|
| 160 |
-
logger.info(
|
|
|
|
|
|
|
|
|
|
| 161 |
return inventory
|
| 162 |
|
| 163 |
@staticmethod
|
|
|
|
| 130 |
|
| 131 |
@staticmethod
|
| 132 |
def create_inventory(db: Session, data: ProjectInventoryCreate, current_user: User) -> ProjectInventory:
|
| 133 |
+
"""
|
| 134 |
+
Create inventory batch at main office
|
| 135 |
+
|
| 136 |
+
Validates that equipment_type matches a code in project.inventory_requirements.
|
| 137 |
+
This ensures only pre-defined inventory types can be received.
|
| 138 |
+
"""
|
| 139 |
# Authorization
|
| 140 |
if not InventoryService.can_user_manage_inventory(current_user, data.project_id, db):
|
| 141 |
raise HTTPException(
|
|
|
|
| 151 |
detail=f"Project not found: {data.project_id}"
|
| 152 |
)
|
| 153 |
|
| 154 |
+
# Validate equipment_type against project inventory requirements
|
| 155 |
+
# equipment_type should match an inventory code from project.inventory_requirements
|
| 156 |
+
inventory_reqs = project.inventory_requirements or {}
|
| 157 |
+
|
| 158 |
+
if data.equipment_type not in inventory_reqs:
|
| 159 |
+
# Provide helpful error with available codes
|
| 160 |
+
available_codes = list(inventory_reqs.keys())
|
| 161 |
+
raise HTTPException(
|
| 162 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 163 |
+
detail={
|
| 164 |
+
"message": f"Invalid equipment_type '{data.equipment_type}'. Must match a code from project inventory requirements.",
|
| 165 |
+
"available_codes": available_codes,
|
| 166 |
+
"hint": "Use GET /api/v1/projects/{project_id}/inventory-requirements to see valid inventory types"
|
| 167 |
+
}
|
| 168 |
+
)
|
| 169 |
+
|
| 170 |
+
# Get the requirement details for validation and auto-population
|
| 171 |
+
inv_req = inventory_reqs[data.equipment_type]
|
| 172 |
+
|
| 173 |
+
# Auto-populate equipment_name from requirement if not provided
|
| 174 |
+
if not data.equipment_name:
|
| 175 |
+
data.equipment_name = inv_req.get('name', data.equipment_type)
|
| 176 |
+
|
| 177 |
+
# Validate item_type matches requirement usage_type
|
| 178 |
+
expected_item_type = 'equipment' if inv_req.get('usage_type') == 'installed' else 'consumable'
|
| 179 |
+
if data.item_type != expected_item_type:
|
| 180 |
+
logger.warning(
|
| 181 |
+
f"Item type mismatch: provided '{data.item_type}' but requirement suggests '{expected_item_type}'. "
|
| 182 |
+
f"Proceeding with provided value."
|
| 183 |
+
)
|
| 184 |
+
|
| 185 |
# Create inventory
|
| 186 |
inventory = ProjectInventory(
|
| 187 |
**data.model_dump(),
|
|
|
|
| 193 |
db.commit()
|
| 194 |
db.refresh(inventory)
|
| 195 |
|
| 196 |
+
logger.info(
|
| 197 |
+
f"Created inventory: {inventory.id} for project {data.project_id}, "
|
| 198 |
+
f"type: {data.equipment_type} ({inv_req.get('name')})"
|
| 199 |
+
)
|
| 200 |
return inventory
|
| 201 |
|
| 202 |
@staticmethod
|
src/app/services/ticket_completion_service.py
CHANGED
|
@@ -65,10 +65,38 @@ class TicketCompletionService:
|
|
| 65 |
# Combine ALL field requirements (activation + inventory)
|
| 66 |
# Project manager decides what to populate - we just read and validate
|
| 67 |
activation_reqs = project.activation_requirements or []
|
| 68 |
-
inventory_reqs = project.inventory_requirements or []
|
| 69 |
|
| 70 |
-
#
|
| 71 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
|
| 73 |
# Cache for next time
|
| 74 |
ProjectRequirementsCache.set(project_id, photo_reqs, field_reqs)
|
|
|
|
| 65 |
# Combine ALL field requirements (activation + inventory)
|
| 66 |
# Project manager decides what to populate - we just read and validate
|
| 67 |
activation_reqs = project.activation_requirements or []
|
|
|
|
| 68 |
|
| 69 |
+
# Extract inventory requirements that should be in completion form
|
| 70 |
+
# Only "installed" items with include_in_completion=True
|
| 71 |
+
inventory_dict = project.inventory_requirements or {}
|
| 72 |
+
inventory_completion_reqs = []
|
| 73 |
+
|
| 74 |
+
for inv_code, inv_req in inventory_dict.items():
|
| 75 |
+
# Only include installed items that are marked for completion
|
| 76 |
+
if inv_req.get('usage_type') == 'installed' and inv_req.get('include_in_completion', True):
|
| 77 |
+
# Convert inventory requirement to field requirement format
|
| 78 |
+
field_label = inv_req.get('completion_field_label') or inv_req.get('name')
|
| 79 |
+
field_type = 'text' # Default to text
|
| 80 |
+
|
| 81 |
+
# If requires serial number, use text with validation
|
| 82 |
+
validation_regex = None
|
| 83 |
+
if inv_req.get('requires_serial_number'):
|
| 84 |
+
field_label = f"{field_label} (Serial Number)"
|
| 85 |
+
validation_regex = r'^[A-Z0-9\-]{4,}$' # Basic serial number pattern
|
| 86 |
+
|
| 87 |
+
inventory_completion_reqs.append({
|
| 88 |
+
'field': f"inventory_{inv_code}", # Prefix with inventory_ to avoid conflicts
|
| 89 |
+
'label': field_label,
|
| 90 |
+
'type': field_type,
|
| 91 |
+
'required': inv_req.get('completion_required', True),
|
| 92 |
+
'placeholder': f"Enter {inv_req.get('name')} details",
|
| 93 |
+
'validation_regex': validation_regex,
|
| 94 |
+
'inventory_code': inv_code, # Track which inventory this is for
|
| 95 |
+
'source': 'inventory' # Mark as coming from inventory requirements
|
| 96 |
+
})
|
| 97 |
+
|
| 98 |
+
# Merge all field requirements: activation + inventory completion fields
|
| 99 |
+
field_reqs = activation_reqs + inventory_completion_reqs
|
| 100 |
|
| 101 |
# Cache for next time
|
| 102 |
ProjectRequirementsCache.set(project_id, photo_reqs, field_reqs)
|