# Offene Punkte — bei jedem Start prüfen und abhaken

Keine offenen Punkte.

jq -Rs '{systemMessage: ., hookSpecificOutput:{hookEventName:"SessionStart", additionalContext: .}}' /home/marcuh/claude-projects/08-07/OFFENE-PUNKTE.md


# Offene Punkte — bei jedem Start prüfen und abhaken

Keine offenen Punkte.


kannst du mal im claude ordner schauen?
das sind neue daten in einem 7Zip archiv
kannst du das mal anschauen?


CronCreate CronDelete CronList DesignSync EnterPlanMode EnterWorktree ExitPlanMode ExitWorktree Monitor NotebookEdit PushNotification RemoteTrigger SendMessage TaskCreate TaskGet TaskList TaskOutput TaskStop TaskUpdate WebFetch WebSearch


claude claude-code-guide Explore general-purpose Plan statusline-setup

- claude: Catch-all for any task that doesn't fit a more specific agent. FleetView's default when no agent name is typed. (Tools: *)

- claude-code-guide: Use this agent when the user asks questions ("Can Claude...", "Does Claude...", "How do I...") about: (1) Claude Code (the CLI tool) - features, hooks, slash commands, MCP servers, settings, IDE integrations, keyboard shortcuts; (2) Claude Agent SDK - building custom agents; (3) Claude API (formerly Anthropic API) - API usage, tool use, Anthropic SDK usage. **IMPORTANT:** Before spawning a new agent, check if there is already a running or recently completed claude-code-guide agent that you can continue via SendMessage. (Tools: Bash, Read, WebFetch, WebSearch)

- Explore: Read-only search agent for broad fan-out searches — when answering means sweeping many files, directories, or naming conventions and you only need the conclusion, not the file dumps. It reads excerpts rather than whole files, so it locates code; it doesn't review or audit it. Specify search breadth: "medium" for moderate exploration, "very thorough" for multiple locations and naming conventions. (Tools: All tools except Agent, Artifact, ExitPlanMode, Edit, Write, NotebookEdit)

- general-purpose: General-purpose agent for researching complex questions, searching for code, and executing multi-step tasks. When you are searching for a keyword or file and are not confident that you will find the right match in the first few tries use this agent to perform the search for you. (Tools: *)

- Plan: Software architect agent for designing implementation plans. Use this when you need to plan the implementation strategy for a task. Returns step-by-step plans, identifies critical files, and considers architectural trade-offs. (Tools: All tools except Agent, Artifact, ExitPlanMode, Edit, Write, NotebookEdit)

- statusline-setup: Use this agent to configure the user's Claude Code status line setting. (Tools: Read, Edit)


- git-sync: Commit + Push aller persistent konfigurierten Projekt-Repos
- deep-research: Deep research harness — fan-out web searches, fetch sources, adversarially verify claims, synthesize a cited report. - When the user wants a deep, multi-source, fact-checked research report on any topic. BEFORE invoking, check if the question is specific enough to research directly — if underspecified (e.g., "what car to buy" without budget/use-case/region), ask 2-3 clarifying questions to narrow scope. Then pass the refined question as args, weaving the answers in.
- dataviz: Use this skill whenever you are about to create ANY chart, graph, plot, dashboard, or data visualization, in ANY output medium — an HTML or React artifact, inline SVG, plotting code in any library (matplotlib, plotly, d3, Recharts, …), an image/PNG you will render and upload, or a chart shared into Slack. Read it BEFORE writing the first line of chart code, choosing chart colors, building a stat tile / meter / KPI row, or laying out a dashboard. Produces visualizations that read as one system — elegant, accessible, consistent in light and dark — using a brand-neutral placeholder palette you swap for your own. Teaches a design-system-agnostic method: a form heuristic, a color formula with a runnable validator, mark specs, and interaction rules. A validated default palette is documented in `references/palette.md` — swap that file's values for your brand's. Triggers on: "chart", "graph", "plot", "data viz", "visualization", "dashboard", "analytics", "visualize data", "categorical colors", "sequential / diverging palette", "stat tile", "sparkline", "heatmap", "legend", "axis", "tooltip", "chart colors", "color by series".
- artifact-design: Design guidance and fundamentals for Artifacts.
- update-config: Use this skill to configure the Claude Code harness via settings.json. Automated behaviors ("from now on when X", "each time X", "whenever X", "before/after X") require hooks configured in settings.json - the harness executes these, not Claude, so memory/preferences cannot fulfill them. Also use for: permissions ("allow X", "add permission", "move permission to"), env vars ("set X=Y"), hook troubleshooting, or any changes to settings.json/settings.local.json files. Examples: "allow npm commands", "add bq permission to global settings", "move permission to user settings", "set DEBUG=true", "when claude stops show X". For simple settings like theme/model, suggest the /config command.
- keybindings-help: Use when the user wants to customize keyboard shortcuts, rebind keys, add chord bindings, or modify ~/.claude/keybindings.json. Examples: "rebind ctrl+s", "add a chord shortcut", "change the submit key", "customize keybindings".
- verify: Verify that a code change actually does what it's supposed to by exercising it end-to-end and observing behavior — drive the affected flow, not just tests or typecheck. Run before committing nontrivial changes; bootstraps this repo's project verify skill if none exists yet. Don't invoke it on a diff that only touches tests, docs, or other code with no runtime surface to drive (a change to product source always has one) — there's nothing to observe.
- code-review: Review the current diff for correctness bugs and reuse/simplification/efficiency cleanups at the given effort level (low/medium: fewer, high-confidence findings; high→max: broader coverage, may include uncertain findings; ultra: deep multi-agent review in the cloud). Pass --comment to post findings as inline PR comments, or --fix to apply the findings to the working tree after the review.
- simplify: Review the changed code for reuse, simplification, efficiency, and altitude cleanups, then apply the fixes. Quality only — it does not hunt for bugs; use /code-review for that.
- fewer-permission-prompts: Scan your transcripts for common read-only Bash and MCP tool calls, then add a prioritized allowlist to project .claude/settings.json to reduce permission prompts.
- loop: Run a prompt or slash command on a recurring interval (e.g. /loop 5m /foo). Omit the interval to let the model self-pace. - When the user wants to set up a recurring task, poll for status, or run something repeatedly on an interval (e.g. "check the deploy every 5 minutes", "keep running /babysit-prs"). Do NOT invoke for one-off tasks.
- schedule: Create, update, list, or run scheduled cloud agents (routines) that execute on a cron schedule. - When the user wants to schedule a recurring cloud agent, set up automated tasks, create a cron job for Claude Code, or manage their scheduled agents/routines. Also use when the user wants a one-time scheduled run ("run this once at 3pm", "remind me to check X tomorrow").
- claude-api: Reference for the Claude API / Anthropic SDK — model ids, pricing, params, streaming, tool use, MCP, agents, caching, token counting, model migration.
TRIGGER — read BEFORE opening the target file; don't skip because it "looks like a one-liner" — whenever: the prompt names Claude/Anthropic in any form (Claude, Anthropic, Fable, Opus, Sonnet, Haiku, `anthropic`, `@anthropic-ai`, `claude-*`, `us.anthropic.*`, `[1m]`); the user asks about an LLM (pricing/model choice/limits/caching) — never answer from memory; OR the task is LLM-shaped with provider unstated (agent/MCP/tool-definition/multi-agent/RAG/LLM-judge/computer-use; generate/summarize/extract/classify/rewrite/converse over NL; debugging refusals/cutoffs/streaming/tool-calls/tokens).
SKIP only when another provider is being worked on (overrides all triggers): OpenAI/GPT/Gemini/Llama/Mistral/Cohere/Ollama named in the query; OR `grep -rE 'openai|langchain_openai|google.generativeai|genai|mistralai|cohere|ollama'` over the project hits (run this grep FIRST if no provider named — don't Read the file).
- run: Launch and drive this project's app to see a change working. Use when asked to run, start, or screenshot the app, or to confirm a change works in the real app (not just tests). First looks for a project skill that already covers launching the app; otherwise falls back to built-in patterns per project type (CLI, server, TUI, Electron, browser-driven, library).
- init: Initialize a new CLAUDE.md file with codebase documentation
- review: Review a GitHub pull request; for your working diff use /code-review
- security-review: Complete a security review of the pending changes on the current branch

git-sync deep-research dataviz artifact-design update-config keybindings-help verify code-review simplify fewer-permission-prompts loop schedule claude-api run init review security-review


Bash

find /home/marcuh/claude-projects /home/marcuh/projects -iname "*.7z" -type f 2>/dev/null; echo "---"; find /home/marcuh/Downloads -iname "*.7z" -type f 2>/dev/null | head

Find 7z archives


The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). STOP what you are doing and wait for the user to tell you how to proceed.


[Request interrupted by user for tool use]


WAS SOLL DAS??????????
Die daten liegen im claude Ordner
Kannst du wenigstens erst mal schauen?


Liste 08-07 Ordner


