Initialize Article Data Manager project
This commit is contained in:
@@ -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
|
||||
+29
@@ -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
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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/`
|
||||
@@ -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.
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,3 @@
|
||||
# Architecture Decision Records
|
||||
|
||||
ADRs dokumentieren zentrale Architekturentscheidungen. Neue ADRs werden fortlaufend nummeriert und sollen Entscheidung, Kontext und Konsequenzen enthalten.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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?
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
@@ -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"
|
||||
|
@@ -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
|
||||
}
|
||||
]
|
||||
@@ -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 = []
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
"""Article Data Manager package."""
|
||||
|
||||
__all__ = ["__version__"]
|
||||
|
||||
__version__ = "0.1.0"
|
||||
@@ -0,0 +1 @@
|
||||
"""Command line interface package."""
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
"""Exporter placeholders."""
|
||||
@@ -0,0 +1 @@
|
||||
"""Importer helpers."""
|
||||
@@ -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),
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
"""Merge planning package."""
|
||||
@@ -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"})
|
||||
@@ -0,0 +1,5 @@
|
||||
"""Domain models."""
|
||||
|
||||
from article_data_manager.models.article import Article
|
||||
|
||||
__all__ = ["Article"]
|
||||
@@ -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.")
|
||||
@@ -0,0 +1,5 @@
|
||||
"""Validation helpers."""
|
||||
|
||||
from article_data_manager.validation.article_number import normalize_article_number
|
||||
|
||||
__all__ = ["normalize_article_number"]
|
||||
@@ -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
|
||||
@@ -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(",", "."))
|
||||
+5
@@ -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
|
||||
|
+6
@@ -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"
|
||||
|
+23
@@ -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
|
||||
}
|
||||
]
|
||||
@@ -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
|
||||
@@ -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]
|
||||
@@ -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]
|
||||
@@ -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
|
||||
@@ -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")
|
||||
Reference in New Issue
Block a user