- 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
64 lines
2.7 KiB
Python
64 lines
2.7 KiB
Python
import uuid
|
|
from django.conf import settings
|
|
from django.db import models
|
|
|
|
|
|
class QCResult(models.Model):
|
|
class VisualGrade(models.TextChoices):
|
|
A = "A", "A"
|
|
B = "B", "B"
|
|
C = "C", "C"
|
|
FAIL = "Fail", "Fail"
|
|
|
|
class VerificationStatus(models.TextChoices):
|
|
DRAFT = "draft", "Draft"
|
|
VALIDATED = "validated", "Validated"
|
|
APPROVED = "approved", "Approved"
|
|
REJECTED = "rejected", "Rejected"
|
|
|
|
class ResultStatus(models.TextChoices):
|
|
WITHIN_SPEC = "within_spec", "Within Specification"
|
|
OUTSIDE_SPEC = "outside_spec", "Outside Specification"
|
|
MISSING = "missing", "Missing"
|
|
UNIT_MISMATCH = "unit_mismatch", "Unit Mismatch"
|
|
|
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
batch = models.ForeignKey("batches.ProcessingBatch", on_delete=models.CASCADE, related_name="qc_results")
|
|
ffa_percentage = models.DecimalField(max_digits=5, decimal_places=2)
|
|
moisture_percentage = models.DecimalField(max_digits=5, decimal_places=2)
|
|
peroxide_value = models.DecimalField(max_digits=5, decimal_places=2)
|
|
visual_grade = models.CharField(max_length=5, choices=VisualGrade.choices)
|
|
overall_pass = models.BooleanField()
|
|
tested_by = models.CharField(max_length=255, blank=True)
|
|
tested_at = models.DateTimeField(auto_now_add=True)
|
|
lab_reference = models.CharField(max_length=255, blank=True)
|
|
|
|
spec_version = models.CharField(max_length=20, blank=True)
|
|
entered_by = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name="qc_entered"
|
|
)
|
|
verified_by = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name="qc_verified"
|
|
)
|
|
verification_status = models.CharField(
|
|
max_length=20, choices=VerificationStatus.choices, default=VerificationStatus.DRAFT
|
|
)
|
|
approved_at = models.DateTimeField(null=True, blank=True)
|
|
method_ffa = models.CharField(max_length=255, blank=True)
|
|
method_moisture = models.CharField(max_length=255, blank=True)
|
|
method_peroxide = models.CharField(max_length=255, blank=True)
|
|
result_status = models.CharField(
|
|
max_length=20, choices=ResultStatus.choices, default=ResultStatus.WITHIN_SPEC
|
|
)
|
|
superseded_by = models.ForeignKey(
|
|
"self", on_delete=models.SET_NULL, null=True, blank=True, related_name="supersedes"
|
|
)
|
|
is_quarantine = models.BooleanField(default=False)
|
|
|
|
class Meta:
|
|
db_table = "qc_results"
|
|
ordering = ["-tested_at"]
|
|
|
|
def __str__(self):
|
|
status = "PASS" if self.overall_pass else "FAIL"
|
|
return f"QC for {self.batch.batch_reference}: {status}"
|