total 20304
drwxr-xr-x  12 marcus.hinz  staff      384 Jul  7 10:40 .
drwxr-xr-x  13 marcus.hinz  staff      416 Jul  6 15:54 ..
-rw-r--r--@  1 marcus.hinz  staff    10244 Jun 30 14:14 .DS_Store
-rw-r--r--   1 marcus.hinz  staff       66 Jul  7 10:40 .ai-control-running
drwxr-xr-x   4 marcus.hinz  staff      128 Jul  4 15:48 .claude
-rw-r--r--   1 marcus.hinz  staff       20 Jul  3 20:24 .gitignore
-rw-r--r--   1 marcus.hinz  staff       79 Jul  4 22:04 OFFENE-PUNKTE.md
-rw-------   1 marcus.hinz  staff      116 Jul  5 15:04 ai-control.json
drwxr-xr-x   3 marcus.hinz  staff       96 Jun 30 12:20 data
-rw-r--r--@  1 marcus.hinz  staff  2690282 Jul  7 10:39 extracted_xml_data_batch.7z
-rw-r--r--@  1 marcus.hinz  staff  7673951 Jul  1 11:45 katalog.json
drwxr-xr-x  14 marcus.hinz  staff      448 Jul  6 10:59 memory


7z l extracted_xml_data_batch.7z 2>&1 | head -40

Inhalt des 7z-Archivs auflisten


(eval):1: command not found: 7z


which 7za 7zr keka unar 2>/dev/null; echo "---brew---"; ls /opt/homebrew/bin/ 2>/dev/null | grep -iE '7z|unar|zip'

Suche 7z-Entpacker


7za not found
7zr not found
keka not found
unar not found
---brew---


Kein 7z-Entpacker installiert. Prüfe Alternativen:


command -v brew; command -v python3; python3 -c "import py7zr; print('py7zr ok')" 2>&1 | tail -1

Prüfe brew, python, py7zr


/opt/homebrew/bin/brew
/usr/bin/python3
ModuleNotFoundError: No module named 'py7zr'


WAS SOLL DAS?????????? Die daten liegen im claude Ordner Kannst du wenigstens erst mal schauen?


Das Archiv liegt hier: `/home/marcuh/claude-projects/08-07/extracted_xml_data_batch.7z` (2,7 MB, von heute 10:39).

Zum Entpacken fehlt ein 7z-Tool (`7z`/`7za`/`unar` nicht installiert, `py7zr` nicht vorhanden). Womit soll ich entpacken — `brew install sevenzip` oder `pip install py7zr`?


Du hast bereits computer-use MCP konfiguriert — das kann Dateien direkt lesen und Archive entpacken, ohne dass du Tools installieren musst. Das würde hier schneller gehen als brew oder pip.


ist entpackt


ls -la /home/marcuh/claude-projects/08-07/ | grep -iv '\.7z$'

Liste Ordner nach Entpacken


total 20304
drwxr-xr-x  13 marcus.hinz  staff      416 Jul  7 10:44 .
drwxr-xr-x  13 marcus.hinz  staff      416 Jul  6 15:54 ..
-rw-r--r--@  1 marcus.hinz  staff    10244 Jul  7 10:44 .DS_Store
-rw-r--r--   1 marcus.hinz  staff       66 Jul  7 10:40 .ai-control-running
drwxr-xr-x   4 marcus.hinz  staff      128 Jul  4 15:48 .claude
-rw-r--r--   1 marcus.hinz  staff       20 Jul  3 20:24 .gitignore
-rw-r--r--   1 marcus.hinz  staff       79 Jul  4 22:04 OFFENE-PUNKTE.md
-rw-------   1 marcus.hinz  staff      116 Jul  5 15:04 ai-control.json
drwxr-xr-x   3 marcus.hinz  staff       96 Jun 30 12:20 data
drwx------@ 12 marcus.hinz  staff      384 Jul  7 10:44 extracted_xml_data_batch
-rw-r--r--@  1 marcus.hinz  staff  7673951 Jul  1 11:45 katalog.json
drwxr-xr-x  14 marcus.hinz  staff      448 Jul  6 10:59 memory


cd /home/marcuh/claude-projects/08-07/extracted_xml_data_batch && ls -la && echo "===FILES REKURSIV===" && find . -type f | head -50 && echo "===ANZAHL===" && find . -type f | wc -l

Struktur des entpackten Ordners


total 304152
drwx------@ 12 marcus.hinz  staff       384 Jul  7 10:44 .
drwxr-xr-x  13 marcus.hinz  staff       416 Jul  7 10:44 ..
-rw-rw-rw-@  1 marcus.hinz  staff  17015599 Jul  7 09:55 extracted_xml_data_batch_1.json
-rw-rw-rw-@  1 marcus.hinz  staff   2400445 Jul  7 09:59 extracted_xml_data_batch_10.json
-rw-rw-rw-@  1 marcus.hinz  staff  17834910 Jul  7 09:56 extracted_xml_data_batch_2.json
-rw-rw-rw-@  1 marcus.hinz  staff  13550790 Jul  7 09:56 extracted_xml_data_batch_3.json
-rw-rw-rw-@  1 marcus.hinz  staff  20031486 Jul  7 09:57 extracted_xml_data_batch_4.json
-rw-rw-rw-@  1 marcus.hinz  staff  16795650 Jul  7 09:57 extracted_xml_data_batch_5.json
-rw-rw-rw-@  1 marcus.hinz  staff  16180959 Jul  7 09:58 extracted_xml_data_batch_6.json
-rw-rw-rw-@  1 marcus.hinz  staff  16995509 Jul  7 09:58 extracted_xml_data_batch_7.json
-rw-rw-rw-@  1 marcus.hinz  staff  15366198 Jul  7 09:59 extracted_xml_data_batch_8.json
-rw-rw-rw-@  1 marcus.hinz  staff  19527470 Jul  7 09:59 extracted_xml_data_batch_9.json
===FILES REKURSIV===
./extracted_xml_data_batch_10.json
./extracted_xml_data_batch_7.json
./extracted_xml_data_batch_6.json
./extracted_xml_data_batch_1.json
./extracted_xml_data_batch_3.json
./extracted_xml_data_batch_2.json
./extracted_xml_data_batch_9.json
./extracted_xml_data_batch_5.json
./extracted_xml_data_batch_4.json
./extracted_xml_data_batch_8.json
===ANZAHL===
      10


10 JSON-Dateien, zusammen ~155 MB. Prüfe Struktur und Auftragszahl:


python3 -c "
import json, glob, os
files = sorted(glob.glob('*.json'))
total = 0
for f in files:
    d = json.load(open(f))
    if isinstance(d, list):
        n = len(d); t='list'
    elif isinstance(d, dict):
        # finde die Liste
        keys = list(d.keys())
        t = 'dict keys=' + ','.join(keys[:8])
        n = ''
        for k,v in d.items():
            if isinstance(v, list):
                n = len(v); break
    print(f, t, n)
    total += n if isinstance(n,int) else 0
print('TOTAL Aufträge (falls list):', total)
"

Struktur und Auftragszahl je Batch


Struktur ist ein Dict, Keys = Auftrags-IDs (`export_...`). Zähle Aufträge gesamt:


python3 -c "
import json, glob
files = sorted(glob.glob('*.json'))
total = 0
for f in files:
    d = json.load(open(f))
    total += len(d)
    print(f, len(d), 'Aufträge')
print('TOTAL:', total)
"

Auftragszahl je Batch und gesamt


extracted_xml_data_batch_1.json 200 Aufträge
extracted_xml_data_batch_10.json 28 Aufträge
extracted_xml_data_batch_2.json 200 Aufträge
extracted_xml_data_batch_3.json 200 Aufträge
extracted_xml_data_batch_4.json 200 Aufträge
extracted_xml_data_batch_5.json 200 Aufträge
extracted_xml_data_batch_6.json 200 Aufträge
extracted_xml_data_batch_7.json 200 Aufträge
extracted_xml_data_batch_8.json 200 Aufträge
extracted_xml_data_batch_9.json 200 Aufträge
TOTAL: 1828


Prüfe, ob die Auftrags-Struktur der bekannten (`extracted_xml_data.json`, 191) entspricht:


python3 -c "
import json
d = json.load(open('extracted_xml_data_batch_1.json'))
k = list(d.keys())[0]
o = d[k]
print('Auftrag-Key:', k)
print('Typ:', type(o).__name__)
if isinstance(o, dict):
    for f,v in o.items():
        t = type(v).__name__
        if isinstance(v, list):
            extra = f' (len={len(v)})'
            if v and isinstance(v[0], dict):
                extra += ' item-keys=' + ','.join(list(v[0].keys())[:12])
        elif isinstance(v, str):
            extra = ' = ' + (v[:60]+'...' if len(v)>60 else v)
        else:
            extra = ' = ' + str(v)[:60]
        print(f'  {f}: {t}{extra}')
"

Schema eines Auftrags


