import uuid from django.conf import settings from django.db import models class ProductSpecification(models.Model): class Status(models.TextChoices): DRAFT = "draft", "Draft" APPROVED = "approved", "Approved" SUPERSEDED = "superseded", "Superseded" id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) product = models.CharField(max_length=100) version = models.CharField(max_length=20) status = models.CharField(max_length=20, choices=Status.choices, default=Status.DRAFT) effective_from = models.DateField(null=True, blank=True) effective_to = models.DateField(null=True, blank=True) is_current = models.BooleanField(default=False) approved_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name="specs_approved", ) approved_at = models.DateTimeField(null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: db_table = "product_specifications" unique_together = [("product", "version")] ordering = ["product", "-version"] def __str__(self): return f"{self.product} v{self.version} ({self.status})" @classmethod def current(cls, product): return cls.objects.filter(product=product, status=cls.Status.APPROVED, is_current=True).first() class SpecParameter(models.Model): class Parameter(models.TextChoices): FFA = "ffa", "Free Fatty Acid" MOISTURE = "moisture", "Moisture" PEROXIDE = "peroxide", "Peroxide Value" VISUAL = "visual", "Visual Grade" id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) spec = models.ForeignKey(ProductSpecification, on_delete=models.CASCADE, related_name="parameters") parameter = models.CharField(max_length=20, choices=Parameter.choices) unit = models.CharField(max_length=50, blank=True) min_value = models.DecimalField(max_digits=8, decimal_places=3, null=True, blank=True) max_value = models.DecimalField(max_digits=8, decimal_places=3, null=True, blank=True) allowed_values = models.CharField(max_length=255, blank=True) required = models.BooleanField(default=True) sort_order = models.IntegerField(default=0) class Meta: db_table = "spec_parameters" unique_together = [("spec", "parameter")] ordering = ["sort_order"] def __str__(self): return f"{self.get_parameter_display()} ({self.unit})" def check_value(self, value): if value is None or value == "": if self.required: return ("missing", "Value required") return ("within", "Optional value absent") if self.parameter == self.Parameter.VISUAL: allowed = [v.strip() for v in self.allowed_values.split(",") if v.strip()] if allowed and str(value) not in allowed: return ("outside", f"Visual grade '{value}' not in allowed values: {', '.join(allowed)}") return ("within", "Within specification") try: numeric = float(value) except (TypeError, ValueError): return ("missing", "Non-numeric value") if self.min_value is not None and numeric < float(self.min_value): return ("outside", f"{value} below minimum {self.min_value} {self.unit}") if self.max_value is not None and numeric > float(self.max_value): return ("outside", f"{value} above maximum {self.max_value} {self.unit}") return ("within", "Within specification") class Facility(models.Model): class FacilityType(models.TextChoices): PRODUCTION = "production", "Production" PROCESSING = "processing", "Processing" id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) name = models.CharField(max_length=255) country = models.CharField(max_length=100) region = models.CharField(max_length=100, blank=True) address = models.TextField(blank=True) facility_type = models.CharField(max_length=20, choices=FacilityType.choices, default=FacilityType.PRODUCTION) is_active = models.BooleanField(default=True) created_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = "facilities" def __str__(self): return self.name class OriginZone(models.Model): class VerificationStatus(models.TextChoices): UNVERIFIED = "unverified", "Unverified" VERIFIED = "verified", "Verified" REJECTED = "rejected", "Rejected" id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) facility = models.ForeignKey(Facility, on_delete=models.CASCADE, related_name="origin_zones") zone_code = models.CharField(max_length=100, unique=True) country = models.CharField(max_length=100) region = models.CharField(max_length=100, blank=True) geometry = models.JSONField(default=dict, blank=True) coordinate_source = models.CharField(max_length=255, blank=True) coordinates_collected_by = models.CharField(max_length=255, blank=True) coordinates_collected_at = models.DateField(null=True, blank=True) evidence_file = models.CharField(max_length=255, blank=True) verification_status = models.CharField( max_length=20, choices=VerificationStatus.choices, default=VerificationStatus.UNVERIFIED ) verified_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name="zones_verified" ) verified_at = models.DateTimeField(null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = "origin_zones" def __str__(self): return f"{self.zone_code} ({self.country})" class CollectorGroup(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) name = models.CharField(max_length=255) contact_reference = models.CharField(max_length=255, blank=True) origin_zone = models.ForeignKey( OriginZone, on_delete=models.SET_NULL, null=True, blank=True, related_name="collector_groups" ) notes = models.TextField(blank=True) created_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = "collector_groups" def __str__(self): return self.name class HarvestIntake(models.Model): class Status(models.TextChoices): DRAFT = "draft", "Draft" RECEIVED = "received", "Received" VERIFIED = "verified", "Verified" REJECTED = "rejected", "Rejected" id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) intake_reference = models.CharField(max_length=100, unique=True) collector_group = models.ForeignKey( CollectorGroup, on_delete=models.SET_NULL, null=True, blank=True, related_name="intakes" ) origin_zone = models.ForeignKey( OriginZone, on_delete=models.SET_NULL, null=True, blank=True, related_name="intakes" ) intake_date = models.DateField(null=True, blank=True) weight_kg = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) grade = models.CharField(max_length=5, blank=True) receiving_officer = models.CharField(max_length=255, blank=True) status = models.CharField(max_length=20, choices=Status.choices, default=Status.DRAFT) evidence_file = models.CharField(max_length=255, blank=True) created_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = "harvest_intakes" def __str__(self): return self.intake_reference class IntakeBatchLineage(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) batch = models.ForeignKey("batches.ProcessingBatch", on_delete=models.CASCADE, related_name="intake_lineage") harvest_intake = models.ForeignKey(HarvestIntake, on_delete=models.CASCADE, related_name="batch_lineage") weight_allocated_kg = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = "intake_batch_lineage" unique_together = [("batch", "harvest_intake")] def __str__(self): return f"{self.batch.batch_reference} <- {self.harvest_intake.intake_reference}" class DueDiligenceCase(models.Model): class Status(models.TextChoices): INCOMPLETE = "incomplete", "Incomplete" COMPLETE_FOR_REVIEW = "complete_for_review", "Complete for Review" RISK_IDENTIFIED = "risk_identified", "Risk Identified" MITIGATION_REQUIRED = "mitigation_required", "Mitigation Required" APPROVED = "approved", "Approved" REJECTED = "rejected", "Rejected" class RiskLevel(models.TextChoices): LOW = "low", "Low" STANDARD = "standard", "Standard" HIGH = "high", "High" id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) batch = models.OneToOneField("batches.ProcessingBatch", on_delete=models.CASCADE, related_name="due_diligence_case") status = models.CharField(max_length=30, choices=Status.choices, default=Status.INCOMPLETE) risk_level = models.CharField(max_length=20, choices=RiskLevel.choices, default=RiskLevel.STANDARD) completeness_results = models.JSONField(default=dict, blank=True) assessed_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name="eudr_cases_assessed" ) assessed_at = models.DateTimeField(null=True, blank=True) approved_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name="eudr_cases_approved" ) approved_at = models.DateTimeField(null=True, blank=True) rejected_reason = models.TextField(blank=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: db_table = "due_diligence_cases" def __str__(self): return f"EUDR case - {self.batch.batch_reference} ({self.status})" class EUDREvidence(models.Model): class EvidenceType(models.TextChoices): GEOLOCATION = "geolocation", "Geolocation" HARVEST_DOCUMENT = "harvest_document", "Harvest Document" SUPPLIER_IDENTITY = "supplier_identity", "Supplier Identity" LEGALITY = "legality", "Legality" OTHER = "other", "Other" id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) case = models.ForeignKey(DueDiligenceCase, on_delete=models.CASCADE, related_name="evidence") evidence_type = models.CharField(max_length=30, choices=EvidenceType.choices, default=EvidenceType.OTHER) file = models.CharField(max_length=255) source = models.CharField(max_length=255, blank=True) collected_at = models.DateField(null=True, blank=True) verified_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name="eudr_evidence_verified" ) verified_at = models.DateTimeField(null=True, blank=True) notes = models.TextField(blank=True) created_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = "eudr_evidence" def __str__(self): return f"{self.get_evidence_type_display()} - {self.case.batch.batch_reference}" class RiskAssessment(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) case = models.ForeignKey(DueDiligenceCase, on_delete=models.CASCADE, related_name="risk_assessments") risk_level = models.CharField(max_length=20, choices=DueDiligenceCase.RiskLevel.choices, default=DueDiligenceCase.RiskLevel.STANDARD) basis = models.TextField(blank=True) findings = models.TextField(blank=True) assessed_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name="risk_assessments" ) assessed_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = "risk_assessments" ordering = ["-assessed_at"] def __str__(self): return f"Risk {self.risk_level} - {self.case.batch.batch_reference}" class MitigationAction(models.Model): class Status(models.TextChoices): OPEN = "open", "Open" IN_PROGRESS = "in_progress", "In Progress" COMPLETED = "completed", "Completed" VERIFIED = "verified", "Verified" id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) case = models.ForeignKey(DueDiligenceCase, on_delete=models.CASCADE, related_name="mitigation_actions") action = models.TextField() status = models.CharField(max_length=20, choices=Status.choices, default=Status.OPEN) owner = models.CharField(max_length=255, blank=True) due_date = models.DateField(null=True, blank=True) evidence_file = models.CharField(max_length=255, blank=True) completed_at = models.DateTimeField(null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = "mitigation_actions" def __str__(self): return f"Mitigation - {self.case.batch.batch_reference} ({self.status})" class DueDiligenceStatementReference(models.Model): class ReconciliationStatus(models.TextChoices): PENDING = "pending", "Pending" RECONCILED = "reconciled", "Reconciled" id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) case = models.ForeignKey(DueDiligenceCase, on_delete=models.CASCADE, related_name="statement_references") dds_reference = models.CharField(max_length=255, blank=True) submission_date = models.DateField(null=True, blank=True) submitted_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name="dds_submissions" ) reconciliation_status = models.CharField( max_length=20, choices=ReconciliationStatus.choices, default=ReconciliationStatus.PENDING ) notes = models.TextField(blank=True) created_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = "due_diligence_statement_references" def __str__(self): return f"DDS {self.dds_reference or '-'} - {self.case.batch.batch_reference}"