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:
parent
88eede5d7a
commit
30d07dba41
1 changed files with 52 additions and 13 deletions
|
|
@ -173,7 +173,15 @@ class Command(BaseCommand):
|
||||||
|
|
||||||
# --------------------------------------------------------------------- steps
|
# --------------------------------------------------------------------- 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(
|
facility, _ = Facility.objects.update_or_create(
|
||||||
name="IXG Processing Facility - Katsina",
|
name="IXG Processing Facility - Katsina",
|
||||||
defaults={
|
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-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"),
|
("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, _ = HarvestIntake.objects.update_or_create(
|
||||||
intake_reference=ref,
|
intake_reference=ref,
|
||||||
defaults={
|
defaults={
|
||||||
|
|
@ -243,20 +257,27 @@ class Command(BaseCommand):
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
intakes.append(intake)
|
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:
|
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, created = ProcessingBatch.objects.update_or_create(
|
||||||
batch_reference=ref,
|
batch_reference=ref,
|
||||||
defaults={"origin_facility": "ASI - Katsina State, Nigeria", "created_by": enterer, **fields},
|
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):
|
for intake, weight in zip(intakes, allocations):
|
||||||
IntakeBatchLineage.objects.update_or_create(
|
IntakeBatchLineage.objects.update_or_create(
|
||||||
batch=batch,
|
batch=batch,
|
||||||
harvest_intake=intake,
|
harvest_intake=intake,
|
||||||
defaults={"weight_allocated_kg": weight},
|
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
|
return batch
|
||||||
|
|
||||||
def _qc(self, batch: ProcessingBatch, enterer: User, verifier: User, spec: ProductSpecification | None) -> QCResult:
|
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 (
|
for etype, name in (
|
||||||
(EUDREvidence.EvidenceType.GEOLOCATION, f"{zone.zone_code}-geolocation.json"),
|
(EUDREvidence.EvidenceType.GEOLOCATION, evidence_files[0]),
|
||||||
(EUDREvidence.EvidenceType.HARVEST_DOCUMENT, f"{intakes[0].intake_reference}-weighbridge-ticket.pdf"),
|
(EUDREvidence.EvidenceType.HARVEST_DOCUMENT, evidence_files[1]),
|
||||||
(EUDREvidence.EvidenceType.SUPPLIER_IDENTITY, "collector-group-register.pdf"),
|
(EUDREvidence.EvidenceType.SUPPLIER_IDENTITY, evidence_files[2]),
|
||||||
(EUDREvidence.EvidenceType.LEGALITY, "export-permit-and-land-use.pdf"),
|
(EUDREvidence.EvidenceType.LEGALITY, evidence_files[3]),
|
||||||
):
|
):
|
||||||
EUDREvidence.objects.update_or_create(
|
EUDREvidence.objects.update_or_create(
|
||||||
case=case, evidence_type=etype, file=name,
|
case=case, evidence_type=etype, file=name,
|
||||||
|
|
@ -511,6 +538,12 @@ class Command(BaseCommand):
|
||||||
"notes": "Synthetic evidence record for training.",
|
"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(
|
RiskAssessment.objects.update_or_create(
|
||||||
case=case, risk_level=DueDiligenceCase.RiskLevel.LOW,
|
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.")
|
self.stderr.write("No ops user found - create one before seeding.")
|
||||||
return
|
return
|
||||||
|
|
||||||
zone, intakes = self._origin_chain(verifier or enterer)
|
zone, intakes, inflight_intakes = self._origin_chain(verifier or enterer)
|
||||||
self.stdout.write(f" origin: {zone.zone_code}, {len(intakes)} harvest intakes")
|
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
|
# 1. completed happy path
|
||||||
completed = self._batch(
|
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")
|
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 = 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"),
|
harvest_date=date(2026, 9, 4), weight_kg=Decimal("1500.00"),
|
||||||
grade=ProcessingBatch.Grade.B, status=ProcessingBatch.Status.IN_TRANSIT,
|
grade=ProcessingBatch.Grade.B, status=ProcessingBatch.Status.IN_TRANSIT,
|
||||||
notes="Synthetic batch currently in transit - used to show work in progress.",
|
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)
|
qc2 = self._qc(inflight, enterer, verifier or enterer, spec)
|
||||||
self._documents(inflight, qc2, enterer, verifier or enterer, date(2026, 9, 10),
|
self._documents(inflight, qc2, enterer, verifier or enterer, date(2026, 9, 10),
|
||||||
is_completed=False, coa_published=False)
|
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._shipment(inflight, delivered=False)
|
||||||
self._esg(inflight, [(date(2026, 9, 6), 27, 16, "Synthetic report: intake day observations.")])
|
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")
|
self.stdout.write(f" {INFLIGHT_BATCH}: in transit, CoA awaiting release, EUDR complete for review")
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue