From 30d07dba4123a7e9c61e8b76cb427f769fbe5801 Mon Sep 17 00:00:00 2001 From: ixgadmin Date: Thu, 17 Sep 2026 14:48:11 +0000 Subject: [PATCH] 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. --- .../management/commands/seed_walkthrough.py | 65 +++++++++++++++---- 1 file changed, 52 insertions(+), 13 deletions(-) diff --git a/apps/core/management/commands/seed_walkthrough.py b/apps/core/management/commands/seed_walkthrough.py index 40106a4..6c34af4 100644 --- a/apps/core/management/commands/seed_walkthrough.py +++ b/apps/core/management/commands/seed_walkthrough.py @@ -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")