- 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
111 lines
4.9 KiB
Python
111 lines
4.9 KiB
Python
import uuid
|
|
from django.conf import settings
|
|
from django.db import models
|
|
|
|
|
|
class CertificateOfAnalysis(models.Model):
|
|
class Status(models.TextChoices):
|
|
PENDING_APPROVAL = "pending_approval", "Pending Approval"
|
|
PUBLISHED = "published", "Published"
|
|
SUPERSEDED = "superseded", "Superseded"
|
|
|
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
batch = models.ForeignKey("batches.ProcessingBatch", on_delete=models.CASCADE, related_name="certificates_of_analysis")
|
|
qc_result = models.ForeignKey(
|
|
"qc.QCResult", on_delete=models.SET_NULL, null=True, blank=True, related_name="certificates"
|
|
)
|
|
file_url = models.TextField()
|
|
file_name = models.CharField(max_length=255)
|
|
issuer = models.CharField(max_length=255, blank=True)
|
|
issue_date = models.DateField(null=True, blank=True)
|
|
expiry_date = models.DateField(null=True, blank=True)
|
|
uploaded_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True)
|
|
uploaded_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
status = models.CharField(max_length=20, choices=Status.choices, default=Status.PENDING_APPROVAL)
|
|
version = models.IntegerField(default=1)
|
|
sha256_hash = models.CharField(max_length=64, blank=True)
|
|
spec_version = models.CharField(max_length=20, blank=True)
|
|
source_document = models.ForeignKey(
|
|
"Document", on_delete=models.SET_NULL, null=True, blank=True, related_name="source_for_coa"
|
|
)
|
|
quality_approved_by = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name="coas_quality_approved"
|
|
)
|
|
quality_approved_at = models.DateTimeField(null=True, blank=True)
|
|
released_by = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name="coas_released"
|
|
)
|
|
released_at = models.DateTimeField(null=True, blank=True)
|
|
published_at = models.DateTimeField(null=True, blank=True)
|
|
superseded_at = models.DateTimeField(null=True, blank=True)
|
|
superseded_by = models.ForeignKey(
|
|
"self", on_delete=models.SET_NULL, null=True, blank=True, related_name="supersedes"
|
|
)
|
|
|
|
class Meta:
|
|
db_table = "certificates_of_analysis"
|
|
verbose_name_plural = "Certificates of Analysis"
|
|
|
|
def __str__(self):
|
|
return f"CoA - {self.batch.batch_reference} - {self.file_name}"
|
|
|
|
|
|
class RegulatoryCert(models.Model):
|
|
class CertType(models.TextChoices):
|
|
PHYTOSANITARY = "phytosanitary", "Phytosanitary"
|
|
EXPORT_PERMIT = "export_permit", "Export Permit"
|
|
ORGANIC = "organic", "Organic"
|
|
FAIR_TRADE = "fair_trade", "Fair Trade"
|
|
EUDR_DDS = "eudr_dds", "EUDR DDS"
|
|
OTHER = "other", "Other"
|
|
|
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
batch = models.ForeignKey("batches.ProcessingBatch", on_delete=models.CASCADE, related_name="regulatory_certs")
|
|
cert_type = models.CharField(max_length=20, choices=CertType.choices)
|
|
file_url = models.TextField()
|
|
file_name = models.CharField(max_length=255)
|
|
cert_reference = models.CharField(max_length=255, blank=True)
|
|
issue_date = models.DateField(null=True, blank=True)
|
|
expiry_date = models.DateField(null=True, blank=True)
|
|
uploaded_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
class Meta:
|
|
db_table = "regulatory_certs"
|
|
verbose_name = "Regulatory Certificate"
|
|
|
|
def __str__(self):
|
|
return f"{self.get_cert_type_display()} - {self.batch.batch_reference}"
|
|
|
|
|
|
class Document(models.Model):
|
|
class DocType(models.TextChoices):
|
|
COA = "coa", "Certificate of Analysis"
|
|
LAB_REPORT = "lab_report", "Laboratory Report"
|
|
REGULATORY = "regulatory", "Regulatory"
|
|
INVOICE = "invoice", "Invoice"
|
|
PACKING_LIST = "packing_list", "Packing List"
|
|
BL = "bl", "Bill of Lading"
|
|
INSURANCE = "insurance", "Insurance"
|
|
OTHER = "other", "Other"
|
|
|
|
class Visibility(models.TextChoices):
|
|
BUYER = "buyer", "Buyer"
|
|
OPS = "ops", "Operations"
|
|
ADMIN = "admin", "Admin"
|
|
|
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
batch = models.ForeignKey("batches.ProcessingBatch", on_delete=models.CASCADE, null=True, blank=True, related_name="documents")
|
|
doc_type = models.CharField(max_length=20, choices=DocType.choices)
|
|
file_url = models.TextField()
|
|
file_name = models.CharField(max_length=255)
|
|
file_size_bytes = models.IntegerField(null=True, blank=True)
|
|
visibility = models.CharField(max_length=10, choices=Visibility.choices, default=Visibility.BUYER)
|
|
uploaded_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True)
|
|
uploaded_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
class Meta:
|
|
db_table = "documents"
|
|
|
|
def __str__(self):
|
|
return f"{self.get_doc_type_display()} - {self.file_name}"
|