ixg_platform/apps/compliance/views.py
ixgadmin 08096e168f P99C: compliance automation - CoA auto-generation and EUDR due diligence
- 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
2026-08-05 13:58:39 +00:00

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))