Drop stale evidence when the walkthrough seed is re-run

Evidence rows are keyed on the filename, so reassigning the in-flight batch's
intakes left the old harvest_document row behind. Delete anything the case no
longer references, mirroring the lineage cleanup in _batch.
This commit is contained in:
ixgadmin 2026-09-17 14:48:11 +00:00
parent 88eede5d7a
commit 30d07dba41

View file

@ -173,7 +173,15 @@ class Command(BaseCommand):
# --------------------------------------------------------------------- steps
def _origin_chain(self, verifier: User) -> tuple[OriginZone, list[HarvestIntake]]:
def _origin_chain(self, verifier: User) -> tuple[OriginZone, list[HarvestIntake], list[HarvestIntake]]:
"""The zone, the groups, and two separate sets of harvest intakes.
Two sets, not one, because an intake is a finite quantity of shea: the
completed batch and the in-flight batch each consume their own material.
The command used to hand the same three intakes to both batches, which
allocated 800 kg of an 820 kg intake twice over — invisible until the ops
portal grew a lineage screen that refuses exactly that.
"""
facility, _ = Facility.objects.update_or_create(
name="IXG Processing Facility - Katsina",
defaults={
@ -228,7 +236,13 @@ class Command(BaseCommand):
("IXG-INTAKE-2026-0142", group_a, date(2026, 8, 27), Decimal("640.00"), "A"),
("IXG-INTAKE-2026-0143", group_b, date(2026, 8, 29), Decimal("600.00"), "B"),
]
for ref, group, when, weight, grade in intake_specs:
# The in-flight batch's own material, delivered the following week.
inflight_specs = [
("IXG-INTAKE-2026-0144", group_a, date(2026, 8, 30), Decimal("560.00"), "A"),
("IXG-INTAKE-2026-0145", group_b, date(2026, 9, 1), Decimal("540.00"), "A"),
("IXG-INTAKE-2026-0146", group_b, date(2026, 9, 3), Decimal("460.00"), "B"),
]
for ref, group, when, weight, grade in intake_specs + inflight_specs:
intake, _ = HarvestIntake.objects.update_or_create(
intake_reference=ref,
defaults={
@ -243,20 +257,27 @@ class Command(BaseCommand):
},
)
intakes.append(intake)
return zone, intakes
return zone, intakes[: len(intake_specs)], intakes[len(intake_specs):]
def _batch(self, ref: str, intakes: list[HarvestIntake], enterer: User, **fields) -> ProcessingBatch:
allocations = fields.pop("allocations", [Decimal("800.00"), Decimal("640.00"), Decimal("560.00")])
batch, created = ProcessingBatch.objects.update_or_create(
batch_reference=ref,
defaults={"origin_facility": "ASI - Katsina State, Nigeria", "created_by": enterer, **fields},
)
allocations = [Decimal("800.00"), Decimal("640.00"), Decimal("560.00")]
for intake, weight in zip(intakes, allocations):
IntakeBatchLineage.objects.update_or_create(
batch=batch,
harvest_intake=intake,
defaults={"weight_allocated_kg": weight},
)
# ...and drop anything this batch used to be linked to. Without this the
# command is only idempotent while the chain does not change: re-running it
# after a change leaves the old links in place, which is how both demo
# batches came to be traced back to the same three intakes.
IntakeBatchLineage.objects.filter(batch=batch).exclude(
harvest_intake__in=[i.pk for i in intakes]
).delete()
return batch
def _qc(self, batch: ProcessingBatch, enterer: User, verifier: User, spec: ProductSpecification | None) -> QCResult:
@ -495,11 +516,17 @@ class Command(BaseCommand):
},
)
evidence_files = [
f"{zone.zone_code}-geolocation.json",
f"{intakes[0].intake_reference}-weighbridge-ticket.pdf",
"collector-group-register.pdf",
"export-permit-and-land-use.pdf",
]
for etype, name in (
(EUDREvidence.EvidenceType.GEOLOCATION, f"{zone.zone_code}-geolocation.json"),
(EUDREvidence.EvidenceType.HARVEST_DOCUMENT, f"{intakes[0].intake_reference}-weighbridge-ticket.pdf"),
(EUDREvidence.EvidenceType.SUPPLIER_IDENTITY, "collector-group-register.pdf"),
(EUDREvidence.EvidenceType.LEGALITY, "export-permit-and-land-use.pdf"),
(EUDREvidence.EvidenceType.GEOLOCATION, evidence_files[0]),
(EUDREvidence.EvidenceType.HARVEST_DOCUMENT, evidence_files[1]),
(EUDREvidence.EvidenceType.SUPPLIER_IDENTITY, evidence_files[2]),
(EUDREvidence.EvidenceType.LEGALITY, evidence_files[3]),
):
EUDREvidence.objects.update_or_create(
case=case, evidence_type=etype, file=name,
@ -511,6 +538,12 @@ class Command(BaseCommand):
"notes": "Synthetic evidence record for training.",
},
)
# ...and drop anything this case used to carry. Evidence is keyed on the
# filename, so reassigning the batch's intakes leaves the old
# harvest_document row behind. Mirrors the lineage cleanup in _batch.
EUDREvidence.objects.filter(case=case).exclude(
file__in=evidence_files
).delete()
RiskAssessment.objects.update_or_create(
case=case, risk_level=DueDiligenceCase.RiskLevel.LOW,
@ -616,8 +649,12 @@ class Command(BaseCommand):
self.stderr.write("No ops user found - create one before seeding.")
return
zone, intakes = self._origin_chain(verifier or enterer)
self.stdout.write(f" origin: {zone.zone_code}, {len(intakes)} harvest intakes")
zone, intakes, inflight_intakes = self._origin_chain(verifier or enterer)
self.stdout.write(
f" origin: {zone.zone_code}, "
f"{len(intakes) + len(inflight_intakes)} harvest intakes "
f"({len(intakes)} for {COMPLETED_BATCH}, {len(inflight_intakes)} for {INFLIGHT_BATCH})"
)
# 1. completed happy path
completed = self._batch(
@ -636,9 +673,11 @@ class Command(BaseCommand):
])
self.stdout.write(f" {COMPLETED_BATCH}: delivered, QC pass, CoA published ({coa.sha256_hash[:12]}...), EUDR approved")
# 2. mid-flight
# 2. mid-flight. Its own intakes: three intakes cannot be allocated into
# two batches, and the lineage screen would refuse to save that.
inflight = self._batch(
INFLIGHT_BATCH, intakes, enterer,
INFLIGHT_BATCH, inflight_intakes, enterer,
allocations=[Decimal("540.00"), Decimal("520.00"), Decimal("440.00")],
harvest_date=date(2026, 9, 4), weight_kg=Decimal("1500.00"),
grade=ProcessingBatch.Grade.B, status=ProcessingBatch.Status.IN_TRANSIT,
notes="Synthetic batch currently in transit - used to show work in progress.",
@ -646,7 +685,7 @@ class Command(BaseCommand):
qc2 = self._qc(inflight, enterer, verifier or enterer, spec)
self._documents(inflight, qc2, enterer, verifier or enterer, date(2026, 9, 10),
is_completed=False, coa_published=False)
self._eudr(inflight, zone, intakes, enterer, verifier or enterer, approved=False)
self._eudr(inflight, zone, inflight_intakes, enterer, verifier or enterer, approved=False)
self._shipment(inflight, delivered=False)
self._esg(inflight, [(date(2026, 9, 6), 27, 16, "Synthetic report: intake day observations.")])
self.stdout.write(f" {INFLIGHT_BATCH}: in transit, CoA awaiting release, EUDR complete for review")