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

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}"