- 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
235 lines
15 KiB
Python
235 lines
15 KiB
Python
# Generated by Django 5.2.12 on 2026-08-05 13:36
|
|
|
|
import django.db.models.deletion
|
|
import uuid
|
|
from django.conf import settings
|
|
from django.db import migrations, models
|
|
|
|
|
|
class Migration(migrations.Migration):
|
|
|
|
initial = True
|
|
|
|
dependencies = [
|
|
('batches', '0001_initial'),
|
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
]
|
|
|
|
operations = [
|
|
migrations.CreateModel(
|
|
name='CollectorGroup',
|
|
fields=[
|
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
|
('name', models.CharField(max_length=255)),
|
|
('contact_reference', models.CharField(blank=True, max_length=255)),
|
|
('notes', models.TextField(blank=True)),
|
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
],
|
|
options={
|
|
'db_table': 'collector_groups',
|
|
},
|
|
),
|
|
migrations.CreateModel(
|
|
name='Facility',
|
|
fields=[
|
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
|
('name', models.CharField(max_length=255)),
|
|
('country', models.CharField(max_length=100)),
|
|
('region', models.CharField(blank=True, max_length=100)),
|
|
('address', models.TextField(blank=True)),
|
|
('facility_type', models.CharField(choices=[('production', 'Production'), ('processing', 'Processing')], default='production', max_length=20)),
|
|
('is_active', models.BooleanField(default=True)),
|
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
],
|
|
options={
|
|
'db_table': 'facilities',
|
|
},
|
|
),
|
|
migrations.CreateModel(
|
|
name='DueDiligenceCase',
|
|
fields=[
|
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
|
('status', models.CharField(choices=[('incomplete', 'Incomplete'), ('complete_for_review', 'Complete for Review'), ('risk_identified', 'Risk Identified'), ('mitigation_required', 'Mitigation Required'), ('approved', 'Approved'), ('rejected', 'Rejected')], default='incomplete', max_length=30)),
|
|
('risk_level', models.CharField(choices=[('low', 'Low'), ('standard', 'Standard'), ('high', 'High')], default='standard', max_length=20)),
|
|
('completeness_results', models.JSONField(blank=True, default=dict)),
|
|
('assessed_at', models.DateTimeField(blank=True, null=True)),
|
|
('approved_at', models.DateTimeField(blank=True, null=True)),
|
|
('rejected_reason', models.TextField(blank=True)),
|
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
('updated_at', models.DateTimeField(auto_now=True)),
|
|
('approved_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='eudr_cases_approved', to=settings.AUTH_USER_MODEL)),
|
|
('assessed_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='eudr_cases_assessed', to=settings.AUTH_USER_MODEL)),
|
|
('batch', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='due_diligence_case', to='batches.processingbatch')),
|
|
],
|
|
options={
|
|
'db_table': 'due_diligence_cases',
|
|
},
|
|
),
|
|
migrations.CreateModel(
|
|
name='DueDiligenceStatementReference',
|
|
fields=[
|
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
|
('dds_reference', models.CharField(blank=True, max_length=255)),
|
|
('submission_date', models.DateField(blank=True, null=True)),
|
|
('reconciliation_status', models.CharField(choices=[('pending', 'Pending'), ('reconciled', 'Reconciled')], default='pending', max_length=20)),
|
|
('notes', models.TextField(blank=True)),
|
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
('case', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='statement_references', to='compliance.duediligencecase')),
|
|
('submitted_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='dds_submissions', to=settings.AUTH_USER_MODEL)),
|
|
],
|
|
options={
|
|
'db_table': 'due_diligence_statement_references',
|
|
},
|
|
),
|
|
migrations.CreateModel(
|
|
name='EUDREvidence',
|
|
fields=[
|
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
|
('evidence_type', models.CharField(choices=[('geolocation', 'Geolocation'), ('harvest_document', 'Harvest Document'), ('supplier_identity', 'Supplier Identity'), ('legality', 'Legality'), ('other', 'Other')], default='other', max_length=30)),
|
|
('file', models.CharField(max_length=255)),
|
|
('source', models.CharField(blank=True, max_length=255)),
|
|
('collected_at', models.DateField(blank=True, null=True)),
|
|
('verified_at', models.DateTimeField(blank=True, null=True)),
|
|
('notes', models.TextField(blank=True)),
|
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
('case', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='evidence', to='compliance.duediligencecase')),
|
|
('verified_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='eudr_evidence_verified', to=settings.AUTH_USER_MODEL)),
|
|
],
|
|
options={
|
|
'db_table': 'eudr_evidence',
|
|
},
|
|
),
|
|
migrations.CreateModel(
|
|
name='MitigationAction',
|
|
fields=[
|
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
|
('action', models.TextField()),
|
|
('status', models.CharField(choices=[('open', 'Open'), ('in_progress', 'In Progress'), ('completed', 'Completed'), ('verified', 'Verified')], default='open', max_length=20)),
|
|
('owner', models.CharField(blank=True, max_length=255)),
|
|
('due_date', models.DateField(blank=True, null=True)),
|
|
('evidence_file', models.CharField(blank=True, max_length=255)),
|
|
('completed_at', models.DateTimeField(blank=True, null=True)),
|
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
('case', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='mitigation_actions', to='compliance.duediligencecase')),
|
|
],
|
|
options={
|
|
'db_table': 'mitigation_actions',
|
|
},
|
|
),
|
|
migrations.CreateModel(
|
|
name='OriginZone',
|
|
fields=[
|
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
|
('zone_code', models.CharField(max_length=100, unique=True)),
|
|
('country', models.CharField(max_length=100)),
|
|
('region', models.CharField(blank=True, max_length=100)),
|
|
('geometry', models.JSONField(blank=True, default=dict)),
|
|
('coordinate_source', models.CharField(blank=True, max_length=255)),
|
|
('coordinates_collected_by', models.CharField(blank=True, max_length=255)),
|
|
('coordinates_collected_at', models.DateField(blank=True, null=True)),
|
|
('evidence_file', models.CharField(blank=True, max_length=255)),
|
|
('verification_status', models.CharField(choices=[('unverified', 'Unverified'), ('verified', 'Verified'), ('rejected', 'Rejected')], default='unverified', max_length=20)),
|
|
('verified_at', models.DateTimeField(blank=True, null=True)),
|
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
('facility', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='origin_zones', to='compliance.facility')),
|
|
('verified_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='zones_verified', to=settings.AUTH_USER_MODEL)),
|
|
],
|
|
options={
|
|
'db_table': 'origin_zones',
|
|
},
|
|
),
|
|
migrations.CreateModel(
|
|
name='HarvestIntake',
|
|
fields=[
|
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
|
('intake_reference', models.CharField(max_length=100, unique=True)),
|
|
('intake_date', models.DateField(blank=True, null=True)),
|
|
('weight_kg', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)),
|
|
('grade', models.CharField(blank=True, max_length=5)),
|
|
('receiving_officer', models.CharField(blank=True, max_length=255)),
|
|
('status', models.CharField(choices=[('draft', 'Draft'), ('received', 'Received'), ('verified', 'Verified'), ('rejected', 'Rejected')], default='draft', max_length=20)),
|
|
('evidence_file', models.CharField(blank=True, max_length=255)),
|
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
('collector_group', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='intakes', to='compliance.collectorgroup')),
|
|
('origin_zone', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='intakes', to='compliance.originzone')),
|
|
],
|
|
options={
|
|
'db_table': 'harvest_intakes',
|
|
},
|
|
),
|
|
migrations.AddField(
|
|
model_name='collectorgroup',
|
|
name='origin_zone',
|
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='collector_groups', to='compliance.originzone'),
|
|
),
|
|
migrations.CreateModel(
|
|
name='ProductSpecification',
|
|
fields=[
|
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
|
('product', models.CharField(max_length=100)),
|
|
('version', models.CharField(max_length=20)),
|
|
('status', models.CharField(choices=[('draft', 'Draft'), ('approved', 'Approved'), ('superseded', 'Superseded')], default='draft', max_length=20)),
|
|
('effective_from', models.DateField(blank=True, null=True)),
|
|
('effective_to', models.DateField(blank=True, null=True)),
|
|
('is_current', models.BooleanField(default=False)),
|
|
('approved_at', models.DateTimeField(blank=True, null=True)),
|
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
('updated_at', models.DateTimeField(auto_now=True)),
|
|
('approved_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='specs_approved', to=settings.AUTH_USER_MODEL)),
|
|
],
|
|
options={
|
|
'db_table': 'product_specifications',
|
|
'ordering': ['product', '-version'],
|
|
'unique_together': {('product', 'version')},
|
|
},
|
|
),
|
|
migrations.CreateModel(
|
|
name='RiskAssessment',
|
|
fields=[
|
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
|
('risk_level', models.CharField(choices=[('low', 'Low'), ('standard', 'Standard'), ('high', 'High')], default='standard', max_length=20)),
|
|
('basis', models.TextField(blank=True)),
|
|
('findings', models.TextField(blank=True)),
|
|
('assessed_at', models.DateTimeField(auto_now_add=True)),
|
|
('assessed_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='risk_assessments', to=settings.AUTH_USER_MODEL)),
|
|
('case', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='risk_assessments', to='compliance.duediligencecase')),
|
|
],
|
|
options={
|
|
'db_table': 'risk_assessments',
|
|
'ordering': ['-assessed_at'],
|
|
},
|
|
),
|
|
migrations.CreateModel(
|
|
name='IntakeBatchLineage',
|
|
fields=[
|
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
|
('weight_allocated_kg', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)),
|
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
('batch', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='intake_lineage', to='batches.processingbatch')),
|
|
('harvest_intake', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='batch_lineage', to='compliance.harvestintake')),
|
|
],
|
|
options={
|
|
'db_table': 'intake_batch_lineage',
|
|
'unique_together': {('batch', 'harvest_intake')},
|
|
},
|
|
),
|
|
migrations.CreateModel(
|
|
name='SpecParameter',
|
|
fields=[
|
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
|
('parameter', models.CharField(choices=[('ffa', 'Free Fatty Acid'), ('moisture', 'Moisture'), ('peroxide', 'Peroxide Value'), ('visual', 'Visual Grade')], max_length=20)),
|
|
('unit', models.CharField(blank=True, max_length=50)),
|
|
('min_value', models.DecimalField(blank=True, decimal_places=3, max_digits=8, null=True)),
|
|
('max_value', models.DecimalField(blank=True, decimal_places=3, max_digits=8, null=True)),
|
|
('allowed_values', models.CharField(blank=True, max_length=255)),
|
|
('required', models.BooleanField(default=True)),
|
|
('sort_order', models.IntegerField(default=0)),
|
|
('spec', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='parameters', to='compliance.productspecification')),
|
|
],
|
|
options={
|
|
'db_table': 'spec_parameters',
|
|
'ordering': ['sort_order'],
|
|
'unique_together': {('spec', 'parameter')},
|
|
},
|
|
),
|
|
]
|