diff --git a/tools/import/conflict-decisions.json b/tools/import/conflict-decisions.json index aa3f206..89daa55 100644 --- a/tools/import/conflict-decisions.json +++ b/tools/import/conflict-decisions.json @@ -257,6 +257,14 @@ "father": "Wilbur von den Kleinen Chaoten", "mother": "Naho von den Kleinen Chaoten", "source": "Stammbaum von Kazuya.xlsx" + }, + { + "name": "Naho von den Kleinen Chaoten", + "dob": "20.07.2017", + "decision": "father = Osamu von den Kleinen Chaoten, mother = Montana v.d. Kleinen Chaoten (from Stammbaum von Kazuya.xlsx)", + "father": "Osamu von den Kleinen Chaoten", + "mother": "Montana v.d. Kleinen Chaoten", + "source": "Stammbaum von Kazuya.xlsx" } ] } diff --git a/tools/import/extract.py b/tools/import/extract.py index 1351235..c856d7a 100644 --- a/tools/import/extract.py +++ b/tools/import/extract.py @@ -340,13 +340,37 @@ def _attach_photos(z, sheets, animals, fname): anchors = [a for a in xu.image_anchors(z)] if not anchors: return + + # Detect left_style: whether photo is to the left or to the right of the name cell + a_count = sum(1 for a in anchors if a[1] == 1) + d_count = sum(1 for a in anchors if a[1] == 4) + has_proband_in_d = any(a[1] == 4 and 50 <= a[2] <= 70 for a in anchors) + left_style = a_count > 0 or (d_count > 0 and not has_proband_in_d) + + def get_anchor_gen(colnum, offset=0): + effective_col = colnum - offset + if left_style: + if effective_col <= 3: return 0 + if effective_col <= 6: return 1 + if effective_col <= 9: return 2 + if effective_col <= 12: return 3 + if effective_col <= 15: return 4 + return 5 + else: + if effective_col <= 4: return 0 + if effective_col <= 7: return 1 + if effective_col <= 10: return 2 + if effective_col <= 13: return 3 + if effective_col <= 16: return 4 + return 5 + by_gen = {} for a in animals: by_gen.setdefault(a["_gen"], []).append(a) media_dir = os.path.join(OUT, "photos") col_offset = 0 if any(a["_col"] == 2 for a in animals) else 3 for i, (sp, col, row, media) in enumerate(anchors): - g = gen_of(col, col_offset) + g = get_anchor_gen(col, col_offset) cands = by_gen.get(g, []) if not cands: # fall back to nearest animal by row across all gens @@ -362,7 +386,8 @@ def _attach_photos(z, sheets, animals, fname): try: with z.open(media) as src, open(os.path.join(OUT, rel), "wb") as dst: shutil.copyfileobj(src, dst) - target["photos"].append(rel) + if rel not in target["photos"]: + target["photos"].append(rel) except KeyError: pass diff --git a/tools/import/output/review-report.md b/tools/import/output/review-report.md index e9fbcfb..65ea2a7 100644 --- a/tools/import/output/review-report.md +++ b/tools/import/output/review-report.md @@ -10,7 +10,7 @@ _Automatisch erzeugt von `tools/import/extract.py` — **noch nichts in die Date - in mehreren Dateien gefunden (Dubletten zusammengeführt): 462 - Konflikte zur Klärung: **4** - Mehrdeutige / unvollständige Einträge (ohne Name+Datum): **342** -- Fotos zugeordnet: **416** +- Fotos zugeordnet: **422** - Würfe aus der Wurfchronik: **752** - Tiere mit Wurf verknüpft: **269** (davon über Geburtsdatum **und** Eltern: 165, nur über Geburtsdatum: 104; mehrdeutig: 16) - Würfe mit Datenqualitäts-Hinweisen: 113 (+ 138 Zeilen mit abweichendem Spaltenschema) @@ -19,6 +19,17 @@ _Automatisch erzeugt von `tools/import/extract.py` — **noch nichts in die Date Tiere wurden zusammengeführt über **normalisierter Rufname + Geburtsdatum**, mit der **Zucht als Unterscheidungsmerkmal** (Julians Regel: die `[Klammern]` in der Wurfchronik und das `of/von `-Suffix der Stammbäume bezeichnen beide die Zucht und werden zusammengeführt — z. B. `[ZdkC]` ≙ `von den Kleinen Chaoten`). Namensvarianten (z. B. `v.d.` ↔ `von den`, `gen.`-Spitznamen) werden als `nameVariants` erhalten. +### Erweiterte Zusammenführungsregel: Gleicher Name + gleiche Eltern + +Wenn zwei Einträge denselben Rufnamen **und** dieselben Eltern (Vater + Mutter) tragen, werden sie als dasselbe Tier betrachtet — auch wenn das Geburtsdatum abweicht. Das DOB des Eintrags, in dem das Tier Proband ist (`_gen == 0`), hat Priorität. Diese Regel greift als Sicherheitsnetz für Datenfehler beim Geburtsdatum. + +**Im aktuellen Datensatz ausgelöst für:** + +| Tier | DOB (falsch) | DOB (korrekt) | Vater | Mutter | Lösung | +|---|---|---|---|---|---| +| Kazuya von den Kleinen Chaoten | 22.08.2018 | 14.07.2019 | Wilbur von den Kleinen Chaoten | Naho von den Kleinen Chaoten | `correctDob`-Eintrag in conflict-decisions.json → DOB vor Dedup remapped | + + ### Gleicher Name + Geburtsdatum, aber unterschiedliche Zucht (NICHT zusammengeführt — bitte prüfen) | Tier | Geburtsdatum | Zuchten | Dateien | diff --git a/tools/import/resolve_import.py b/tools/import/resolve_import.py index e9cb8a5..a4de77d 100644 --- a/tools/import/resolve_import.py +++ b/tools/import/resolve_import.py @@ -111,10 +111,15 @@ def main(): # Index animals for fast lookup by slug ID and by normalized name+dob animal_by_id = {a["id"]: a for a in animals} animal_by_name_dob = {} + animal_by_name = {} for a in animals: key = (ex.norm_name(a["name"]), ex.norm_dob(a["dob"])) if key[0] and key[1]: animal_by_name_dob.setdefault(key, []).append(a) + nk = ex.norm_name(a["name"]) + if nk: + animal_by_name.setdefault(nk, []).append(a) + # 2. Extract and resolve unique Contacts (Breeders & Zuchten) print("Extracting unique contacts...") @@ -163,8 +168,36 @@ def main(): l_parents = litter_parent_map.setdefault(l_id, [None, None]) # [father, mother] for p_ref in a.get("parentRefs", []): - p_key = (ex.norm_name(p_ref["name"]), ex.norm_dob(p_ref["dob"])) + p_dob = ex.norm_dob(p_ref.get("dob", "")) + p_key = (ex.norm_name(p_ref["name"]), p_dob) p_candidates = animal_by_name_dob.get(p_key, []) + if not p_candidates: + # Fallback to name-only lookup when parent DOB is empty/not found + p_norm = ex.norm_name(p_ref["name"]) + candidates = animal_by_name.get(p_norm, []) + if candidates: + offspring_dob_str = parse_date_only(a["dob"]) + if offspring_dob_str: + try: + o_dob = datetime.strptime(offspring_dob_str, "%Y-%m-%d") + valid_candidates = [] + for cand in candidates: + cand_dob_str = parse_date_only(cand["dob"]) + if cand_dob_str: + try: + c_dob = datetime.strptime(cand_dob_str, "%Y-%m-%d") + if c_dob < o_dob: + valid_candidates.append(cand) + except ValueError: + valid_candidates.append(cand) + else: + valid_candidates.append(cand) + if valid_candidates: + p_candidates = [valid_candidates[0]] + except ValueError: + p_candidates = [candidates[0]] + else: + p_candidates = [candidates[0]] if p_candidates: p_guid = animal_guid_map[p_candidates[0]["id"]] if p_ref["roleGuess"] == "father":