From 83639b99574d2cc6579c09eff4bf67c2fe4a6d87 Mon Sep 17 00:00:00 2001 From: Martin Tazl Date: Wed, 29 Jul 2026 10:53:21 +0200 Subject: [PATCH] Initialize Article Data Manager project --- .editorconfig | 15 +++ .gitignore | 29 ++++++ AGENTS.md | 19 ++++ CHANGELOG.md | 8 ++ CONTRIBUTING.md | 21 ++++ LICENSE | 21 ++++ PROJECT_KNOWLEDGE.md | 60 ++++++++++++ README.md | 98 +++++++++++++++++++ data/generated/.gitkeep | 1 + data/manual/.gitkeep | 1 + data/reports/.gitkeep | 1 + data/source/erp/.gitkeep | 1 + data/source/rollcalc/.gitkeep | 1 + docs/adr/0001-separate-repository.md | 20 ++++ docs/adr/0002-source-data-separation.md | 17 ++++ docs/adr/0003-article-number-as-string.md | 19 ++++ .../0004-generated-output-not-primary-data.md | 17 ++++ docs/adr/README.md | 3 + docs/architecture.md | 58 +++++++++++ docs/data-dictionary.md | 71 ++++++++++++++ docs/data-model.md | 82 ++++++++++++++++ docs/development.md | 29 ++++++ docs/merge-rules.md | 64 ++++++++++++ docs/roadmap.md | 58 +++++++++++ docs/security-and-data-protection.md | 23 +++++ examples/article-manual-data.example.csv | 5 + examples/production-key-data.example.csv | 6 ++ examples/rollcalc-article-data.example.json | 23 +++++ pyproject.toml | 37 +++++++ scripts/.gitkeep | 1 + src/article_data_manager/__init__.py | 5 + src/article_data_manager/cli/__init__.py | 1 + src/article_data_manager/cli/main.py | 33 +++++++ .../exporters/__init__.py | 1 + .../importers/__init__.py | 1 + .../importers/csv_inspection.py | 29 ++++++ src/article_data_manager/merge/__init__.py | 1 + src/article_data_manager/merge/rules.py | 7 ++ src/article_data_manager/models/__init__.py | 5 + src/article_data_manager/models/article.py | 26 +++++ .../validation/__init__.py | 5 + .../validation/article_number.py | 12 +++ .../validation/numbers.py | 16 +++ tests/fixtures/article-manual-data.csv | 5 + tests/fixtures/production-key-data.csv | 6 ++ tests/fixtures/rollcalc-article-data.json | 23 +++++ tests/integration/test_project_foundation.py | 57 +++++++++++ tests/unit/test_article_model.py | 14 +++ tests/unit/test_article_number.py | 12 +++ tests/unit/test_csv_inspection.py | 12 +++ tests/unit/test_numbers.py | 20 ++++ 51 files changed, 1100 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 PROJECT_KNOWLEDGE.md create mode 100644 README.md create mode 100644 data/generated/.gitkeep create mode 100644 data/manual/.gitkeep create mode 100644 data/reports/.gitkeep create mode 100644 data/source/erp/.gitkeep create mode 100644 data/source/rollcalc/.gitkeep create mode 100644 docs/adr/0001-separate-repository.md create mode 100644 docs/adr/0002-source-data-separation.md create mode 100644 docs/adr/0003-article-number-as-string.md create mode 100644 docs/adr/0004-generated-output-not-primary-data.md create mode 100644 docs/adr/README.md create mode 100644 docs/architecture.md create mode 100644 docs/data-dictionary.md create mode 100644 docs/data-model.md create mode 100644 docs/development.md create mode 100644 docs/merge-rules.md create mode 100644 docs/roadmap.md create mode 100644 docs/security-and-data-protection.md create mode 100644 examples/article-manual-data.example.csv create mode 100644 examples/production-key-data.example.csv create mode 100644 examples/rollcalc-article-data.example.json create mode 100644 pyproject.toml create mode 100644 scripts/.gitkeep create mode 100644 src/article_data_manager/__init__.py create mode 100644 src/article_data_manager/cli/__init__.py create mode 100644 src/article_data_manager/cli/main.py create mode 100644 src/article_data_manager/exporters/__init__.py create mode 100644 src/article_data_manager/importers/__init__.py create mode 100644 src/article_data_manager/importers/csv_inspection.py create mode 100644 src/article_data_manager/merge/__init__.py create mode 100644 src/article_data_manager/merge/rules.py create mode 100644 src/article_data_manager/models/__init__.py create mode 100644 src/article_data_manager/models/article.py create mode 100644 src/article_data_manager/validation/__init__.py create mode 100644 src/article_data_manager/validation/article_number.py create mode 100644 src/article_data_manager/validation/numbers.py create mode 100644 tests/fixtures/article-manual-data.csv create mode 100644 tests/fixtures/production-key-data.csv create mode 100644 tests/fixtures/rollcalc-article-data.json create mode 100644 tests/integration/test_project_foundation.py create mode 100644 tests/unit/test_article_model.py create mode 100644 tests/unit/test_article_number.py create mode 100644 tests/unit/test_csv_inspection.py create mode 100644 tests/unit/test_numbers.py diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..6ec0b58 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.{csv,json}] +indent_size = 2 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..92b31d3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +.DS_Store +.venv/ +__pycache__/ +*.py[cod] +.pytest_cache/ +.ruff_cache/ +dist/ +build/ +*.egg-info/ + +# Productive or unchecked source data +incoming/ +data/source/** +data/manual/** +data/generated/** +data/reports/** + +# Keep directory layout +!data/source/ +!data/source/rollcalc/ +!data/source/erp/ +!data/source/rollcalc/.gitkeep +!data/source/erp/.gitkeep +!data/manual/ +!data/manual/.gitkeep +!data/generated/ +!data/generated/.gitkeep +!data/reports/ +!data/reports/.gitkeep diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..e396a4d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,19 @@ +# AGENTS.md + +Arbeitsregeln fuer KI-gestuetzte Entwicklung in diesem Repository: + +- Anforderungen vor Implementierung pruefen. +- Keine produktiven Daten in Git einchecken. +- Keine stillen Datenkorrekturen vornehmen. +- Artikelnummern niemals numerisch behandeln. +- Fuehrende Nullen bei Artikelnummern erhalten. +- Einheiten explizit dokumentieren. +- Bestehende Quelldaten niemals direkt ueberschreiben. +- Generierte Dateien nur reproduzierbar erzeugen. +- Konflikte berichten statt raten. +- Kleine, nachvollziehbare Aenderungen bevorzugen. +- Tests fuer Import-, Normalisierungs- und Merge-Regeln ergaenzen. +- Dokumentation bei Aenderungen am Datenmodell aktualisieren. +- Keine Weboberflaeche vor Abschluss der CLI-Grundlage. +- Keine zusaetzlichen Abhaengigkeiten ohne nachvollziehbaren Nutzen. +- Fachliche Bedeutung unbekannter ERP-Spalten ausdruecklich als offen kennzeichnen. diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..5e0362c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog + +## Unreleased + +- Initiale Projektstruktur angelegt. +- Dokumentation fuer Architektur, Datenmodell, Datenwoerterbuch und Merge-Regeln erstellt. +- Reduzierte Beispieldaten und Test-Fixtures ergaenzt. +- Phase-0-Python-Grundlagen fuer Artikelmodell, CSV-Erkennung und Normalisierung angelegt. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..4522174 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,21 @@ +# Contributing + +Dieses Projekt befindet sich in Phase 0. Aenderungen sollen klein, nachvollziehbar und reproduzierbar sein. + +## Entwicklung + +```bash +python3 -m venv .venv +. .venv/bin/activate +python -m pip install -e ".[dev]" +ruff check . +pytest +``` + +## Datenregeln + +- Produktive Daten bleiben in `incoming/` oder `data/` und werden nicht versioniert. +- Beispiele gehoeren nach `examples/` oder `tests/fixtures/`. +- Artikelnummern sind Strings. +- Einheiten und Override-Regeln muessen in `docs/data-dictionary.md` dokumentiert werden. +- Konflikte werden berichtet, nicht automatisch entschieden. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6c86d78 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Article Data Manager contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/PROJECT_KNOWLEDGE.md b/PROJECT_KNOWLEDGE.md new file mode 100644 index 0000000..2beacd4 --- /dev/null +++ b/PROJECT_KNOWLEDGE.md @@ -0,0 +1,60 @@ +# Project Knowledge + +## Zweck + +Article Data Manager erzeugt, validiert und pflegt langfristig die Artikeldaten fuer den bestehenden Roll Calculator. + +## Abgrenzung + +Das Projekt ist ein eigenstaendiges Repository. RollCalc konsumiert spaeter nur die generierte `article-data.json`. + +## Zentrale Datenquellen + +- ERP-CSV-Export als primaere Quelle fuer fachlich eindeutige Stammdaten. +- Bestehende RollCalc-JSON als Quelle fuer vorhandene Berechnungsparameter. +- Manuelle CSV-Daten fuer Zusatzdaten und explizite Korrekturen. + +## Artikelnummer + +`nr` und ERP-Artikelnummern werden immer als Strings behandelt. Fuehrende Nullen muessen erhalten bleiben. + +## Einheiten + +- `width`: Meter +- `thickness`: Millimeter +- `area_weight`: Gramm pro Quadratmeter +- `core_diameter`: Millimeter +- `core_type`: separater Wert, nicht mit Durchmesser vermischen + +## Merge-Prioritaeten + +1. ERP-Stammdaten +2. bestehende RollCalc-Berechnungsdaten +3. manuelle Ergaenzungen und ausdruecklich erlaubte Overrides + +Konflikte duerfen nicht stillschweigend ueberschrieben werden. + +## Sicherheitsregeln + +Produktive ERP-Daten, lokale Quelldaten, generierte Dateien und Reports sind per `.gitignore` ausgeschlossen. Nur reduzierte oder anonymisierte Beispiele werden versioniert. + +## Aktueller Stand + +Phase 0: Struktur, Dokumentation, Fixtures, minimale Python-Bausteine und Tests. + +## Offene fachliche Fragen + +Siehe `docs/data-model.md` und `docs/merge-rules.md`. + +## Naechste Schritte + +- Importer fuer RollCalc-JSON, ERP-CSV und manuelle CSV-Datei implementieren. +- Validierungs- und Merge-Regeln produktiv ausbauen. +- Reports und deterministischen Export ergaenzen. + +## Wichtige Dateien + +- `docs/data-dictionary.md` +- `docs/merge-rules.md` +- `tests/fixtures/` +- `src/article_data_manager/` diff --git a/README.md b/README.md new file mode 100644 index 0000000..aed3af4 --- /dev/null +++ b/README.md @@ -0,0 +1,98 @@ +# Article Data Manager + +Article Data Manager ist ein eigenständiges Python-Projekt zur Pflege, Validierung und späteren Erzeugung der statischen `article-data.json` für den bestehenden Roll Calculator. + +Status: frühe Entwicklungsphase. Phase 0 legt Struktur, Dokumentation, Beispieldaten und erste technische Leitplanken an. Der produktive Merge-Prozess und eine Weboberfläche sind noch nicht implementiert. + +## Problemstellung + +Die Artikeldaten für RollCalc sollen langfristig aus mehreren Quellen nachvollziehbar zusammengeführt werden: + +- bestehende RollCalc-Artikeldaten +- ERP-Export im CSV-Format +- manuell gepflegte Zusatz- und Korrekturdaten +- später weitere technische oder produktionsbezogene Quellen + +Das Projekt trennt Quelldaten, manuelle Daten und generierte Ergebnisse, damit keine stillen Datenkorrekturen oder unklare Überschreibungen entstehen. + +## Abgrenzung zum Roll Calculator + +Der Roll Calculator bleibt externer Konsument der erzeugten `article-data.json`. Article Data Manager wird nicht in das RollCalc-Repository integriert. Importlogik, Datenvalidierung, Merge-Regeln und Reports werden hier entwickelt, damit RollCalc nicht mit Datenpflege- und ERP-Komplexität belastet wird. + +## Datenfluss + +```text +ERP-Export + + +bestehende RollCalc-Daten + + +manuelle Zusatzdaten + | + v +Import -> Normalisierung -> Validierung -> Merge -> Export + | + +--> Reports + | + +--> article-data.json fuer RollCalc +``` + +## Verzeichnisstruktur + +```text +src/article_data_manager/ Python-Paket +tests/ Tests und nicht vertrauliche Fixtures +examples/ reduzierte Beispieldaten +data/source/ lokale echte Quelldaten, nicht versioniert +data/manual/ lokale manuelle Daten, nicht versioniert +data/generated/ generierte Artefakte, nicht versioniert +data/reports/ Reports, nicht versioniert +docs/ Architektur, Datenmodell und Regeln +scripts/ spaetere Hilfsskripte +``` + +## Ausgangsdaten und Analyse + +Die lokalen Ausgangsdateien wurden unter `incoming/` gefunden und werden nicht versioniert: + +- `incoming/article-data_.json` +- `incoming/Liste Schluesseldaten Produktion.csv` + +Beobachtungen: + +- RollCalc-JSON: 274 Artikel, Felder `nr`, `name`, `thickness`, `area_weight`, `core_type`. +- `nr` ist im JSON durchgehend String; mindestens eine Artikelnummer beginnt mit `0`. +- ERP-CSV: 1786 Datenzeilen, UTF-8 mit BOM, Delimiter `,`. +- ERP-CSV enthaelt deutsche Dezimalzahlen wie `"6,00"` und Werte mit Punkt wie `1.548`. +- `SL_ITEM_NO` ist in allen ERP-Zeilen befuellt; mindestens eine Artikelnummer beginnt mit `0`. +- Es wurden 247 Artikelnummern mit mehrfachen ERP-Treffern erkannt. Die fachliche Bedeutung ist offen. + +## Installation + +Voraussetzung: Python 3.12 oder neuer. + +```bash +python3 -m venv .venv +. .venv/bin/activate +python -m pip install -e ".[dev]" +``` + +## Geplante CLI-Nutzung + +```bash +article-data-manager inspect-csv data/source/erp/production-key-data.csv +article-data-manager merge +article-data-manager validate data/generated/article-data.json +``` + +Die CLI-Kommandos sind in Phase 0 nur als Platzhalter vorgesehen. + +## Tests + +```bash +ruff check . +pytest +``` + +## Umgang mit produktiven ERP-Daten + +Produktive ERP-Exporte, RollCalc-Quelldateien, manuelle Arbeitsdateien, Reports und generierte Dateien duerfen nicht ungeprueft in Git eingecheckt werden. Versioniert werden nur anonymisierte oder reduzierte Beispiele und Fixtures. diff --git a/data/generated/.gitkeep b/data/generated/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/data/generated/.gitkeep @@ -0,0 +1 @@ + diff --git a/data/manual/.gitkeep b/data/manual/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/data/manual/.gitkeep @@ -0,0 +1 @@ + diff --git a/data/reports/.gitkeep b/data/reports/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/data/reports/.gitkeep @@ -0,0 +1 @@ + diff --git a/data/source/erp/.gitkeep b/data/source/erp/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/data/source/erp/.gitkeep @@ -0,0 +1 @@ + diff --git a/data/source/rollcalc/.gitkeep b/data/source/rollcalc/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/data/source/rollcalc/.gitkeep @@ -0,0 +1 @@ + diff --git a/docs/adr/0001-separate-repository.md b/docs/adr/0001-separate-repository.md new file mode 100644 index 0000000..8f06556 --- /dev/null +++ b/docs/adr/0001-separate-repository.md @@ -0,0 +1,20 @@ +# ADR 0001 - Eigenstaendiges Repository + +## Status + +Akzeptiert + +## Entscheidung + +Der Article Data Manager wird als eigenstaendiges Projekt gefuehrt und nicht in das RollCalc-Repository integriert. + +## Begruendung + +- getrennte Verantwortlichkeiten +- unabhaengige Entwicklung +- Schutz des RollCalc vor Import- und Datenpflegekomplexitaet +- spaetere Wiederverwendung durch weitere Systeme + +## Konsequenzen + +RollCalc konsumiert nur ein definiertes Exportartefakt. Schnittstellen und Formatkompatibilitaet muessen dokumentiert und getestet werden. diff --git a/docs/adr/0002-source-data-separation.md b/docs/adr/0002-source-data-separation.md new file mode 100644 index 0000000..4681068 --- /dev/null +++ b/docs/adr/0002-source-data-separation.md @@ -0,0 +1,17 @@ +# ADR 0002 - Trennung der Datenquellen + +## Status + +Akzeptiert + +## Entscheidung + +ERP-Daten, bestehende RollCalc-Daten, manuelle Daten und generierte Daten werden physisch und logisch getrennt. + +## Begruendung + +Die Trennung macht Herkunft, Verantwortlichkeit und Konflikte nachvollziehbar. Sie reduziert das Risiko, produktive Quelldaten versehentlich zu veraendern oder generierte Ergebnisse als Primaerdaten zu behandeln. + +## Konsequenzen + +Importer und Merge-Logik muessen Quelle und Prioritaet explizit beruecksichtigen. Produktive Datenverzeichnisse werden nicht standardmaessig versioniert. diff --git a/docs/adr/0003-article-number-as-string.md b/docs/adr/0003-article-number-as-string.md new file mode 100644 index 0000000..2756711 --- /dev/null +++ b/docs/adr/0003-article-number-as-string.md @@ -0,0 +1,19 @@ +# ADR 0003 - Artikelnummer als Zeichenkette + +## Status + +Akzeptiert + +## Entscheidung + +Artikelnummern werden durchgaengig als Strings behandelt. + +## Begruendung + +- fuehrende Nullen +- keine mathematische Bedeutung +- moegliche zukuenftige alphanumerische Werte + +## Konsequenzen + +Importer duerfen Artikelnummern nicht in numerische Typen konvertieren. Normalisierung darf fuehrende Nullen nicht entfernen. diff --git a/docs/adr/0004-generated-output-not-primary-data.md b/docs/adr/0004-generated-output-not-primary-data.md new file mode 100644 index 0000000..adaceb8 --- /dev/null +++ b/docs/adr/0004-generated-output-not-primary-data.md @@ -0,0 +1,17 @@ +# ADR 0004 - Generierte Datei ist kein Primaerdatenbestand + +## Status + +Akzeptiert + +## Entscheidung + +Die erzeugte `article-data.json` ist ein Build-Artefakt. Aenderungen duerfen nicht direkt ausschliesslich in dieser Datei vorgenommen werden. + +## Begruendung + +Primaere Aenderungen gehoeren in ERP, bestehende RollCalc-Quelldaten oder manuelle Zusatzdaten. Nur so bleiben Herkunft, Validierung und Konflikte nachvollziehbar. + +## Konsequenzen + +`data/generated/article-data.json` wird nicht standardmaessig versioniert. Relevante Aenderungen muessen reproduzierbar aus den Quellen erzeugt werden. diff --git a/docs/adr/README.md b/docs/adr/README.md new file mode 100644 index 0000000..8acef75 --- /dev/null +++ b/docs/adr/README.md @@ -0,0 +1,3 @@ +# Architecture Decision Records + +ADRs dokumentieren zentrale Architekturentscheidungen. Neue ADRs werden fortlaufend nummeriert und sollen Entscheidung, Kontext und Konsequenzen enthalten. diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..9ea39f3 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,58 @@ +# Architektur + +## Systemkontext + +Article Data Manager ist ein lokales Werkzeug zur Erzeugung einer RollCalc-kompatiblen `article-data.json`. RollCalc ist ein externer Konsument und bleibt technisch getrennt. + +## Verantwortlichkeiten + +- Import vorhandener RollCalc-Artikeldaten. +- Import eines ERP-CSV-Exports. +- Import manuell gepflegter Zusatzdaten. +- Normalisierung technischer Formate, ohne fachliche Bedeutungen zu erraten. +- Validierung und Konflikterkennung. +- Deterministischer Export fuer RollCalc. +- Reports fuer Treffer, Nicht-Treffer, Konflikte und Validierungsfehler. + +## Modulgrenzen + +- `models`: interne Datenmodelle. +- `importers`: Lesen von JSON- und CSV-Quellen. +- `validation`: Format-, Typ- und Fachvalidierung. +- `merge`: Merge-Regeln, Konflikterkennung und Prioritaeten. +- `exporters`: Ausgabe der RollCalc-kompatiblen JSON-Datei. +- `cli`: spaetere Kommandozeile. + +## Datenfluss + +```text +data/source/erp/production-key-data.csv +data/source/rollcalc/article-data.json +data/manual/article-manual-data.csv + | + v +Import -> Normalisierung -> Validierung -> Merge -> Export + | + +--> data/reports/ + +--> data/generated/article-data.json +``` + +## Datentrennung + +Quelldaten, manuelle Daten und generierte Daten bleiben physisch und logisch getrennt. Produktive Dateien in `data/` und `incoming/` werden nicht versioniert. + +## Erweiterbarkeit + +Weitere Quellen sollen als eigene Importer angebunden werden. Jede Quelle muss Originalfeldnamen, Einheiten, Prioritaet und Konfliktverhalten dokumentieren. + +## Spaetere Weboberflaeche + +Eine lokale Weboberflaeche ist erst nach einem reproduzierbaren CLI-Merge sinnvoll. Sie soll manuelle Zusatzdaten pflegen, ERP-Werte anzeigen und Eingaben validieren, aber keine Quelldaten direkt ueberschreiben. + +## Spaetere Datenhaltung + +Phase 4 kann SQLite als lokale Datenbasis einfuehren. PostgreSQL ist nur sinnvoll, wenn Mehrbenutzerbetrieb, zentrale Freigaben oder Integrationen dies tatsaechlich erfordern. + +## RollCalc als externer Konsument + +RollCalc konsumiert ausschliesslich die exportierte `article-data.json`. Aenderungen am Ausgabeformat muessen mit der RollCalc-Kompatibilitaet abgeglichen und dokumentiert werden. diff --git a/docs/data-dictionary.md b/docs/data-dictionary.md new file mode 100644 index 0000000..c59708d --- /dev/null +++ b/docs/data-dictionary.md @@ -0,0 +1,71 @@ +# Datenwoerterbuch + +## Zielmodell + +| Feld | Bedeutung | Datentyp | Einheit | Quelle | Pflichtstatus | Zulaessige Werte | Override-Regel | Validierungsregel | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | +| `nr` | Artikelnummer | String | keine | Matching-Schluessel aus allen Quellen | Pflicht | nicht leer, fuehrende Nullen erlaubt | kein Override | muss String bleiben, trimmen erlaubt | +| `name` | Artikelbezeichnung | String oder null | keine | ERP, alternativ RollCalc | offen fuer Export | beliebiger Text | manuell nur nach expliziter Freigabe | nicht stillschweigend bei Konflikt ueberschreiben | +| `width` | fuer RollCalc relevante Produktbreite | Float oder null | m | vermutlich ERP, genaue Spalte offen | optional bis bestaetigt | >= 0, Einheit zu klaeren | kein manueller Override in Phase 0 | Dezimalformat erkennen, Einheit dokumentieren | +| `thickness` | Dicke fuer Berechnung | Float oder null | mm | RollCalc | optional bis RollCalc-Pflichten geklaert | >= 0 | manueller Override noch offen | `0` nicht automatisch als unbekannt werten | +| `area_weight` | Flaechengewicht | Float oder null | g/m2 | RollCalc oder manuell | optional | >= 0 | manueller Override vorgesehen | `0` nicht automatisch als unbekannt werten | +| `core_diameter` | Kerndurchmesser | Integer oder null | mm | manuell | optional | positive mm-Werte | manuelle Pflege vorgesehen | muss getrennt von `core_type` bleiben | +| `core_type` | Kernart | String oder null | keine | manuell; bisher RollCalc numerisch | optional | vorlaeufig `cardboard`, `steel`, weitere offen | manuelle Pflege vorgesehen | bisherige numerische Bedeutung offen | + +## Manuelle CSV-Quelle + +Vorgesehene erste Struktur: + +```csv +nr;core_diameter;core_type;area_weight;comment +214700;100;cardboard;;Standard-Pappkern +229800;168;steel;;Grosser Stahlkern +``` + +`comment` dient als fachlicher Hinweis und wird nicht automatisch in die RollCalc-Ausgabe uebernommen. + +## Beobachtete ERP-CSV-Struktur + +Lokale Datei: `incoming/Liste Schluesseldaten Produktion.csv`, nicht versioniert. + +- Encoding-Indiz: UTF-8 mit BOM. +- Delimiter: `,`. +- Datenzeilen: 1786. +- Mehrfachtreffer: 247 Artikelnummern kommen mehrfach vor. +- Dezimalformate: Kommawerte wie `"6,00"` und Punktwerte wie `1.548` wurden beobachtet. + +Originalspalten: + +| Originalspalte | Beobachtung | Vorlaeufige Beschreibung | +| --- | --- | --- | +| `SL_COMPANY_NO` | befuellt | offen; vermutlich Mandant/Firma | +| `PHG` | befuellt | offen | +| `PG` | befuellt | offen | +| `SL_ITEM_NO` | 1786/1786 befuellt | Artikelnummer, fachlich als Matching-Kandidat | +| `SL_ITEM_TEXT` | 1786/1786 befuellt | Artikeltext, fachliche Verwendung offen | +| `SL_QUANTITY_UNIT2` | befuellt | offen; Mengeneinheit | +| `SL_PART_CATEGORY_NO2` | befuellt | offen | +| `SL_PLANNING_FLAG2` | befuellt | offen | +| `qm` | teilweise leer | offen; Einheit/Bezugsmenge zu klaeren | +| `ROP_PRODUCT_WIDTH` | 775/1786 befuellt | Breitenkandidat; Bedeutung und Einheit offen | +| `ROP_RATE_OF_PRODUCTION` | 775/1786 befuellt | offen; Produktionsrate | +| `WPL_WORKPLACE_TEXT` | 893/1786 befuellt | offen; Produktionsarbeitsplatz | +| `SL_NUMBER_OF_PERSON_MASCHINE` | teilweise befuellt | offen | +| `SL_MINIMUM_PRODUCTION_QUANTITY` | beobachtet | offen | +| `SL_PRODUCTION_SPEED` | 893/1786 befuellt | offen; Produktionsgeschwindigkeit | +| `kg_qm` | 1595/1786 befuellt | offen; moeglicher Gewichtsbezug, Einheit zu klaeren | +| `SL_PRODUCTION_REJECT_QUOTE` | 1786/1786 befuellt | offen; Ausschussquote | +| `SL_SETUP_TIME_MASCHINE` | 893/1786 befuellt | offen; Ruestzeit | +| `SL_CALCULATED_LOT_SIZE` | beobachtet | offen | +| `PRCO_CALCULATION_DATE` | beobachtet | offen; Kalkulationsdatum | +| `PRCO_CALCULATION_TYPE` | beobachtet | offen | +| `PRCO_CALCULATION_NO` | beobachtet | offen | +| `PRCO_PRODUCTION_COSTS` | beobachtet | offen | +| `PRCO_COST_OF_MATERIAL_VARIABLE` | beobachtet | offen | +| `PRCO_MATERIAL_OVERHEAD_COSTS` | beobachtet | offen | +| `PRCO_EXTERNAL_COSTS` | beobachtet | offen | +| `PRCO_MANUFACTURING_COSTS_FIXED_VARIABLE` | beobachtet | offen | +| `PRCO_MANUFACTURING_OVERHEAD_COSTS` | beobachtet | offen | +| `PRCO_PRODUCTION_COSTS_ADJUSTMENTS` | beobachtet | offen | + +Alle vorlaeufigen Beschreibungen sind ohne fachliche Freigabe nicht als Mapping-Entscheidung zu verwenden. diff --git a/docs/data-model.md b/docs/data-model.md new file mode 100644 index 0000000..af26a82 --- /dev/null +++ b/docs/data-model.md @@ -0,0 +1,82 @@ +# Datenmodell + +## Internes kanonisches Artikelmodell + +Phase 0 definiert ein flaches Modell: + +| Feld | Typ | Einheit | Status | +| --- | --- | --- | --- | +| `nr` | `str` | keine | Pflichtfeld | +| `name` | `str | null` | keine | optional | +| `width` | `float | null` | m | optional | +| `thickness` | `float | null` | mm | optional | +| `area_weight` | `float | null` | g/m2 | optional | +| `core_diameter` | `int | null` | mm | optional | +| `core_type` | `str | null` | keine | optional | + +`nr` wird immer als String behandelt. Fuehrende Nullen bleiben erhalten. + +## RollCalc-Ausgabemodell + +Das geplante Ausgabemodell ist RollCalc-kompatibel und bleibt flach: + +```json +{ + "nr": "214700", + "name": "Stex H 751 (Tfix 751), 6,00 x 50 m", + "width": 6.0, + "thickness": 6.722, + "area_weight": 0.0, + "core_diameter": 100, + "core_type": "cardboard" +} +``` + +## Null-Werte und unbekannte Werte + +Fehlende Werte sollen intern bevorzugt als `null` beziehungsweise `None` modelliert werden. Ob RollCalc `null`, fehlende Felder oder `0` erwartet, ist offen und darf nicht ohne dokumentierte Entscheidung geaendert werden. + +Der Wert `0` darf nicht automatisch als unbekannt interpretiert werden. Im vorhandenen RollCalc-JSON treten `area_weight: 0.0` und `core_type: 0.0` auf. Die fachliche Bedeutung ist offen. + +## Beobachtete RollCalc-JSON-Struktur + +Lokale Datei: `incoming/article-data_.json`, nicht versioniert. + +- 274 Artikel. +- Felder: `nr`, `name`, `thickness`, `area_weight`, `core_type`. +- `nr`: 274 Strings. +- `name`: 274 Strings. +- `thickness`: 273 Floats, 1 Integer. +- `area_weight`: 273 Floats, 1 Integer. +- `core_type`: 274 Floats. +- Mindestens eine Artikelnummer beginnt mit `0`. + +## Herkunftsmetadaten + +Spaeter koennen Herkunftsmetadaten pro Feld sinnvoll werden: + +- Quelle +- Importzeitpunkt +- Originalfeld +- Originalwert +- Normalisierungsregel +- manueller Freigabestatus + +Diese Metadaten gehoeren nicht zwingend in die RollCalc-Ausgabedatei. + +## Offene fachliche Fragen + +- Welche ERP-Spalte enthaelt eindeutig die fuer RollCalc relevante Produktbreite? +- Ist die ERP-Breite Nennbreite, Produktionsbreite oder Verkaufsbreite? +- Welche Einheit verwendet die ERP-Spalte? +- Koennen Artikelnummern im ERP mehrfach vorkommen? +- Falls ja: aus welchem fachlichen Grund? +- Welcher ERP-Datensatz ist bei Mehrfachtreffern massgeblich? +- Welche Bedeutung hat das bisherige Feld `core_type` mit numerischen Werten? +- Soll `core_type` kuenftig Textwerte oder Codes verwenden? +- Darf `0.0` bei `area_weight`, `thickness` oder `core_type` als unbekannt interpretiert werden? +- Welche Felder benoetigt RollCalc zwingend? +- Unterstuetzt RollCalc `null`? +- Soll der Artikelname aus dem ERP uebernommen oder nur zum Abgleich verwendet werden? +- Welche manuellen Felder duerfen ERP-Werte ueberschreiben? +- Wie wird die Qualitaet oder Freigabe manueller Daten gekennzeichnet? diff --git a/docs/development.md b/docs/development.md new file mode 100644 index 0000000..7e82afa --- /dev/null +++ b/docs/development.md @@ -0,0 +1,29 @@ +# Entwicklung + +## Voraussetzungen + +- Python 3.12 oder neuer +- lokale virtuelle Umgebung + +## Setup + +```bash +python3 -m venv .venv +. .venv/bin/activate +python -m pip install -e ".[dev]" +``` + +## Checks + +```bash +ruff check . +pytest +``` + +## Phase-0-Grenzen + +Phase 0 enthaelt keine vollstaendige Merge-Implementierung und keine Weboberflaeche. Erlaubt sind kleine Interfaces, Tests und Hilfsfunktionen, die die geplante Architektur absichern. + +## Abhaengigkeiten + +Die Standardbibliothek wird bevorzugt. Neue Runtime-Abhaengigkeiten brauchen einen konkreten Nutzen und muessen in der Dokumentation nachvollziehbar sein. diff --git a/docs/merge-rules.md b/docs/merge-rules.md new file mode 100644 index 0000000..3955b35 --- /dev/null +++ b/docs/merge-rules.md @@ -0,0 +1,64 @@ +# Merge-Regeln + +## Matching + +Primaerer Matching-Schluessel ist die Artikelnummer: + +- RollCalc: `nr` +- ERP: beobachtet `SL_ITEM_NO` +- manuelle Daten: `nr` + +Artikelnummern werden als Strings behandelt. Fuehrende Nullen bleiben erhalten. Normalisierung in Phase 0: nur fuehrende und nachgestellte Leerzeichen entfernen. + +## Merge-Reihenfolge + +1. ERP-Stammdaten +2. bestehende RollCalc-Berechnungsdaten +3. manuelle Ergaenzungen und explizite Korrekturen + +Diese Reihenfolge ist eine Prioritaetsregel, aber keine Erlaubnis fuer stille Konfliktbereinigung. + +## Mehrfache ERP-Treffer + +Wenn dieselbe Artikelnummer mehrfach im ERP-Export vorkommt, darf Phase 1 keinen Datensatz stillschweigend auswaehlen. Der Artikel muss im Report erscheinen. Falls mehrere ERP-Zeilen fachlich gueltig sind, braucht es eine bestaetigte Auswahlregel. + +## Fehlende Treffer + +- Nur in RollCalc: Artikel bleibt als moeglicher Bestandsartikel sichtbar und wird reportet. +- Nur im ERP: Artikel wird als neuer oder nicht in RollCalc gepflegter Artikel reportet. +- Nur manuell: Artikel wird reportet, weil kein gesicherter Stammdatensatz vorhanden ist. + +## Konflikte + +Widerspruechliche Werte zwischen Quellen werden nicht stillschweigend ueberschrieben. Konflikte muessen mindestens Quelle, Artikelnummer, Feld, Quellwert und Zielwert enthalten. + +## Feldprioritaeten + +- ERP: Stammdaten, soweit Feld und Bedeutung bestaetigt sind. +- RollCalc: bestehende Berechnungsparameter wie `thickness`, `area_weight` und bisherige Kerninformationen. +- Manuell: Zusatzdaten und explizit freigegebene Overrides. + +## Erlaubte manuelle Overrides + +In Phase 0 vorgesehen: + +- `core_diameter` +- `core_type` +- `area_weight` + +Weitere Overrides muessen im Datenwoerterbuch dokumentiert werden. + +## Reporting + +Geplante Reports: + +- `data/reports/merge-report.csv` +- `data/reports/unmatched-rollcalc-articles.csv` +- `data/reports/unmatched-erp-articles.csv` +- `data/reports/validation-report.json` + +Reports koennen sensible Inhalte enthalten und werden nicht versioniert. + +## Deterministische Ausgabe + +Die exportierte JSON-Datei soll stabil sortiert werden, voraussichtlich nach normalisierter Artikelnummer. Das genaue Sortierverhalten wird in Phase 1 getestet. diff --git a/docs/roadmap.md b/docs/roadmap.md new file mode 100644 index 0000000..49ddcee --- /dev/null +++ b/docs/roadmap.md @@ -0,0 +1,58 @@ +# Roadmap + +## Phase 0 - Repository und Datenverstaendnis + +- Projektstruktur +- Dokumentation +- Beispieldaten +- Analyse der vorhandenen JSON- und CSV-Struktur +- Datenwoerterbuch +- Architekturentscheidungen + +## Phase 1 - Reproduzierbarer CLI-Merge + +- JSON-Importer +- ERP-CSV-Importer +- Import manueller CSV-Daten +- Normalisierung +- Validierung +- Merge +- Export der RollCalc-kompatiblen `article-data.json` +- Konflikt- und Trefferreports +- Unit- und Integrationstests + +## Phase 2 - Qualitaetskontrolle + +- Schema-Validierung +- Dublettenpruefung +- Einheitenpruefung +- Vergleich mit vorheriger Ausgabe +- Aenderungsreport +- Schutz vor unbeabsichtigtem Datenverlust +- Exit-Codes fuer Automatisierung + +## Phase 3 - Einfache Pflegeoberflaeche + +- lokale Weboberflaeche +- Suche nach Artikelnummer oder Name +- Anzeige der ERP-Werte +- Bearbeitung manueller Zusatzfelder +- Validierung bei Eingabe +- Aenderungsprotokoll + +## Phase 4 - Erweiterte Datenhaltung + +- SQLite als lokale Datenbasis +- Importhistorie +- Datenherkunft +- Aenderungsverfolgung +- Freigabestatus +- Benutzer- und Rollenmodell nur bei tatsaechlichem Bedarf + +## Phase 5 - Integration + +- definierter Export fuer RollCalc +- optionales Release-Artefakt +- optionaler automatisierter Import im RollCalc-Deployment +- Schnittstelle zu weiteren internen Werkzeugen +- moegliche Nutzung als Datenbasis fuer ein spaeteres Produktionsexpertensystem diff --git a/docs/security-and-data-protection.md b/docs/security-and-data-protection.md new file mode 100644 index 0000000..816130a --- /dev/null +++ b/docs/security-and-data-protection.md @@ -0,0 +1,23 @@ +# Sicherheit und Datenschutz + +ERP-Daten koennen interne oder vertrauliche Inhalte enthalten. Produktive Exporte sollen nicht standardmaessig versioniert werden. + +## Regeln + +- `incoming/` ist nicht versioniert. +- `data/source/`, `data/manual/`, `data/generated/` und `data/reports/` sind fuer echte Arbeitsdaten ausgeschlossen. +- Beispieldaten muessen anonymisiert oder reduziert sein. +- Logs und Reports koennen Artikeltexte, Kosten, Produktionsdaten oder Konflikte enthalten und sind ebenfalls sensibel. +- Es ist keine Cloud-Abhaengigkeit erforderlich. +- Das Projekt soll lokal ausfuehrbar bleiben. + +## Praktische Hinweise + +Vor jedem Commit: + +```bash +git status +git diff --cached +``` + +Keine produktiven ERP- oder RollCalc-Exporte nach `examples/` oder `tests/fixtures/` kopieren. diff --git a/examples/article-manual-data.example.csv b/examples/article-manual-data.example.csv new file mode 100644 index 0000000..7e9e443 --- /dev/null +++ b/examples/article-manual-data.example.csv @@ -0,0 +1,5 @@ +nr;core_diameter;core_type;area_weight;comment +214700;100;cardboard;;Standard-Pappkern +000123;76;cardboard;355.5;Manueller Override des Flaechengewichts +229800;168;steel;;Grosser Stahlkern +777777;100;cardboard;;Konfliktfall fuer Tests diff --git a/examples/production-key-data.example.csv b/examples/production-key-data.example.csv new file mode 100644 index 0000000..290e5ba --- /dev/null +++ b/examples/production-key-data.example.csv @@ -0,0 +1,6 @@ +SL_COMPANY_NO,PHG,PG,SL_ITEM_NO,SL_ITEM_TEXT,SL_QUANTITY_UNIT2,SL_PART_CATEGORY_NO2,SL_PLANNING_FLAG2,qm,ROP_PRODUCT_WIDTH,ROP_RATE_OF_PRODUCTION,WPL_WORKPLACE_TEXT,SL_NUMBER_OF_PERSON_MASCHINE,SL_MINIMUM_PRODUCTION_QUANTITY,SL_PRODUCTION_SPEED,kg_qm,SL_PRODUCTION_REJECT_QUOTE,SL_SETUP_TIME_MASCHINE,SL_CALCULATED_LOT_SIZE,PRCO_CALCULATION_DATE,PRCO_CALCULATION_TYPE,PRCO_CALCULATION_NO,PRCO_PRODUCTION_COSTS,PRCO_COST_OF_MATERIAL_VARIABLE,PRCO_MATERIAL_OVERHEAD_COSTS,PRCO_EXTERNAL_COSTS,PRCO_MANUFACTURING_COSTS_FIXED_VARIABLE,PRCO_MANUFACTURING_OVERHEAD_COSTS,PRCO_PRODUCTION_COSTS_ADJUSTMENTS +001,Example PHG,Example PG,214700,"Example Mat A, 6,00 x 50 m",QM,B,J,"300,00","6,00","4,30",K 7,"3,0",50.000,1.548,"0,774","9,0%","0,60",13.600,17.07.2026,K,26,"1,69","1,36","0,03","0,00","0,29","0,02","0,00" +001,Example PHG,Example PG,000123,"Example Leading Zero, 1.20 x 10 m",QM,B,J,"12.50","1.20","2.50",K 1,"1,0",1.000,100,"0.350","5,0%","0.50",1.000,17.07.2026,K,27,"1.00","0.80","0.02","0.00","0.15","0.03","0.00" +001,Example PHG,Example PG,800001,"Example ERP Only",QM,B,J,"","2,00","",K 2,"",0,invalid,"","5,0%","",0,17.07.2026,K,28,"","","","","","","" +001,Example PHG,Example PG,777777,"Example Duplicate A",QM,B,J,"100,00","1,00","3,00",K 3,"2,0",500,300,"0,500","6,0%","1,00",2.000,17.07.2026,K,29,"1,10","0,90","0,02","0,00","0,16","0,02","0,00" +001,Example PHG,Example PG,777777,"Example Duplicate B Conflict",QM,B,J,"100,00","1,10","3,00",K 4,"2,0",500,300,"0,500","6,0%","1,00",2.000,17.07.2026,K,30,"1,10","0,90","0,02","0,00","0,16","0,02","0,00" diff --git a/examples/rollcalc-article-data.example.json b/examples/rollcalc-article-data.example.json new file mode 100644 index 0000000..9bd5017 --- /dev/null +++ b/examples/rollcalc-article-data.example.json @@ -0,0 +1,23 @@ +[ + { + "nr": "214700", + "name": "Example Mat A, 6,00 x 50 m", + "thickness": 6.722, + "area_weight": 0.0, + "core_type": 0.0 + }, + { + "nr": "000123", + "name": "Example Leading Zero, 1,20 x 10 m", + "thickness": 1.5, + "area_weight": 350.0, + "core_type": 0.0 + }, + { + "nr": "900001", + "name": "Example RollCalc Only", + "thickness": 2.0, + "area_weight": 0.0, + "core_type": 0.0 + } +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..7a1d183 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,37 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "article-data-manager" +version = "0.1.0" +description = "Tools for preparing Roll Calculator article data from ERP, existing RollCalc, and manual sources." +readme = "README.md" +requires-python = ">=3.12" +license = { text = "MIT" } +authors = [{ name = "Article Data Manager contributors" }] +dependencies = [] + +[project.optional-dependencies] +dev = [ + "pytest>=8", + "ruff>=0.5", +] + +[project.scripts] +article-data-manager = "article_data_manager.cli.main:main" + +[tool.hatch.build.targets.wheel] +packages = ["src/article_data_manager"] + +[tool.pytest.ini_options] +testpaths = ["tests"] +pythonpath = ["src"] + +[tool.ruff] +line-length = 100 +target-version = "py312" + +[tool.ruff.lint] +select = ["E", "F", "I", "UP", "B", "SIM"] +ignore = [] diff --git a/scripts/.gitkeep b/scripts/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/scripts/.gitkeep @@ -0,0 +1 @@ + diff --git a/src/article_data_manager/__init__.py b/src/article_data_manager/__init__.py new file mode 100644 index 0000000..c170976 --- /dev/null +++ b/src/article_data_manager/__init__.py @@ -0,0 +1,5 @@ +"""Article Data Manager package.""" + +__all__ = ["__version__"] + +__version__ = "0.1.0" diff --git a/src/article_data_manager/cli/__init__.py b/src/article_data_manager/cli/__init__.py new file mode 100644 index 0000000..014d530 --- /dev/null +++ b/src/article_data_manager/cli/__init__.py @@ -0,0 +1 @@ +"""Command line interface package.""" diff --git a/src/article_data_manager/cli/main.py b/src/article_data_manager/cli/main.py new file mode 100644 index 0000000..3be135d --- /dev/null +++ b/src/article_data_manager/cli/main.py @@ -0,0 +1,33 @@ +"""Minimal Phase 0 CLI entry point.""" + +from __future__ import annotations + +import argparse +from pathlib import Path + +from article_data_manager.importers.csv_inspection import inspect_csv + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(prog="article-data-manager") + subparsers = parser.add_subparsers(dest="command") + + inspect_parser = subparsers.add_parser("inspect-csv", help="Inspect CSV structure.") + inspect_parser.add_argument("path", type=Path) + + return parser + + +def main(argv: list[str] | None = None) -> int: + parser = build_parser() + args = parser.parse_args(argv) + + if args.command == "inspect-csv": + result = inspect_csv(args.path) + print(f"delimiter={result.delimiter!r}") + print(f"rows={result.row_count}") + print("columns=" + ",".join(result.columns)) + return 0 + + parser.print_help() + return 0 diff --git a/src/article_data_manager/exporters/__init__.py b/src/article_data_manager/exporters/__init__.py new file mode 100644 index 0000000..fa4e960 --- /dev/null +++ b/src/article_data_manager/exporters/__init__.py @@ -0,0 +1 @@ +"""Exporter placeholders.""" diff --git a/src/article_data_manager/importers/__init__.py b/src/article_data_manager/importers/__init__.py new file mode 100644 index 0000000..cb679e7 --- /dev/null +++ b/src/article_data_manager/importers/__init__.py @@ -0,0 +1 @@ +"""Importer helpers.""" diff --git a/src/article_data_manager/importers/csv_inspection.py b/src/article_data_manager/importers/csv_inspection.py new file mode 100644 index 0000000..60c0ba2 --- /dev/null +++ b/src/article_data_manager/importers/csv_inspection.py @@ -0,0 +1,29 @@ +"""Small CSV inspection helpers used in Phase 0 tests and documentation.""" + +from __future__ import annotations + +import csv +from dataclasses import dataclass +from pathlib import Path + + +@dataclass(frozen=True, slots=True) +class CsvInspection: + delimiter: str + columns: tuple[str, ...] + row_count: int + + +def inspect_csv(path: Path, *, delimiters: str = ",;\t") -> CsvInspection: + """Inspect delimiter, columns, and row count without interpreting business meaning.""" + + with path.open("r", encoding="utf-8-sig", newline="") as csv_file: + sample = csv_file.read(4096) + csv_file.seek(0) + dialect = csv.Sniffer().sniff(sample, delimiters=delimiters) + reader = csv.DictReader(csv_file, dialect=dialect) + return CsvInspection( + delimiter=dialect.delimiter, + columns=tuple(reader.fieldnames or ()), + row_count=sum(1 for _ in reader), + ) diff --git a/src/article_data_manager/merge/__init__.py b/src/article_data_manager/merge/__init__.py new file mode 100644 index 0000000..f9fcdb5 --- /dev/null +++ b/src/article_data_manager/merge/__init__.py @@ -0,0 +1 @@ +"""Merge planning package.""" diff --git a/src/article_data_manager/merge/rules.py b/src/article_data_manager/merge/rules.py new file mode 100644 index 0000000..e1eec3d --- /dev/null +++ b/src/article_data_manager/merge/rules.py @@ -0,0 +1,7 @@ +"""Declared Phase 0 merge constants. + +The complete merge implementation belongs to Phase 1. +""" + +MERGE_PRIORITY: tuple[str, ...] = ("erp", "rollcalc", "manual") +MANUAL_OVERRIDE_FIELDS: frozenset[str] = frozenset({"area_weight", "core_diameter", "core_type"}) diff --git a/src/article_data_manager/models/__init__.py b/src/article_data_manager/models/__init__.py new file mode 100644 index 0000000..f9d40c1 --- /dev/null +++ b/src/article_data_manager/models/__init__.py @@ -0,0 +1,5 @@ +"""Domain models.""" + +from article_data_manager.models.article import Article + +__all__ = ["Article"] diff --git a/src/article_data_manager/models/article.py b/src/article_data_manager/models/article.py new file mode 100644 index 0000000..6b47daa --- /dev/null +++ b/src/article_data_manager/models/article.py @@ -0,0 +1,26 @@ +"""Canonical article model for Phase 0 assumptions.""" + +from dataclasses import dataclass + + +@dataclass(frozen=True, slots=True) +class Article: + """Flat RollCalc-compatible article shape. + + Missing optional values are represented as None internally. Export compatibility + with RollCalc must be confirmed before changing existing production behavior. + """ + + nr: str + name: str | None = None + width: float | None = None + thickness: float | None = None + area_weight: float | None = None + core_diameter: int | None = None + core_type: str | None = None + + def __post_init__(self) -> None: + if not isinstance(self.nr, str): + raise TypeError("Article number 'nr' must be a string.") + if self.nr == "": + raise ValueError("Article number 'nr' must not be empty.") diff --git a/src/article_data_manager/validation/__init__.py b/src/article_data_manager/validation/__init__.py new file mode 100644 index 0000000..5c2ebf5 --- /dev/null +++ b/src/article_data_manager/validation/__init__.py @@ -0,0 +1,5 @@ +"""Validation helpers.""" + +from article_data_manager.validation.article_number import normalize_article_number + +__all__ = ["normalize_article_number"] diff --git a/src/article_data_manager/validation/article_number.py b/src/article_data_manager/validation/article_number.py new file mode 100644 index 0000000..279c417 --- /dev/null +++ b/src/article_data_manager/validation/article_number.py @@ -0,0 +1,12 @@ +"""Article number normalization.""" + + +def normalize_article_number(value: str) -> str: + """Trim surrounding whitespace while preserving leading zeros and text content.""" + + if not isinstance(value, str): + raise TypeError("Article number must be provided as a string.") + normalized = value.strip() + if normalized == "": + raise ValueError("Article number must not be empty.") + return normalized diff --git a/src/article_data_manager/validation/numbers.py b/src/article_data_manager/validation/numbers.py new file mode 100644 index 0000000..ca0989a --- /dev/null +++ b/src/article_data_manager/validation/numbers.py @@ -0,0 +1,16 @@ +"""Number parsing helpers for CSV values.""" + + +def parse_decimal(value: str) -> float | None: + """Parse simple German or English decimal strings. + + Empty strings map to None. Thousands separators are intentionally not inferred + in Phase 0 because ERP semantics must be confirmed first. + """ + + cleaned = value.strip() + if cleaned == "": + return None + if "," in cleaned and "." in cleaned: + raise ValueError(f"Ambiguous decimal value: {value!r}") + return float(cleaned.replace(",", ".")) diff --git a/tests/fixtures/article-manual-data.csv b/tests/fixtures/article-manual-data.csv new file mode 100644 index 0000000..7e9e443 --- /dev/null +++ b/tests/fixtures/article-manual-data.csv @@ -0,0 +1,5 @@ +nr;core_diameter;core_type;area_weight;comment +214700;100;cardboard;;Standard-Pappkern +000123;76;cardboard;355.5;Manueller Override des Flaechengewichts +229800;168;steel;;Grosser Stahlkern +777777;100;cardboard;;Konfliktfall fuer Tests diff --git a/tests/fixtures/production-key-data.csv b/tests/fixtures/production-key-data.csv new file mode 100644 index 0000000..290e5ba --- /dev/null +++ b/tests/fixtures/production-key-data.csv @@ -0,0 +1,6 @@ +SL_COMPANY_NO,PHG,PG,SL_ITEM_NO,SL_ITEM_TEXT,SL_QUANTITY_UNIT2,SL_PART_CATEGORY_NO2,SL_PLANNING_FLAG2,qm,ROP_PRODUCT_WIDTH,ROP_RATE_OF_PRODUCTION,WPL_WORKPLACE_TEXT,SL_NUMBER_OF_PERSON_MASCHINE,SL_MINIMUM_PRODUCTION_QUANTITY,SL_PRODUCTION_SPEED,kg_qm,SL_PRODUCTION_REJECT_QUOTE,SL_SETUP_TIME_MASCHINE,SL_CALCULATED_LOT_SIZE,PRCO_CALCULATION_DATE,PRCO_CALCULATION_TYPE,PRCO_CALCULATION_NO,PRCO_PRODUCTION_COSTS,PRCO_COST_OF_MATERIAL_VARIABLE,PRCO_MATERIAL_OVERHEAD_COSTS,PRCO_EXTERNAL_COSTS,PRCO_MANUFACTURING_COSTS_FIXED_VARIABLE,PRCO_MANUFACTURING_OVERHEAD_COSTS,PRCO_PRODUCTION_COSTS_ADJUSTMENTS +001,Example PHG,Example PG,214700,"Example Mat A, 6,00 x 50 m",QM,B,J,"300,00","6,00","4,30",K 7,"3,0",50.000,1.548,"0,774","9,0%","0,60",13.600,17.07.2026,K,26,"1,69","1,36","0,03","0,00","0,29","0,02","0,00" +001,Example PHG,Example PG,000123,"Example Leading Zero, 1.20 x 10 m",QM,B,J,"12.50","1.20","2.50",K 1,"1,0",1.000,100,"0.350","5,0%","0.50",1.000,17.07.2026,K,27,"1.00","0.80","0.02","0.00","0.15","0.03","0.00" +001,Example PHG,Example PG,800001,"Example ERP Only",QM,B,J,"","2,00","",K 2,"",0,invalid,"","5,0%","",0,17.07.2026,K,28,"","","","","","","" +001,Example PHG,Example PG,777777,"Example Duplicate A",QM,B,J,"100,00","1,00","3,00",K 3,"2,0",500,300,"0,500","6,0%","1,00",2.000,17.07.2026,K,29,"1,10","0,90","0,02","0,00","0,16","0,02","0,00" +001,Example PHG,Example PG,777777,"Example Duplicate B Conflict",QM,B,J,"100,00","1,10","3,00",K 4,"2,0",500,300,"0,500","6,0%","1,00",2.000,17.07.2026,K,30,"1,10","0,90","0,02","0,00","0,16","0,02","0,00" diff --git a/tests/fixtures/rollcalc-article-data.json b/tests/fixtures/rollcalc-article-data.json new file mode 100644 index 0000000..9bd5017 --- /dev/null +++ b/tests/fixtures/rollcalc-article-data.json @@ -0,0 +1,23 @@ +[ + { + "nr": "214700", + "name": "Example Mat A, 6,00 x 50 m", + "thickness": 6.722, + "area_weight": 0.0, + "core_type": 0.0 + }, + { + "nr": "000123", + "name": "Example Leading Zero, 1,20 x 10 m", + "thickness": 1.5, + "area_weight": 350.0, + "core_type": 0.0 + }, + { + "nr": "900001", + "name": "Example RollCalc Only", + "thickness": 2.0, + "area_weight": 0.0, + "core_type": 0.0 + } +] diff --git a/tests/integration/test_project_foundation.py b/tests/integration/test_project_foundation.py new file mode 100644 index 0000000..e63aa5b --- /dev/null +++ b/tests/integration/test_project_foundation.py @@ -0,0 +1,57 @@ +import csv +import json +from collections import Counter +from pathlib import Path + +from article_data_manager.merge.rules import MANUAL_OVERRIDE_FIELDS, MERGE_PRIORITY + + +def test_expected_documentation_files_exist() -> None: + expected = [ + "README.md", + "AGENTS.md", + "PROJECT_KNOWLEDGE.md", + "docs/architecture.md", + "docs/data-model.md", + "docs/data-dictionary.md", + "docs/merge-rules.md", + "docs/security-and-data-protection.md", + "docs/roadmap.md", + "docs/adr/0001-separate-repository.md", + "docs/adr/0002-source-data-separation.md", + "docs/adr/0003-article-number-as-string.md", + "docs/adr/0004-generated-output-not-primary-data.md", + ] + + for file_name in expected: + assert Path(file_name).is_file() + + +def test_fixtures_cover_phase_zero_cases() -> None: + rollcalc = json.loads(Path("tests/fixtures/rollcalc-article-data.json").read_text()) + with Path("tests/fixtures/production-key-data.csv").open(newline="") as csv_file: + erp_rows = list(csv.DictReader(csv_file)) + with Path("tests/fixtures/article-manual-data.csv").open(newline="") as csv_file: + manual_rows = list(csv.DictReader(csv_file, delimiter=";")) + + rollcalc_numbers = {row["nr"] for row in rollcalc} + erp_numbers = [row["SL_ITEM_NO"] for row in erp_rows] + manual_numbers = {row["nr"] for row in manual_rows} + + assert "214700" in rollcalc_numbers and "214700" in erp_numbers + assert "900001" in rollcalc_numbers and "900001" not in erp_numbers + assert "800001" in erp_numbers and "800001" not in rollcalc_numbers + assert "000123" in rollcalc_numbers and "000123" in erp_numbers + assert Counter(erp_numbers)["777777"] == 2 + assert "214700" in manual_numbers + assert "000123" in manual_numbers + assert any(row["SL_PRODUCTION_SPEED"] == "invalid" for row in erp_rows) + assert any(row["ROP_PRODUCT_WIDTH"] == "6,00" for row in erp_rows) + assert any(row["ROP_PRODUCT_WIDTH"] == "1.20" for row in erp_rows) + assert any(row["qm"] == "" for row in erp_rows) + assert len({row["ROP_PRODUCT_WIDTH"] for row in erp_rows if row["SL_ITEM_NO"] == "777777"}) == 2 + + +def test_declared_merge_policy_is_phase_zero_contract() -> None: + assert MERGE_PRIORITY == ("erp", "rollcalc", "manual") + assert frozenset({"area_weight", "core_diameter", "core_type"}) == MANUAL_OVERRIDE_FIELDS diff --git a/tests/unit/test_article_model.py b/tests/unit/test_article_model.py new file mode 100644 index 0000000..0234475 --- /dev/null +++ b/tests/unit/test_article_model.py @@ -0,0 +1,14 @@ +import pytest + +from article_data_manager.models import Article + + +def test_article_number_is_string_and_preserves_leading_zero() -> None: + article = Article(nr="000123", name="Example") + + assert article.nr == "000123" + + +def test_article_number_rejects_numeric_value() -> None: + with pytest.raises(TypeError): + Article(nr=123) # type: ignore[arg-type] diff --git a/tests/unit/test_article_number.py b/tests/unit/test_article_number.py new file mode 100644 index 0000000..1a374eb --- /dev/null +++ b/tests/unit/test_article_number.py @@ -0,0 +1,12 @@ +import pytest + +from article_data_manager.validation import normalize_article_number + + +def test_normalize_article_number_trims_but_preserves_leading_zero() -> None: + assert normalize_article_number(" 000123 ") == "000123" + + +def test_normalize_article_number_rejects_non_string() -> None: + with pytest.raises(TypeError): + normalize_article_number(123) # type: ignore[arg-type] diff --git a/tests/unit/test_csv_inspection.py b/tests/unit/test_csv_inspection.py new file mode 100644 index 0000000..44c19e7 --- /dev/null +++ b/tests/unit/test_csv_inspection.py @@ -0,0 +1,12 @@ +from pathlib import Path + +from article_data_manager.importers.csv_inspection import inspect_csv + + +def test_inspect_csv_detects_fixture_shape() -> None: + result = inspect_csv(Path("tests/fixtures/production-key-data.csv")) + + assert result.delimiter == "," + assert result.row_count == 5 + assert result.columns[:4] == ("SL_COMPANY_NO", "PHG", "PG", "SL_ITEM_NO") + assert "ROP_PRODUCT_WIDTH" in result.columns diff --git a/tests/unit/test_numbers.py b/tests/unit/test_numbers.py new file mode 100644 index 0000000..bda45b1 --- /dev/null +++ b/tests/unit/test_numbers.py @@ -0,0 +1,20 @@ +import pytest + +from article_data_manager.validation.numbers import parse_decimal + + +def test_parse_decimal_accepts_comma_decimal() -> None: + assert parse_decimal("6,00") == 6.0 + + +def test_parse_decimal_accepts_point_decimal() -> None: + assert parse_decimal("1.20") == 1.2 + + +def test_parse_decimal_maps_empty_value_to_none() -> None: + assert parse_decimal("") is None + + +def test_parse_decimal_rejects_ambiguous_number() -> None: + with pytest.raises(ValueError): + parse_decimal("1.234,56")