Zones Views - API Endpoints
Location: api/nextango/apps/zones/views.py
Last Updated: 2025-12-21
Overview
The zones views module provides comprehensive REST API endpoints for zone management, merchandising, compliance, inventory tracking, and planogram operations. Built with Django REST Framework ViewSets, these endpoints support the zone-driven inventory architecture.
Key Features:
- Zone management: CRUD operations with nested zone types (compliance, merchandising, inventory)
- Par level operations: Product assignment management with SKU validation
- Planogram management: Visual merchandising layouts with product assignments
- Inventory sync: Automated infrastructure creation from par levels
- Store-based filtering: Zone organization by store location
- Service layer integration: ZoneAssignmentService for business logic
- Redis caching: List/retrieve operations with signal-based invalidation
ViewSets (7 total):
- ZoneViewSet - Main zone CRUD with custom actions
- ZoneComplianceViewSet - Compliance configuration management
- ZoneMerchandisingViewSet - Merchandising standards management
- ZoneInventoryViewSet - Inventory configuration with validation
- ParLevelItemViewSet - Par level CRUD with inventory creation
- PlanogramViewSet - Planogram CRUD with target assignments
- PlanogramProductViewSet - Planogram product CRUD
URL Patterns:
/api/zones/ # ZoneViewSet
/api/zones/compliance/ # ZoneComplianceViewSet
/api/zones/merchandising/ # ZoneMerchandisingViewSet
/api/zones/inventory/ # ZoneInventoryViewSet
/api/zones/par-levels/ # ParLevelItemViewSet
/api/planograms/ # PlanogramViewSet
/api/planograms/products/ # PlanogramProductViewSet
ZoneViewSet
Main ViewSet for zone management with zone-specific actions.
Purpose
Provides comprehensive zone CRUD operations with nested zone type management (compliance, merchandising, inventory) and integration with ZoneAssignmentService for product assignments and inventory sync.
Caching
Cache Configuration:
- Prefix:
zone: - Timeout: 1200 seconds (20 minutes)
- Cached Actions:
list,retrieve - Invalidation: On
Zonesave/delete via signals
Base Configuration
Queryset Optimization:
def get_queryset(self):
return Zone.objects.select_related(
'store_location'
).prefetch_related(
'zone_compliance',
'zone_merchandising',
'zone_inventory'
)
Filtering:
- Search:
zone_name,store_location__name - Filters:
is_shown,store_location,zone_name(icontains),store_name(icontains) - Ordering:
zone_name,display_order,created_at - Default ordering:
store_location__name,display_order,zone_name
Standard CRUD Operations
List Zones
URL: GET /api/zones/
Query Parameters:
zone_name- Filter by zone name (case-insensitive contains)store_name- Filter by store name (case-insensitive contains)is_shown- Filter by visibility (true/false)store_location- Filter by store IDsearch- Search zone name or store nameordering- Order byzone_name,display_order, orcreated_at
Response:
[
{
"id": "uuid-123",
"sanity_id": "zone-abc",
"zone_name": "Coffee Bar",
"store_location": "uuid-store-456",
"zone_type": {
"type": "inventory",
"data": { /* zone inventory data */ }
},
"is_shown": true,
"display_order": 1,
"created_at": "2025-11-18T10:00:00Z",
"updated_at": "2025-11-18T10:00:00Z"
}
]
Retrieve Zone
URL: GET /api/zones/{id}/
Response: Single zone object with nested zone type data
Create Zone
URL: POST /api/zones/
Request Body:
{
"sanity_id": "zone-xyz",
"zone_name": "Bakery Display",
"store_location": "uuid-store-789",
"zone_type": {
"type": "merchandising",
"data": {
"display_standards": ["Eye level placement", "FIFO rotation"]
}
},
"is_shown": true,
"display_order": 3
}
Update Zone
URL: PUT /api/zones/{id}/ or PATCH /api/zones/{id}/
Delete Zone
URL: DELETE /api/zones/{id}/
Custom Actions
Get Zone Products
URL: GET /api/zones/{id}/products/
Purpose: Get all products assigned to this zone through par levels.
Service Integration:
@action(detail=True, methods=['get'])
def products(self, request, pk=None):
zone = self.get_object()
zone_service = ZoneAssignmentService()
products = zone_service.get_products_for_zone(zone)
return Response(products)
Response:
[
{
"product_id": "uuid-product-123",
"product_name": "Dark Roast Coffee",
"variant_sku": "DARK-12OZ",
"variant_name": "12oz Bag",
"par_quantity": 10,
"par_level_id": "uuid-par-456"
},
{
"product_id": "uuid-product-124",
"product_name": "Light Roast Coffee",
"variant_sku": "LIGHT-12OZ",
"variant_name": "12oz Bag",
"par_quantity": 8,
"par_level_id": "uuid-par-457"
}
]
Use Case: Display all products that should be stocked in a specific zone
Sync Inventory Infrastructure
URL: POST /api/zones/{id}/sync_inventory/
Purpose: Create InventoryLevel records for all par level items in this zone.
Service Integration:
@action(detail=True, methods=['post'])
def sync_inventory(self, request, pk=None):
zone = self.get_object()
zone_service = ZoneAssignmentService()
stats = zone_service.create_inventory_infrastructure_for_zone(zone)
return Response({
'message': f'Inventory sync completed for zone {zone.zone_name}',
'stats': stats
})
Response:
{
"message": "Inventory sync completed for zone Coffee Bar",
"stats": {
"created": 8,
"existing": 2,
"failed": 0,
"errors": []
}
}
Use Case:
- After adding new par levels to a zone
- Ensuring inventory tracking infrastructure exists
- Bulk creation of InventoryLevel records
Get Zones by Store
URL: GET /api/zones/by_store/?store_id={store_id}
Purpose: Get all zones for a specific store location.
Query Parameters:
store_id(required) - UUID of the store
Response:
[
{
"id": "uuid-zone-1",
"zone_name": "Coffee Bar",
"zone_type": { "type": "inventory" },
"display_order": 1
},
{
"id": "uuid-zone-2",
"zone_name": "Bakery Display",
"zone_type": { "type": "merchandising" },
"display_order": 2
}
]
Error Response (400):
{
"error": "store_id parameter is required"
}
Use Case: Display all zones for a specific store in management interface
ZoneComplianceViewSet
ViewSet for zone compliance configuration management.
Purpose
Manages compliance requirements for zones (checklists, photo requirements, frequency).
Configuration
Queryset Optimization:
def get_queryset(self):
return ZoneCompliance.objects.select_related('zone__store_location')
Filtering:
- Filters:
frequency,requires_photo - Search:
zone__zone_name,zone__store_location__name
CRUD Operations
Base URL: /api/zones/compliance/
List: GET /api/zones/compliance/
- Filter by
frequency(daily, weekly, monthly) - Filter by
requires_photo(true/false) - Search by zone name or store name
Retrieve: GET /api/zones/compliance/{id}/
Create: POST /api/zones/compliance/
{
"sanity_id": "compliance-xyz",
"checklist_items": [
"Verify zone cleanliness",
"Check product placement",
"Ensure proper lighting"
],
"requires_photo": true,
"frequency": "daily"
}
Update: PUT/PATCH /api/zones/compliance/{id}/
Delete: DELETE /api/zones/compliance/{id}/
ZoneMerchandisingViewSet
ViewSet for zone merchandising standards management.
Purpose
Manages merchandising standards and required product configurations for zones.
Configuration
Queryset Optimization:
def get_queryset(self):
return ZoneMerchandising.objects.select_related(
'zone__store_location'
).prefetch_related('required_products')
Filtering:
- Search:
zone__zone_name,zone__store_location__name
CRUD Operations
Base URL: /api/zones/merchandising/
List: GET /api/zones/merchandising/
- Search by zone name or store name
- Includes
required_products_countcomputed field
Retrieve: GET /api/zones/merchandising/{id}/
Create: POST /api/zones/merchandising/
{
"sanity_id": "merchandising-xyz",
"display_standards": [
"Products at eye level",
"FIFO rotation",
"Price tags visible"
]
}
Update: PUT/PATCH /api/zones/merchandising/{id}/
Delete: DELETE /api/zones/merchandising/{id}/
ZoneInventoryViewSet
ViewSet for zone inventory configuration with SKU validation.
Purpose
Manages zone inventory configurations with par levels and provides SKU validation functionality.
Configuration
Queryset Optimization:
def get_queryset(self):
return ZoneInventory.objects.select_related('zone__store_location')
Serializer Selection:
def get_serializer_class(self):
if self.action == 'retrieve':
return ZoneInventoryDetailSerializer # Includes nested par levels
return ZoneInventorySerializer
Filtering:
- Filters:
count_frequency - Search:
zone__zone_name,zone__store_location__name
CRUD Operations
Base URL: /api/zones/inventory/
List: GET /api/zones/inventory/
- Filter by
count_frequency(daily, weekly, monthly) - Uses basic serializer (no nested par levels)
Retrieve: GET /api/zones/inventory/{id}/
- Uses detailed serializer with nested par levels
- Includes
par_levels_countcomputed field
Response (Retrieve):
{
"id": "uuid-123",
"sanity_id": "inventory-abc",
"count_frequency": "weekly",
"last_count_date": "2025-11-15",
"par_levels_count": 12,
"par_levels": [
{
"id": "uuid-par-1",
"product_name": "Dark Roast Coffee",
"variant_sku": "DARK-12OZ",
"quantity": 10,
"variant_exists": true,
"inventory_level_exists": true
}
],
"created_at": "2025-11-18T10:00:00Z",
"updated_at": "2025-11-18T10:00:00Z"
}
Custom Action: Validate SKUs
URL: POST /api/zones/inventory/{id}/validate_skus/
Purpose: Validate that all par level SKUs match actual ProductVariant records.
Service Integration:
@action(detail=True, methods=['post'])
def validate_skus(self, request, pk=None):
zone_inventory = self.get_object()
zone_service = ZoneAssignmentService()
results = zone_service.validate_sku_assignments(zone_inventory)
return Response(results)
Response (All Valid):
{
"valid_count": 12,
"invalid_count": 0,
"invalid_items": [],
"total_count": 12
}
Response (Some Invalid):
{
"valid_count": 10,
"invalid_count": 2,
"invalid_items": [
{
"par_level_id": "uuid-par-999",
"product_name": "Dark Roast Coffee",
"variant_sku": "INVALID-SKU",
"error": "No ProductVariant found with SKU 'INVALID-SKU' for product 'Dark Roast Coffee'"
},
{
"par_level_id": "uuid-par-998",
"product_name": "Light Roast Coffee",
"variant_sku": "WRONG-SKU",
"error": "No ProductVariant found with SKU 'WRONG-SKU' for product 'Light Roast Coffee'"
}
],
"total_count": 12
}
Use Case:
- After importing zone inventory from Sanity
- Debugging SKU mismatches
- Validating data integrity
ParLevelItemViewSet
ViewSet for par level item management with inventory infrastructure creation.
Purpose
Manages par level assignments (product quantities per zone) with automatic InventoryLevel creation and SKU validation.
Configuration
Queryset Optimization:
def get_queryset(self):
return ParLevelItem.objects.select_related(
'zone_inventory__zone__store_location', 'product'
)
Serializer Selection:
def get_serializer_class(self):
if self.action in ['create', 'update', 'partial_update']:
return ParLevelItemCreateSerializer # With validation
return ParLevelItemSerializer # Read-only
Filtering:
- Filters:
zone_inventory,product,zone_inventory__zone__store_location - Search:
product__name,variant_sku,zone_inventory__zone__zone_name - Ordering:
product__name,variant_sku,quantity,created_at - Default ordering:
zone_inventory__zone__zone_name,product__name,variant_sku
CRUD Operations
Base URL: /api/zones/par-levels/
List: GET /api/zones/par-levels/
- Filter by zone inventory, product, or store location
- Search by product name, variant SKU, or zone name
- Order by product name, SKU, quantity, or creation date
Retrieve: GET /api/zones/par-levels/{id}/
Response:
{
"id": "uuid-par-123",
"sanity_id": "par-level-abc",
"product": "uuid-product-456",
"product_name": "Dark Roast Coffee",
"variant_sku": "DARK-12OZ",
"quantity": 10,
"variant_exists": true,
"inventory_level_exists": true,
"created_at": "2025-11-18T10:00:00Z",
"updated_at": "2025-11-18T10:00:00Z"
}
Create: POST /api/zones/par-levels/
{
"zone_inventory": "uuid-zone-inventory-123",
"product": "uuid-product-456",
"variant_sku": "DARK-12OZ",
"quantity": 10,
"sanity_id": "par-level-xyz"
}
Validation (Create/Update):
- Validates variant_sku exists for the product
- Prevents duplicate par levels for same zone/product/variant
Update: PUT/PATCH /api/zones/par-levels/{id}/
Delete: DELETE /api/zones/par-levels/{id}/
Custom Actions
Ensure Inventory Level
URL: POST /api/zones/par-levels/{id}/ensure_inventory_level/
Purpose: Create InventoryLevel record for this par level item if it doesn't exist.
Implementation:
@action(detail=True, methods=['post'])
def ensure_inventory_level(self, request, pk=None):
par_level = self.get_object()
inventory_level = par_level.ensure_inventory_level_exists()
if inventory_level:
return Response({
'message': 'InventoryLevel ensured',
'inventory_level_id': inventory_level.pk,
'created': hasattr(inventory_level, '_created')
})
else:
return Response(
{'error': 'Failed to ensure InventoryLevel exists'},
status=500
)
Response (Created):
{
"message": "InventoryLevel ensured",
"inventory_level_id": "uuid-inv-level-789",
"created": true
}
Response (Already Exists):
{
"message": "InventoryLevel ensured",
"inventory_level_id": "uuid-inv-level-789",
"created": false
}
Error Response (500):
{
"error": "Failed to ensure InventoryLevel exists"
}
Use Case:
- After creating a new par level
- Ensuring inventory tracking infrastructure exists
- Debugging missing InventoryLevel records
Get Par Levels by Product
URL: GET /api/zones/par-levels/by_product/?product_id={product_id}&variant_sku={variant_sku}
Purpose: Get all par level assignments for a specific product variant across all zones.
Query Parameters:
product_id(required) - UUID of the productvariant_sku(required) - SKU of the variant
Response:
[
{
"id": "uuid-par-1",
"zone_inventory": "uuid-zone-inv-1",
"product_name": "Dark Roast Coffee",
"variant_sku": "DARK-12OZ",
"quantity": 10,
"variant_exists": true,
"inventory_level_exists": true
},
{
"id": "uuid-par-2",
"zone_inventory": "uuid-zone-inv-2",
"product_name": "Dark Roast Coffee",
"variant_sku": "DARK-12OZ",
"quantity": 8,
"variant_exists": true,
"inventory_level_exists": true
}
]
Error Response (400):
{
"error": "product_id and variant_sku parameters are required"
}
Use Case:
- Display all locations where a product should be stocked
- Product distribution analysis
- Par level management interface
PlanogramViewSet
ViewSet for planogram management with target store and zone assignments.
Purpose
Manages planograms (visual merchandising layouts) with product assignments, campaign relationships, and target store/zone configurations.
Configuration
Queryset Optimization:
def get_queryset(self):
return Planogram.objects.select_related('campaign').prefetch_related(
Prefetch('planogram_products', queryset=PlanogramProduct.objects.select_related('product')),
'target_stores', 'target_zones'
)
Filtering:
- Filters:
campaign,effective_date - Search:
planogram_name,version,campaign__name - Ordering:
planogram_name,effective_date,created_at - Default ordering:
planogram_name,-effective_date(newest first)
CRUD Operations
Base URL: /api/planograms/
List: GET /api/planograms/
- Filter by campaign or effective date
- Search by planogram name, version, or campaign name
- Order by name, effective date, or creation date
Retrieve: GET /api/planograms/{id}/
- Includes nested planogram products
- Includes target stores and target zones
Create: POST /api/planograms/
{
"sanity_id": "planogram-xyz",
"planogram_name": "Holiday Coffee Display",
"version": "v2.0",
"effective_date": "2025-12-01",
"campaign": "uuid-campaign-123",
"diagram_asset": {
"_type": "image",
"asset": {
"_ref": "image-abc123"
}
}
}
Update: PUT/PATCH /api/planograms/{id}/
Delete: DELETE /api/planograms/{id}/
Custom Action: Get Target Assignments
URL: GET /api/planograms/{id}/target_assignments/
Purpose: Get all target store and zone assignments for this planogram.
Implementation:
@action(detail=True, methods=['get'])
def target_assignments(self, request, pk=None):
planogram = self.get_object()
target_stores = list(planogram.target_stores.values('id', 'name', 'store_code'))
target_zones = list(planogram.target_zones.select_related('store_location').values(
'id', 'zone_name', 'store_location__name', 'zone_type'
))
return Response({
'target_stores': target_stores,
'target_zones': target_zones
})
Response:
{
"target_stores": [
{
"id": "uuid-store-1",
"name": "Downtown Location",
"store_code": "DT-001"
},
{
"id": "uuid-store-2",
"name": "Airport Location",
"store_code": "AP-002"
}
],
"target_zones": [
{
"id": "uuid-zone-1",
"zone_name": "Coffee Bar",
"store_location__name": "Downtown Location",
"zone_type": {"type": "merchandising"}
},
{
"id": "uuid-zone-2",
"zone_name": "Coffee Bar",
"store_location__name": "Airport Location",
"zone_type": {"type": "merchandising"}
}
]
}
Use Case:
- Display planogram rollout plan
- Track which stores/zones should implement planogram
- Planogram assignment management
PlanogramProductViewSet
ViewSet for planogram product assignments.
Purpose
Manages product assignments within planograms (which products, quantities, positioning).
Configuration
Queryset Optimization:
def get_queryset(self):
return PlanogramProduct.objects.select_related('planogram', 'product')
Serializer Selection:
def get_serializer_class(self):
if self.action in ['create', 'update', 'partial_update']:
return PlanogramProductCreateSerializer # With validation
return PlanogramProductSerializer # Read-only
Filtering:
- Filters:
planogram,product - Search:
product__name,variant_sku,planogram__planogram_name - Ordering:
product__name,variant_sku,quantity,created_at - Default ordering:
planogram__planogram_name,product__name,variant_sku
CRUD Operations
Base URL: /api/planograms/products/
List: GET /api/planograms/products/
- Filter by planogram or product
- Search by product name, variant SKU, or planogram name
- Order by product name, SKU, quantity, or creation date
Retrieve: GET /api/planograms/products/{id}/
Response:
{
"id": "uuid-plano-prod-123",
"sanity_id": "planogram-product-abc",
"planogram": "uuid-planogram-456",
"product": "uuid-product-789",
"product_name": "Dark Roast Coffee",
"variant_sku": "DARK-12OZ",
"quantity": 15,
"shelf_position": "A3",
"facing_count": 3,
"created_at": "2025-11-18T10:00:00Z",
"updated_at": "2025-11-18T10:00:00Z"
}
Create: POST /api/planograms/products/
{
"planogram": "uuid-planogram-456",
"product": "uuid-product-789",
"variant_sku": "DARK-12OZ",
"quantity": 15,
"shelf_position": "A3",
"facing_count": 3,
"sanity_id": "planogram-product-xyz"
}
Validation (Create/Update):
- Validates variant_sku exists for the product
- Prevents duplicate products in same planogram
Update: PUT/PATCH /api/planograms/products/{id}/
Delete: DELETE /api/planograms/products/{id}/
ZoneFilter
Custom filter for Zone model with enhanced filtering capabilities.
Configuration
class ZoneFilter(django_filters.FilterSet):
zone_name = django_filters.CharFilter(lookup_expr='icontains')
store_name = django_filters.CharFilter(field_name='store_location__name', lookup_expr='icontains')
class Meta:
model = Zone
fields = {
'is_shown': ['exact'],
'store_location': ['exact'],
}
Available Filters
| Filter | Type | Lookup | Description |
|---|---|---|---|
zone_name | CharFilter | icontains | Case-insensitive zone name search |
store_name | CharFilter | icontains | Case-insensitive store name search |
is_shown | BooleanFilter | exact | Filter by visibility status |
store_location | UUIDFilter | exact | Filter by store ID |
Note: zone_type filter was removed because zone_type is now a JSONField with complex data structure, not a simple string choice field.
Permission Configuration
Current State: All ViewSets have permission_classes = [] (no authentication required)
Production Recommendation:
from rest_framework.permissions import IsAuthenticated
class ZoneViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated] # Require authentication
Granular Permissions:
from rest_framework.permissions import IsAuthenticated, DjangoModelPermissions
class ZoneViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated, DjangoModelPermissions]
# Requires: zones.view_zone, zones.add_zone, zones.change_zone, zones.delete_zone
Integration Patterns
Zone Creation from Sanity Webhook
Workflow:
1. Sanity CMS sends webhook with zone data
2. ZoneSerializer.to_internal_value() converts camelCase to snake_case
3. ZoneSerializer.create() creates Zone instance
4. ZoneSerializer._process_zone_type_modules() creates nested zone type
5. Returns Zone with nested relationships
Example:
# Webhook payload (camelCase)
{
"_id": "zone-abc",
"zoneName": "Coffee Bar",
"storeLocation": {"_ref": "store-123"},
"zoneType": [{
"_type": "zoneInventory",
"countFrequency": "weekly"
}]
}
# Result: Zone created with ZoneInventory nested object
Inventory Infrastructure Creation
Workflow:
1. Par levels created in zone via Sanity webhook
2. POST /api/zones/{id}/sync_inventory/ to create infrastructure
3. ZoneAssignmentService.create_inventory_infrastructure_for_zone()
4. For each par level: ParLevelItem.ensure_inventory_level_exists()
5. InventoryLevel records created for tracking
Product Distribution Analysis
Workflow:
1. GET /api/zones/par-levels/by_product/?product_id=X&variant_sku=Y
2. Returns all zones where product should be stocked
3. Shows par quantities across all locations
4. Enables distribution planning and analysis
Related Documentation
Zones Domain
- README - Zones domain overview
- models.md - Zone, ZoneInventory, ParLevelItem, Planogram models
- serializers.md - Zone serializers with Sanity webhook processing
- services.md - ZoneAssignmentService business logic
- signals.md - Zone sync patterns
Related Domains
- products/models.md - Product, ProductVariant models
- products/views.md - Product API endpoints
- stores/models.md - Store model
- stores/views.md - Store API endpoints
DRF Documentation
Summary: Zones views provide comprehensive REST API endpoints for zone management, par level operations, planogram management, and inventory infrastructure creation with integration to ZoneAssignmentService for business logic.