Files
Tim Hadwen 2477fd1539 Initial release: RMA Automation plugin with repair parts tracking
Features:
- Automatic stock status updates based on return order outcomes
- Repair parts allocation and consumption tracking
- Configurable status mappings for each outcome type
- React-based UI panel for managing repair parts
- Location display for easy part retrieval
- Available stock filtering (excludes allocated items)
2026-01-18 20:56:07 +10:00

154 lines
5.3 KiB
Python

"""API endpoints for the RMA Automation plugin."""
from rest_framework import serializers, generics, permissions
from rest_framework.response import Response
from inventree_rma_plugin.models import RepairStockAllocation
class RepairAllocationSerializer(serializers.ModelSerializer):
"""Serializer for RepairStockAllocation model."""
# Read-only fields for display
stock_item_detail = serializers.SerializerMethodField(read_only=True)
line_item_detail = serializers.SerializerMethodField(read_only=True)
return_order_line_detail = serializers.SerializerMethodField(read_only=True)
return_order_id = serializers.SerializerMethodField(read_only=True)
class Meta:
"""Serializer metadata."""
model = RepairStockAllocation
fields = [
'id',
'return_order_line',
'return_order_line_detail',
'line_item_detail',
'return_order_id',
'stock_item',
'stock_item_detail',
'quantity',
'consumed',
'created',
'notes',
]
read_only_fields = ['id', 'consumed', 'created']
def get_stock_item_detail(self, obj):
"""Get stock item details for display."""
stock_item = obj.stock_item
return {
'pk': stock_item.pk,
'part': stock_item.part.pk,
'part_name': stock_item.part.name,
'quantity': float(stock_item.quantity),
'serial': stock_item.serial,
'batch': stock_item.batch,
'location': stock_item.location.pk if stock_item.location else None,
'location_name': str(stock_item.location) if stock_item.location else None,
}
def get_line_item_detail(self, obj):
"""Get the line item details (the item being repaired)."""
line = obj.return_order_line
item = line.item if line else None
if not item:
return {
'pk': None,
'part_name': 'Unknown',
'serial': None,
}
return {
'pk': item.pk,
'part_name': item.part.name if item.part else 'Unknown',
'serial': item.serial,
'batch': item.batch,
}
def get_return_order_line_detail(self, obj):
"""Get return order line item details for display."""
line = obj.return_order_line
return {
'pk': line.pk,
'item_pk': line.item.pk if line.item else None,
'item_name': str(line.item) if line.item else None,
}
def get_return_order_id(self, obj):
"""Get the parent return order ID."""
return obj.return_order_line.order.pk
def validate(self, data):
"""Validate the allocation data."""
stock_item = data.get('stock_item')
quantity = data.get('quantity', 1)
if stock_item:
# Check available quantity
available = stock_item.quantity
# Subtract existing allocations for this stock item
existing_allocations = RepairStockAllocation.objects.filter(
stock_item=stock_item,
consumed=False,
)
# Exclude current instance if updating
if self.instance:
existing_allocations = existing_allocations.exclude(pk=self.instance.pk)
allocated = sum(float(a.quantity) for a in existing_allocations)
available -= allocated
if quantity > available:
raise serializers.ValidationError({
'quantity': f'Only {available} available (already allocated: {allocated})',
})
return data
class RepairAllocationList(generics.ListCreateAPIView):
"""List and create repair stock allocations."""
serializer_class = RepairAllocationSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
"""Filter allocations based on query parameters."""
queryset = RepairStockAllocation.objects.all()
# Filter by return order
return_order = self.request.query_params.get('return_order')
if return_order:
queryset = queryset.filter(return_order_line__order__pk=return_order)
# Filter by return order line
return_order_line = self.request.query_params.get('return_order_line')
if return_order_line:
queryset = queryset.filter(return_order_line__pk=return_order_line)
# Filter by consumed status
consumed = self.request.query_params.get('consumed')
if consumed is not None:
consumed_bool = consumed.lower() in ('true', '1', 'yes')
queryset = queryset.filter(consumed=consumed_bool)
return queryset.select_related(
'stock_item',
'stock_item__part',
'stock_item__location',
'return_order_line',
'return_order_line__order',
'return_order_line__item',
'return_order_line__item__part',
)
class RepairAllocationDetail(generics.RetrieveUpdateDestroyAPIView):
"""Retrieve, update, or delete a repair stock allocation."""
serializer_class = RepairAllocationSerializer
permission_classes = [permissions.IsAuthenticated]
queryset = RepairStockAllocation.objects.all()