- Versioned product specification table (Unrefined Shea Butter v1.0 seed) with spec-based QC evaluation replacing hard-coded thresholds - Structured QC results with test methods and four-eyes verification (submitter cannot self-approve) - reportlab IXG Quality Summary PDF auto-generated on quality approval, SHA-256 hashed; original independent lab PDF kept authoritative - CertificateOfAnalysis version/status/hash/approval/supersede lifecycle; commercial release publishes to buyers and triggers notification - EUDR evidence workflow: Facility, OriginZone, CollectorGroup, HarvestIntake, IntakeBatchLineage, DueDiligenceCase, EUDREvidence, RiskAssessment, MitigationAction, DueDiligenceStatementReference with rule-based completeness check and risk assessment (no AI legal conclusions) - Ops portal: QC Approvals, CoA Reviews, EUDR Compliance screens - Buyer portal: /portal/compliance page - 22 automated tests covering four-eyes, CAPA, PDF+hash, publish/notify, EUDR blocks and buyer isolation
170 lines
6.8 KiB
Python
170 lines
6.8 KiB
Python
from rest_framework import viewsets, permissions, status
|
|
from rest_framework.decorators import action
|
|
from rest_framework.response import Response
|
|
|
|
from .models import (
|
|
ProductSpecification, Facility, OriginZone, CollectorGroup, HarvestIntake,
|
|
IntakeBatchLineage, DueDiligenceCase, EUDREvidence, RiskAssessment,
|
|
MitigationAction, DueDiligenceStatementReference,
|
|
)
|
|
from .serializers import (
|
|
ProductSpecificationSerializer, FacilitySerializer, OriginZoneSerializer,
|
|
CollectorGroupSerializer, HarvestIntakeSerializer, IntakeBatchLineageSerializer,
|
|
DueDiligenceCaseSerializer, EUDREvidenceSerializer, RiskAssessmentSerializer,
|
|
MitigationActionSerializer, DueDiligenceStatementReferenceSerializer,
|
|
)
|
|
from .permissions import IsOpsOrAdmin, IsAdmin
|
|
from . import services
|
|
|
|
|
|
class ProductSpecificationViewSet(viewsets.ModelViewSet):
|
|
queryset = ProductSpecification.objects.prefetch_related("parameters").all()
|
|
serializer_class = ProductSpecificationSerializer
|
|
permission_classes = [IsOpsOrAdmin]
|
|
|
|
def get_permissions(self):
|
|
if self.action in ("create", "update", "partial_update", "destroy"):
|
|
return [permission() for permission in [IsAdmin]]
|
|
return [permission() for permission in [IsOpsOrAdmin]]
|
|
|
|
|
|
class FacilityViewSet(viewsets.ModelViewSet):
|
|
queryset = Facility.objects.all()
|
|
serializer_class = FacilitySerializer
|
|
permission_classes = [IsOpsOrAdmin]
|
|
|
|
|
|
class OriginZoneViewSet(viewsets.ModelViewSet):
|
|
queryset = OriginZone.objects.select_related("facility", "verified_by").all()
|
|
serializer_class = OriginZoneSerializer
|
|
permission_classes = [IsOpsOrAdmin]
|
|
|
|
@action(detail=True, methods=["post"])
|
|
def verify(self, request, pk=None):
|
|
zone = self.get_object()
|
|
outcome = request.data.get("verified", "true")
|
|
verified = str(outcome).lower() in ("true", "1", "yes")
|
|
zone.verification_status = "verified" if verified else "rejected"
|
|
zone.verified_by = request.user
|
|
from django.utils import timezone
|
|
zone.verified_at = timezone.now()
|
|
zone.save()
|
|
return Response(OriginZoneSerializer(zone).data)
|
|
|
|
|
|
class CollectorGroupViewSet(viewsets.ModelViewSet):
|
|
queryset = CollectorGroup.objects.all()
|
|
serializer_class = CollectorGroupSerializer
|
|
permission_classes = [IsOpsOrAdmin]
|
|
|
|
|
|
class HarvestIntakeViewSet(viewsets.ModelViewSet):
|
|
queryset = HarvestIntake.objects.select_related("origin_zone", "collector_group").all()
|
|
serializer_class = HarvestIntakeSerializer
|
|
permission_classes = [IsOpsOrAdmin]
|
|
|
|
|
|
class IntakeBatchLineageViewSet(viewsets.ModelViewSet):
|
|
queryset = IntakeBatchLineage.objects.select_related("batch", "harvest_intake").all()
|
|
serializer_class = IntakeBatchLineageSerializer
|
|
permission_classes = [IsOpsOrAdmin]
|
|
|
|
def perform_create(self, serializer):
|
|
lineage = serializer.save()
|
|
services.ensure_due_diligence_case(lineage.batch)
|
|
|
|
|
|
class EUDREvidenceViewSet(viewsets.ModelViewSet):
|
|
queryset = EUDREvidence.objects.select_related("case", "verified_by").all()
|
|
serializer_class = EUDREvidenceSerializer
|
|
permission_classes = [IsOpsOrAdmin]
|
|
|
|
def perform_create(self, serializer):
|
|
evidence = serializer.save()
|
|
case = evidence.case
|
|
services.run_completeness_check(case)
|
|
|
|
|
|
class RiskAssessmentViewSet(viewsets.ModelViewSet):
|
|
queryset = RiskAssessment.objects.select_related("case", "assessed_by").all()
|
|
serializer_class = RiskAssessmentSerializer
|
|
permission_classes = [IsOpsOrAdmin]
|
|
|
|
|
|
class MitigationActionViewSet(viewsets.ModelViewSet):
|
|
queryset = MitigationAction.objects.select_related("case").all()
|
|
serializer_class = MitigationActionSerializer
|
|
permission_classes = [IsOpsOrAdmin]
|
|
|
|
@action(detail=True, methods=["post"])
|
|
def complete(self, request, pk=None):
|
|
mitigation = self.get_object()
|
|
mitigation.status = MitigationAction.Status.VERIFIED
|
|
from django.utils import timezone
|
|
mitigation.completed_at = timezone.now()
|
|
mitigation.save()
|
|
services.run_completeness_check(mitigation.case)
|
|
return Response(MitigationActionSerializer(mitigation).data)
|
|
|
|
|
|
class DueDiligenceStatementReferenceViewSet(viewsets.ModelViewSet):
|
|
queryset = DueDiligenceStatementReference.objects.select_related("case", "submitted_by").all()
|
|
serializer_class = DueDiligenceStatementReferenceSerializer
|
|
permission_classes = [IsOpsOrAdmin]
|
|
|
|
|
|
class DueDiligenceCaseViewSet(viewsets.ModelViewSet):
|
|
queryset = DueDiligenceCase.objects.select_related(
|
|
"batch", "approved_by", "assessed_by"
|
|
).prefetch_related("evidence", "risk_assessments", "mitigation_actions", "statement_references").order_by("-updated_at").all()
|
|
serializer_class = DueDiligenceCaseSerializer
|
|
permission_classes = [IsOpsOrAdmin]
|
|
|
|
def perform_create(self, serializer):
|
|
serializer.save()
|
|
|
|
@action(detail=True, methods=["post"])
|
|
def check(self, request, pk=None):
|
|
case = self.get_object()
|
|
services.run_completeness_check(case)
|
|
return Response(DueDiligenceCaseSerializer(case).data)
|
|
|
|
@action(detail=True, methods=["post"])
|
|
def assess_risk(self, request, pk=None):
|
|
case = self.get_object()
|
|
risk = services.evaluate_risk(case, user=request.user)
|
|
return Response({"risk_assessment_id": str(risk.id), **RiskAssessmentSerializer(risk).data})
|
|
|
|
@action(detail=True, methods=["post"])
|
|
def approve(self, request, pk=None):
|
|
case = self.get_object()
|
|
try:
|
|
services.approve_case(case, request.user)
|
|
except ValueError as exc:
|
|
return Response({"detail": str(exc)}, status=status.HTTP_400_BAD_REQUEST)
|
|
return Response(DueDiligenceCaseSerializer(case).data)
|
|
|
|
@action(detail=True, methods=["post"])
|
|
def reject(self, request, pk=None):
|
|
case = self.get_object()
|
|
reason = request.data.get("reason", "")
|
|
try:
|
|
services.reject_case(case, request.user, reason)
|
|
except ValueError as exc:
|
|
return Response({"detail": str(exc)}, status=status.HTTP_400_BAD_REQUEST)
|
|
return Response(DueDiligenceCaseSerializer(case).data)
|
|
|
|
@action(detail=True, methods=["post"])
|
|
def record_submission(self, request, pk=None):
|
|
case = self.get_object()
|
|
dds_reference = request.data.get("dds_reference", "")
|
|
submission_date = request.data.get("submission_date")
|
|
if not dds_reference:
|
|
return Response({"detail": "dds_reference is required"}, status=status.HTTP_400_BAD_REQUEST)
|
|
ref = services.record_submission(case, request.user, dds_reference, submission_date)
|
|
return Response(DueDiligenceStatementReferenceSerializer(ref).data)
|
|
|
|
@action(detail=True, methods=["get"])
|
|
def evidence_package(self, request, pk=None):
|
|
case = self.get_object()
|
|
return Response(services.evidence_package(case))
|