fix(tickets): 16 Tickets der Züchterin — Genetik, Importer, Daten, Akte
Genetik-Engine (bde4ec70, f89e95ad, 2322c2a8):
- Saphir/Platin/Platin-Hell unterscheiden sich NUR in der C-Zygotie und waren im
Katalog identisch (locusToken reduziert auf das dominante Allel) → Saphir war
unerreichbar. Token darf jetzt ein exaktes, ungeordnetes Allelpaar "x/y"
verlangen: Platin C/C, Saphir C/cchm, Platin-Hell C/ch. TS- und Python-Mirror
identisch, Seeds regeneriert, Migration ReseedColorVarietiesGen6Saphir.
- c[hm] wird als Alias auf c[chm] normalisiert (Tippfehler in zwei Charts) —
vorher war Jays Gencode unparsebar und ergab "Zobel-Hell".
- 6 Tiere wechseln den errechneten Farbschlag (5x Platin→Saphir, Jay→Zobel).
Importer-Logik:
- renameTo-Notiz-Sweep: der alte Name blieb in Wurf-Notizen stehen ("Blacky +
Kruke") — jetzt wortgenau ersetzt, mit vier Guards gegen Kollateralschaden.
- Neuer Resolution-Schlüssel goHomeDate (autoritativ, zieht GivenAway nach) und
spottingType; Gencode-Overrides gewinnen jetzt bei präzisem Match (vorher
wurden Entscheidungen stumm verschluckt, z. B. Eliza und Chris).
- parse_date verwirft implausible Jahre (JackJack hatte 1310-05-13).
- extract.py erkennt Stammbaum-Blöcke auch ohne Stern vor dem Geburtsdatum
(Bijou bekam dadurch den Nachbar-Ast als Mutter).
- Backend-Ingest überträgt SpottingType (nur bei Payload-Wert).
Daten (conflict-decisions.json, re-ingest-stabil): Kuke-Merge + Zuchtname,
Jamie- und Sakura-Dubletten, Phantom-Tier "Unbekannt", Eltern von Kathlin,
Fast Boy/Ziwa und dem Q4-/TS-Wurf, Eliza-Gencode, Merle-Abgabe, JackJack.
Frontend (24522f5f): Abgabedatum steht in der Tier-Akte jetzt direkt unter dem
Abnehmer und hängt nicht mehr am Status.
Tests: +25 Checks test_merge_resolve, +7 test_genotype, +8 genetics.test.ts,
neuer Extract- und Ingest-Test, e2e GOHOME-ROW. Alles grün.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -606,6 +606,11 @@ def parse_date(d):
|
||||
d = f"{year}-{parts[1]}-{parts[2]}"
|
||||
elif year == 1900:
|
||||
return None
|
||||
# Implausible Jahre verwerfen (Ticket 24522f5f): eine Wurfchronik-Transkription
|
||||
# baute aus „13.5.10" das Datum „1310-05-13" zusammen. Lieber KEIN Datum als ein
|
||||
# Datum aus dem 14. Jahrhundert (oder aus der fernen Zukunft).
|
||||
if year < 1900 or year > datetime.now().year + 1:
|
||||
return None
|
||||
return d
|
||||
# Try DD.MM.YYYY or D.M.YY
|
||||
match = re.match(r"^(\d{1,2})\.(\d{1,2})\.(\d{2,4})$", d)
|
||||
@@ -619,6 +624,9 @@ def parse_date(d):
|
||||
year += 100
|
||||
elif year == 1900:
|
||||
return None
|
||||
# s. o. — Plausibilitäts-Guard NACH der Jahres-Normalisierung
|
||||
if year < 1900 or year > datetime.now().year + 1:
|
||||
return None
|
||||
try:
|
||||
return datetime(year, month, day).strftime("%Y-%m-%d")
|
||||
except ValueError:
|
||||
@@ -632,6 +640,8 @@ def parse_date(d):
|
||||
return dt.strftime("%Y-%m-%d")
|
||||
elif year == 1900:
|
||||
return None
|
||||
if dt.year < 1900 or dt.year > datetime.now().year + 1:
|
||||
return None
|
||||
return dt.strftime("%Y-%m-%d")
|
||||
except ValueError:
|
||||
pass
|
||||
@@ -1389,6 +1399,244 @@ def enrich_from_contracts(contracts, resolved_gerbils, contact_by_norm_name,
|
||||
return stats, sale_contracts
|
||||
|
||||
|
||||
def apply_rename_note_sweep(resolved_gerbils, resolved_litters):
|
||||
"""Zieht ein `renameTo` in die Freitext-Notizen nach (Tickets ea803c3d/24006ccd).
|
||||
|
||||
`renameTo` korrigiert nur das Namensfeld; die aus der Wurfchronik übernommenen
|
||||
Wurf-Notizen („Blacky + Kruke v. 21.09.2010; WS=4") tragen den alten Namen weiter,
|
||||
und genau dort sieht ihn die Züchterin. Ersetzt wird wortgenau an Wortgrenzen und durch
|
||||
den RUFNAMEN des neuen Namens — Notizen bleiben so lesbar (nicht „Kuke von Marion
|
||||
Knoss" mitten im Satz). MUSS als letzter Schritt vor dem Payload-Dump laufen: die
|
||||
Notizen werden vorher fürs Dedup-Scoring gelesen. Gibt die Zahl der geänderten
|
||||
Notizen zurück.
|
||||
"""
|
||||
_rename_pairs = []
|
||||
for g in resolved_gerbils:
|
||||
_old = (g.get("_pre_rename_name") or "").strip()
|
||||
_new = (g.get("Name") or "").strip()
|
||||
if not _old or not _new or _old == _new:
|
||||
continue
|
||||
_new_call = get_call_name(_new) or _new
|
||||
# Nur ein Zuchtname-Suffix dazubekommen („Jana" → „Jana of little longnoses")
|
||||
# ist keine Namenskorrektur → Notizen unangetastet lassen.
|
||||
if normalize_name(_old) == normalize_name(_new_call):
|
||||
continue
|
||||
# Guards gegen Kollateralschaden (im ersten Probelauf real aufgetreten):
|
||||
# (1) zu kurze Quellnamen („M") treffen Abkürzungen im Freitext
|
||||
# („Natascha M.", „Sch. M. 2", „60g; M: 17.3.19").
|
||||
if len(_old) < 4:
|
||||
continue
|
||||
# (2) mehr als zwei Wörter = kein Rufname, sondern eine Phrase (z. B. der
|
||||
# Sentinel „Zucht der Kleinen Chaoten" als Quellname eines Stubs).
|
||||
if len(_old.split()) > 2:
|
||||
continue
|
||||
# (3) steckt der alte Name als Wort im neuen Rufnamen („Missy" → „Missy gen.
|
||||
# Krümmel"), würde die Notiz den Zusatz bei jedem Lauf verdoppeln.
|
||||
if re.search(r"\b" + re.escape(_old) + r"\b", _new_call):
|
||||
continue
|
||||
_rename_pairs.append((_old, _new_call))
|
||||
_notes_fixed = 0
|
||||
for _old, _new_call in _rename_pairs:
|
||||
_rx = re.compile(r"\b" + re.escape(_old) + r"\b")
|
||||
for _rec in list(resolved_litters) + list(resolved_gerbils):
|
||||
_val = _rec.get("Notes")
|
||||
if not _val or not _rx.search(_val):
|
||||
continue
|
||||
# (4) Notizen, die die Umbenennung SELBST dokumentieren („hieß ursprünglich
|
||||
# Drake Jr, wurde später in Gale umbenannt"), müssen den alten Namen
|
||||
# behalten — sonst wird der Satz sinnlos.
|
||||
_low = _val.lower()
|
||||
if "umbenannt" in _low or "ursprünglich" in _low or "urspruenglich" in _low:
|
||||
continue
|
||||
_rec["Notes"] = _rx.sub(_new_call, _val)
|
||||
_notes_fixed += 1
|
||||
return _notes_fixed
|
||||
|
||||
|
||||
def apply_decision_overrides(resolved_gerbils, decisions, variety_map,
|
||||
resolve_contact_id=None):
|
||||
"""Wendet die Feld-Overrides aus conflict-decisions.json auf die finalen Tiere an.
|
||||
|
||||
Als Modul-Funktion (statt inline in main()) herausgezogen, damit die
|
||||
Override-Semantik regressionsgetestet werden kann — insbesondere die
|
||||
AUTORITATIVEN Felder (genotype/spottingType/goHomeDate), die auch einen
|
||||
bereits gefüllten Wert überschreiben. Gibt die Zahl der geänderten Tiere zurück.
|
||||
"""
|
||||
# ── isResident/notes/receiver-Overrides (Mensch-Entscheidung) ─────────────
|
||||
# NACH dem Dedup anwenden, sonst würde die Dedup-Zusammenführung (IsResident=True
|
||||
# falls eine Variante resident ist) sie wieder überschreiben. Auch NACH der
|
||||
# Residenz-Propagation (die nur auf Stammbaum-Roh-Tiere wirkt) → diese Overrides
|
||||
# sind das letzte Wort. Matcht primär über `externalRef` (präzise, auch für
|
||||
# namenlose Tiere) oder sonst über normalize(call-name)+ISO-dob (leere dob =
|
||||
# name-only, für Vorfahren ohne Datum). Ein Match kann zusätzlich `notes` (→
|
||||
# Notes, autoritativ) und `receiver` (Kontaktname → ReceiverContactId, Status=
|
||||
# GivenAway) setzen — so bekommt ein bereits existierendes Tier (z. B. Akane)
|
||||
# seinen Lebenslauf + Abnehmer, ohne ein addAnimals-Stub zu sein.
|
||||
_ovr_by_extref = {}
|
||||
_ovr_by_namedob = {}
|
||||
for d in decisions:
|
||||
has_ovr = (("isResident" in d) or d.get("notes") or d.get("receiver")
|
||||
or d.get("renameTo") or d.get("deceased") or d.get("dateOfDeath")
|
||||
or d.get("correctDob") or d.get("genotype") or d.get("farbschlag")
|
||||
or d.get("gender") or d.get("father") or d.get("mother")
|
||||
or d.get("originBreeder") or d.get("spottingType")
|
||||
or d.get("goHomeDate"))
|
||||
if not has_ovr:
|
||||
continue
|
||||
er = (d.get("externalRef") or "").strip()
|
||||
if er:
|
||||
_ovr_by_extref[er] = d
|
||||
nm = d.get("name")
|
||||
if nm is not None:
|
||||
ck = normalize_name(get_call_name(nm or ""))
|
||||
iso = parse_date(d.get("dob")) if d.get("dob") else ""
|
||||
_ovr_by_namedob[(ck, iso or "")] = d
|
||||
_isres_applied = 0
|
||||
if _ovr_by_extref or _ovr_by_namedob:
|
||||
for g in resolved_gerbils:
|
||||
er = g.get("ExternalRef") or ""
|
||||
ck = normalize_name(get_call_name(g.get("Name") or ""))
|
||||
iso = g.get("DateOfBirth") or ""
|
||||
# `_precise` = die Entscheidung meint NACHWEISLICH dieses Tier (externalRef
|
||||
# oder name+dob). Ein name-only-Match kann dagegen einen Namensvetter
|
||||
# treffen → autoritative Overrides (Gencode) dürfen dort nichts
|
||||
# überschreiben. (Ticket a547be62 Eliza / 7037f5d8 Merle of Samsimar.)
|
||||
_precise = False
|
||||
if er and er in _ovr_by_extref:
|
||||
d = _ovr_by_extref[er]
|
||||
_precise = True
|
||||
elif er and any(er.endswith(k) for k in _ovr_by_extref):
|
||||
d = next(v for k, v in _ovr_by_extref.items() if er.endswith(k))
|
||||
_precise = True
|
||||
elif (ck, iso) in _ovr_by_namedob:
|
||||
d = _ovr_by_namedob[(ck, iso)]
|
||||
_precise = bool(iso)
|
||||
elif (ck, "") in _ovr_by_namedob:
|
||||
d = _ovr_by_namedob[(ck, "")]
|
||||
else:
|
||||
continue
|
||||
applied = False
|
||||
if "originBreeder" in d:
|
||||
val = d["originBreeder"]
|
||||
if g.get("OriginBreeder") != val:
|
||||
g["OriginBreeder"] = val
|
||||
applied = True
|
||||
if "isResident" in d:
|
||||
val = bool(d["isResident"])
|
||||
# Ein expliziter Override (true ODER false) ist das letzte Wort und
|
||||
# darf vom späteren Residenz-Sweep NICHT überschrieben werden.
|
||||
g["_resident_override"] = True
|
||||
if g.get("IsResident") != val:
|
||||
g["IsResident"] = val
|
||||
# Ein Nicht-Bestandstier ist kein eigenes Zuchttier → den eigenen
|
||||
# Zucht-Breeder entfernen (nur, wenn er auf die eigene Zucht zeigt).
|
||||
if not val and g.get("OriginBreeder") == "Zucht der kleinen Chaoten":
|
||||
g["OriginBreeder"] = None
|
||||
applied = True
|
||||
notes_ovr = (d.get("notes") or "").strip()
|
||||
if notes_ovr and g.get("Notes") != notes_ovr:
|
||||
g["Notes"] = notes_ovr
|
||||
applied = True
|
||||
rec_name = d.get("receiver")
|
||||
if rec_name:
|
||||
rec_cid = resolve_contact_id(rec_name) if resolve_contact_id else None
|
||||
if rec_cid and g.get("ReceiverContactId") != rec_cid:
|
||||
g["ReceiverContactId"] = rec_cid
|
||||
if g.get("Status") not in ("Deceased",):
|
||||
g["Status"] = "GivenAway"
|
||||
applied = True
|
||||
# renameTo: korrigiert den Anzeigenamen eines bestehenden Tieres. Der
|
||||
# Match-Key bleibt der ursprüngliche name/externalRef (NICHT umdeuten) —
|
||||
# erst HIER wird der Name überschrieben.
|
||||
rename_to = (d.get("renameTo") or "").strip()
|
||||
if rename_to and g.get("Name") != rename_to:
|
||||
# Quell-Namen merken, damit spätere Eltern-Verknüpfungen, die noch den
|
||||
# alten Namen referenzieren, weiter auflösen (siehe Namens-Index unten,
|
||||
# Ticket e0a0c304 Kruke→Kuke: sonst verlieren Kukes Würfe die Mutter).
|
||||
g["_pre_rename_name"] = g.get("Name")
|
||||
g["Name"] = rename_to
|
||||
applied = True
|
||||
# deceased: markiert ein bestehendes Tier als verstorben, falls noch
|
||||
# nicht. dateOfDeath wird bewusst NICHT angefasst.
|
||||
if d.get("deceased") and g.get("Status") != "Deceased":
|
||||
g["Status"] = "Deceased"
|
||||
applied = True
|
||||
# dateOfDeath: setzt das Todesdatum (falls noch keins) + Status Deceased.
|
||||
# rpro3 hat keine Sterbespalte, daher gibt die Züchterin/das Ticket das Datum vor.
|
||||
if d.get("dateOfDeath") and not g.get("DateOfDeath"):
|
||||
_dod = parse_date(d["dateOfDeath"])
|
||||
if _dod:
|
||||
g["DateOfDeath"] = _dod
|
||||
g["Status"] = "Deceased"
|
||||
applied = True
|
||||
# goHomeDate: Abgabedatum aus einer Züchterin-Entscheidung. AUTORITATIV —
|
||||
# überschreibt auch einen vorhandenen Wert, weil Wurfchronik-Transkriptionen
|
||||
# das Datum falsch zusammenbauen können (JackJack: „1310-05-13" statt
|
||||
# 13.05.2010, Ticket 24522f5f). Zieht Status GivenAway nach, sofern das Tier
|
||||
# nicht verstorben ist (Ticket 7037f5d8 Merle) — „GivenAway" verhindert
|
||||
# außerdem, dass die 6-Jahres-Sterbe-Heuristik greift.
|
||||
if d.get("goHomeDate"):
|
||||
_gh = parse_date(d["goHomeDate"])
|
||||
if _gh:
|
||||
if g.get("GoHomeDate") != _gh:
|
||||
g["GoHomeDate"] = _gh
|
||||
applied = True
|
||||
if g.get("Status") not in ("Deceased", "GivenAway"):
|
||||
g["Status"] = "GivenAway"
|
||||
applied = True
|
||||
if d.get("correctDob") and not g.get("DateOfBirth"):
|
||||
dob_iso = parse_date(d["correctDob"])
|
||||
g["DateOfBirth"] = dob_iso
|
||||
g["_birth_date"] = dob_iso
|
||||
g["_eff_dob"] = dob_iso
|
||||
applied = True
|
||||
# Ein expliziter Gencode aus einer Züchterin-Entscheidung ist AUTORITATIV
|
||||
# und überschreibt auch einen vorhandenen Wert — bei Wurfchronik-Tieren ist
|
||||
# der meist nur der Kanon-Fill des Farbschlags (resolve_color_and_genotype),
|
||||
# keine echte Gencode-Quelle. Nur bei PRÄZISEM Match, damit eine name-only-
|
||||
# Entscheidung nicht den Gencode eines gleichnamigen fremden Tieres
|
||||
# überschreibt. (Ticket a547be62 Eliza; repariert auch Chris/page_0013.)
|
||||
if d.get("genotype"):
|
||||
_geno_ovr = d["genotype"].strip()
|
||||
if (_precise or not g.get("Genotype")) and g.get("Genotype") != _geno_ovr:
|
||||
g["Genotype"] = _geno_ovr
|
||||
applied = True
|
||||
# Scheckungsart (Freitext, Werte wie in de.ts `spottingTypes`, z. B.
|
||||
# „Ansatzschecke"). Der Importer leitet sie aus keiner Quelle ab — sie ist
|
||||
# ausschließlich per Entscheidung setzbar.
|
||||
if d.get("spottingType"):
|
||||
_sp_ovr = d["spottingType"].strip()
|
||||
if g.get("SpottingType") != _sp_ovr:
|
||||
g["SpottingType"] = _sp_ovr
|
||||
applied = True
|
||||
if d.get("farbschlag") and not g.get("ColorVarietyId"):
|
||||
cv_id = variety_map.get(d["farbschlag"].strip().lower())
|
||||
if cv_id:
|
||||
g["ColorVarietyId"] = cv_id
|
||||
applied = True
|
||||
if d.get("gender") and g.get("Gender") in ("unknown", None):
|
||||
gender = d["gender"].strip().lower()
|
||||
gender = {"m": "male", "männlich": "male", "w": "female",
|
||||
"f": "female", "weiblich": "female"}.get(gender, gender)
|
||||
if gender in ("male", "female"):
|
||||
g["Gender"] = gender
|
||||
applied = True
|
||||
if d.get("father"):
|
||||
g["_override_father_name"] = d["father"].strip()
|
||||
g["_override_father_dob"] = d.get("fatherDob")
|
||||
applied = True
|
||||
if d.get("mother"):
|
||||
g["_override_mother_name"] = d["mother"].strip()
|
||||
g["_override_mother_dob"] = d.get("motherDob")
|
||||
applied = True
|
||||
if applied:
|
||||
_isres_applied += 1
|
||||
if _isres_applied:
|
||||
print("isResident/notes/receiver/dob/geno/spotting/abgabe-Overrides angewandt: "
|
||||
f"{_isres_applied}")
|
||||
return _isres_applied
|
||||
|
||||
|
||||
def main():
|
||||
print("Loading color variety seeds...")
|
||||
variety_map = {}
|
||||
@@ -3196,136 +3444,9 @@ def main():
|
||||
print(f"Deduplicated to {len(resolved_gerbils)} unique gerbil records.")
|
||||
|
||||
# ── isResident/notes/receiver-Overrides (Mensch-Entscheidung) ─────────────
|
||||
# NACH dem Dedup anwenden, sonst würde die Dedup-Zusammenführung (IsResident=True
|
||||
# falls eine Variante resident ist) sie wieder überschreiben. Auch NACH der
|
||||
# Residenz-Propagation (die nur auf Stammbaum-Roh-Tiere wirkt) → diese Overrides
|
||||
# sind das letzte Wort. Matcht primär über `externalRef` (präzise, auch für
|
||||
# namenlose Tiere) oder sonst über normalize(call-name)+ISO-dob (leere dob =
|
||||
# name-only, für Vorfahren ohne Datum). Ein Match kann zusätzlich `notes` (→
|
||||
# Notes, autoritativ) und `receiver` (Kontaktname → ReceiverContactId, Status=
|
||||
# GivenAway) setzen — so bekommt ein bereits existierendes Tier (z. B. Akane)
|
||||
# seinen Lebenslauf + Abnehmer, ohne ein addAnimals-Stub zu sein.
|
||||
_ovr_by_extref = {}
|
||||
_ovr_by_namedob = {}
|
||||
for d in _decisions:
|
||||
has_ovr = (("isResident" in d) or d.get("notes") or d.get("receiver")
|
||||
or d.get("renameTo") or d.get("deceased") or d.get("dateOfDeath")
|
||||
or d.get("correctDob") or d.get("genotype") or d.get("farbschlag")
|
||||
or d.get("gender") or d.get("father") or d.get("mother")
|
||||
or d.get("originBreeder"))
|
||||
if not has_ovr:
|
||||
continue
|
||||
er = (d.get("externalRef") or "").strip()
|
||||
if er:
|
||||
_ovr_by_extref[er] = d
|
||||
nm = d.get("name")
|
||||
if nm is not None:
|
||||
ck = normalize_name(get_call_name(nm or ""))
|
||||
iso = parse_date(d.get("dob")) if d.get("dob") else ""
|
||||
_ovr_by_namedob[(ck, iso or "")] = d
|
||||
_isres_applied = 0
|
||||
if _ovr_by_extref or _ovr_by_namedob:
|
||||
for g in resolved_gerbils:
|
||||
er = g.get("ExternalRef") or ""
|
||||
ck = normalize_name(get_call_name(g.get("Name") or ""))
|
||||
iso = g.get("DateOfBirth") or ""
|
||||
if er and er in _ovr_by_extref:
|
||||
d = _ovr_by_extref[er]
|
||||
elif er and any(er.endswith(k) for k in _ovr_by_extref):
|
||||
d = next(v for k, v in _ovr_by_extref.items() if er.endswith(k))
|
||||
elif (ck, iso) in _ovr_by_namedob:
|
||||
d = _ovr_by_namedob[(ck, iso)]
|
||||
elif (ck, "") in _ovr_by_namedob:
|
||||
d = _ovr_by_namedob[(ck, "")]
|
||||
else:
|
||||
continue
|
||||
applied = False
|
||||
if "originBreeder" in d:
|
||||
val = d["originBreeder"]
|
||||
if g.get("OriginBreeder") != val:
|
||||
g["OriginBreeder"] = val
|
||||
applied = True
|
||||
if "isResident" in d:
|
||||
val = bool(d["isResident"])
|
||||
# Ein expliziter Override (true ODER false) ist das letzte Wort und
|
||||
# darf vom späteren Residenz-Sweep NICHT überschrieben werden.
|
||||
g["_resident_override"] = True
|
||||
if g.get("IsResident") != val:
|
||||
g["IsResident"] = val
|
||||
# Ein Nicht-Bestandstier ist kein eigenes Zuchttier → den eigenen
|
||||
# Zucht-Breeder entfernen (nur, wenn er auf die eigene Zucht zeigt).
|
||||
if not val and g.get("OriginBreeder") == "Zucht der kleinen Chaoten":
|
||||
g["OriginBreeder"] = None
|
||||
applied = True
|
||||
notes_ovr = (d.get("notes") or "").strip()
|
||||
if notes_ovr and g.get("Notes") != notes_ovr:
|
||||
g["Notes"] = notes_ovr
|
||||
applied = True
|
||||
rec_name = d.get("receiver")
|
||||
if rec_name:
|
||||
rec_cid = _resolve_contact_id_by_name(rec_name)
|
||||
if rec_cid and g.get("ReceiverContactId") != rec_cid:
|
||||
g["ReceiverContactId"] = rec_cid
|
||||
if g.get("Status") not in ("Deceased",):
|
||||
g["Status"] = "GivenAway"
|
||||
applied = True
|
||||
# renameTo: korrigiert den Anzeigenamen eines bestehenden Tieres. Der
|
||||
# Match-Key bleibt der ursprüngliche name/externalRef (NICHT umdeuten) —
|
||||
# erst HIER wird der Name überschrieben.
|
||||
rename_to = (d.get("renameTo") or "").strip()
|
||||
if rename_to and g.get("Name") != rename_to:
|
||||
# Quell-Namen merken, damit spätere Eltern-Verknüpfungen, die noch den
|
||||
# alten Namen referenzieren, weiter auflösen (siehe Namens-Index unten,
|
||||
# Ticket e0a0c304 Kruke→Kuke: sonst verlieren Kukes Würfe die Mutter).
|
||||
g["_pre_rename_name"] = g.get("Name")
|
||||
g["Name"] = rename_to
|
||||
applied = True
|
||||
# deceased: markiert ein bestehendes Tier als verstorben, falls noch
|
||||
# nicht. dateOfDeath wird bewusst NICHT angefasst.
|
||||
if d.get("deceased") and g.get("Status") != "Deceased":
|
||||
g["Status"] = "Deceased"
|
||||
applied = True
|
||||
# dateOfDeath: setzt das Todesdatum (falls noch keins) + Status Deceased.
|
||||
# rpro3 hat keine Sterbespalte, daher gibt die Züchterin/das Ticket das Datum vor.
|
||||
if d.get("dateOfDeath") and not g.get("DateOfDeath"):
|
||||
_dod = parse_date(d["dateOfDeath"])
|
||||
if _dod:
|
||||
g["DateOfDeath"] = _dod
|
||||
g["Status"] = "Deceased"
|
||||
applied = True
|
||||
if d.get("correctDob") and not g.get("DateOfBirth"):
|
||||
dob_iso = parse_date(d["correctDob"])
|
||||
g["DateOfBirth"] = dob_iso
|
||||
g["_birth_date"] = dob_iso
|
||||
g["_eff_dob"] = dob_iso
|
||||
applied = True
|
||||
if d.get("genotype") and not g.get("Genotype"):
|
||||
g["Genotype"] = d["genotype"].strip()
|
||||
applied = True
|
||||
if d.get("farbschlag") and not g.get("ColorVarietyId"):
|
||||
cv_id = variety_map.get(d["farbschlag"].strip().lower())
|
||||
if cv_id:
|
||||
g["ColorVarietyId"] = cv_id
|
||||
applied = True
|
||||
if d.get("gender") and g.get("Gender") in ("unknown", None):
|
||||
gender = d["gender"].strip().lower()
|
||||
gender = {"m": "male", "männlich": "male", "w": "female",
|
||||
"f": "female", "weiblich": "female"}.get(gender, gender)
|
||||
if gender in ("male", "female"):
|
||||
g["Gender"] = gender
|
||||
applied = True
|
||||
if d.get("father"):
|
||||
g["_override_father_name"] = d["father"].strip()
|
||||
g["_override_father_dob"] = d.get("fatherDob")
|
||||
applied = True
|
||||
if d.get("mother"):
|
||||
g["_override_mother_name"] = d["mother"].strip()
|
||||
g["_override_mother_dob"] = d.get("motherDob")
|
||||
applied = True
|
||||
if applied:
|
||||
_isres_applied += 1
|
||||
if _isres_applied:
|
||||
print(f"isResident/notes/receiver/dob/geno-Overrides angewandt: {_isres_applied}")
|
||||
# Logik siehe apply_decision_overrides() (Modul-Funktion, damit testbar).
|
||||
apply_decision_overrides(resolved_gerbils, _decisions, variety_map,
|
||||
_resolve_contact_id_by_name)
|
||||
|
||||
|
||||
|
||||
@@ -4459,6 +4580,12 @@ def main():
|
||||
if _origin_fixed:
|
||||
print(f"Herkunft normalisiert auf 'Zucht der Kleinen Chaoten': {_origin_fixed} Tier(e)")
|
||||
|
||||
# ── Umbenennungs-Sweep in Freitext-Notizen (Tickets ea803c3d/24006ccd) ──
|
||||
# Finaler Anzeige-Sweep NACH aller Logik — s. apply_rename_note_sweep().
|
||||
_notes_fixed = apply_rename_note_sweep(resolved_gerbils, resolved_litters)
|
||||
if _notes_fixed:
|
||||
print(f"renameTo-Notiz-Sweep: {_notes_fixed} Notiz(en) auf den korrigierten Namen gesetzt")
|
||||
|
||||
# 6. Save final output JSON payload
|
||||
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
||||
payload = {
|
||||
|
||||
Reference in New Issue
Block a user