kamau1 commited on
Commit
6b29be6
·
1 Parent(s): 6c59dab

Implemented improved inventory requirements system with API endpoints, validation, and comprehensive documentation.

Browse files
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
- # Project Configuration (JSONB)
59
- # Example: {"ont_devices": {"quantity": 1000, "model": "ZTE F670L"}, "fiber_cable": {"quantity": 5000, "unit": "meters"}}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- """Schema for inventory requirement (JSONB validation)"""
82
- item_name: str = Field(..., min_length=1, max_length=200, description="Item name")
83
- quantity: int = Field(..., ge=1, description="Required quantity")
84
- unit: Optional[str] = Field(None, max_length=50, description="Unit of measurement")
85
- model: Optional[str] = Field(None, max_length=200, description="Model/specification")
86
- notes: Optional[str] = Field(None, max_length=500, description="Additional notes")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- """Create inventory batch at main office"""
 
 
 
 
 
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(f"Created inventory: {inventory.id} for project {data.project_id}")
 
 
 
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
- # Merge both lists - project can have both activation AND inventory requirements
71
- field_reqs = activation_reqs + inventory_reqs
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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)