fix(import): Eltern-/Herkunft-/Dubletten-Korrekturen — Ticket-Triage Cluster
Akane (Roni=Vater male + Mutter Fumi), Sunny (Bill+Melly), Danielle (Mutter Ella, motherDob-Disambiguierung), Cherry Berry (female), Black-Forest-Label vereinheitlicht, namenloser Bock entdoppelt (mergeExternalRefs). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"_doc": "Human conflict resolutions for the import quarantine (HUMANQUESTION section D / C6). The importer consumes this to UN-QUARANTINE an animal: for a matching (name + dob) it accepts the given authoritative field(s) — `genotype`, `farbschlag`, and/or `dateOfDeath` (DD.MM.YYYY) — and skips the conflict. Special field `correctDob` (DD.MM.YYYY): the matched (name + dob) record is a DUPLICATE with a WRONG birthdate — remap its DOB to `correctDob` BEFORE dedup so it merges into the canonical same-named animal. Key match = normalize(name) + dob, same identity as dedup. Maintained by god (Michael) as Julian/his wife answer the D-conflicts; originals (xlsx) stay read-only.",
|
"_doc": "Human conflict resolutions for the import quarantine (HUMANQUESTION section D / C6). The importer consumes this to UN-QUARANTINE an animal: for a matching (name + dob) it accepts the given authoritative field(s) — `genotype`, `farbschlag`, and/or `dateOfDeath` (DD.MM.YYYY) — and skips the conflict. Special field `correctDob` (DD.MM.YYYY): the matched (name + dob) record is a DUPLICATE with a WRONG birthdate — remap its DOB to `correctDob` BEFORE dedup so it merges into the canonical same-named animal. Other override fields per resolution: `gender` (male|female|m|w) — fix a misread box-colour gender (applies to stammbaum AND Wurfchronik/docx animals via merge_and_resolve.apply_decision_overrides); `father`/`mother` — authoritative parent NAMES; optional `fatherDob`/`motherDob` (DD.MM.YYYY) disambiguate a parent when several same-named animals exist. Special top-level array `addAnimals` [{name, gender, zucht?, dob?}] materialises a non-resident stub gerbil for a KNOWN parent that has no own source record (e.g. a mother named only on a Wurfchronik litter), so the litter's parent link resolves. Key match = normalize(call-name) + dob, same identity as dedup. Maintained by god (Michael) as Julian/his wife answer the D-conflicts; originals (xlsx) stay read-only.",
|
||||||
"resolutions": [
|
"resolutions": [
|
||||||
{
|
{
|
||||||
"name": "Firefly von den Kleinen Chaoten",
|
"name": "Firefly von den Kleinen Chaoten",
|
||||||
@@ -328,6 +328,55 @@
|
|||||||
"father": "Taro von den kleinen Chaoten",
|
"father": "Taro von den kleinen Chaoten",
|
||||||
"mother": "Beatrice von den kleinen Chaoten",
|
"mother": "Beatrice von den kleinen Chaoten",
|
||||||
"source": "Züchterin 2026-06-22 — Ticket #11"
|
"source": "Züchterin 2026-06-22 — Ticket #11"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Roni",
|
||||||
|
"dob": "27.01.2022",
|
||||||
|
"decision": "gender = male. Roni is the FATHER (not mother) of the T21/Z21/22 litters (Wurfchronik names father=Roni, mother=Fumi). Box colour / chart had her typed female, so role normalisation wrongly put Roni in the mother slot and left the father empty. Setting male lets Roni take the father role; Fumi (added via addAnimals) fills the mother slot.",
|
||||||
|
"gender": "male",
|
||||||
|
"source": "Züchterin 2026-06-22 — Ticket 88389f8e (Akane: Roni ist Vater)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Cherry Berry's Quqquluuruu",
|
||||||
|
"dob": "",
|
||||||
|
"decision": "gender = female (weisse Box = weiblich, blaue = maennlich). Pure pedigree ancestor without a birthdate, so the box-colour gender was never read — the Stammbaum showed a question mark instead of the female icon. dob MUST be empty so the name-only override key matches.",
|
||||||
|
"gender": "female",
|
||||||
|
"source": "Züchterin 2026-06-22 — Ticket 94892100 (Weibchen-Icon fehlt)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Sunny von PZ Karl",
|
||||||
|
"dob": "10.04.2014",
|
||||||
|
"decision": "father is Bill von Privat, mother is Melly von Privat (from Stammbaum von Yurikas und Pintos Sohn.xlsx / Renner-Pro-3). The chart-position resolver had wrongly given Hiro of Golden Lights as father.",
|
||||||
|
"father": "Bill von Privat",
|
||||||
|
"mother": "Melly von Privat",
|
||||||
|
"source": "Züchterin 2026-06-22 — Ticket 0ba551a3 (Sunny falscher Vater)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Danielle von den Kleinen Chaoten",
|
||||||
|
"dob": "04.03.2020",
|
||||||
|
"decision": "mother is Ella (Tochter von Louis+Roswitha, *10.06.2019) — Louis & Roswitha are Danielle's GRANDparents, not her parents. The father is an unnamed brother of Ella (a sibling pairing) with no own record, so he stays unknown. Two 'Ella' exist; motherDob pins the correct one (*10.06.2019), and the second Ella (*13.04.2023, born after Danielle) is age-impossible anyway.",
|
||||||
|
"mother": "Ella",
|
||||||
|
"motherDob": "10.06.2019",
|
||||||
|
"source": "Züchterin 2026-06-22 — Tickets f77aa4a6 / 4692fd5c (Danielle echte Mutter Ella)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Unbekannt",
|
||||||
|
"dob": "15.02.2024",
|
||||||
|
"decision": "Nameless buck *15.02.2024 (son of Inochi gen. Picu) is recorded twice — once via 'Stammbaum von Picus Son.xlsx' (externalRef stammbaum-unbekannt-15022024-2) and once via 'Stammbaum von Alberto Kids.xlsx' (externalRef stammbaum-unbekannt-15022024). Same buck. The two reconstructed litters (Picus Son vs Alberto Kids) differ only by the mother's birth YEAR (a typo): mother 13.11.2023 vs 13.11.2022. Merge the two bucks and the two mothers into one each via mergeExternalRefs.",
|
||||||
|
"mergeExternalRefs": [
|
||||||
|
["stammbaum-unbekannt-15022024-2", "stammbaum-unbekannt-15022024"],
|
||||||
|
["stammbaum-unbekannt-13112023", "stammbaum-unbekannt-13112022"]
|
||||||
|
],
|
||||||
|
"source": "Züchterin 2026-06-22 — Ticket f618dcc3 (doppelter namenloser Bock)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"addAnimals": [
|
||||||
|
{
|
||||||
|
"name": "Fumi von den Kleinen Chaoten",
|
||||||
|
"gender": "female",
|
||||||
|
"zucht": "Zucht der kleinen Chaoten",
|
||||||
|
"decision": "Mother of the T21/Z21/22 litters (Wurfchronik names mother=Fumi, father=Roni) but she has no own source record, so her litters showed an empty mother. Materialised as a non-resident stub so the parent link resolves; her own parents stay unknown.",
|
||||||
|
"source": "Züchterin 2026-06-22 — Ticket 88389f8e (Akane: Mutter Fumi)"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1151,7 +1151,10 @@ def apply_conflict_decisions(merged, conflicts, path):
|
|||||||
if d.get("father"):
|
if d.get("father"):
|
||||||
new_refs.append({
|
new_refs.append({
|
||||||
"name": d["father"],
|
"name": d["father"],
|
||||||
"dob": "",
|
# Optional fatherDob disambiguates a parent when several
|
||||||
|
# same-named animals exist (the downstream resolver matches
|
||||||
|
# name AND, when given, dob — e.g. two „Ella“).
|
||||||
|
"dob": d.get("fatherDob", ""),
|
||||||
"roleGuess": "father",
|
"roleGuess": "father",
|
||||||
"method": "decision",
|
"method": "decision",
|
||||||
"confidence": "high"
|
"confidence": "high"
|
||||||
@@ -1159,7 +1162,7 @@ def apply_conflict_decisions(merged, conflicts, path):
|
|||||||
if d.get("mother"):
|
if d.get("mother"):
|
||||||
new_refs.append({
|
new_refs.append({
|
||||||
"name": d["mother"],
|
"name": d["mother"],
|
||||||
"dob": "",
|
"dob": d.get("motherDob", ""),
|
||||||
"roleGuess": "mother",
|
"roleGuess": "mother",
|
||||||
"method": "decision",
|
"method": "decision",
|
||||||
"confidence": "high"
|
"confidence": "high"
|
||||||
|
|||||||
@@ -17,6 +17,29 @@ DIR_PATH = r"C:\Users\gulum\dev\Wurfchronik_Bilder"
|
|||||||
SEEDS_PATH = r"C:\Users\gulum\dev\GerbilManager\gerbil-manager-web\src\genetics\colorVarietySeed.backend.json"
|
SEEDS_PATH = r"C:\Users\gulum\dev\GerbilManager\gerbil-manager-web\src\genetics\colorVarietySeed.backend.json"
|
||||||
OUTPUT_DIR = r"C:\Users\gulum\dev\GerbilManager\tools\import\output"
|
OUTPUT_DIR = r"C:\Users\gulum\dev\GerbilManager\tools\import\output"
|
||||||
OUTPUT_FILE = os.path.join(OUTPUT_DIR, "resolved_import.json")
|
OUTPUT_FILE = os.path.join(OUTPUT_DIR, "resolved_import.json")
|
||||||
|
CONFLICT_DECISIONS_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||||
|
"conflict-decisions.json")
|
||||||
|
|
||||||
|
|
||||||
|
def load_conflict_decisions():
|
||||||
|
"""Load tools/import/conflict-decisions.json (the breeder's authoritative
|
||||||
|
answers). Returns (resolutions, addAnimals). Tolerates a missing/garbled file
|
||||||
|
by returning empty lists — the import must never crash on a bad decision file."""
|
||||||
|
try:
|
||||||
|
with open(CONFLICT_DECISIONS_PATH, encoding="utf-8") as fh:
|
||||||
|
data = json.load(fh)
|
||||||
|
return (data.get("resolutions") or [], data.get("addAnimals") or [])
|
||||||
|
except (OSError, ValueError):
|
||||||
|
return ([], [])
|
||||||
|
|
||||||
|
|
||||||
|
def _norm_decision_gender(g):
|
||||||
|
"""Map a free-text gender override ('m'/'w'/'männlich'/…) to 'male'/'female'
|
||||||
|
or None when it is neither (so a typo never blanks a real gender)."""
|
||||||
|
g = (g or "").strip().lower()
|
||||||
|
return {"m": "male", "männlich": "male", "male": "male",
|
||||||
|
"w": "female", "f": "female", "weiblich": "female",
|
||||||
|
"female": "female"}.get(g)
|
||||||
|
|
||||||
def generate_guid(key_str):
|
def generate_guid(key_str):
|
||||||
"""Generate a stable UUID string based on a key."""
|
"""Generate a stable UUID string based on a key."""
|
||||||
@@ -201,7 +224,8 @@ def get_normalized_contact_name(name):
|
|||||||
"birgitkropp": "Birgit Knopp",
|
"birgitkropp": "Birgit Knopp",
|
||||||
"birgittabüskens": "Birgitta Müller-Büskens",
|
"birgittabüskens": "Birgitta Müller-Büskens",
|
||||||
"birgittamüllerbüskens": "Brigitta Müller-Büskens",
|
"birgittamüllerbüskens": "Brigitta Müller-Büskens",
|
||||||
"blackforestgv": "Black Forest",
|
"blackforest": "Clan of Black Forest",
|
||||||
|
"blackforestgv": "Clan of Black Forest",
|
||||||
"brigittast": "Brigitta Struve",
|
"brigittast": "Brigitta Struve",
|
||||||
"buntefellnasen": "bunten Fellnasen",
|
"buntefellnasen": "bunten Fellnasen",
|
||||||
"buntenfellnase": "bunten Fellnasen",
|
"buntenfellnase": "bunten Fellnasen",
|
||||||
@@ -1664,12 +1688,20 @@ def main():
|
|||||||
|
|
||||||
a["_mapped_litter_scoped_id"] = None
|
a["_mapped_litter_scoped_id"] = None
|
||||||
|
|
||||||
|
# A human conflict-decision override is authoritative — never let the
|
||||||
|
# Wurfchronik-Vorrang heuristic below re-attach the animal to a same-date
|
||||||
|
# chronicle litter and overrule the breeder's named parents.
|
||||||
|
_has_decision_parent = (
|
||||||
|
(father_ref or {}).get("method") == "decision" or
|
||||||
|
(mother_ref or {}).get("method") == "decision")
|
||||||
|
|
||||||
# Wurfchronik-Vorrang: if the chart parents don't yield an exact
|
# Wurfchronik-Vorrang: if the chart parents don't yield an exact
|
||||||
# Wurfchronik match but EXACTLY ONE Wurfchronik litter (with named
|
# Wurfchronik match but EXACTLY ONE Wurfchronik litter (with named
|
||||||
# parents) exists for this birthdate, that authoritative litter wins over
|
# parents) exists for this birthdate, that authoritative litter wins over
|
||||||
# a fabricated chart-position litter (#12 Tony, #15 Odelia, #31 Jamie).
|
# a fabricated chart-position litter (#12 Tony, #15 Odelia, #31 Jamie).
|
||||||
dob_val_attach = parse_date(a.get("dob"))
|
dob_val_attach = parse_date(a.get("dob"))
|
||||||
if dob_val_attach and not is_external_origin(a.get("name"), a.get("zucht"), a.get("breeder")):
|
if dob_val_attach and not _has_decision_parent \
|
||||||
|
and not is_external_origin(a.get("name"), a.get("zucht"), a.get("breeder")):
|
||||||
same_date = md_litters_by_date.get(dob_val_attach, [])
|
same_date = md_litters_by_date.get(dob_val_attach, [])
|
||||||
exact_hit = None
|
exact_hit = None
|
||||||
if father_ref and mother_ref:
|
if father_ref and mother_ref:
|
||||||
@@ -1763,6 +1795,47 @@ def main():
|
|||||||
"_scoped_mother_id": m_scoped_id or generate_guid(f"stammbaum-animal-{normalize_name(m_name)}")
|
"_scoped_mother_id": m_scoped_id or generate_guid(f"stammbaum-animal-{normalize_name(m_name)}")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
elif _has_decision_parent and (father_ref or mother_ref):
|
||||||
|
# Single KNOWN parent from a human override (the other parent is
|
||||||
|
# genuinely unknown — e.g. a sibling-pairing where only the mother is
|
||||||
|
# named: Danielle's mother = Ella, father = unnamed brother).
|
||||||
|
# Create a one-parent virtual litter; the global name resolver fills
|
||||||
|
# the known side (gender + age-plausibility pick the right same-named
|
||||||
|
# animal, e.g. Ella *10.06.2019 over Ella *13.04.2023), the other
|
||||||
|
# side stays null.
|
||||||
|
known = father_ref or mother_ref
|
||||||
|
known_role = "father" if father_ref else "mother"
|
||||||
|
known_name = get_normalized_gerbil_name(known.get("name"))
|
||||||
|
dob_val = parse_date(a.get("dob"))
|
||||||
|
v_key = (normalize_name(known_name), known_role, dob_val or "0001-01-01")
|
||||||
|
if v_key in created_virtual_litters:
|
||||||
|
a["_mapped_litter_scoped_id"] = created_virtual_litters[v_key]
|
||||||
|
else:
|
||||||
|
l_scoped_id = generate_guid(
|
||||||
|
f"virtual-litter-1p-{v_key[0]}-{v_key[1]}-{v_key[2]}")
|
||||||
|
created_virtual_litters[v_key] = l_scoped_id
|
||||||
|
a["_mapped_litter_scoped_id"] = l_scoped_id
|
||||||
|
raw_litters.append({
|
||||||
|
"Id": l_scoped_id,
|
||||||
|
"Name": f"Wurf von {known_name}",
|
||||||
|
"Date": dob_val,
|
||||||
|
"TotalBorn": None,
|
||||||
|
"DeathsWithin8Weeks": None,
|
||||||
|
"FatherId": None,
|
||||||
|
"MotherId": None,
|
||||||
|
"ExpectedGoHomeDate": None,
|
||||||
|
"Notes": "Pedigree virtual litter (ein Elternteil bekannt)",
|
||||||
|
"PairingCode": None,
|
||||||
|
"ExternalRef": f"virtual-{l_scoped_id}",
|
||||||
|
"LitterLetter": None,
|
||||||
|
"_father_name": known_name if known_role == "father" else "",
|
||||||
|
"_mother_name": known_name if known_role == "mother" else "",
|
||||||
|
"_filename": "Stammbaum",
|
||||||
|
"_scoped_id": l_scoped_id,
|
||||||
|
"_scoped_father_id": None,
|
||||||
|
"_scoped_mother_id": None,
|
||||||
|
})
|
||||||
|
|
||||||
# 2. Resolve Contacts globally (deduplicate by normalized name)
|
# 2. Resolve Contacts globally (deduplicate by normalized name)
|
||||||
|
|
||||||
contact_by_norm_name = {}
|
contact_by_norm_name = {}
|
||||||
@@ -2362,6 +2435,144 @@ def main():
|
|||||||
"_old_id": name_val
|
"_old_id": name_val
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# ── Conflict-decision overrides at the gerbil level ───────────────────────
|
||||||
|
# extract.apply_conflict_decisions already applies genotype/farbschlag/dod/
|
||||||
|
# gender/parent overrides to STAMMBAUM animals (animals.json). Wurfchronik/
|
||||||
|
# docx animals (docx_animals.json) never pass through that path, so a gender
|
||||||
|
# override on a Wurfchronik animal (e.g. „Roni“ — actually the FATHER) would
|
||||||
|
# otherwise be lost. Apply gender overrides here uniformly to every processed
|
||||||
|
# gerbil, materialise `addAnimals` stubs for known parents that have no own
|
||||||
|
# source record (e.g. „Fumi“), and collapse explicitly-paired duplicate
|
||||||
|
# records via `mergeExternalRefs` (nameless animals never auto-merge).
|
||||||
|
_decisions, _add_animals = load_conflict_decisions()
|
||||||
|
|
||||||
|
def _gerbil_call_key(g):
|
||||||
|
return normalize_name(get_call_name(g.get("Name") or ""))
|
||||||
|
|
||||||
|
# 1) gender overrides (match normalize(call-name) + ISO dob; empty decision
|
||||||
|
# dob = name-only, for ancestors without a birthdate).
|
||||||
|
_gender_overrides = {}
|
||||||
|
for d in _decisions:
|
||||||
|
gg = _norm_decision_gender(d.get("gender"))
|
||||||
|
if not gg:
|
||||||
|
continue
|
||||||
|
ck = normalize_name(get_call_name(d.get("name") or ""))
|
||||||
|
iso = parse_date(d.get("dob")) if d.get("dob") else ""
|
||||||
|
_gender_overrides[(ck, iso or "")] = gg
|
||||||
|
_gender_applied = 0
|
||||||
|
for g in all_processed_gerbils:
|
||||||
|
ck = _gerbil_call_key(g)
|
||||||
|
iso = g.get("DateOfBirth") or ""
|
||||||
|
gg = _gender_overrides.get((ck, iso)) or _gender_overrides.get((ck, ""))
|
||||||
|
if gg and g.get("Gender") != gg:
|
||||||
|
g["Gender"] = gg
|
||||||
|
g["_resolved_by_decision"] = True
|
||||||
|
_gender_applied += 1
|
||||||
|
|
||||||
|
# 2) addAnimals — materialise a non-resident stub for a KNOWN parent that has
|
||||||
|
# no own source record, so its litter's parent link can resolve. Skip when
|
||||||
|
# a record of that name already exists (avoid duplicates).
|
||||||
|
_existing_call_keys = {_gerbil_call_key(g) for g in all_processed_gerbils}
|
||||||
|
_stubs_added = 0
|
||||||
|
for a in _add_animals:
|
||||||
|
nm = (a.get("name") or "").strip()
|
||||||
|
if not nm:
|
||||||
|
continue
|
||||||
|
ck = normalize_name(get_call_name(nm))
|
||||||
|
if ck in _existing_call_keys:
|
||||||
|
continue
|
||||||
|
gender = _norm_decision_gender(a.get("gender")) or "unknown"
|
||||||
|
dob_iso = parse_date(a.get("dob")) if a.get("dob") else None
|
||||||
|
zucht = (a.get("zucht") or "").strip() or None
|
||||||
|
breeder_disp = None
|
||||||
|
if zucht:
|
||||||
|
nb, keep = get_normalized_contact_name(zucht)
|
||||||
|
breeder_disp = nb if keep else None
|
||||||
|
stub_id = generate_guid(f"decision-animal-{normalize_name(nm)}")
|
||||||
|
all_processed_gerbils.append({
|
||||||
|
"Id": stub_id,
|
||||||
|
"Name": get_normalized_gerbil_name(nm),
|
||||||
|
"Gender": gender,
|
||||||
|
"Status": "Breeding",
|
||||||
|
"LitterId": None,
|
||||||
|
"OriginContactId": None,
|
||||||
|
"ReceiverContactId": None,
|
||||||
|
"EnclosureId": None,
|
||||||
|
"ColorVarietyId": None,
|
||||||
|
"DateOfBirth": dob_iso,
|
||||||
|
"DateOfDeath": None,
|
||||||
|
"CauseOfDeath": None,
|
||||||
|
"GoHomeDate": None,
|
||||||
|
"Genotype": None,
|
||||||
|
"Notes": None,
|
||||||
|
"ImportSource": "conflict-decisions.json",
|
||||||
|
"ExternalRef": f"decision-{normalize_name(nm)}",
|
||||||
|
"RawImportData": json.dumps({"addedByDecision": True}, ensure_ascii=False),
|
||||||
|
"OriginBreeder": breeder_disp,
|
||||||
|
"NameSearch": normalize_name(get_normalized_gerbil_name(nm)),
|
||||||
|
"CharacterTraits": [],
|
||||||
|
"CharacterNote": None,
|
||||||
|
"IsDeaf": None,
|
||||||
|
"IsResident": False,
|
||||||
|
"parentRefs": [],
|
||||||
|
"_photos": [],
|
||||||
|
"_old_scoped_litter_id": None,
|
||||||
|
"_eff_dob": dob_iso or "2015-01-01",
|
||||||
|
"_birth_date": dob_iso,
|
||||||
|
"_filename": "conflict-decisions.json",
|
||||||
|
"_old_id": stub_id,
|
||||||
|
"_resolved_by_decision": True,
|
||||||
|
})
|
||||||
|
_existing_call_keys.add(ck)
|
||||||
|
_stubs_added += 1
|
||||||
|
|
||||||
|
# 3) mergeExternalRefs — collapse explicitly-paired duplicate records (esp.
|
||||||
|
# nameless animals that the name-based dedup keeps separate). Each pair is
|
||||||
|
# [keepRefSuffix, dropRefSuffix]; the drop record is removed and its old id
|
||||||
|
# mapped onto the keep record. Refs are matched as a suffix of ExternalRef
|
||||||
|
# (extract animals become ExternalRef "stammbaum-<id>").
|
||||||
|
_by_ext = {}
|
||||||
|
for g in all_processed_gerbils:
|
||||||
|
er = g.get("ExternalRef")
|
||||||
|
if er:
|
||||||
|
_by_ext.setdefault(er, []).append(g)
|
||||||
|
|
||||||
|
def _find_by_ref_suffix(suffix):
|
||||||
|
hits = [g for er, gs in _by_ext.items() if er and er.endswith(suffix) for g in gs]
|
||||||
|
return hits
|
||||||
|
|
||||||
|
_premerged_ids = {} # dropped old id -> kept id (consumed in step 5 mapping)
|
||||||
|
_merge_pairs_done = 0
|
||||||
|
for d in _decisions:
|
||||||
|
for pair in (d.get("mergeExternalRefs") or []):
|
||||||
|
if not isinstance(pair, (list, tuple)) or len(pair) != 2:
|
||||||
|
continue
|
||||||
|
keep_hits = _find_by_ref_suffix(pair[0])
|
||||||
|
drop_hits = _find_by_ref_suffix(pair[1])
|
||||||
|
if not keep_hits or not drop_hits:
|
||||||
|
continue
|
||||||
|
keep = keep_hits[0]
|
||||||
|
for drop in drop_hits:
|
||||||
|
if drop is keep:
|
||||||
|
continue
|
||||||
|
# Fill gaps on the keeper from the dropped twin, then remove it.
|
||||||
|
for fld in ("DateOfBirth", "DateOfDeath", "Genotype",
|
||||||
|
"ColorVarietyId", "LitterId", "_old_scoped_litter_id",
|
||||||
|
"OriginContactId"):
|
||||||
|
if not keep.get(fld) and drop.get(fld):
|
||||||
|
keep[fld] = drop[fld]
|
||||||
|
if keep.get("Gender") in (None, "unknown") and drop.get("Gender") not in (None, "unknown"):
|
||||||
|
keep["Gender"] = drop["Gender"]
|
||||||
|
_premerged_ids[drop["Id"]] = keep["Id"]
|
||||||
|
_merge_pairs_done += 1
|
||||||
|
|
||||||
|
if _premerged_ids:
|
||||||
|
all_processed_gerbils = [g for g in all_processed_gerbils
|
||||||
|
if g["Id"] not in _premerged_ids]
|
||||||
|
|
||||||
|
print(f" Entscheidungs-Overrides: Geschlecht={_gender_applied}, "
|
||||||
|
f"Stub-Tiere={_stubs_added}, ExternalRef-Merges={_merge_pairs_done}.")
|
||||||
|
|
||||||
# Build parenting dates lookup using old scoped IDs
|
# Build parenting dates lookup using old scoped IDs
|
||||||
parent_litter_dates = {}
|
parent_litter_dates = {}
|
||||||
|
|
||||||
@@ -2810,6 +3021,13 @@ def main():
|
|||||||
|
|
||||||
# Map raw Guid if present (convert if old_id mapped to new_guid)
|
# Map raw Guid if present (convert if old_id mapped to new_guid)
|
||||||
for l in resolved_litters:
|
for l in resolved_litters:
|
||||||
|
# First redirect any parent pointing at a record dropped by an explicit
|
||||||
|
# mergeExternalRefs pre-merge onto its surviving twin (then through the
|
||||||
|
# normal id map), so the two collapsed litters share the same parents.
|
||||||
|
if l["FatherId"] in _premerged_ids:
|
||||||
|
l["FatherId"] = _premerged_ids[l["FatherId"]]
|
||||||
|
if l["MotherId"] in _premerged_ids:
|
||||||
|
l["MotherId"] = _premerged_ids[l["MotherId"]]
|
||||||
if l["FatherId"] in gerbil_id_map:
|
if l["FatherId"] in gerbil_id_map:
|
||||||
l["FatherId"] = gerbil_id_map[l["FatherId"]]
|
l["FatherId"] = gerbil_id_map[l["FatherId"]]
|
||||||
if l["MotherId"] in gerbil_id_map:
|
if l["MotherId"] in gerbil_id_map:
|
||||||
|
|||||||
@@ -490,6 +490,50 @@ if _os.path.exists(_resolved):
|
|||||||
_check_parents("#35 Zac", "Zac gen. Action", "Vance", "Dorie")
|
_check_parents("#35 Zac", "Zac gen. Action", "Vance", "Dorie")
|
||||||
_check_parents("#9 Beatrice", "Beatrice von den kleinen", "Dante", "Malina")
|
_check_parents("#9 Beatrice", "Beatrice von den kleinen", "Dante", "Malina")
|
||||||
_check_parents("#30 Theodore", "Theodore von den Kleinen", "BlackFire", "Katara")
|
_check_parents("#30 Theodore", "Theodore von den Kleinen", "BlackFire", "Katara")
|
||||||
|
|
||||||
|
# ── New ticket-triage fixes (non-genetics import cluster) ─────────────────
|
||||||
|
# Akane: Roni is the FATHER (gender flipped male), mother = Fumi (stub).
|
||||||
|
_check_parents("Akane (wrong-parents)", "Akane", "Roni", "Fumi")
|
||||||
|
_roni = next((g for g in _d["gerbils"]
|
||||||
|
if g["Name"] == "Roni" and g.get("DateOfBirth") == "2022-01-27"), None)
|
||||||
|
check("Roni: gender flipped to male", _roni is not None and _roni.get("Gender") == "male")
|
||||||
|
_fumi = _find("Fumi von den Kleinen")
|
||||||
|
check("Fumi: materialised as a non-resident stub",
|
||||||
|
_fumi is not None and _fumi.get("IsResident") is False)
|
||||||
|
|
||||||
|
# Sunny von PZ Karl: father corrected Hiro → Bill von Privat.
|
||||||
|
_check_parents("Sunny (parents)", "Sunny von PZ Karl", "Bill von Privat", "Melly von Privat")
|
||||||
|
|
||||||
|
# Danielle: mother = Ella *10.06.2019 (the elder one), father unknown.
|
||||||
|
_dan = _find("Danielle von den Kleinen")
|
||||||
|
_df, _dm = _parents(_dan)
|
||||||
|
check("Danielle: mother is Ella", (_dm or "") == "Ella")
|
||||||
|
check("Danielle: father unknown (sibling pairing)", _df is None)
|
||||||
|
if _dan and _dan.get("LitterId"):
|
||||||
|
_dl = _L.get(_dan["LitterId"])
|
||||||
|
_dmom = _G.get(_dl.get("MotherId")) if _dl else None
|
||||||
|
check("Danielle: mother Ella is the *2019-06-10 one (not the *2023 Ella)",
|
||||||
|
_dmom is not None and _dmom.get("DateOfBirth") == "2019-06-10")
|
||||||
|
|
||||||
|
# Cherry Berry's Quqquluuruu: gender override female (box colour misread).
|
||||||
|
_cherry = _find("Cherry Berry")
|
||||||
|
check("Cherry Berry: gender override female",
|
||||||
|
_cherry is not None and _cherry.get("Gender") == "female")
|
||||||
|
|
||||||
|
# Origin-label: all 'of Black Forest' animals → 'Clan of Black Forest', no
|
||||||
|
# animal left on the old 'Black Forest' label and no duplicate contact.
|
||||||
|
_bf_left = [g for g in _d["gerbils"] if (g.get("OriginBreeder") or "") == "Black Forest"]
|
||||||
|
check("Origin-label: no animal still on 'Black Forest'", len(_bf_left) == 0)
|
||||||
|
_bf_contacts = [c for c in _d["contacts"] if c["Name"] == "Black Forest"]
|
||||||
|
check("Origin-label: stray 'Black Forest' contact merged away", len(_bf_contacts) == 0)
|
||||||
|
|
||||||
|
# Duplicate-merge: the nameless buck *15.02.2024 (son of Inochi gen. Picu)
|
||||||
|
# exists only once after the externalRef merge.
|
||||||
|
_bucks = [g for g in _d["gerbils"]
|
||||||
|
if g.get("DateOfBirth") == "2024-02-15"
|
||||||
|
and "unbekannt" in (g.get("ExternalRef") or "").lower()]
|
||||||
|
check("Duplicate-merge: nameless buck *15.02.2024 deduped to one record",
|
||||||
|
len(_bucks) == 1)
|
||||||
else:
|
else:
|
||||||
print("note: output/resolved_import.json not present — skipped integration assertions")
|
print("note: output/resolved_import.json not present — skipped integration assertions")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user