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

132 lines
4.9 KiB
Python

from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.db.models import Count
from django.views.generic import TemplateView, ListView, DetailView
from apps.batches.models import ProcessingBatch
from apps.shipments.models import Shipment
from apps.documents.models import Document
from apps.esg.models import ESGFieldReport
from apps.subscriptions.models import Subscription
class BuyerRequiredMixin(UserPassesTestMixin):
def test_func(self):
user = self.request.user
return user.is_authenticated and (user.role in ("buyer", "admin") or user.is_superuser)
class DashboardView(BuyerRequiredMixin, TemplateView):
template_name = "portal/dashboard.html"
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx["total_batches"] = ProcessingBatch.objects.count()
ctx["recent_batches"] = ProcessingBatch.objects.order_by("-created_at")[:5]
ctx["documents"] = Document.objects.filter(visibility="buyer").order_by("-uploaded_at")[:5]
ctx["active_shipments"] = Shipment.objects.exclude(status="delivered").count()
ctx["subscription"] = Subscription.objects.filter(user=self.request.user).first()
return ctx
class BatchListView(BuyerRequiredMixin, ListView):
model = ProcessingBatch
template_name = "portal/batch_list.html"
context_object_name = "batches"
paginate_by = 25
def get_queryset(self):
qs = ProcessingBatch.objects.all()
status = self.request.GET.get("status")
q = self.request.GET.get("q")
if status:
qs = qs.filter(status=status)
if q:
qs = qs.filter(batch_reference__icontains=q)
return qs
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx["status_choices"] = ProcessingBatch.Status.choices
ctx["current_status"] = self.request.GET.get("status", "")
ctx["current_q"] = self.request.GET.get("q", "")
return ctx
class BatchDetailView(BuyerRequiredMixin, DetailView):
model = ProcessingBatch
template_name = "portal/batch_detail.html"
context_object_name = "batch"
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
b = self.object
ctx["shipments"] = Shipment.objects.filter(batch=b)
ctx["documents"] = Document.objects.filter(batch=b, visibility="buyer")
ctx["esg_reports"] = ESGFieldReport.objects.filter(batch=b)
return ctx
class DocumentListView(BuyerRequiredMixin, ListView):
model = Document
template_name = "portal/document_list.html"
context_object_name = "documents"
paginate_by = 25
def get_queryset(self):
qs = Document.objects.filter(visibility="buyer").select_related("batch", "uploaded_by")
doc_type = self.request.GET.get("doc_type")
if doc_type:
qs = qs.filter(doc_type=doc_type)
return qs
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx["doc_type_choices"] = Document.DocType.choices
ctx["current_doc_type"] = self.request.GET.get("doc_type", "")
return ctx
class SubscriptionView(BuyerRequiredMixin, DetailView):
model = Subscription
template_name = "portal/subscription.html"
context_object_name = "sub"
def get_object(self):
return Subscription.objects.filter(user=self.request.user).first()
class ChatView(BuyerRequiredMixin, TemplateView):
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
from apps.ai.models import AgentConfig
user = self.request.user
if user.role in ("admin", "ops") or user.is_superuser:
ctx["agents"] = AgentConfig.objects.filter(is_active=True)
else:
ctx["agents"] = AgentConfig.objects.filter(is_active=True, user__isnull=True)
return ctx
template_name = "portal/chat.html"
class ChatEmbedView(BuyerRequiredMixin, TemplateView):
template_name = "portal/chat_embed.html"
def dispatch(self, request, *args, **kwargs):
response = super().dispatch(request, *args, **kwargs)
response["X-Frame-Options"] = "ALLOWALL"
return response
class ComplianceView(BuyerRequiredMixin, TemplateView):
template_name = "portal/compliance.html"
def get_context_data(self, **kwargs):
from apps.compliance.models import DueDiligenceCase
from apps.documents.models import CertificateOfAnalysis
ctx = super().get_context_data(**kwargs)
ctx["approved_coas"] = CertificateOfAnalysis.objects.filter(
status=CertificateOfAnalysis.Status.PUBLISHED
).select_related("batch", "quality_approved_by").order_by("-published_at")
ctx["eudr_cases"] = DueDiligenceCase.objects.select_related("batch", "approved_by").order_by("-updated_at")
return ctx