- 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
132 lines
4.9 KiB
Python
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
|