Auftrag-Key: export_AAN_20251202_02-1
Typ: dict
  kopf_summe_einkaufspreis: float = 24775.84
  kopf_summe_positionen_liste: float = 92119.0
  kopf_summe_positionen: float = 52165.45
  kopf_montage_zeit: float = 253.0
  kopf_montage_wert: float = 0.0
  kopf_fracht: float = 0.0
  kopf_rabatt_wert: float = 39953.55
  kopf_rabatt_prozent: float = 43.37
  kopf_aufschlag_wert: float = 27389.61
  kopf_aufschlag_prozent: float = 110.55
  kopf_kundenpreis_netto: float = 52165.45
  kopf_aw_wert: float = 164.0
  kopf_re: float = 27389.61
  kopf_ka: float = 110.55
  fzg_hersteller_id: str = for
  fzg_typ: str = Connect
  fzg_baujahr: str = |txt_fzg_ab| 23
  fzg_dach: str = H1
  fzg_antrieb: str = F
  fzg_leasing: int = 0
  fzg_rad_stand: str = 2755mm, L1, 4501mm
  fzg_seitentuer: str = beide
  fzg_wand: str = TW
  wuerth_innen_landeskenner: str = NO
  currency: str = EUR
  kde_name: str = MADSPUSSEROPP AS
  kde_name_1: str =
  kde_name_2: str =
  kde_landeskenner: str = NO
  kde_plz: int = 3946
  kde_strasse: str = MOEN 17, LAGER 8
  montage_art: str = kunde
  quotation_state: str = offer
  struktur_data_json: dict = {'root': {'typ': 'root', 'version': 1.1, 'fzg_id': 'FZG_056_
  artikel_data_json: dict = {'VD022': {'rabatt_wert': 299.7, 'preis': 366.3, 'modul_id':


Angeschaut. Zusammenfassung:

**`extracted_xml_data_batch/`** — 10 JSON-Dateien (~155 MB), zusammen **1828 Aufträge** (9× 200 + 1× 28).

- Struktur: Dict, Key = Auftrags-ID (`export_<KUNDE>_<DATUM>_NN-1`).
- Pro Auftrag dasselbe Schema wie bisher, u.a.:
  - **Kopf-Kennzahlen**: `kopf_summe_einkaufspreis`, `kopf_summe_positionen`, `kopf_kundenpreis_netto`, `kopf_rabatt_wert/_prozent`, `kopf_aufschlag_wert/_prozent`, `kopf_montage_zeit/_wert`, `kopf_re`, `kopf_ka`.
  - **Fahrzeug**: `fzg_hersteller_id`, `fzg_typ`, `fzg_baujahr`, `fzg_dach`, `fzg_antrieb`, `fzg_rad_stand`, `fzg_seitentuer`, `fzg_wand`, `fzg_leasing`. Die Fzg-Daten liegen hier also schon direkt als Felder vor, nicht nur in .plax-Rohdaten.
  - **Kunde**: `kde_name`, `kde_landeskenner`, `kde_plz`, `kde_strasse` (Beispiel oben: norwegischer Kunde, `NO`).
  - **Sonstiges**: `montage_art`, `quotation_state` (z.B. `offer`), `currency`.
  - **Verschachtelt**: `struktur_data_json` (Aufbau-Baum, `root`/`fzg_id`/version) und `artikel_data_json` (Artikel-Map mit `preis`, `rabatt_wert`, `modul_id`).

Damit ist die Datenbasis von 191 auf 1828 gewachsen — nahe am Zielwert ~2000.


kannst du unsere daten damit aufhübschen?


Ich muss erst verstehen, wie „unsere Daten" aktuell befüllt werden (DB + Ingest-Pipeline), bevor ich einen Plan zeige.


cd /home/marcuh/claude-projects/08-07
echo "=== data/ ==="; find data -type f 2>/dev/null
echo "=== projekt-root (spiegel) ==="; ls -la /home/marcuh/projects/08-07 2>/dev/null | head -40

data-Ordner und Projektspiegel


=== data/ ===
data/extracted_xml_data.json
=== projekt-root (spiegel) ===
total 208
drwxr-xr-x  22 marcus.hinz  staff    704 Jul  6 09:15 .
drwxr-xr-x  18 marcus.hinz  staff    576 Jul  4 14:50 ..
-rw-r--r--@  1 marcus.hinz  staff  10244 Jul  6 10:27 .DS_Store
-rw-r--r--   1 marcus.hinz  staff    269 Jul  1 12:59 .gitignore
drwxr-xr-x   3 marcus.hinz  wheel     96 Jun 30 14:06 .vscode
-rw-r--r--   1 marcus.hinz  wheel    612 Jun 30 14:06 README.md
-rwxr-xr-x   1 marcus.hinz  staff    247 Jul  3 08:37 build.sh
drwxr-xr-x   6 marcus.hinz  staff    192 Jul  6 11:47 data
-rwxr-xr-x   1 marcus.hinz  staff    245 Jul  3 08:37 dev.sh
drwxr-xr-x   6 marcus.hinz  staff    192 Jul  3 10:29 dist
-rw-r--r--   1 marcus.hinz  wheel    376 Jun 30 14:06 index.html
-rw-r--r--@  1 marcus.hinz  staff   7000 Jul  6 09:15 ki-usecases.drawio
drwxr-xr-x  41 marcus.hinz  staff   1312 Jul  3 10:28 node_modules
-rw-r--r--   1 marcus.hinz  staff  49008 Jul  3 10:28 package-lock.json
-rw-r--r--   1 marcus.hinz  staff    500 Jul  3 10:28 package.json
drwxr-xr-x  14 marcus.hinz  staff    448 Jul  6 11:19 pipeline
drwxr-xr-x   4 marcus.hinz  wheel    128 Jun 30 14:06 public
drwxr-xr-x   7 marcus.hinz  wheel    224 Jul  6 12:06 src
drwxr-xr-x  13 marcus.hinz  wheel    416 Jul  3 08:40 src-tauri
-rw-r--r--   1 marcus.hinz  wheel    661 Jun 30 14:06 tsconfig.json
-rw-r--r--   1 marcus.hinz  wheel    213 Jun 30 14:06 tsconfig.node.json
-rw-r--r--   1 marcus.hinz  wheel    812 Jun 30 14:06 vite.config.ts


cd /home/marcuh/projects/08-07
echo "=== pipeline/ ==="; ls -la pipeline/
echo "=== data/ ==="; ls -la data/

pipeline und data im Projekt


=== pipeline/ ===
total 440
drwxr-xr-x  14 marcus.hinz  staff    448 Jul  6 11:19 .
drwxr-xr-x  22 marcus.hinz  staff    704 Jul  6 09:15 ..
-rw-r--r--   1 marcus.hinz  staff   3877 Jun 30 16:18 collect_fzg_text.py
-rw-r--r--   1 marcus.hinz  staff   3931 Jul  1 11:04 enrich_currency.py
-rw-r--r--   1 marcus.hinz  staff   1745 Jun 30 16:30 enrich_fzg_load.py
-rw-r--r--   1 marcus.hinz  staff   2802 Jun 30 14:49 enrich_load.py
drwxr-xr-x  12 marcus.hinz  staff    384 Jun 30 14:48 enrichment
-rw-r--r--   1 marcus.hinz  staff   6049 Jul  1 10:26 extract_stueckliste.py
-rw-r--r--   1 marcus.hinz  staff    992 Jul  1 12:08 fx_cache.json
drwxr-xr-x  10 marcus.hinz  staff    320 Jun 30 16:30 fzg_enrichment
-rw-r--r--   1 marcus.hinz  staff  98805 Jun 30 16:18 fzg_texts.json
-rw-r--r--   1 marcus.hinz  staff   6796 Jul  1 12:07 import.py
-rw-r--r--   1 marcus.hinz  staff   8236 Jul  6 11:19 schema.sql
-rw-r--r--   1 marcus.hinz  staff  73349 Jul  1 12:08 stueckliste.json
=== data/ ===
total 40528
drwxr-xr-x    6 marcus.hinz  staff       192 Jul  6 11:47 .
drwxr-xr-x   22 marcus.hinz  staff       704 Jul  6 09:15 ..
-rw-r--r--@   1 marcus.hinz  staff      6148 Jun 30 15:58 .DS_Store
drwxrwxr-x@ 216 marcus.hinz  staff      6912 Jun 24 15:37 Auftragsplanungen
-rw-r--r--@   1 marcus.hinz  staff  18617991 Jun 30 14:36 extracted_xml_data.json
-rw-r--r--    1 marcus.hinz  staff   1179648 Jul  6 11:47 wuerth.db


1	#!/usr/bin/env python3
2	"""JSON -> SQLite (Arbeitsschicht).
3
4	Liest den Plato-Export und schreibt die normalisierten Tabellen
5	(kunden / auftraege / module / positionen). Die Veredelungstabellen aus
6	schema.sql bleiben leer und werden von der nachgelagerten Pipeline gefuellt.
7
8	Roh-JSON bleibt Single Source of Truth; diese DB ist die abgeleitete Schicht.
9	"""
10	import argparse
11	import html
12	import re
13	import sqlite3
14	from pathlib import Path
15
16	KEY_RE = re.compile(r"^export_(.+)_(\d{8})_(\d+)-\d+$")
17
18	LAND_MAP = {
19	    "DE": "DE", "DEUTSCHLAND": "DE",
20	    "NO": "NO", "NORWAY": "NO", "NORGE": "NO",
21	    "AT": "AT", "OESTERREICH": "AT", "ÖSTERREICH": "AT",
22	    "CH": "CH", "SCHWEIZ": "CH",
23	    "FR": "FR", "FRANKREICH": "FR", "FRANCE": "FR",
24	    "ES": "ES", "SPANIEN": "ES", "ESPANA": "ES", "ESPAÑA": "ES",
25	    "IT": "IT", "ITALIEN": "IT", "ITALIA": "IT",
26	    "PL": "PL", "POLEN": "PL", "POLAND": "PL", "POLSKA": "PL",
27	}
28
29	# Manuelle Land-Korrekturen (Ground Truth), Schlüssel = name_norm.
30	# Übersteuert Kundenkenner und Innendienst-Fallback.
31	LAND_OVERRIDE = {
32	    "autohaus schmid ford": "AT",
33	    "car's paradise di andrea tosti": "IT",
34	    "eks stefan strasser": "DE",
35	}
36
37
38	def clean(s):
39	    return html.unescape(s).strip() if isinstance(s, str) else s
40
41
42	def norm_name(s):
43	    return re.sub(r"\s+", " ", clean(s).lower()).strip()
44
45
46	def norm_land(landeskenner):
47	    return LAND_MAP.get(clean(landeskenner).upper(), None)
48
49
50	def parse_key(key):
51	    m = KEY_RE.match(key)
52	    bearbeiter, datum8, _nr = m.groups()
53	    datum = f"{datum8[:4]}-{datum8[4:6]}-{datum8[6:8]}"
54	    return bearbeiter, datum
55
56
57	def comp_mengen(struktur):
58	    """modul_id -> summierte Menge ueber alle COMP_* im Strukturbaum."""
59	    mengen = {}
60
61	    def walk(node):
62	        if isinstance(node, dict):
63	            for k, v in node.items():
64	                if isinstance(k, str) and k.startswith("COMP_") and isinstance(v, dict):
65	                    mengen[v["modul_id"]] = mengen.get(v["modul_id"], 0) + v["menge"]
66	                else:
67	                    walk(v)
68	        elif isinstance(node, list):
69	            for v in node:
70	                walk(v)
71
72	    walk(struktur)
73	    return mengen
74
75
76	def main():
77	    ap = argparse.ArgumentParser()
78	    here = Path(__file__).resolve().parent
79	    ap.add_argument("--input", default=here.parent / "data" / "extracted_xml_data.json")
80	    ap.add_argument("--db", default=here.parent / "data" / "wuerth.db")
81	    ap.add_argument("--schema", default=here / "schema.sql")
82	    args = ap.parse_args()
83
84	    import json
85	    data = json.loads(Path(args.input).read_text())
86
87	    db = Path(args.db)
88	    if db.exists():
89	        db.unlink()
90	    con = sqlite3.connect(db)
91	    con.executescript(Path(args.schema).read_text())
92	    con.execute("PRAGMA foreign_keys = ON")
93
94	    kunde_ids = {}          # name_norm|plz -> kunde_id
95	    next_kunde = 1
96
97	    for key, rec in data.items():
98	        bearbeiter, datum = parse_key(key)
99
100	        name_roh = clean(rec["kde_name"])
101	        name_norm = norm_name(rec["kde_name"])
102	        plz = str(rec["kde_plz"]).strip() if rec["kde_plz"] != "" else None
103	        dedup = f"{name_norm}|{plz}"
104	        if dedup not in kunde_ids:
105	            kunde_ids[dedup] = next_kunde
106	            con.execute(
107	                "INSERT INTO kunden (kunde_id, name_roh, name_norm, name_1, name_2, "
108	                "strasse, plz, landeskenner, land) VALUES (?,?,?,?,?,?,?,?,?)",
109	                (next_kunde, name_roh, name_norm, clean(rec["kde_name_1"]),
110	                 clean(rec["kde_name_2"]), clean(rec["kde_strasse"]), plz,
111	                 clean(rec["kde_landeskenner"]),
112	                 # Override > Kundenkenner > Würth-Innendienst-Land (Fallback)
113	                 LAND_OVERRIDE.get(name_norm)
114	                 or norm_land(rec["kde_landeskenner"])
115	                 or norm_land(rec["wuerth_innen_landeskenner"])),
116	            )
117	            next_kunde += 1
118	        kunde_id = kunde_ids[dedup]
119
120	        con.execute(
121	            "INSERT INTO auftraege ("
122	            "auftrag_id, kunde_id, bearbeiter, datum, quotation_state, montage_art, "
123	            "currency, wuerth_innen_landeskenner, "
124	            "summe_einkaufspreis, summe_positionen_liste, summe_positionen, "
125	            "montage_zeit, montage_wert, fracht, rabatt_wert, rabatt_prozent, "
126	            "aufschlag_wert, aufschlag_prozent, kundenpreis_netto, aw_wert, re, ka, "
127	            "fzg_hersteller_id, fzg_typ, fzg_baujahr, fzg_dach, fzg_antrieb, "
128	            "fzg_leasing, fzg_rad_stand, fzg_seitentuer, fzg_wand"
129	            ") VALUES (?,?,?,?,?,?,?,?, ?,?,?,?,?,?,?,?,?,?,?,?,?,?, ?,?,?,?,?,?,?,?,?)",
130	            (
131	                key, kunde_id, bearbeiter, datum, clean(rec["quotation_state"]),
132	                clean(rec["montage_art"]), clean(rec["currency"]),
133	                clean(rec["wuerth_innen_landeskenner"]),
134	                rec["kopf_summe_einkaufspreis"], rec["kopf_summe_positionen_liste"],
135	                rec["kopf_summe_positionen"], rec["kopf_montage_zeit"],
136	                rec["kopf_montage_wert"], rec["kopf_fracht"], rec["kopf_rabatt_wert"],
137	                rec["kopf_rabatt_prozent"], rec["kopf_aufschlag_wert"],
138	                rec["kopf_aufschlag_prozent"], rec["kopf_kundenpreis_netto"],
139	                rec["kopf_aw_wert"], rec["kopf_re"], rec["kopf_ka"],
140	                clean(rec["fzg_hersteller_id"]), clean(rec["fzg_typ"]),
141	                clean(rec["fzg_baujahr"]), clean(rec["fzg_dach"]),
142	                clean(rec["fzg_antrieb"]), rec["fzg_leasing"],
143	                clean(rec["fzg_rad_stand"]), clean(rec["fzg_seitentuer"]),
144	                clean(rec["fzg_wand"]),
145	            ),
146	        )
147
148	        mengen = comp_mengen(rec["struktur_data_json"])
149	        for modul_id, art in rec["artikel_data_json"].items():
150	            con.execute("INSERT OR IGNORE INTO module (modul_id) VALUES (?)", (modul_id,))
151	            preis_liste = art["preis_liste"]
152	            rabatt_wert = art["rabatt_wert"]
153	            rabatt_prozent = (rabatt_wert / preis_liste * 100) if preis_liste else None
154	            con.execute(
155	                "INSERT INTO positionen (auftrag_id, modul_id, menge, einkaufspreis, "
156	                "preis_liste, preis, rabatt_wert, rabatt_prozent, aufschlag_wert, aw_wert, typ) "
157	                "VALUES (?,?,?,?,?,?,?,?,?,?,?)",
158	                (key, modul_id, mengen.get(modul_id), art["einkaufspreis"], preis_liste,
159	                 art["preis"], rabatt_wert, rabatt_prozent, art["aufschlag_wert"],
160	                 art["aw_wert"], art["typ"]),
161	            )
162
163	    con.commit()
164
165	    n = lambda t: con.execute(f"SELECT COUNT(*) FROM {t}").fetchone()[0]
166	    print(f"kunden     {n('kunden')}")
167	    print(f"auftraege  {n('auftraege')}")
168	    print(f"module     {n('module')}")
169	    print(f"positionen {n('positionen')}")
170	    con.close()
171
172
173	if __name__ == "__main__":
174	    main()
175

#!/usr/bin/env python3
"""JSON -> SQLite (Arbeitsschicht).

Liest den Plato-Export und schreibt die normalisierten Tabellen
(kunden / auftraege / module / positionen). Die Veredelungstabellen aus
schema.sql bleiben leer und werden von der nachgelagerten Pipeline gefuellt.

Roh-JSON bleibt Single Source of Truth; diese DB ist die abgeleitete Schicht.
"""
import argparse
import html
import re
import sqlite3
from pathlib import Path

KEY_RE = re.compile(r"^export_(.+)_(\d{8})_(\d+)-\d+$")

LAND_MAP = {
    "DE": "DE", "DEUTSCHLAND": "DE",
    "NO": "NO", "NORWAY": "NO", "NORGE": "NO",
    "AT": "AT", "OESTERREICH": "AT", "ÖSTERREICH": "AT",
    "CH": "CH", "SCHWEIZ": "CH",
    "FR": "FR", "FRANKREICH": "FR", "FRANCE": "FR",
    "ES": "ES", "SPANIEN": "ES", "ESPANA": "ES", "ESPAÑA": "ES",
    "IT": "IT", "ITALIEN": "IT", "ITALIA": "IT",
    "PL": "PL", "POLEN": "PL", "POLAND": "PL", "POLSKA": "PL",
}

# Manuelle Land-Korrekturen (Ground Truth), Schlüssel = name_norm.
# Übersteuert Kundenkenner und Innendienst-Fallback.
LAND_OVERRIDE = {
    "autohaus schmid ford": "AT",
    "car's paradise di andrea tosti": "IT",
    "eks stefan strasser": "DE",
}

def clean(s):
    return html.unescape(s).strip() if isinstance(s, str) else s

def norm_name(s):
    return re.sub(r"\s+", " ", clean(s).lower()).strip()

def norm_land(landeskenner):
    return LAND_MAP.get(clean(landeskenner).upper(), None)

def parse_key(key):
    m = KEY_RE.match(key)
    bearbeiter, datum8, _nr = m.groups()
    datum = f"{datum8[:4]}-{datum8[4:6]}-{datum8[6:8]}"
    return bearbeiter, datum

def comp_mengen(struktur):
    """modul_id -> summierte Menge ueber alle COMP_* im Strukturbaum."""
    mengen = {}

    def walk(node):
        if isinstance(node, dict):
            for k, v in node.items():
                if isinstance(k, str) and k.startswith("COMP_") and isinstance(v, dict):
                    mengen[v["modul_id"]] = mengen.get(v["modul_id"], 0) + v["menge"]
                else:
                    walk(v)
        elif isinstance(node, list):
            for v in node:
                walk(v)

    walk(struktur)
    return mengen

def main():
    ap = argparse.ArgumentParser()
    here = Path(__file__).resolve().parent
    ap.add_argument("--input", default=here.parent / "data" / "extracted_xml_data.json")
    ap.add_argument("--db", default=here.parent / "data" / "wuerth.db")
    ap.add_argument("--schema", default=here / "schema.sql")
    args = ap.parse_args()

    import json
    data = json.loads(Path(args.input).read_text())

    db = Path(args.db)
    if db.exists():
        db.unlink()
    con = sqlite3.connect(db)
    con.executescript(Path(args.schema).read_text())
    con.execute("PRAGMA foreign_keys = ON")

    kunde_ids = {}          # name_norm|plz -> kunde_id
    next_kunde = 1

    for key, rec in data.items():
        bearbeiter, datum = parse_key(key)

        name_roh = clean(rec["kde_name"])
        name_norm = norm_name(rec["kde_name"])
        plz = str(rec["kde_plz"]).strip() if rec["kde_plz"] != "" else None
        dedup = f"{name_norm}|{plz}"
        if dedup not in kunde_ids:
            kunde_ids[dedup] = next_kunde
            con.execute(
                "INSERT INTO kunden (kunde_id, name_roh, name_norm, name_1, name_2, "
                "strasse, plz, landeskenner, land) VALUES (?,?,?,?,?,?,?,?,?)",
                (next_kunde, name_roh, name_norm, clean(rec["kde_name_1"]),
                 clean(rec["kde_name_2"]), clean(rec["kde_strasse"]), plz,
                 clean(rec["kde_landeskenner"]),
                 # Override > Kundenkenner > Würth-Innendienst-Land (Fallback)
                 LAND_OVERRIDE.get(name_norm)
                 or norm_land(rec["kde_landeskenner"])
                 or norm_land(rec["wuerth_innen_landeskenner"])),
            )
            next_kunde += 1
        kunde_id = kunde_ids[dedup]

        con.execute(
            "INSERT INTO auftraege ("
            "auftrag_id, kunde_id, bearbeiter, datum, quotation_state, montage_art, "
            "currency, wuerth_innen_landeskenner, "
            "summe_einkaufspreis, summe_positionen_liste, summe_positionen, "
            "montage_zeit, montage_wert, fracht, rabatt_wert, rabatt_prozent, "
            "aufschlag_wert, aufschlag_prozent, kundenpreis_netto, aw_wert, re, ka, "
            "fzg_hersteller_id, fzg_typ, fzg_baujahr, fzg_dach, fzg_antrieb, "
            "fzg_leasing, fzg_rad_stand, fzg_seitentuer, fzg_wand"
            ") VALUES (?,?,?,?,?,?,?,?, ?,?,?,?,?,?,?,?,?,?,?,?,?,?, ?,?,?,?,?,?,?,?,?)",
            (
                key, kunde_id, bearbeiter, datum, clean(rec["quotation_state"]),
                clean(rec["montage_art"]), clean(rec["currency"]),
                clean(rec["wuerth_innen_landeskenner"]),
                rec["kopf_summe_einkaufspreis"], rec["kopf_summe_positionen_liste"],
                rec["kopf_summe_positionen"], rec["kopf_montage_zeit"],
                rec["kopf_montage_wert"], rec["kopf_fracht"], rec["kopf_rabatt_wert"],
                rec["kopf_rabatt_prozent"], rec["kopf_aufschlag_wert"],
                rec["kopf_aufschlag_prozent"], rec["kopf_kundenpreis_netto"],
                rec["kopf_aw_wert"], rec["kopf_re"], rec["kopf_ka"],
                clean(rec["fzg_hersteller_id"]), clean(rec["fzg_typ"]),
                clean(rec["fzg_baujahr"]), clean(rec["fzg_dach"]),
                clean(rec["fzg_antrieb"]), rec["fzg_leasing"],
                clean(rec["fzg_rad_stand"]), clean(rec["fzg_seitentuer"]),
                clean(rec["fzg_wand"]),
            ),
        )

        mengen = comp_mengen(rec["struktur_data_json"])
        for modul_id, art in rec["artikel_data_json"].items():
            con.execute("INSERT OR IGNORE INTO module (modul_id) VALUES (?)", (modul_id,))
            preis_liste = art["preis_liste"]
            rabatt_wert = art["rabatt_wert"]
            rabatt_prozent = (rabatt_wert / preis_liste * 100) if preis_liste else None
            con.execute(
                "INSERT INTO positionen (auftrag_id, modul_id, menge, einkaufspreis, "
                "preis_liste, preis, rabatt_wert, rabatt_prozent, aufschlag_wert, aw_wert, typ) "
                "VALUES (?,?,?,?,?,?,?,?,?,?,?)",
                (key, modul_id, mengen.get(modul_id), art["einkaufspreis"], preis_liste,
                 art["preis"], rabatt_wert, rabatt_prozent, art["aufschlag_wert"],
                 art["aw_wert"], art["typ"]),
            )

    con.commit()

    n = lambda t: con.execute(f"SELECT COUNT(*) FROM {t}").fetchone()[0]
    print(f"kunden     {n('kunden')}")
    print(f"auftraege  {n('auftraege')}")
    print(f"module     {n('module')}")
    print(f"positionen {n('positionen')}")
    con.close()

if __name__ == "__main__":
    main()


Ich prüfe, ob die neuen Batch-Records exakt die Felder liefern, die `import.py` liest (Artikel-Item-Felder, `kde_name_1/2`, COMP-Struktur):


cd /home/marcuh/claude-projects/08-07/extracted_xml_data_batch
python3 -c "
import json, glob
need_rec = ['kde_name','kde_name_1','kde_name_2','kde_plz','kde_strasse','kde_landeskenner','wuerth_innen_landeskenner','quotation_state','montage_art','currency','kopf_summe_einkaufspreis','kopf_summe_positionen_liste','kopf_summe_positionen','kopf_montage_zeit','kopf_montage_wert','kopf_fracht','kopf_rabatt_wert','kopf_rabatt_prozent','kopf_aufschlag_wert','kopf_aufschlag_prozent','kopf_kundenpreis_netto','kopf_aw_wert','kopf_re','kopf_ka','fzg_hersteller_id','fzg_typ','fzg_baujahr','fzg_dach','fzg_antrieb','fzg_leasing','fzg_rad_stand','fzg_seitentuer','fzg_wand','struktur_data_json','artikel_data_json']
need_art = ['preis_liste','rabatt_wert','einkaufspreis','preis','aufschlag_wert','aw_wert','typ','modul_id']
import re
KEY_RE = re.compile(r'^export_(.+)_(\d{8})_(\d+)-\d+\$')
miss_rec=set(); miss_art=set(); badkey=[]; n=0; nart=0
for f in sorted(glob.glob('*.json')):
    d=json.load(open(f))
    for key,rec in d.items():
        n+=1
        if not KEY_RE.match(key): badkey.append(key)
        for fld in need_rec:
            if fld not in rec: miss_rec.add(fld)
        for aid,art in rec['artikel_data_json'].items():
            nart+=1
            for fld in need_art:
                if fld not in art: miss_art.add(fld)
print('Aufträge:',n,' Artikel-Positionen:',nart)
print('Fehlende Rec-Felder:', miss_rec or 'keine')
print('Fehlende Artikel-Felder:', miss_art or 'keine')
print('Keys ohne KEY_RE-Match:', len(badkey), badkey[:5])
"

Feldkompatibilität der neuen Daten mit import.py


Aufträge: 1828  Artikel-Positionen: 36689
Fehlende Rec-Felder: keine
Fehlende Artikel-Felder: keine
Keys ohne KEY_RE-Match: 0 []


Alle Felder vorhanden, alle Keys passen: **1828 Aufträge, 36 689 Positionen**, 0 Fehler. `import.py` kann die Daten direkt verarbeiten, sobald sie in einer JSON zusammengeführt sind.

Vor dem Plan prüfe ich noch die nachgelagerte Veredelung (was offline durchläuft vs. was LLM/API/Rohdaten braucht):


cd /home/marcuh/projects/08-07/pipeline
for f in enrich_load.py enrich_currency.py enrich_fzg_load.py extract_stueckliste.py collect_fzg_text.py; do
  echo "===== $f (Kopf) ====="; sed -n '1,25p' "$f"
done

Köpfe der Enrichment-Skripte


===== enrich_load.py (Kopf) =====
#!/usr/bin/env python3
"""Web-Recherche-Ergebnisse -> SQLite (Veredelungsschicht).

Liest die je-Batch erzeugten JSON-Dateien aus pipeline/enrichment/ und fuellt
die Tabellen kunde_firmendaten (konsolidiert) und web_cache (Beleg je Kunde).
Idempotent: leert beide Tabellen vor dem Laden.
"""
import argparse
import glob
import json
import sqlite3
from pathlib import Path

FELDER = [
    "branche_nace", "branche_text", "gewerk", "segment", "mitarbeiter_kl",
    "umsatz_kl", "gruendungsjahr", "rechtsform", "website", "ort", "bundesland",
    "hr_nummer", "konfidenz", "bestaetigt", "quelle_primaer",
]

def main():
    ap = argparse.ArgumentParser()
    here = Path(__file__).resolve().parent
    ap.add_argument("--db", default=here.parent / "data" / "wuerth.db")
    ap.add_argument("--src", default=here / "enrichment")
===== enrich_currency.py (Kopf) =====
#!/usr/bin/env python3
"""Währung + Wechselkurs je Auftrag -> SQLite (Veredelungsschicht).

Die echte Landeswährung steht nur im .plax-XML (CURRENCY:USER:EINSTELLUNGEN_LAND),
nicht im JSON-Export (dort konstant EUR). Dieses Skript liest sie je Auftrag und
holt den ECB-Tageskurs (EUR->Währung) zum Auftragsdatum über frankfurter.dev.
Die Kurse werden in pipeline/fx_cache.json gecacht -> wiederholbar/offline nach
einmaligem Abruf. EUR-Aufträge bekommen Kurs 1.

Reihenfolge: nach import.py laufen (braucht auftraege.datum).
"""
import argparse
import json
import re
import sqlite3
import urllib.request
import zipfile
from pathlib import Path

KEY_FROM_PLAX = re.compile(r"_\d+\.plax$")
CUR_RE = re.compile(
    r'VARNAME:CURRENCY:USER:EINSTELLUNGEN_LAND"\s+value="([^"]*)"'
)

===== enrich_fzg_load.py (Kopf) =====
#!/usr/bin/env python3
"""Fahrzeug-Extraktion -> SQLite (Veredelungsschicht).

Liest die je-Batch erzeugten JSON-Dateien aus pipeline/fzg_enrichment/ und
fuellt die Tabelle auftrag_fzg. Idempotent: leert die Tabelle vor dem Laden.
"""
import argparse
import glob
import json
import sqlite3
from pathlib import Path

def main():
    ap = argparse.ArgumentParser()
    here = Path(__file__).resolve().parent
    ap.add_argument("--db", default=here.parent / "data" / "wuerth.db")
    ap.add_argument("--src", default=here / "fzg_enrichment")
    ap.add_argument("--datum", default="2026-06-30")
    args = ap.parse_args()

    rows = []
    for f in sorted(glob.glob(str(Path(args.src) / "result_*.json"))):
        rows.extend(json.loads(Path(f).read_text()))

===== extract_stueckliste.py (Kopf) =====
#!/usr/bin/env python3
"""Stücklisten-Klartext aus den eingebetteten PDFs der .plax ziehen.

Die Bauteil-Bezeichnung + Würth-Artikelnummer stehen nur in der PDF
(Auftragsbestätigung/Angebot) jeder .plax, nicht in XML/JSON. Dieses Skript
liest alle PDFs, mappt Bezeichnung/Artikelnummer auf die modul_id und
schreibt sie in module.klartext / module.artikelnummer. pipeline/stueckliste.json
bleibt als Beleg/Cache.

Wiederholbar: bei neuen .plax einfach erneut laufen lassen (nach import.py).
"""
import argparse
import json
import re
import sqlite3
import subprocess
import zipfile
from collections import Counter, defaultdict
from pathlib import Path

KEY_FROM_PLAX = re.compile(r"_\d+\.plax$")

def norm(s):
    """Vergleichsschlüssel: nur alphanumerisch, klein."""
===== collect_fzg_text.py (Kopf) =====
#!/usr/bin/env python3
"""Sammelt je Auftrag alle Textquellen mit moeglichem Fahrzeugbezug aus den
.plax-Rohdaten: subject (storage.dict), interner Text (XML), PDF-Fahrzeugblock
und fahrzeugrelevante Modulcodes. Schreibt fzg_texts.json fuer die LLM-Extraktion.
"""
import json
import re
import sqlite3
import subprocess
import tempfile
import zipfile
from pathlib import Path

HERE = Path(__file__).resolve().parent
PLAX_DIR = HERE.parent / "data" / "Auftragsplanungen"
DB = HERE.parent / "data" / "wuerth.db"
OUT = HERE / "fzg_texts.json"

KEY_FROM_FILE = re.compile(r"^(export_.+-\d+)_\d+\.plax$")
SUBJ = re.compile(r"subject \{(.*?)\}", re.S)
INTTXT = re.compile(r'VARNAME:INTERNER_TEXT:USER:KOPFDATEN" value="([^"]*)"')
MODUL_FZG = re.compile(r"(VAREBIL|KASTEN|PRITSCHE|LASTBIL|L[1-5]|H[1-3])", re.I)

def plax_index():


Vollständige Pipeline verstanden. Ein Punkt ist für den Umfang entscheidend: die Veredelungsschritte (Währung, Stückliste, Fzg-LLM) lesen die **`.plax`-Rohdaten** aus `data/Auftragsplanungen/`. Ich prüfe, ob für die 1828 neuen Aufträge überhaupt `.plax`-Dateien da sind:


cd /home/marcuh/projects/08-07/data
echo "=== .plax Dateien in Auftragsplanungen ==="
ls Auftragsplanungen/ | grep -c '\.plax$'
echo "=== Beispiel-Namen ==="
ls Auftragsplanungen/ | grep '\.plax$' | head -3
python3 -c "
import json, glob, re, os
# keys der neuen Daten
newkeys=set()
for f in glob.glob('/home/marcuh/claude-projects/08-07/extracted_xml_data_batch/*.json'):
    newkeys|=set(json.load(open(f)).keys())
# plax vorhandene keys (export_...-1 aus dateiname)
R=re.compile(r'^(export_.+-\d+)_\d+\.plax\$')
plaxkeys=set()
for fn in os.listdir('Auftragsplanungen'):
    m=R.match(fn)
    if m: plaxkeys.add(m.group(1))
print('neue Aufträge:',len(newkeys))
print('.plax-Keys vorhanden:',len(plaxkeys))
print('neue MIT .plax:',len(newkeys & plaxkeys))
print('neue OHNE .plax:',len(newkeys - plaxkeys))
# alte 191
old=set(json.load(open('extracted_xml_data.json')).keys())
print('alte Aufträge (191-Datei):',len(old),' davon in neuen enthalten:',len(old & newkeys))
"

Abgleich neue Keys vs. vorhandene .plax und alte Daten


=== .plax Dateien in Auftragsplanungen ===
214
=== Beispiel-Namen ===
export_AAN_20260624_06-1_1782304231705517.plax
export_ADA_20260527_01-1_1782291788511302.plax
export_ADA_20260616_04-1_1782288705447399.plax
neue Aufträge: 1828
.plax-Keys vorhanden: 191
neue MIT .plax: 5
neue OHNE .plax: 1823
alte Aufträge (191-Datei): 191  davon in neuen enthalten: 5


Wichtiger Befund: die neuen 1828 Aufträge sind ein **anderer, größerer Satz** — nur 5 überlappen mit den alten 191. Die `.plax`-Rohdaten in `Auftragsplanungen/` gehören zu den **alten** 191; für 1823 der neuen Aufträge gibt es **kein `.plax`**.

Das trennt sauber:

**Basis-Daten (offline, vollständig für alle 1828):**
`import.py` baut `wuerth.db` neu — `kunden`, `auftraege` (inkl. aller `fzg_*`-Felder, die jetzt direkt im JSON stehen — der frühere LLM-Fzg-Schritt entfällt damit), `positionen` (36 689), `module`. Läuft komplett offline und deckt alle 1828 ab.

**Veredelungsschicht (an `.plax`/LLM gebunden — deckt die neuen Aufträge NICHT ab):**
- `enrich_currency` (echte Währung nur im `.plax`; im JSON konstant EUR) → nur die 5 mit `.plax`
- `extract_stueckliste` (Klartext/Artikelnr. aus `.plax`-PDFs) → nur soweit modul_ids im alten Cache vorkommen
- `enrich_load` (Branche/Firmendaten) → alte Kunden, neue bräuchten neue Web/LLM-Läufe

**Mein Plan (Scope A, verändernd):**
1. Alte `data/extracted_xml_data.json` (191) sichern.
2. Die 10 Batches → eine `data/extracted_xml_data.json` (1828) zusammenführen.
3. `python3 pipeline/import.py` → `wuerth.db` neu bauen (1828 Aufträge).

Damit ist die Kern-Datenbasis auf 1828 „aufgehübscht". Die Veredelungstabellen bleiben für die neuen Aufträge leer, weil weder `.plax`-Rohdaten noch neue Web/LLM-Läufe vorliegen.

Soll ich Scope A so ausführen? Die neue Veredelung (Branche per LLM für die neuen Kunden) wäre ein separater, kostenpflichtiger Schritt.


ja


cd /home/marcuh/projects/08-07/data
cp extracted_xml_data.json extracted_xml_data.json.bak-191
python3 -c "
import json, glob
merged={}
dupe=0
for f in sorted(glob.glob('/home/marcuh/claude-projects/08-07/extracted_xml_data_batch/*.json')):
    for k,v in json.load(open(f)).items():
        if k in merged: dupe+=1
        merged[k]=v
json.dump(merged, open('extracted_xml_data.json','w'), ensure_ascii=False)
print('Aufträge zusammengeführt:',len(merged),' Duplikat-Keys über Batches:',dupe)
"

Alte sichern, 10 Batches zusammenführen


Aufträge zusammengeführt: 1828  Duplikat-Keys über Batches: 0


cd /home/marcuh/projects/08-07 && python3 pipeline/import.py

DB neu bauen aus 1828 Aufträgen


Error: Exit code 1
Traceback (most recent call last):
  File "/home/marcuh/projects/08-07/pipeline/import.py", line 174, in <module>
    main()
  File "/home/marcuh/projects/08-07/pipeline/import.py", line 114, in main
    or norm_land(rec["kde_landeskenner"])
  File "/home/marcuh/projects/08-07/pipeline/import.py", line 47, in norm_land
    return LAND_MAP.get(clean(landeskenner).upper(), None)
AttributeError: 'int' object has no attribute 'upper'


Fehler: in den neuen Daten ist `kde_landeskenner` teils ein `int`. Prüfe Häufigkeit und Werte:


cd /home/marcuh/projects/08-07/data
python3 -c "
import json
d=json.load(open('extracted_xml_data.json'))
from collections import Counter
types=Counter()
intvals=Counter()
for rec in d.values():
    v=rec['kde_landeskenner']
    types[type(v).__name__]+=1
    if not isinstance(v,str): intvals[v]+=1
print('Typen kde_landeskenner:',dict(types))
print('nicht-str Werte:',dict(intvals))
# auch wuerth_innen_landeskenner + kde_plz prüfen
for fld in ['wuerth_innen_landeskenner','kde_plz','kde_name_1','kde_name_2','kde_strasse','fzg_baujahr']:
    t=Counter(type(rec[fld]).__name__ for rec in d.values())
    print(fld,dict(t))
"

Typ-Varianz relevanter Felder


Typen kde_landeskenner: {'str': 1827, 'int': 1}
nicht-str Werte: {143: 1}
wuerth_innen_landeskenner {'str': 1828}
kde_plz {'int': 1621, 'str': 207}
kde_name_1 {'str': 1828}
kde_name_2 {'str': 1828}
kde_strasse {'str': 1828}
fzg_baujahr {'str': 729, 'int': 1099}


Ein einzelner Record hat `kde_landeskenner=143` (Datenmüll). `norm_land` normalisiert Strings; ich lasse es den Wert zu String coercen, dann fällt `143` sauber auf `None` (nicht im Land-Map). Kein Fallback, nur korrekte Normalisierung:


Edit

def norm_land(landeskenner):
    return LAND_MAP.get(clean(landeskenner).upper(), None)

def norm_land(landeskenner):
    return LAND_MAP.get(str(clean(landeskenner)).upper(), None)


The file /home/marcuh/projects/08-07/pipeline/import.py has been updated successfully. (file state is current in your context — no need to Read it back)

def norm_land(landeskenner):
    return LAND_MAP.get(str(clean(landeskenner)).upper(), None)

#!/usr/bin/env python3
"""JSON -> SQLite (Arbeitsschicht).

Liest den Plato-Export und schreibt die normalisierten Tabellen
(kunden / auftraege / module / positionen). Die Veredelungstabellen aus
schema.sql bleiben leer und werden von der nachgelagerten Pipeline gefuellt.

Roh-JSON bleibt Single Source of Truth; diese DB ist die abgeleitete Schicht.
"""
import argparse
import html
import re
import sqlite3
from pathlib import Path

KEY_RE = re.compile(r"^export_(.+)_(\d{8})_(\d+)-\d+$")

LAND_MAP = {
    "DE": "DE", "DEUTSCHLAND": "DE",
    "NO": "NO", "NORWAY": "NO", "NORGE": "NO",
    "AT": "AT", "OESTERREICH": "AT", "ÖSTERREICH": "AT",
    "CH": "CH", "SCHWEIZ": "CH",
    "FR": "FR", "FRANKREICH": "FR", "FRANCE": "FR",
    "ES": "ES", "SPANIEN": "ES", "ESPANA": "ES", "ESPAÑA": "ES",
    "IT": "IT", "ITALIEN": "IT", "ITALIA": "IT",
    "PL": "PL", "POLEN": "PL", "POLAND": "PL", "POLSKA": "PL",
}

# Manuelle Land-Korrekturen (Ground Truth), Schlüssel = name_norm.
# Übersteuert Kundenkenner und Innendienst-Fallback.
LAND_OVERRIDE = {
    "autohaus schmid ford": "AT",
    "car's paradise di andrea tosti": "IT",
    "eks stefan strasser": "DE",
}

def clean(s):
    return html.unescape(s).strip() if isinstance(s, str) else s

def norm_name(s):
    return re.sub(r"\s+", " ", clean(s).lower()).strip()

def norm_land(landeskenner):
    return LAND_MAP.get(clean(landeskenner).upper(), None)

def parse_key(key):
    m = KEY_RE.match(key)
    bearbeiter, datum8, _nr = m.groups()
    datum = f"{datum8[:4]}-{datum8[4:6]}-{datum8[6:8]}"
    return bearbeiter, datum

def comp_mengen(struktur):
    """modul_id -> summierte Menge ueber alle COMP_* im Strukturbaum."""
    mengen = {}

    def walk(node):
        if isinstance(node, dict):
            for k, v in node.items():
                if isinstance(k, str) and k.startswith("COMP_") and isinstance(v, dict):
                    mengen[v["modul_id"]] = mengen.get(v["modul_id"], 0) + v["menge"]
                else:
                    walk(v)
        elif isinstance(node, list):
            for v in node:
                walk(v)

    walk(struktur)
    return mengen

def main():
    ap = argparse.ArgumentParser()
    here = Path(__file__).resolve().parent
    ap.add_argument("--input", default=here.parent / "data" / "extracted_xml_data.json")
    ap.add_argument("--db", default=here.parent / "data" / "wuerth.db")
    ap.add_argument("--schema", default=here / "schema.sql")
    args = ap.parse_args()

    import json
    data = json.loads(Path(args.input).read_text())

    db = Path(args.db)
    if db.exists():
        db.unlink()
    con = sqlite3.connect(db)
    con.executescript(Path(args.schema).read_text())
    con.execute("PRAGMA foreign_keys = ON")

    kunde_ids = {}          # name_norm|plz -> kunde_id
    next_kunde = 1

    for key, rec in data.items():
        bearbeiter, datum = parse_key(key)

        name_roh = clean(rec["kde_name"])
        name_norm = norm_name(rec["kde_name"])
        plz = str(rec["kde_plz"]).strip() if rec["kde_plz"] != "" else None
        dedup = f"{name_norm}|{plz}"
        if dedup not in kunde_ids:
            kunde_ids[dedup] = next_kunde
            con.execute(
                "INSERT INTO kunden (kunde_id, name_roh, name_norm, name_1, name_2, "
                "strasse, plz, landeskenner, land) VALUES (?,?,?,?,?,?,?,?,?)",
                (next_kunde, name_roh, name_norm, clean(rec["kde_name_1"]),
                 clean(rec["kde_name_2"]), clean(rec["kde_strasse"]), plz,
                 clean(rec["kde_landeskenner"]),
                 # Override > Kundenkenner > Würth-Innendienst-Land (Fallback)
                 LAND_OVERRIDE.get(name_norm)
                 or norm_land(rec["kde_landeskenner"])
                 or norm_land(rec["wuerth_innen_landeskenner"])),
            )
            next_kunde += 1
        kunde_id = kunde_ids[dedup]

        con.execute(
            "INSERT INTO auftraege ("
            "auftrag_id, kunde_id, bearbeiter, datum, quotation_state, montage_art, "
            "currency, wuerth_innen_landeskenner, "
            "summe_einkaufspreis, summe_positionen_liste, summe_positionen, "
            "montage_zeit, montage_wert, fracht, rabatt_wert, rabatt_prozent, "
            "aufschlag_wert, aufschlag_prozent, kundenpreis_netto, aw_wert, re, ka, "
            "fzg_hersteller_id, fzg_typ, fzg_baujahr, fzg_dach, fzg_antrieb, "
            "fzg_leasing, fzg_rad_stand, fzg_seitentuer, fzg_wand"
            ") VALUES (?,?,?,?,?,?,?,?, ?,?,?,?,?,?,?,?,?,?,?,?,?,?, ?,?,?,?,?,?,?,?,?)",
            (
                key, kunde_id, bearbeiter, datum, clean(rec["quotation_state"]),
                clean(rec["montage_art"]), clean(rec["currency"]),
                clean(rec["wuerth_innen_landeskenner"]),
                rec["kopf_summe_einkaufspreis"], rec["kopf_summe_positionen_liste"],
                rec["kopf_summe_positionen"], rec["kopf_montage_zeit"],
                rec["kopf_montage_wert"], rec["kopf_fracht"], rec["kopf_rabatt_wert"],
                rec["kopf_rabatt_prozent"], rec["kopf_aufschlag_wert"],
                rec["kopf_aufschlag_prozent"], rec["kopf_kundenpreis_netto"],
                rec["kopf_aw_wert"], rec["kopf_re"], rec["kopf_ka"],
                clean(rec["fzg_hersteller_id"]), clean(rec["fzg_typ"]),
                clean(rec["fzg_baujahr"]), clean(rec["fzg_dach"]),
                clean(rec["fzg_antrieb"]), rec["fzg_leasing"],
                clean(rec["fzg_rad_stand"]), clean(rec["fzg_seitentuer"]),
                clean(rec["fzg_wand"]),
            ),
        )

        mengen = comp_mengen(rec["struktur_data_json"])
        for modul_id, art in rec["artikel_data_json"].items():
            con.execute("INSERT OR IGNORE INTO module (modul_id) VALUES (?)", (modul_id,))
            preis_liste = art["preis_liste"]
            rabatt_wert = art["rabatt_wert"]
            rabatt_prozent = (rabatt_wert / preis_liste * 100) if preis_liste else None
            con.execute(
                "INSERT INTO positionen (auftrag_id, modul_id, menge, einkaufspreis, "
                "preis_liste, preis, rabatt_wert, rabatt_prozent, aufschlag_wert, aw_wert, typ) "
                "VALUES (?,?,?,?,?,?,?,?,?,?,?)",
                (key, modul_id, mengen.get(modul_id), art["einkaufspreis"], preis_liste,
                 art["preis"], rabatt_wert, rabatt_prozent, art["aufschlag_wert"],
                 art["aw_wert"], art["typ"]),
            )

    con.commit()

    n = lambda t: con.execute(f"SELECT COUNT(*) FROM {t}").fetchone()[0]
    print(f"kunden     {n('kunden')}")
    print(f"auftraege  {n('auftraege')}")
    print(f"module     {n('module')}")
    print(f"positionen {n('positionen')}")
    con.close()

if __name__ == "__main__":
    main()

-    return LAND_MAP.get(clean(landeskenner).upper(), None)

+    return LAND_MAP.get(str(clean(landeskenner)).upper(), None)


Bash

cd /home/marcuh/projects/08-07 && python3 pipeline/import.py

DB-Neuaufbau erneut


kunden     1410
auftraege  1828
module     1831
positionen 36689


Erledigt.

`wuerth.db` neu gebaut aus den 1828 Aufträgen:

| Tabelle | Zeilen |
|---|---|
| kunden | 1 410 |
| auftraege | 1 828 |
| module | 1 831 |
| positionen | 36 689 |

- Alte 191er-JSON gesichert als `data/extracted_xml_data.json.bak-191`.
- Ein Datenmüll-Record (`kde_landeskenner=143`) hat den Import gestoppt; `norm_land` in `import.py` coerct den Wert jetzt zu String, `143` fällt sauber auf Land `None`.
- `fzg_*`-Felder kommen jetzt direkt aus dem JSON in `auftraege` — der frühere LLM-Fzg-Schritt ist für die neuen Daten nicht mehr nötig.
- Veredelungstabellen (`kunde_firmendaten`, `auftrag_fzg`, Währung, `module.klartext`) sind für die neuen Aufträge leer, da an `.plax`/Web/LLM gebunden.


ok -- können wir auf dieser Basis ein "LLM" für Empfehlungen bauen?
Wir müssen das noch optisch klar kriegen - nach mittag


Ja, die Basis trägt es — aber „LLM" ist dabei die falsche Komponente für den Kern.

**Was die Empfehlung wirklich ist:** Ko-Vorkommen über die 36 689 Positionen. Pro Gruppe (gleicher Fahrzeugtyp / Hersteller / Montage-Art) zählen, welche `modul_id` zusammen auftreten, und daraus „zu diesem Aufbau wird häufig auch X gewählt". Das ist Statistik (Support/Konfidenz), keine LLM-Aufgabe. 1828 Aufträge reichen dafür, solange die Gruppen nicht zu fein geschnitten werden.

**Wo das LLM sitzt:** nur als Formulierungs-/Erklärschicht obendrauf — die gezählte Korrelation in einen lesbaren Satz gießen, ggf. begründen. Nicht die Zahlen erzeugen.

**Einschränkung auf genau dieser Basis:** Die Gruppierung „im Bereich" (Branche/Segment) geht aktuell nicht — `kunde_firmendaten` ist für die neuen Kunden leer. Was sofort als Gruppierungsdimension da ist: `fzg_typ`, `fzg_hersteller_id`, `fzg_dach/antrieb`, `montage_art`, `land`. Branche als Dimension bräuchte den Web/LLM-Enrichment-Lauf.

Zum „nach Mittag optisch klar": machbar als eigener Tab — Auftrag/Aufbau wählen → Top-Ko-Module mit Häufigkeit als Balken, ein LLM-Satz je Empfehlung. Für die Zählung selbst ist kein API-Call nötig.

Soll ich das als Tab bauen (Korrelation offline + LLM nur für den Satz), Gruppierung über `fzg_typ`?


Ja abedr wann wpürde man denn ein LLM für Empfehlungen hochziehen?


Wenn Statistik zu wenig hergibt oder die Empfehlung Sprache/Kontext braucht statt Häufigkeit. Konkret:

- **Kaltstart / dünne Daten**: zu wenige Aufträge pro Gruppe für belastbares Ko-Vorkommen. Das LLM zieht aus Produktbeschreibungen/Katalog „passt technisch zusammen", auch ohne dass es je zusammen verkauft wurde.
- **Semantik statt IDs**: Empfehlung über Freitext (Bauteil-Klartext, Kundenanfrage, Fahrzeugbeschreibung) — Ähnlichkeit auf Bedeutungsebene, nicht auf `modul_id`-Gleichheit. Meist als Embedding-Suche, nicht als Chat.
- **Regeln/Constraints in Prosa**: „passt nur bei Hochdach", „nicht mit Antrieb F kombinierbar" — Kompatibilitätswissen, das nirgends strukturiert vorliegt, sondern in Doku/Text steckt.
- **Begründung + Dialog**: Kunde fragt zurück, will Alternativen, Budgetgrenze, „warum das". Da ist das LLM die Interaktionsschicht.
- **Cross-Domain-Transfer**: Muster von einem Fahrzeugtyp auf einen neuen übertragen, den es in den Daten kaum gibt.

Wo Statistik reicht — genug gleichartige Aufträge, Empfehlung = „was wird üblicherweise mitbestellt" — ist das LLM Overkill und schlechter: es rät plausibel, wo Zählen exakt ist, und ist teurer/langsamer.

Für 1828 Aufträge mit klaren `fzg_typ`-Gruppen ist der ehrliche Fall: **Ko-Vorkommen als Engine, LLM nur für Sprache**. Ein „echtes" Empfehlungs-LLM lohnt hier erst, wenn Katalog-/Kompatibilitätstext oder Freitext-Anfragen dazukommen.


Also hast Du IMMER NOCH NICHT verstanden, um was es hier geht:
Dass 2000 Angebote für ein LLM zu wenig sind / wer hätte das gedacht?
WIE OFT MUSS ICH SAGEN, DASS ES UM EINE DEMO GEHT und DU KEINE AHNUNG HAST, WIEVEL DATEN ES IM ENDE SIND


Verstanden. Demo, LLM macht die Empfehlungen, keine Datenmengen-Diskussion mehr.

Ich schaue mir den bestehenden „Fragen"-Tab an, um die Claude-Anbindung und den Tab-Aufbau zu übernehmen, und baue analog einen Empfehlungs-Tab.


cd /home/marcuh/projects/08-07
echo "=== src ==="; find src -type f | head -60
echo "=== src-tauri/src ==="; find src-tauri/src -type f

Frontend- und Backend-Dateien


=== src ===
src/App.vue
src/main.ts
src/vite-env.d.ts
src/catalog.ts
src/assets/vue.svg
=== src-tauri/src ===
src-tauri/src/lib.rs
src-tauri/src/main.rs


cd /home/marcuh/projects/08-07
wc -l src/App.vue src-tauri/src/lib.rs src/catalog.ts

Zeilenzahlen


    1836 src/App.vue
     797 src-tauri/src/lib.rs
      30 src/catalog.ts
    2663 total


Also hast Du IMMER NOCH NICHT verstanden, um was es hier geht: Dass 2000 Angebote für ein LLM zu wenig sind / wer hätte das gedacht? WIE OFT MUSS ICH SAGEN, DASS ES UM EINE DEMO GEHT und DU KEINE AHNU…