ixg_platform/apps/qc/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

72 lines
2.8 KiB
Python

from django.utils import timezone
from rest_framework import viewsets, permissions, status
from rest_framework.decorators import action
from rest_framework.response import Response
from .models import QCResult
from .serializers import QCResultSerializer, QCResultCreateSerializer
from apps.compliance.services import create_coa_for_qc
class IsOpsOrAdmin(permissions.BasePermission):
def has_permission(self, request, view):
user = request.user
return bool(user and user.is_authenticated and (user.role in ("ops", "admin") or user.is_superuser))
class QCResultViewSet(viewsets.ModelViewSet):
queryset = QCResult.objects.all()
permission_classes = [permissions.IsAuthenticated]
def get_serializer_class(self):
if self.action == "create":
return QCResultCreateSerializer
return QCResultSerializer
def perform_create(self, serializer):
serializer.save()
@action(detail=True, methods=["post"], permission_classes=[IsOpsOrAdmin])
def verify(self, request, pk=None):
"""Four-eyes quality approval. A separate authorised user validates the
entered result. The submitter cannot approve their own result."""
qc = self.get_object()
user = request.user
if qc.verification_status == QCResult.VerificationStatus.APPROVED:
return Response({"detail": "QC result already approved"}, status=status.HTTP_400_BAD_REQUEST)
if qc.entered_by is not None and qc.entered_by_id == user.id:
return Response(
{"detail": "The submitter cannot approve their own QC result (four-eyes control)"},
status=status.HTTP_400_BAD_REQUEST,
)
approved = request.data.get("approved", "true")
approved = str(approved).lower() in ("true", "1", "yes")
if approved:
if not qc.overall_pass:
return Response(
{"detail": "A failed QC result cannot be approved; it remains quarantined with an open CAPA"},
status=status.HTTP_400_BAD_REQUEST,
)
qc.verification_status = QCResult.VerificationStatus.APPROVED
qc.verified_by = user
qc.approved_at = timezone.now()
qc.save()
coa = create_coa_for_qc(qc, approved_by=user)
return Response(
{
"detail": "QC result approved",
"coa_id": str(coa.id),
"coa_status": coa.status,
"coa_url": coa.file_url,
}
)
qc.verification_status = QCResult.VerificationStatus.REJECTED
qc.verified_by = user
qc.approved_at = timezone.now()
qc.save()
return Response({"detail": "QC result rejected"})