neues skript sql_dump_diff.py zum Vergleich von 2 sql-dumps.

- leere, frisch initialisierte Datenbanken der Versionen verglichen
- datasetprogramme erkennen jetzt auch suche nach standardtabellen wie uom
This commit is contained in:
2026-08-22 17:45:46 +02:00
parent c0dd24b6c5
commit a55774eff2
19 changed files with 78583 additions and 269 deletions
+33 -4
View File
@@ -124,7 +124,7 @@ def load_spec(path):
return json.loads(text) return json.loads(text)
def resolve_value(value, created): def resolve_value(value, created, search_cache):
""" """
Löst Referenzen der Form {"$ref": "irgendein_key"} auf bereits in Löst Referenzen der Form {"$ref": "irgendein_key"} auf bereits in
diesem Lauf erzeugte Datensätze auf (z. B. um eine Adresse mit der diesem Lauf erzeugte Datensätze auf (z. B. um eine Adresse mit der
@@ -135,6 +135,21 @@ def resolve_value(value, created):
Werte, die kein einfacher JSON-Typ sind: Werte, die kein einfacher JSON-Typ sind:
{"$decimal": "12.34"} -> decimal.Decimal("12.34") {"$decimal": "12.34"} -> decimal.Decimal("12.34")
{"$date": "2024-01-15"} -> datetime.date(2024, 1, 15) {"$date": "2024-01-15"} -> datetime.date(2024, 1, 15)
{"$search": {"model": "product.uom",
"domain": [["name", "=", "Unit"]]}}
-> sucht den Record live auf dem
Zielserver (Model.find(domain)) -
für Stammdaten, die nicht Teil des
Testdatensatzes sind, sondern beim
Modul-Aktivieren per XML-Fixture
angelegt werden (Länder, Währungen,
Einheiten, Standard-Lagerorte, ...).
Wirft einen Fehler, wenn nichts
gefunden wird, statt einen neuen
Record anzulegen - ein fehlender
Treffer bedeutet meist, dass sich
die Fixture-Daten zwischen den
Versionen geändert haben.
{"$manual": "<ausdruck>"} -> Fehler mit Klartext, da diese {"$manual": "<ausdruck>"} -> Fehler mit Klartext, da diese
Stelle noch von Hand ausgefüllt Stelle noch von Hand ausgefüllt
werden muss. werden muss.
@@ -147,6 +162,19 @@ def resolve_value(value, created):
f"fehlgeschlagen." f"fehlgeschlagen."
) )
return created[value["$ref"]] return created[value["$ref"]]
if isinstance(value, dict) and "$search" in value:
spec = value["$search"]
cache_key = (spec["model"], json.dumps(spec["domain"], sort_keys=True))
if cache_key not in search_cache:
matches = Model.get(spec["model"]).find(spec["domain"])
if not matches:
raise ValueError(
f"$search fand keinen Treffer: model={spec['model']!r} "
f"domain={spec['domain']!r} - Stammdaten auf diesem "
f"Server geändert/entfernt?"
)
search_cache[cache_key] = matches[0].id
return search_cache[cache_key]
if isinstance(value, dict) and "$decimal" in value: if isinstance(value, dict) and "$decimal" in value:
return decimal.Decimal(value["$decimal"]) return decimal.Decimal(value["$decimal"])
if isinstance(value, dict) and "$date" in value: if isinstance(value, dict) and "$date" in value:
@@ -158,9 +186,9 @@ def resolve_value(value, created):
f"dieser Datensatz angelegt wird." f"dieser Datensatz angelegt wird."
) )
if isinstance(value, list): if isinstance(value, list):
return [resolve_value(v, created) for v in value] return [resolve_value(v, created, search_cache) for v in value]
if isinstance(value, dict): if isinstance(value, dict):
return {k: resolve_value(v, created) for k, v in value.items()} return {k: resolve_value(v, created, search_cache) for k, v in value.items()}
return value return value
@@ -187,6 +215,7 @@ def serialize(value):
def create_records(spec, created, results): def create_records(spec, created, results):
fields_cache = {} # model_name -> Set der auf DIESEM Server bekannten Felder fields_cache = {} # model_name -> Set der auf DIESEM Server bekannten Felder
search_cache = {} # (model, domain-json) -> id, damit dieselbe Stammdaten-Suche nicht mehrfach läuft
for entry in spec["records"]: for entry in spec["records"]:
key = entry["key"] key = entry["key"]
@@ -212,7 +241,7 @@ def create_records(spec, created, results):
for field_name, raw_value in fields.items(): for field_name, raw_value in fields.items():
actual_name = resolve_field_name(RecordModel, model_name, field_name, fields_cache) actual_name = resolve_field_name(RecordModel, model_name, field_name, fields_cache)
actual_names[field_name] = actual_name actual_names[field_name] = actual_name
setattr(record, actual_name, resolve_value(raw_value, created)) setattr(record, actual_name, resolve_value(raw_value, created, search_cache))
record.save() record.save()
created[key] = record.id created[key] = record.id
+35005
View File
File diff suppressed because it is too large Load Diff
+38412
View File
File diff suppressed because it is too large Load Diff
+60
View File
@@ -25,6 +25,27 @@ Pro Modell wird zusätzlich `persisted` erkannt (erbt die Klasse `ModelSQL`
o. ä., oder nur `ModelView` - dann ist es ein reines Wizard-/ o. ä., oder nur `ModelView` - dann ist es ein reines Wizard-/
Transient-Formular ohne eigene Tabelle). Transient-Formular ohne eigene Tabelle).
Verfolgt zusätzlich ganz normale **Python-Vererbung innerhalb derselben
Datei**: erbt eine Klasse mit eigenem `__name__` von einer anderen lokalen
Klasse - z. B. `class BOMOutput(BOMInput): __name__ = 'production.bom.output'`
in `production/bom.py`, die selbst keine einzige `fields.X(...)`-Zeile
deklariert, sondern alle Felder von `BOMInput` übernimmt - werden die
geerbten Felder mit aufgenommen (eigene Felder überschreiben geerbte bei
Namensgleichheit). Ohne das sähe ein rein über Vererbung wiederverwendetes
Modell wie eines OHNE Felder aus, und Änderungen daran (z. B. eine
Feldumbenennung im Elternteil) blieben komplett unsichtbar - genau das hat
`sql_dump_diff.py` (Schritt 4) einmal live aufgedeckt.
Die Basisklassen-Auflösung ist bewusst auf "gleiche Datei" beschränkt
(nicht "gleiches Modul"): derselbe lokale Klassenname wie `Cron` taucht
innerhalb eines Moduls oft mehrfach für völlig unterschiedliche Klassen
auf (Tryton-Konvention: eine PoolMeta-Erweiterung heißt oft genauso wie
das Original, z. B. `currency/currency.py` definiert `class Cron(ModelSQL,
ModelView): __name__ = 'currency.cron'`, während `currency/ir.py`
unabhängig davon eine andere Klasse `class Cron(metaclass=PoolMeta):` zur
Erweiterung von `ir.cron` definiert) - ein modulweites Registry würde sich
da gegenseitig überschreiben.
```bash ```bash
python3 extract_model_tree.py --repo /mnt/tryton \ python3 extract_model_tree.py --repo /mnt/tryton \
--modules company country currency party product ... \ --modules company country currency party product ... \
@@ -81,11 +102,50 @@ python3 model_coverage.py --model-tree reports/model_tree_5.0.json \
--report reports/coverage_5.0.md --report reports/coverage_5.0.md
``` ```
### 4. `sql_dump_diff.py` – gegen den echten DB-Zustand validieren
Alles bisherige ist reine Code-Analyse. Zum Cross-Check: zwei mit
`pg_dump` (Klartext-Format) erzeugte Dumps frisch initialisierter (leerer)
Datenbanken beider Versionen strukturell vergleichen - Tabellen
entfernt/neu, Spalten hinzugefügt/entfernt/Typ geändert, Zeilenzahl
(z. B. mehr XML-Fixture-Daten in der neueren Version). Zeilen werden
bereits vollständig geparst (für eine spätere inhaltliche
Vergleichs-Erweiterung, sobald es Snapshots mit echten Testdaten gibt),
aktuell nutzt der Report davon aber nur die Zeilenzahl.
Mit `--model-diff reports/model_diff.json` wird pro betroffener Tabelle
zusätzlich geprüft, ob die rein statische Code-Analyse (Schritt 2)
dieselbe Änderung schon vorhergesagt hatte (Modellname -> Tabellenname:
Punkte durch Unterstriche ersetzen, wie `extract_updates.py`/
`compare_snapshots.py` es auch tun) - Übereinstimmung bzw. Abweichung wird
markiert.
```bash
python3 sql_dump_diff.py --dump-a data/leer5.sql --label-a 5.0 \
--dump-b data/leer8.sql --label-b 8.0 \
--model-diff reports/model_diff.json \
--output reports/dump_diff_5.0_8.0.json --report reports/dump_diff_5.0_8.0.md
```
**Stand:** 78 von 92 vergleichbaren Fällen stimmen überein (~85%). Die
verbleibenden Abweichungen gehen meist in eine erwartbare Richtung: die
Code-Analyse findet MEHR Veränderung, als im leeren DB-Schema sichtbar
ist (z. B. ein entferntes Function-Feld ohne eigene Spalte, oder
Selection-Wertänderungen ohne Typänderung) - kein Fehler, sondern zwei
Werkzeuge, die unterschiedliche Ebenen messen (Python-Feld-Definition vs.
physisches SQL-Schema). Der `production.bom.output`-Fund oben (Schritt 1)
kam genau aus einer vorherigen Abweichung dieser Art zustande.
## Bekannte Grenzen ## Bekannte Grenzen
- Rein statische Analyse: Felder, die nicht als einfaches - Rein statische Analyse: Felder, die nicht als einfaches
`name = fields.X(...)` auf Klassenebene stehen (z. B. per Schleife/Helper `name = fields.X(...)` auf Klassenebene stehen (z. B. per Schleife/Helper
dynamisch erzeugt), werden nicht erkannt. dynamisch erzeugt), werden nicht erkannt.
- Python-Vererbung wird nur INNERHALB derselben Datei aufgelöst (s. o.).
Erbt eine Klasse von einer Basisklasse aus einer anderen Datei desselben
oder eines anderen Moduls (Import statt lokaler Definition), werden
deren Felder nicht gefunden - unauffällig, aber ein bekannter blinder
Fleck.
- `persisted`-Erkennung ist eine Textsuche über die Basisklassen-Ausdrücke - `persisted`-Erkennung ist eine Textsuche über die Basisklassen-Ausdrücke
(`ModelSQL`/`Workflow`/`DeactivableMixin`) - deckt den Normalfall ab, ist (`ModelSQL`/`Workflow`/`DeactivableMixin`) - deckt den Normalfall ab, ist
aber kein echtes Type-Checking. aber kein echtes Type-Checking.
+31 -4
View File
@@ -42,10 +42,27 @@ erkennt:
→ wird zu einem YAML-Record mit `key`, `model`, `fields`. Referenzen auf → wird zu einem YAML-Record mit `key`, `model`, `fields`. Referenzen auf
zuvor im selben File angelegte Records werden zu `{"$ref": "..."}`, zuvor im selben File angelegte Records werden zu `{"$ref": "..."}`,
`Decimal(...)`/`date(...)` zu `{"$decimal": ...}`/`{"$date": ...}`. Alles `Decimal(...)`/`date(...)` zu `{"$decimal": ...}`/`{"$date": ...}`.
nicht automatisch Auflösbare (Wizards, `.click(...)`, Helper-Funktionsaufrufe)
wird als `{"$manual": "<Ausdruck>"}` markiert oder ganz übersprungen - Ein weiteres, sehr häufiges Muster wird ebenfalls erkannt: Zugriff auf
bewusst sichtbar unvollständig statt still falsch. bereits vorhandene Stammdaten (Länder, Währungen, Einheiten,
Standard-Lagerorte, ... - i. d. R. per XML-Fixture beim Modul-Aktivieren
angelegt, nicht Teil des eigentlichen Testdatensatzes):
```
>>> ProductUom = Model.get('product.uom')
>>> unit, = ProductUom.find([('name', '=', 'Unit')])
```
→ wird zu `{"$search": {"model": "product.uom", "domain": [["name", "=", "Unit"]]}}`
- aufgelöst wird das erst live beim Anlegen durch `create_dataset.py`
(Schritt 3), nicht schon hier, da diese Records ja nicht selbst Teil der
YAML sind. Nur literale Domains werden erkannt (keine Variablen darin);
sonst bleibt es wie bisher `$manual`.
Alles sonst nicht automatisch Auflösbare (Wizards, `.click(...)`,
Helper-Funktionsaufrufe) wird als `{"$manual": "<Ausdruck>"}` markiert oder
ganz übersprungen - bewusst sichtbar unvollständig statt still falsch.
Record-Keys werden mit dem Dateinamen präfixiert, damit beim Record-Keys werden mit dem Dateinamen präfixiert, damit beim
Zusammenführen mehrerer Module keine Kollisionen entstehen. Zusammenführen mehrerer Module keine Kollisionen entstehen.
@@ -83,6 +100,16 @@ bleibt trotzdem ein Fehler - das ist ja gerade das gesuchte Signal für eine
echte Schema-Änderung. Weitere Umbenennungen bei Bedarf in `FIELD_RENAMES` echte Schema-Änderung. Weitere Umbenennungen bei Bedarf in `FIELD_RENAMES`
ergänzen, sobald ein Lauf sie aufdeckt. ergänzen, sobald ein Lauf sie aufdeckt.
**`$search`-Auflösung:** Werte der Form `{"$search": {"model": ...,
"domain": ...}}` (von `rst_to_dataset.py` erzeugt, siehe Schritt 1) werden
live per `Model.get(model).find(domain)` gegen den Zielserver aufgelöst -
für Stammdaten, die nicht selbst angelegt werden, sondern schon per
XML-Fixture da sind. Ergebnis wird pro Lauf gecacht (dieselbe Suche, z. B.
nach der Einheit "Unit", kommt in vielen Records vor). Kein Treffer ->
Fehler mit Klartext statt eines neu angelegten Records - das ist selbst
ein Signal, dass sich die Stammdaten zwischen den Versionen geändert
haben.
### 4. `compare_snapshots.py` – zwei Versionen gegenüberstellen ### 4. `compare_snapshots.py` – zwei Versionen gegenüberstellen
Vergleicht zwei Snapshots (gleiche YAML, zwei Versionen/Datenbanken) und Vergleicht zwei Snapshots (gleiche YAML, zwei Versionen/Datenbanken) und
+132 -31
View File
@@ -13,6 +13,18 @@ z. B. stock_split eine Klasse mit __name__ = 'stock.move' hinzu, landen
deren Felder unter demselben Modell-Eintrag, mit Vermerk, aus welchem deren Felder unter demselben Modell-Eintrag, mit Vermerk, aus welchem
Modul sie stammen. Modul sie stammen.
Zusätzlich wird ganz normale Python-Vererbung innerhalb eines Moduls
verfolgt: erbt eine Klasse mit eigenem __name__ von einer anderen lokalen
Klasse (z. B. `class BOMOutput(BOMInput): __name__ = 'production.bom.output'`
in production/bom.py, die selbst keine einzige fields.X(...)-Zeile
deklariert, sondern alle Felder von BOMInput übernimmt), werden die
geerbten Felder mit aufgenommen. Ohne das sähe ein rein über Vererbung
wiederverwendetes Modell wie ein Modell OHNE Felder aus - und Änderungen
daran (z. B. eine Feldumbenennung im Elternteil) blieben unsichtbar.
Löst nur Basisklassen auf, die im selben Modul per einfachem Namen (kein
Attribut-Zugriff auf ein anderes Modul) gefunden werden - externe Basen
wie ModelSQL/ModelView/Workflow tragen ohnehin keine fields.X(...) bei.
Zweck hier: BEVOR versucht wird, dieselbe dataset.yaml gegen 5.0 und 8.0 Zweck hier: BEVOR versucht wird, dieselbe dataset.yaml gegen 5.0 und 8.0
laufen zu lassen, erstmal Schwarz auf Weiß sehen, welche Modelle/Felder in laufen zu lassen, erstmal Schwarz auf Weiß sehen, welche Modelle/Felder in
der 5.0-Codebasis (bei den tatsächlich aktiven Modulen) überhaupt der 5.0-Codebasis (bei den tatsächlich aktiven Modulen) überhaupt
@@ -132,44 +144,117 @@ def is_persisted_class(node):
return False return False
def scan_file(path, module_name, module_dir, models, persisted): def own_fields_of(node, module_name, module_dir, path):
"""Nur die in DIESEM Klassenkörper direkt deklarierten fields.X(...)."""
fields = {}
for stmt in node.body:
if not (
isinstance(stmt, ast.Assign)
and len(stmt.targets) == 1
and isinstance(stmt.targets[0], ast.Name)
):
continue
info = fields_call_info(stmt.value)
if info is None:
continue
field_type, relation, required, is_function, has_setter = info
fields[stmt.targets[0].id] = {
"type": field_type,
"relation": relation,
"required": required,
"settable": (not is_function) or has_setter,
"module": module_name,
"file": f"{module_name}/{path.relative_to(module_dir)}",
}
return fields
def base_class_names(node):
"""Best-effort Namen der Basisklassen als String, nur einfache
`class X(Y):`-Fälle (kein Attribut-Zugriff wie `other.Y`, das wäre
ohnehin eine externe/andere Modul-Klasse, die wir hier nicht kennen)."""
return [base.id for base in node.bases if isinstance(base, ast.Name)]
def collect_classes(path, module_name, module_dir, registry):
"""Erste Durchsicht: registriert jede Klasse (lokaler Python-Name,
Basisklassen, eigene Felder, ggf. Tryton-__name__, ggf. direkt
persistent) - unabhängig davon, ob/wie sie später gebraucht wird.
Die eigentliche Feld-/Persisted-Auflösung inkl. Vererbung passiert
danach in resolve_class_fields()/resolve_class_persisted().
Registry-Key ist (module_name, Dateipfad), NICHT nur module_name:
derselbe lokale Klassenname (z. B. "Cron") kommt innerhalb eines
Moduls in mehreren Dateien für kompott unterschiedliche Klassen vor
(Tryton-Konvention: eine PoolMeta-Erweiterung heißt oft genauso wie
das Original) - ein modulweites Registry würde sich da gegenseitig
überschreiben. Basisklassen-Auflösung bleibt dadurch bewusst auf
"gleiche Datei" beschränkt, was dem tatsächlichen Python-Scoping in
der weit überwiegenden Mehrheit der Fälle entspricht."""
try: try:
tree = ast.parse(path.read_text(encoding="utf-8")) tree = ast.parse(path.read_text(encoding="utf-8"))
except (SyntaxError, UnicodeDecodeError) as e: except (SyntaxError, UnicodeDecodeError) as e:
print(f" WARNUNG: {path} übersprungen ({e})", file=sys.stderr) print(f" WARNUNG: {path} übersprungen ({e})", file=sys.stderr)
return return
file_key = (module_name, str(path))
for node in ast.walk(tree): for node in ast.walk(tree):
if not isinstance(node, ast.ClassDef): if not isinstance(node, ast.ClassDef):
continue continue
model_name = find_name_assignment(node.body) registry.setdefault(file_key, {})[node.name] = {
if not model_name: "bases": base_class_names(node),
continue "own_fields": own_fields_of(node, module_name, module_dir, path),
"model_name": find_name_assignment(node.body),
"persisted_here": is_persisted_class(node),
}
if is_persisted_class(node):
persisted.add(model_name)
model = models.setdefault(model_name, {}) def resolve_class_fields(scope_key, class_name, registry, cache, visiting):
for stmt in node.body: """Felder einer lokalen Klasse inkl. per Basisklasse(n) in derselben
if not ( Datei (scope_key = (module_name, Dateipfad)) geerbter Felder (eigene
isinstance(stmt, ast.Assign) Felder überschreiben geerbte bei Namensgleichheit, wie bei echter
and len(stmt.targets) == 1 Python-Attributauflösung)."""
and isinstance(stmt.targets[0], ast.Name) key = (scope_key, class_name)
): if key in cache:
continue return cache[key]
field_name = stmt.targets[0].id if key in visiting:
info = fields_call_info(stmt.value) return {} # zyklische Vererbung - sollte nicht vorkommen, Notbremse
if info is None: class_info = registry.get(scope_key, {}).get(class_name)
continue if class_info is None:
field_type, relation, required, is_function, has_setter = info return {} # externe/unbekannte Basisklasse (ModelSQL, Workflow, ...)
model[field_name] = {
"type": field_type, visiting.add(key)
"relation": relation, fields = {}
"required": required, for base_name in class_info["bases"]:
"settable": (not is_function) or has_setter, fields.update(resolve_class_fields(scope_key, base_name, registry, cache, visiting))
"module": module_name, fields.update(class_info["own_fields"])
"file": f"{module_name}/{path.relative_to(module_dir)}", visiting.discard(key)
}
cache[key] = fields
return fields
def resolve_class_persisted(scope_key, class_name, registry, cache, visiting):
"""True, wenn diese Klasse selbst oder eine ihrer lokalen Basisklassen
ModelSQL/Workflow/DeactivableMixin als Basis hat."""
key = (scope_key, class_name)
if key in cache:
return cache[key]
if key in visiting:
return False
class_info = registry.get(scope_key, {}).get(class_name)
if class_info is None:
return False
visiting.add(key)
result = class_info["persisted_here"] or any(
resolve_class_persisted(scope_key, base_name, registry, cache, visiting)
for base_name in class_info["bases"]
)
visiting.discard(key)
cache[key] = result
return result
def module_directory(repo, module_name): def module_directory(repo, module_name):
@@ -181,8 +266,8 @@ def module_directory(repo, module_name):
def scan_modules(repo, modules): def scan_modules(repo, modules):
models = {} # Phase 1: alle Klassen roh einsammeln (Basen, eigene Felder, __name__).
persisted = set() registry = {}
for module_name in modules: for module_name in modules:
module_dir = module_directory(repo, module_name) module_dir = module_directory(repo, module_name)
if not module_dir.is_dir(): if not module_dir.is_dir():
@@ -191,10 +276,26 @@ def scan_modules(repo, modules):
for path in sorted(module_dir.rglob("*.py")): for path in sorted(module_dir.rglob("*.py")):
if "tests" in path.relative_to(module_dir).parts: if "tests" in path.relative_to(module_dir).parts:
continue continue
scan_file(path, module_name, module_dir, models, persisted) collect_classes(path, module_name, module_dir, registry)
# Phase 2: pro Klasse mit __name__ die Felder (inkl. lokaler Python-
# Vererbung) auflösen und unter dem Tryton-Modellnamen zusammenführen -
# mehrere Klassen mit demselben __name__ (PoolMeta-Erweiterungen aus
# anderen Modulen) tragen weiterhin gemeinsam zum selben Eintrag bei.
fields_cache, persisted_cache = {}, {}
models = {}
persisted_models = set()
for scope_key, classes in registry.items():
for class_name, info in classes.items():
if not info["model_name"]:
continue
resolved = resolve_class_fields(scope_key, class_name, registry, fields_cache, set())
models.setdefault(info["model_name"], {}).update(resolved)
if resolve_class_persisted(scope_key, class_name, registry, persisted_cache, set()):
persisted_models.add(info["model_name"])
return { return {
model_name: {"persisted": model_name in persisted, "fields": fields} model_name: {"persisted": model_name in persisted_models, "fields": fields}
for model_name, fields in models.items() for model_name, fields in models.items()
} }
+270 -27
View File
@@ -77,7 +77,16 @@
"fields": { "fields": {
"name": "product", "name": "product",
"default_uom": { "default_uom": {
"$manual": "unit" "$search": {
"model": "product.uom",
"domain": [
[
"name",
"=",
"Unit"
]
]
}
}, },
"type": "goods", "type": "goods",
"producible": true, "producible": true,
@@ -92,7 +101,16 @@
"fields": { "fields": {
"name": "component 1", "name": "component 1",
"default_uom": { "default_uom": {
"$manual": "unit" "$search": {
"model": "product.uom",
"domain": [
[
"name",
"=",
"Unit"
]
]
}
}, },
"type": "goods", "type": "goods",
"list_price": { "list_price": {
@@ -106,7 +124,16 @@
"fields": { "fields": {
"name": "component 2", "name": "component 2",
"default_uom": { "default_uom": {
"$manual": "meter" "$search": {
"model": "product.uom",
"domain": [
[
"name",
"=",
"Meter"
]
]
}
}, },
"type": "goods", "type": "goods",
"list_price": { "list_price": {
@@ -120,7 +147,16 @@
"fields": { "fields": {
"name": "Product", "name": "Product",
"default_uom": { "default_uom": {
"$manual": "unit" "$search": {
"model": "product.uom",
"domain": [
[
"name",
"=",
"Unit"
]
]
}
}, },
"type": "goods", "type": "goods",
"list_price": { "list_price": {
@@ -135,7 +171,16 @@
"fields": { "fields": {
"name": "Product", "name": "Product",
"default_uom": { "default_uom": {
"$manual": "kg" "$search": {
"model": "product.uom",
"domain": [
[
"name",
"=",
"Kilogram"
]
]
}
}, },
"type": "goods", "type": "goods",
"list_price": { "list_price": {
@@ -150,7 +195,16 @@
"fields": { "fields": {
"name": "Product", "name": "Product",
"default_uom": { "default_uom": {
"$manual": "unit" "$search": {
"model": "product.uom",
"domain": [
[
"name",
"=",
"Unit"
]
]
}
}, },
"type": "goods", "type": "goods",
"list_price": { "list_price": {
@@ -229,7 +283,16 @@
"model": "stock.inventory", "model": "stock.inventory",
"fields": { "fields": {
"location": { "location": {
"$manual": "storage_loc" "$search": {
"model": "stock.location",
"domain": [
[
"code",
"=",
"STO"
]
]
}
}, },
"empty_quantity": "keep" "empty_quantity": "keep"
} }
@@ -239,7 +302,16 @@
"model": "stock.inventory", "model": "stock.inventory",
"fields": { "fields": {
"location": { "location": {
"$manual": "storage_loc" "$search": {
"model": "stock.location",
"domain": [
[
"code",
"=",
"STO"
]
]
}
}, },
"empty_quantity": "empty" "empty_quantity": "empty"
} }
@@ -251,7 +323,16 @@
"name": "Storage Sub", "name": "Storage Sub",
"type": "storage", "type": "storage",
"parent": { "parent": {
"$manual": "storage_loc" "$search": {
"model": "stock.location",
"domain": [
[
"code",
"=",
"STO"
]
]
}
} }
} }
}, },
@@ -261,7 +342,16 @@
"fields": { "fields": {
"name": "Child Location", "name": "Child Location",
"parent": { "parent": {
"$manual": "storage_loc" "$search": {
"model": "stock.location",
"domain": [
[
"code",
"=",
"STO"
]
]
}
}, },
"code": "CHI" "code": "CHI"
} }
@@ -272,7 +362,16 @@
"fields": { "fields": {
"name": "Storage 1", "name": "Storage 1",
"parent": { "parent": {
"$manual": "storage_loc" "$search": {
"model": "stock.location",
"domain": [
[
"code",
"=",
"STO"
]
]
}
} }
} }
}, },
@@ -307,14 +406,41 @@
"$manual": "product" "$manual": "product"
}, },
"uom": { "uom": {
"$manual": "unit" "$search": {
"model": "product.uom",
"domain": [
[
"name",
"=",
"Unit"
]
]
}
}, },
"quantity": 1, "quantity": 1,
"from_location": { "from_location": {
"$manual": "supplier_loc" "$search": {
"model": "stock.location",
"domain": [
[
"code",
"=",
"SUP"
]
]
}
}, },
"to_location": { "to_location": {
"$manual": "storage_loc" "$search": {
"model": "stock.location",
"domain": [
[
"code",
"=",
"STO"
]
]
}
}, },
"planned_date": { "planned_date": {
"$manual": "today" "$manual": "today"
@@ -344,7 +470,16 @@
"$ref": "scenario_stock_product_location__supplier" "$ref": "scenario_stock_product_location__supplier"
}, },
"warehouse": { "warehouse": {
"$manual": "warehouse_loc" "$search": {
"model": "stock.location",
"domain": [
[
"code",
"=",
"WH"
]
]
}
}, },
"company": { "company": {
"$manual": "company" "$manual": "company"
@@ -362,7 +497,16 @@
"$ref": "scenario_stock_reporting__supplier" "$ref": "scenario_stock_reporting__supplier"
}, },
"warehouse": { "warehouse": {
"$manual": "warehouse_loc" "$search": {
"model": "stock.location",
"domain": [
[
"code",
"=",
"WH"
]
]
}
}, },
"company": { "company": {
"$manual": "company" "$manual": "company"
@@ -377,7 +521,16 @@
"$ref": "scenario_stock_reporting__supplier" "$ref": "scenario_stock_reporting__supplier"
}, },
"warehouse": { "warehouse": {
"$manual": "warehouse_loc" "$search": {
"model": "stock.location",
"domain": [
[
"code",
"=",
"WH"
]
]
}
}, },
"company": { "company": {
"$manual": "company" "$manual": "company"
@@ -395,10 +548,28 @@
"$manual": "company" "$manual": "company"
}, },
"from_location": { "from_location": {
"$manual": "storage_loc" "$search": {
"model": "stock.location",
"domain": [
[
"code",
"=",
"STO"
]
]
}
}, },
"to_location": { "to_location": {
"$manual": "lost_loc" "$search": {
"model": "stock.location",
"domain": [
[
"type",
"=",
"lost_found"
]
]
}
} }
} }
}, },
@@ -413,7 +584,16 @@
"$ref": "scenario_stock_shipment_internal__internal_loc" "$ref": "scenario_stock_shipment_internal__internal_loc"
}, },
"to_location": { "to_location": {
"$manual": "storage_loc" "$search": {
"model": "stock.location",
"domain": [
[
"code",
"=",
"STO"
]
]
}
} }
} }
}, },
@@ -428,7 +608,16 @@
"$ref": "scenario_stock_lot_shipment_out__customer" "$ref": "scenario_stock_lot_shipment_out__customer"
}, },
"warehouse": { "warehouse": {
"$manual": "warehouse_loc" "$search": {
"model": "stock.location",
"domain": [
[
"code",
"=",
"WH"
]
]
}
}, },
"company": { "company": {
"$manual": "company" "$manual": "company"
@@ -446,7 +635,16 @@
"$ref": "scenario_stock_lot_shipment_out__customer" "$ref": "scenario_stock_lot_shipment_out__customer"
}, },
"warehouse": { "warehouse": {
"$manual": "warehouse_loc" "$search": {
"model": "stock.location",
"domain": [
[
"code",
"=",
"WH"
]
]
}
}, },
"company": { "company": {
"$manual": "company" "$manual": "company"
@@ -464,7 +662,16 @@
"$ref": "scenario_stock_lot_shipment_out__customer" "$ref": "scenario_stock_lot_shipment_out__customer"
}, },
"warehouse": { "warehouse": {
"$manual": "warehouse_loc" "$search": {
"model": "stock.location",
"domain": [
[
"code",
"=",
"WH"
]
]
}
}, },
"company": { "company": {
"$manual": "company" "$manual": "company"
@@ -479,7 +686,16 @@
"$ref": "scenario_stock_lot_shipment_out__customer" "$ref": "scenario_stock_lot_shipment_out__customer"
}, },
"warehouse": { "warehouse": {
"$manual": "warehouse_loc" "$search": {
"model": "stock.location",
"domain": [
[
"code",
"=",
"WH"
]
]
}
}, },
"company": { "company": {
"$manual": "company" "$manual": "company"
@@ -506,10 +722,28 @@
"$ref": "scenario_stock_product_location__supplier" "$ref": "scenario_stock_product_location__supplier"
}, },
"from_location": { "from_location": {
"$manual": "output_loc" "$search": {
"model": "stock.location",
"domain": [
[
"code",
"=",
"OUT"
]
]
}
}, },
"to_location": { "to_location": {
"$manual": "supplier_loc" "$search": {
"model": "stock.location",
"domain": [
[
"code",
"=",
"SUP"
]
]
}
} }
} }
}, },
@@ -521,7 +755,16 @@
"$ref": "scenario_stock_lot_shipment_out__customer" "$ref": "scenario_stock_lot_shipment_out__customer"
}, },
"warehouse": { "warehouse": {
"$manual": "warehouse_loc" "$search": {
"model": "stock.location",
"domain": [
[
"code",
"=",
"WH"
]
]
}
}, },
"company": { "company": {
"$manual": "company" "$manual": "company"
File diff suppressed because it is too large Load Diff
+7 -6
View File
@@ -1,7 +1,7 @@
# Testdaten-Abdeckung gegen den 5.0-Modellbaum # Testdaten-Abdeckung gegen den 5.0-Modellbaum
## Komplett unabgedeckte Modelle (35) ## Komplett unabgedeckte Modelle (36)
Für diese Modelle gibt es noch gar keinen Record in der dataset.yaml: Für diese Modelle gibt es noch gar keinen Record in der dataset.yaml:
@@ -16,10 +16,9 @@ Für diese Modelle gibt es noch gar keinen Record in der dataset.yaml:
- `product.list_price` _[geändert]_ - `product.list_price` _[geändert]_
- `product.product` _[geändert]_ - `product.product` _[geändert]_
- `production.bom.input` _[geändert]_ - `production.bom.input` _[geändert]_
- `production.bom.output` _[geändert]_
- `stock.inventory.line` _[geändert]_ - `stock.inventory.line` _[geändert]_
- `stock.lot` _[geändert]_
- `stock.product.location` _[geändert]_ - `stock.product.location` _[geändert]_
- `stock.shipment.in.return` _[geändert]_
- `country.zip` _[entfernt]_ - `country.zip` _[entfernt]_
- `stock.lot.type` _[entfernt]_ - `stock.lot.type` _[entfernt]_
- `country.country` _[nur ergänzt]_ - `country.country` _[nur ergänzt]_
@@ -29,6 +28,8 @@ Für diese Modelle gibt es noch gar keinen Record in der dataset.yaml:
- `product.configuration` _[nur ergänzt]_ - `product.configuration` _[nur ergänzt]_
- `production.configuration` _[nur ergänzt]_ - `production.configuration` _[nur ergänzt]_
- `stock.configuration` _[nur ergänzt]_ - `stock.configuration` _[nur ergänzt]_
- `stock.lot` _[nur ergänzt]_
- `stock.shipment.in.return` _[nur ergänzt]_
- `party.category` _[unverändert]_ - `party.category` _[unverändert]_
- `party.party.lang` _[unverändert]_ - `party.party.lang` _[unverändert]_
- `party.party.location` _[unverändert]_ - `party.party.location` _[unverändert]_
@@ -69,9 +70,6 @@ Modell hat mind. einen Record, aber nicht alle setzbaren Felder werden benutzt:
### `stock.shipment.in` _[geändert]_ ### `stock.shipment.in` _[geändert]_
- optional, ungenutzt: contact_address, effective_date, incoming_moves, inventory_moves, moves, number, reference, state - optional, ungenutzt: contact_address, effective_date, incoming_moves, inventory_moves, moves, number, reference, state
### `stock.shipment.internal` _[geändert]_
- optional, ungenutzt: effective_date, effective_start_date, incoming_moves, moves, number, outgoing_moves, planned_start_date, reference, state
### `stock.shipment.out` _[geändert]_ ### `stock.shipment.out` _[geändert]_
- **required, aber ungenutzt:** delivery_address - **required, aber ungenutzt:** delivery_address
- optional, ungenutzt: effective_date, inventory_moves, moves, number, outgoing_moves, reference, state - optional, ungenutzt: effective_date, inventory_moves, moves, number, outgoing_moves, reference, state
@@ -95,6 +93,9 @@ Modell hat mind. einen Record, aber nicht alle setzbaren Felder werden benutzt:
- **required, aber ungenutzt:** left, right - **required, aber ungenutzt:** left, right
- optional, ungenutzt: address, childs, flat_childs, input_location, output_location, picking_location, production_location, storage_location - optional, ungenutzt: address, childs, flat_childs, input_location, output_location, picking_location, production_location, storage_location
### `stock.shipment.internal` _[nur ergänzt]_
- optional, ungenutzt: effective_date, effective_start_date, incoming_moves, moves, number, outgoing_moves, planned_start_date, reference, state
### `ir.attachment` _[unverändert]_ ### `ir.attachment` _[unverändert]_
- **required, aber ungenutzt:** type - **required, aber ungenutzt:** type
- optional, ungenutzt: data, description, file_id, link - optional, ungenutzt: data, description, file_id, link
File diff suppressed because it is too large Load Diff
+294
View File
@@ -0,0 +1,294 @@
# SQL-Dump-Diff: 5.0 -> 8.0
## Tabellen entfernt (4)
- `country_zip` _(`country.zip`=entfernt [übereinstimmung])_
- `cron_company_rel` _(kein passendes Modell in model_diff gefunden)_
- `party_category_rel` _(kein passendes Modell in model_diff gefunden)_
- `stock_lot_type` _(`stock.lot.type`=entfernt [übereinstimmung])_
## Tabellen neu (31)
- `company_company_logo_cache` _(`company.company.logo.cache`=neu [übereinstimmung])_
- `company_company_tax_identifier` _(`company.company.tax_identifier`=neu [übereinstimmung])_
- `country_organization` _(`country.organization`=neu [übereinstimmung])_
- `country_organization_member` _(`country.organization.member`=neu [übereinstimmung])_
- `country_postal_code` _(`country.postal_code`=neu [übereinstimmung])_
- `country_region` _(`country.region`=neu [übereinstimmung])_
- `currency_cron` _(`currency.cron`=neu [übereinstimmung])_
- `ir_avatar` _(`ir.avatar`=neu [übereinstimmung])_
- `ir_avatar_cache` _(`ir.avatar.cache`=neu [übereinstimmung])_
- `ir_calendar_day` _(`ir.calendar.day`=neu [übereinstimmung])_
- `ir_calendar_month` _(`ir.calendar.month`=neu [übereinstimmung])_
- `ir_chat_channel` _(`ir.chat.channel`=neu [übereinstimmung])_
- `ir_chat_follower` _(`ir.chat.follower`=neu [übereinstimmung])_
- `ir_chat_message` _(`ir.chat.message`=neu [übereinstimmung])_
- `ir_cron_log` _(`ir.cron.log`=neu [übereinstimmung])_
- `ir_email` _(`ir.email`=neu [übereinstimmung])_
- `ir_email_address` _(`ir.email.address`=neu [übereinstimmung])_
- `ir_email_template` _(`ir.email.template`=neu [übereinstimmung])_
- `ir_error` _(`ir.error`=neu [übereinstimmung])_
- `ir_filestore_queue` _(`ir.filestore.queue`=neu [übereinstimmung])_
- `ir_message` _(`ir.message`=neu [übereinstimmung])_
- `ir_model_log` _(`ir.model.log`=neu [übereinstimmung])_
- `ir_ui_view_tree_optional` _(`ir.ui.view_tree_optional`=neu [übereinstimmung])_
- `party_address_subdivision_type` _(`party.address.subdivision_type`=neu [übereinstimmung])_
- `party_contact_mechanism_language` _(`party.contact_mechanism.language`=neu [übereinstimmung])_
- `product_configuration_default_lot_sequence` _(`product.configuration.default_lot_sequence`=neu [übereinstimmung])_
- `product_cost_price_revision` _(`product.cost_price.revision`=neu [übereinstimmung])_
- `product_identifier` _(`product.identifier`=neu [übereinstimmung])_
- `res_notification` _(`res.notification`=neu [übereinstimmung])_
- `res_user_device` _(`res.user.device`=neu [übereinstimmung])_
- `stock_location_waste` _(`stock.location.waste`=neu [übereinstimmung])_
## Tabellen mit Schema-Änderung (59)
Spalten wurden hinzugefügt/entfernt oder haben einen anderen Typ - vergleichbar mit einer model_diff.py-Kategorie.
### `company_company` _(`company.company`=geändert [übereinstimmung])_
- neue Spalten: logo
- entfernte Spalten: parent
### `company_employee` _(`company.employee`=nur ergänzt [übereinstimmung])_
- neue Spalten: supervisor
### `country_country` _(`country.country`=nur ergänzt [übereinstimmung])_
- neue Spalten: active, region
### `country_subdivision` _(`country.subdivision`=geändert [ABWEICHUNG])_
- neue Spalten: active
- Zeilenzahl: 4835 -> 5046
### `ir_action` _(`ir.action`=geändert [ABWEICHUNG])_
- neue Spalten: records
- Zeilenzahl: 122 -> 167
### `ir_action_report` _(`ir.action.report`=geändert [übereinstimmung])_
- neue Spalten: record_name
- entfernte Spalten: email
### `ir_action_wizard` _(`ir.action.wizard`=geändert [übereinstimmung])_
- entfernte Spalten: email
- Zeilenzahl: 25 -> 32
### `ir_attachment` _(`ir.attachment`=unverändert [ABWEICHUNG])_
- neue Spalten: copy_to_resources
### `ir_cache` _(`ir.cache`=unverändert [ABWEICHUNG])_
- **create_date**: timestamp(6) without time zone -> timestamp without time zone
- **write_date**: timestamp(6) without time zone -> timestamp without time zone
- Zeilenzahl: 21 -> 43
### `ir_configuration` _(`ir.configuration`=nur ergänzt [übereinstimmung])_
- neue Spalten: product_price_decimal, series
### `ir_cron` _(`ir.cron`=geändert [übereinstimmung])_
- neue Spalten: day, hour, method, minute, weekday
- entfernte Spalten: args, function, model, name, number_calls, repeat_missed, request_user, user
- Zeilenzahl: 1 -> 10
### `ir_export` _(`ir.export`=nur ergänzt [übereinstimmung])_
- neue Spalten: header, ignore_search_limit, records, user
### `ir_lang` _(`ir.lang`=nur ergänzt [übereinstimmung])_
- neue Spalten: am, nplurals, pg_text_search, plural, pm
- Zeilenzahl: 19 -> 25
### `ir_model` _(`ir.model`=geändert [übereinstimmung])_
- neue Spalten: string
- entfernte Spalten: model
- Zeilenzahl: 159 -> 215
### `ir_model_access` _(`ir.model.access`=geändert [übereinstimmung])_
- neue Spalten: active
- **model**: integer -> character varying
- Zeilenzahl: 138 -> 163
### `ir_model_button` _(`ir.model.button`=geändert [übereinstimmung])_
- neue Spalten: active
- **model**: integer -> character varying
- Zeilenzahl: 67 -> 80
### `ir_model_data` _(`ir.model.data`=geändert [übereinstimmung])_
- neue Spalten: field_names
- entfernte Spalten: fs_values, values
- Zeilenzahl: 6583 -> 2080
### `ir_model_field` _(`ir.model.field`=geändert [übereinstimmung])_
- neue Spalten: access, string
- entfernte Spalten: field_description
- **model**: integer -> character varying
- Zeilenzahl: 1709 -> 2858
### `ir_model_field_access` _(`ir.model.field.access`=geändert [übereinstimmung])_
- neue Spalten: active, model
- **field**: integer -> character varying
### `ir_note` _(`ir.note`=unverändert [ABWEICHUNG])_
- neue Spalten: copy_to_resources
### `ir_queue` _(`ir.queue`=unverändert [ABWEICHUNG])_
- **data**: text -> jsonb
### `ir_rule_group` _(`ir.rule.group`=geändert [übereinstimmung])_
- **model**: integer -> character varying
- Zeilenzahl: 26 -> 34
### `ir_sequence` _(`ir.sequence`=geändert [übereinstimmung])_
- neue Spalten: sequence_type
- entfernte Spalten: code
### `ir_sequence_strict` _(`ir.sequence.strict`=geändert [übereinstimmung])_
- neue Spalten: sequence_type
- entfernte Spalten: code
### `ir_sequence_type` _(`ir.sequence.type`=geändert [übereinstimmung])_
- entfernte Spalten: code
- Zeilenzahl: 8 -> 13
### `ir_session` _(`ir.session`=nur ergänzt [übereinstimmung])_
- neue Spalten: ip_address
### `ir_translation` _(`ir.translation`=geändert [übereinstimmung])_
- neue Spalten: src_plural, value_1, value_2, value_3
- entfernte Spalten: src_md5
- Zeilenzahl: 8173 -> 8940
### `ir_trigger` _(`ir.trigger`=geändert [übereinstimmung])_
- neue Spalten: action, on_create_, on_delete_, on_time_, on_write_
- entfernte Spalten: action_function, action_model, on_create, on_delete, on_time, on_write
### `ir_ui_menu_favorite` _(`ir.ui.menu.favorite`=unverändert [ABWEICHUNG])_
- entfernte Spalten: sequence
### `ir_ui_view_tree_width` _(`ir.ui.view_tree_width`=geändert [ABWEICHUNG])_
- neue Spalten: occurrence, screen_width
### `party_address` _(`party.address`=geändert [übereinstimmung])_
- neue Spalten: attn, building_name, building_number, floor_number, post_box, post_office, postal_code, private_bag, room_number, street_name, street_unstructured, unit_number
- entfernte Spalten: name, street, zip
### `party_address_format` _(`party.address.format`=geändert [übereinstimmung])_
- neue Spalten: building_number_format, country_code, floor_number_format, language_code, post_box_format, post_office_format, private_bag_format, room_number_format, street_format, unit_number_format
- entfernte Spalten: country, language
- Zeilenzahl: 66 -> 222
### `party_configuration` _(`party.configuration`=nur ergänzt [übereinstimmung])_
- neue Spalten: identifier_types
### `party_contact_mechanism` _(`party.contact_mechanism`=geändert [ABWEICHUNG])_
- neue Spalten: address, delivery
### `party_identifier` _(`party.identifier`=nur ergänzt [übereinstimmung])_
- neue Spalten: active, address, code_compact
### `party_party` _(`party.party`=nur ergänzt [übereinstimmung])_
- neue Spalten: code_alnum, code_digit
### `product_category` _(`product.category`=nur ergänzt [übereinstimmung])_
- neue Spalten: code
### `product_configuration` _(`product.configuration`=nur ergänzt [übereinstimmung])_
- neue Spalten: category_sequence, product_sequence, template_sequence
### `product_list_price` _(`product.list_price`=geändert [ABWEICHUNG])_
- neue Spalten: product
### `product_product` _(`product.product`=geändert [ABWEICHUNG])_
- neue Spalten: position, replaced_by, suffix_code
### `product_template` _(`product.template`=geändert [ABWEICHUNG])_
- neue Spalten: code, lot_required, lot_sequence
### `production` _(`production`=geändert [übereinstimmung])_
- neue Spalten: assigned_by, done_by, origin, origin_planned_date, run_by, type, unit
- entfernte Spalten: uom
### `production_bom` _(`production.bom`=nur ergänzt [übereinstimmung])_
- neue Spalten: code, phantom, phantom_quantity, phantom_unit
### `production_bom_input` _(`production.bom.input`=geändert [übereinstimmung])_
- neue Spalten: phantom_bom, unit
- entfernte Spalten: uom
### `production_bom_output` _(`production.bom.output`=geändert [übereinstimmung])_
- neue Spalten: phantom_bom, unit
- entfernte Spalten: uom
### `production_configuration` _(`production.configuration`=nur ergänzt [übereinstimmung])_
- neue Spalten: bom_sequence
### `res_group` _(`res.group`=nur ergänzt [übereinstimmung])_
- neue Spalten: active, parent
- Zeilenzahl: 10 -> 13
### `res_user` _(`res.user`=geändert [übereinstimmung])_
- neue Spalten: company_filter, notifications, warehouse
- entfernte Spalten: main_company
- Zeilenzahl: 3 -> 2
### `res_user_login_attempt` _(`res.user.login.attempt`=nur ergänzt [übereinstimmung])_
- neue Spalten: device_cookie
### `stock_inventory` _(`stock.inventory`=geändert [übereinstimmung])_
- entfernte Spalten: lost_found
### `stock_location` _(`stock.location`=nur ergänzt [übereinstimmung])_
- neue Spalten: allow_pickup, lost_found_location, production_output_location, production_picking_location
### `stock_lot` _(`stock.lot`=nur ergänzt [übereinstimmung])_
- neue Spalten: active
### `stock_move` _(`stock.move`=geändert [übereinstimmung])_
- neue Spalten: origin_planned_date, product_cost_price, production_cost_price_updated, unit, unit_price_updated
- entfernte Spalten: internal_volume, internal_weight, uom
### `stock_product_location` _(`stock.product.location`=geändert [ABWEICHUNG])_
- neue Spalten: template
### `stock_shipment_in` _(`stock.shipment.in`=geändert [ABWEICHUNG])_
- neue Spalten: origin_planned_date, warehouse_input, warehouse_storage
### `stock_shipment_in_return` _(`stock.shipment.in.return`=nur ergänzt [übereinstimmung])_
- neue Spalten: origin_planned_date
### `stock_shipment_internal` _(`stock.shipment.internal`=nur ergänzt [übereinstimmung])_
- neue Spalten: internal_transit_location, origin_planned_date, packed_by
### `stock_shipment_out` _(`stock.shipment.out`=geändert [übereinstimmung])_
- neue Spalten: origin_planned_date, picked_by, shipped_by, warehouse_output, warehouse_storage
- entfernte Spalten: assigned_by
### `stock_shipment_out_return` _(`stock.shipment.out.return`=geändert [übereinstimmung])_
- neue Spalten: contact_address, origin_planned_date, warehouse_input, warehouse_storage
- entfernte Spalten: delivery_address
## Nur Zeilenzahl unterschiedlich, Schema identisch (15)
Keine Code-Definitionsänderung zu erwarten (z. B. mehr XML-Fixture-Daten in der neueren Version) - daher kein model_diff-Abgleich.
- `currency_currency`: 170 -> 178
- `currency_currency_rate`: 1 -> 0
- `ir_action_act_window`: 87 -> 125
- `ir_action_act_window_domain`: 39 -> 47
- `ir_action_act_window_view`: 146 -> 225
- `ir_action_keyword`: 117 -> 165
- `ir_module`: 138 -> 219
- `ir_module_config_wizard_item`: 3 -> 4
- `ir_module_dependency`: 587 -> 951
- `ir_rule`: 34 -> 38
- `ir_ui_icon`: 16 -> 20
- `ir_ui_menu`: 93 -> 108
- `ir_ui_view`: 261 -> 367
- `product_uom`: 32 -> 35
- `product_uom_category`: 6 -> 7
## Unverändert (30)
`ir_action_url`, `ir_export_line`, `ir_model_button_click`, `ir_model_button_rule`, `ir_note_read`, `ir_session_wizard`, `ir_trigger_log`, `ir_ui_view_search`, `ir_ui_view_tree_state`, `party_category`, `party_configuration_party_lang`, `party_configuration_party_sequence`, `party_party_lang`, `party_party_location`, `product_configuration_default_cost_price_method`, `product_cost_price`, `product_cost_price_method`, `production_configuration_production_sequence`, `production_lead_time`, `res_user_application`, `res_user_warning`, `stock_configuration`, `stock_configuration_location`, `stock_configuration_sequence`, `stock_inventory_line`, `stock_location_lead_time`, `stock_location_lot_shelf_life`, `stock_period`, `stock_period_cache`, `stock_period_cache_lot`
+88 -63
View File
@@ -167,9 +167,7 @@
"type: Function[Binary] -> Function[Field]" "type: Function[Binary] -> Function[Field]"
] ]
}, },
"removed": [ "removed": []
"action"
]
}, },
"ir.action.act_window.domain": { "ir.action.act_window.domain": {
"added": [], "added": [],
@@ -214,25 +212,21 @@
"category": "geändert", "category": "geändert",
"changed": {}, "changed": {},
"removed": [ "removed": [
"action",
"email", "email",
"pyson_email" "pyson_email"
] ]
}, },
"ir.action.url": { "ir.action.url": {
"added": [], "added": [],
"category": "geändert", "category": "unverändert",
"changed": {}, "changed": {},
"removed": [ "removed": []
"action"
]
}, },
"ir.action.wizard": { "ir.action.wizard": {
"added": [], "added": [],
"category": "geändert", "category": "geändert",
"changed": {}, "changed": {},
"removed": [ "removed": [
"action",
"email" "email"
] ]
}, },
@@ -736,10 +730,16 @@
] ]
}, },
"ir.sequence.strict": { "ir.sequence.strict": {
"added": [], "added": [
"category": "unverändert", "preview",
"sequence_type"
],
"category": "geändert",
"changed": {}, "changed": {},
"removed": [] "removed": [
"code",
"groups"
]
}, },
"ir.sequence.type": { "ir.sequence.type": {
"added": [], "added": [],
@@ -1429,10 +1429,20 @@
] ]
}, },
"production.bom.output": { "production.bom.output": {
"added": [], "added": [
"category": "unverändert", "phantom_bom",
"changed": {}, "unit"
"removed": [] ],
"category": "geändert",
"changed": {
"product": [
"required: True -> None"
]
},
"removed": [
"unit_digits",
"uom"
]
}, },
"production.bom.tree": { "production.bom.tree": {
"added": [ "added": [
@@ -1724,18 +1734,12 @@
}, },
"stock.lot": { "stock.lot": {
"added": [ "added": [
"default_uom" "default_uom",
"has_sequence"
], ],
"category": "geändert", "category": "nur ergänzt",
"changed": {}, "changed": {},
"removed": [ "removed": []
"expiration_date",
"expiration_state",
"number",
"product",
"shelf_life_expiration_date",
"shelf_life_expiration_state"
]
}, },
"stock.lot.trace": { "stock.lot.trace": {
"added": [], "added": [],
@@ -1820,7 +1824,11 @@
}, },
"stock.package": { "stock.package": {
"added": [ "added": [
"additional_weight_uom" "additional_weight_uom",
"internal_volume",
"internal_weight",
"volume_uom",
"weight_uom"
], ],
"category": "nur ergänzt", "category": "nur ergänzt",
"changed": {}, "changed": {},
@@ -2015,7 +2023,14 @@
"removed": [] "removed": []
}, },
"stock.shipment.in": { "stock.shipment.in": {
"added": [], "added": [
"delay",
"internal_volume",
"internal_weight",
"origin_planned_date",
"volume_uom",
"weight_uom"
],
"category": "geändert", "category": "geändert",
"changed": { "changed": {
"warehouse_input": [ "warehouse_input": [
@@ -2029,12 +2044,7 @@
"required: None -> True" "required: None -> True"
] ]
}, },
"removed": [ "removed": []
"effective_date",
"number",
"planned_date",
"reference"
]
}, },
"stock.shipment.in.restocking_list": { "stock.shipment.in.restocking_list": {
"added": [], "added": [],
@@ -2044,16 +2054,20 @@
}, },
"stock.shipment.in.return": { "stock.shipment.in.return": {
"added": [ "added": [
"warehouse" "delay",
"internal_volume",
"internal_weight",
"origin_planned_date",
"packages_volume",
"packages_weight",
"partially_assigned",
"volume_uom",
"warehouse",
"weight_uom"
], ],
"category": "geändert", "category": "nur ergänzt",
"changed": {}, "changed": {},
"removed": [ "removed": []
"effective_date",
"number",
"planned_date",
"reference"
]
}, },
"stock.shipment.in.return.assign": { "stock.shipment.in.return.assign": {
"added": [], "added": [],
@@ -2069,18 +2083,22 @@
}, },
"stock.shipment.internal": { "stock.shipment.internal": {
"added": [ "added": [
"delay",
"internal_transit_location", "internal_transit_location",
"internal_volume",
"internal_weight",
"origin_planned_date",
"partially_assigned",
"to_warehouse", "to_warehouse",
"warehouse" "volume",
"volume_uom",
"warehouse",
"weight",
"weight_uom"
], ],
"category": "geändert", "category": "nur ergänzt",
"changed": {}, "changed": {},
"removed": [ "removed": []
"effective_date",
"number",
"planned_date",
"reference"
]
}, },
"stock.shipment.internal.assign": { "stock.shipment.internal.assign": {
"added": [], "added": [],
@@ -2101,7 +2119,17 @@
"removed": [] "removed": []
}, },
"stock.shipment.out": { "stock.shipment.out": {
"added": [], "added": [
"delay",
"internal_volume",
"internal_weight",
"origin_planned_date",
"packages_volume",
"packages_weight",
"partially_assigned",
"volume_uom",
"weight_uom"
],
"category": "geändert", "category": "geändert",
"changed": { "changed": {
"warehouse_output": [ "warehouse_output": [
@@ -2115,12 +2143,7 @@
"required: None -> True" "required: None -> True"
] ]
}, },
"removed": [ "removed": []
"effective_date",
"number",
"planned_date",
"reference"
]
}, },
"stock.shipment.out.assign": { "stock.shipment.out.assign": {
"added": [], "added": [],
@@ -2148,7 +2171,13 @@
}, },
"stock.shipment.out.return": { "stock.shipment.out.return": {
"added": [ "added": [
"contact_address" "contact_address",
"delay",
"internal_volume",
"internal_weight",
"origin_planned_date",
"volume_uom",
"weight_uom"
], ],
"category": "geändert", "category": "geändert",
"changed": { "changed": {
@@ -2164,11 +2193,7 @@
] ]
}, },
"removed": [ "removed": [
"delivery_address", "delivery_address"
"effective_date",
"number",
"planned_date",
"reference"
] ]
}, },
"stock.shipment.out.return.restocking_list": { "stock.shipment.out.return.restocking_list": {
+22 -27
View File
@@ -16,7 +16,7 @@
- `stock.shipment.out.assign` - `stock.shipment.out.assign`
- `stock.shipment.out.assign.failed` - `stock.shipment.out.assign.failed`
## geändert (57) - Feld(er) entfernt oder geändert - Kandidat für einen echten Vergleichslauf ## geändert (55) - Feld(er) entfernt oder geändert - Kandidat für einen echten Vergleichslauf
### `company.company` ### `company.company`
@@ -40,7 +40,6 @@
- (zusätzlich neue Felder: action, records) - (zusätzlich neue Felder: action, records)
### `ir.action.act_window` ### `ir.action.act_window`
- entfernte Felder: action
- **domains**: type: Function[Binary] -> Function[Field] - **domains**: type: Function[Binary] -> Function[Field]
- **views**: type: Function[Binary] -> Function[Field] - **views**: type: Function[Binary] -> Function[Field]
@@ -57,14 +56,11 @@
- **groups**: type: Function[One2Many] -> Function[Many2Many] - **groups**: type: Function[One2Many] -> Function[Many2Many]
### `ir.action.report` ### `ir.action.report`
- entfernte Felder: action, email, pyson_email - entfernte Felder: email, pyson_email
- (zusätzlich neue Felder: record_name, report_content_html) - (zusätzlich neue Felder: record_name, report_content_html)
### `ir.action.url`
- entfernte Felder: action
### `ir.action.wizard` ### `ir.action.wizard`
- entfernte Felder: action, email - entfernte Felder: email
### `ir.cron` ### `ir.cron`
- entfernte Felder: args, function, model, name, number_calls, repeat_missed, request_user, user - entfernte Felder: args, function, model, name, number_calls, repeat_missed, request_user, user
@@ -113,6 +109,10 @@
- entfernte Felder: code, groups - entfernte Felder: code, groups
- (zusätzlich neue Felder: preview, sequence_type) - (zusätzlich neue Felder: preview, sequence_type)
### `ir.sequence.strict`
- entfernte Felder: code, groups
- (zusätzlich neue Felder: preview, sequence_type)
### `ir.sequence.type` ### `ir.sequence.type`
- entfernte Felder: code - entfernte Felder: code
@@ -177,6 +177,11 @@
- **product**: required: True -> None - **product**: required: True -> None
- (zusätzlich neue Felder: phantom_bom, unit) - (zusätzlich neue Felder: phantom_bom, unit)
### `production.bom.output`
- entfernte Felder: unit_digits, uom
- **product**: required: True -> None
- (zusätzlich neue Felder: phantom_bom, unit)
### `production.bom.tree` ### `production.bom.tree`
- entfernte Felder: unit_digits, uom - entfernte Felder: unit_digits, uom
- (zusätzlich neue Felder: unit) - (zusätzlich neue Felder: unit)
@@ -212,10 +217,6 @@
- entfernte Felder: unit_digits, uom - entfernte Felder: unit_digits, uom
- (zusätzlich neue Felder: inventory_date, inventory_location, unit) - (zusätzlich neue Felder: inventory_date, inventory_location, unit)
### `stock.lot`
- entfernte Felder: expiration_date, expiration_state, number, product, shelf_life_expiration_date, shelf_life_expiration_state
- (zusätzlich neue Felder: default_uom)
### `stock.move` ### `stock.move`
- entfernte Felder: unit_digits, uom - entfernte Felder: unit_digits, uom
- (zusätzlich neue Felder: cost_price_required, delay, from_location_name, origin_planned_date, outcome_moves, product_cost_price, production, production_cost_price_updated, to_location_name, unit, unit_price_company, unit_price_updated) - (zusätzlich neue Felder: cost_price_required, delay, from_location_name, origin_planned_date, outcome_moves, product_cost_price, production, production_cost_price_updated, to_location_name, unit, unit_price_company, unit_price_updated)
@@ -229,28 +230,20 @@
- (zusätzlich neue Felder: template) - (zusätzlich neue Felder: template)
### `stock.shipment.in` ### `stock.shipment.in`
- entfernte Felder: effective_date, number, planned_date, reference
- **warehouse_input**: type: Function[Many2One] -> Many2One; relation: None -> stock.location; required: None -> True - **warehouse_input**: type: Function[Many2One] -> Many2One; relation: None -> stock.location; required: None -> True
- **warehouse_storage**: type: Function[Many2One] -> Many2One; relation: None -> stock.location; required: None -> True - **warehouse_storage**: type: Function[Many2One] -> Many2One; relation: None -> stock.location; required: None -> True
- (zusätzlich neue Felder: delay, internal_volume, internal_weight, origin_planned_date, volume_uom, weight_uom)
### `stock.shipment.in.return`
- entfernte Felder: effective_date, number, planned_date, reference
- (zusätzlich neue Felder: warehouse)
### `stock.shipment.internal`
- entfernte Felder: effective_date, number, planned_date, reference
- (zusätzlich neue Felder: internal_transit_location, to_warehouse, warehouse)
### `stock.shipment.out` ### `stock.shipment.out`
- entfernte Felder: effective_date, number, planned_date, reference
- **warehouse_output**: type: Function[Many2One] -> Many2One; relation: None -> stock.location; required: None -> True - **warehouse_output**: type: Function[Many2One] -> Many2One; relation: None -> stock.location; required: None -> True
- **warehouse_storage**: type: Function[Many2One] -> Many2One; relation: None -> stock.location; required: None -> True - **warehouse_storage**: type: Function[Many2One] -> Many2One; relation: None -> stock.location; required: None -> True
- (zusätzlich neue Felder: delay, internal_volume, internal_weight, origin_planned_date, packages_volume, packages_weight, partially_assigned, volume_uom, weight_uom)
### `stock.shipment.out.return` ### `stock.shipment.out.return`
- entfernte Felder: delivery_address, effective_date, number, planned_date, reference - entfernte Felder: delivery_address
- **warehouse_input**: type: Function[Many2One] -> Many2One; relation: None -> stock.location; required: None -> True - **warehouse_input**: type: Function[Many2One] -> Many2One; relation: None -> stock.location; required: None -> True
- **warehouse_storage**: type: Function[Many2One] -> Many2One; relation: None -> stock.location; required: None -> True - **warehouse_storage**: type: Function[Many2One] -> Many2One; relation: None -> stock.location; required: None -> True
- (zusätzlich neue Felder: contact_address) - (zusätzlich neue Felder: contact_address, delay, internal_volume, internal_weight, origin_planned_date, volume_uom, weight_uom)
## neu (76) - Modell gibt es erst ab 8.0 ## neu (76) - Modell gibt es erst ab 8.0
@@ -331,7 +324,7 @@
- `stock.shipment.assign.partial` - `stock.shipment.assign.partial`
- `stock.shipment.drop` - `stock.shipment.drop`
## nur ergänzt (24) - nur neue Felder dazugekommen, Rest unverändert ## nur ergänzt (27) - nur neue Felder dazugekommen, Rest unverändert
- `company.employee` - `company.employee`
- `country.country` - `country.country`
@@ -353,16 +346,20 @@
- `res.user.login.attempt` - `res.user.login.attempt`
- `stock.configuration` - `stock.configuration`
- `stock.location` - `stock.location`
- `stock.lot`
- `stock.package` - `stock.package`
- `stock.product_quantities_warehouse` - `stock.product_quantities_warehouse`
- `stock.product_quantities_warehouse.context` - `stock.product_quantities_warehouse.context`
- `stock.products_by_locations.context` - `stock.products_by_locations.context`
- `stock.shipment.in.return`
- `stock.shipment.internal`
## unverändert (101) - komplett identisch geblieben ## unverändert (100) - komplett identisch geblieben
- `company.company.config` - `company.company.config`
- `company.company.config.start` - `company.company.config.start`
- `ir.action-res.group` - `ir.action-res.group`
- `ir.action.url`
- `ir.attachment` - `ir.attachment`
- `ir.cache` - `ir.cache`
- `ir.cron-company.company` - `ir.cron-company.company`
@@ -390,7 +387,6 @@
- `ir.queue` - `ir.queue`
- `ir.rule` - `ir.rule`
- `ir.rule.group-res.group` - `ir.rule.group-res.group`
- `ir.sequence.strict`
- `ir.sequence.type-res.group` - `ir.sequence.type-res.group`
- `ir.session.wizard` - `ir.session.wizard`
- `ir.translation.clean` - `ir.translation.clean`
@@ -431,7 +427,6 @@
- `product.template-product.category.all` - `product.template-product.category.all`
- `product.uom` - `product.uom`
- `product.uom.category` - `product.uom.category`
- `production.bom.output`
- `production.bom.tree.open` - `production.bom.tree.open`
- `production.bom.tree.open.tree` - `production.bom.tree.open.tree`
- `production.configuration.production_sequence` - `production.configuration.production_sequence`
+246 -4
View File
@@ -2400,8 +2400,121 @@
"persisted": true "persisted": true
}, },
"ir.sequence.strict": { "ir.sequence.strict": {
"fields": {}, "fields": {
"persisted": false "code": {
"file": "ir/sequence.py",
"module": "ir",
"relation": null,
"required": true,
"settable": true,
"type": "Selection"
},
"company": {
"file": "company/company.py",
"module": "company",
"relation": "company.company",
"required": null,
"settable": true,
"type": "Many2One"
},
"groups": {
"file": "res/ir.py",
"module": "res",
"relation": null,
"required": null,
"settable": false,
"type": "Function[Many2Many]"
},
"last_timestamp": {
"file": "ir/sequence.py",
"module": "ir",
"relation": null,
"required": null,
"settable": true,
"type": "Integer"
},
"name": {
"file": "ir/sequence.py",
"module": "ir",
"relation": null,
"required": true,
"settable": true,
"type": "Char"
},
"number_increment": {
"file": "ir/sequence.py",
"module": "ir",
"relation": null,
"required": null,
"settable": true,
"type": "Integer"
},
"number_next": {
"file": "ir/sequence.py",
"module": "ir",
"relation": null,
"required": null,
"settable": false,
"type": "Function"
},
"number_next_internal": {
"file": "ir/sequence.py",
"module": "ir",
"relation": null,
"required": null,
"settable": true,
"type": "Integer"
},
"padding": {
"file": "ir/sequence.py",
"module": "ir",
"relation": null,
"required": null,
"settable": true,
"type": "Integer"
},
"prefix": {
"file": "ir/sequence.py",
"module": "ir",
"relation": null,
"required": null,
"settable": true,
"type": "Char"
},
"suffix": {
"file": "ir/sequence.py",
"module": "ir",
"relation": null,
"required": null,
"settable": true,
"type": "Char"
},
"timestamp_offset": {
"file": "ir/sequence.py",
"module": "ir",
"relation": null,
"required": true,
"settable": true,
"type": "Float"
},
"timestamp_rounding": {
"file": "ir/sequence.py",
"module": "ir",
"relation": null,
"required": true,
"settable": true,
"type": "Float"
},
"type": {
"file": "ir/sequence.py",
"module": "ir",
"relation": null,
"required": null,
"settable": true,
"type": "Selection"
}
},
"persisted": true
}, },
"ir.sequence.type": { "ir.sequence.type": {
"fields": { "fields": {
@@ -4721,8 +4834,57 @@
"persisted": true "persisted": true
}, },
"production.bom.output": { "production.bom.output": {
"fields": {}, "fields": {
"persisted": false "bom": {
"file": "production/bom.py",
"module": "production",
"relation": "production.bom",
"required": true,
"settable": true,
"type": "Many2One"
},
"product": {
"file": "production/bom.py",
"module": "production",
"relation": "product.product",
"required": true,
"settable": true,
"type": "Many2One"
},
"quantity": {
"file": "production/bom.py",
"module": "production",
"relation": null,
"required": true,
"settable": true,
"type": "Float"
},
"unit_digits": {
"file": "production/bom.py",
"module": "production",
"relation": null,
"required": null,
"settable": false,
"type": "Function[Integer]"
},
"uom": {
"file": "production/bom.py",
"module": "production",
"relation": "product.uom",
"required": true,
"settable": true,
"type": "Many2One"
},
"uom_category": {
"file": "production/bom.py",
"module": "production",
"relation": null,
"required": null,
"settable": false,
"type": "Function[Many2One]"
}
},
"persisted": true
}, },
"production.bom.tree": { "production.bom.tree": {
"fields": { "fields": {
@@ -6269,6 +6431,22 @@
"required": null, "required": null,
"settable": false, "settable": false,
"type": "Function[Float]" "type": "Function[Float]"
},
"volume": {
"file": "stock_shipment_measurements/stock.py",
"module": "stock_shipment_measurements",
"relation": null,
"required": null,
"settable": false,
"type": "Function[Float]"
},
"weight": {
"file": "stock_shipment_measurements/stock.py",
"module": "stock_shipment_measurements",
"relation": null,
"required": null,
"settable": false,
"type": "Function[Float]"
} }
}, },
"persisted": false "persisted": false
@@ -6582,6 +6760,14 @@
"settable": false, "settable": false,
"type": "Function[Many2One]" "type": "Function[Many2One]"
}, },
"volume": {
"file": "stock_shipment_measurements/stock.py",
"module": "stock_shipment_measurements",
"relation": null,
"required": null,
"settable": false,
"type": "Function[Float]"
},
"warehouse": { "warehouse": {
"file": "stock/shipment.py", "file": "stock/shipment.py",
"module": "stock", "module": "stock",
@@ -6605,6 +6791,14 @@
"required": null, "required": null,
"settable": false, "settable": false,
"type": "Function[Many2One]" "type": "Function[Many2One]"
},
"weight": {
"file": "stock_shipment_measurements/stock.py",
"module": "stock_shipment_measurements",
"relation": null,
"required": null,
"settable": false,
"type": "Function[Float]"
} }
}, },
"persisted": true "persisted": true
@@ -6710,6 +6904,22 @@
"required": true, "required": true,
"settable": true, "settable": true,
"type": "Many2One" "type": "Many2One"
},
"volume": {
"file": "stock_shipment_measurements/stock.py",
"module": "stock_shipment_measurements",
"relation": null,
"required": null,
"settable": false,
"type": "Function[Float]"
},
"weight": {
"file": "stock_shipment_measurements/stock.py",
"module": "stock_shipment_measurements",
"relation": null,
"required": null,
"settable": false,
"type": "Function[Float]"
} }
}, },
"persisted": true "persisted": true
@@ -6975,6 +7185,14 @@
"settable": true, "settable": true,
"type": "Selection" "type": "Selection"
}, },
"volume": {
"file": "stock_shipment_measurements/stock.py",
"module": "stock_shipment_measurements",
"relation": null,
"required": null,
"settable": false,
"type": "Function[Float]"
},
"warehouse": { "warehouse": {
"file": "stock/shipment.py", "file": "stock/shipment.py",
"module": "stock", "module": "stock",
@@ -6998,6 +7216,14 @@
"required": null, "required": null,
"settable": false, "settable": false,
"type": "Function[Many2One]" "type": "Function[Many2One]"
},
"weight": {
"file": "stock_shipment_measurements/stock.py",
"module": "stock_shipment_measurements",
"relation": null,
"required": null,
"settable": false,
"type": "Function[Float]"
} }
}, },
"persisted": true "persisted": true
@@ -7133,6 +7359,14 @@
"settable": true, "settable": true,
"type": "Selection" "type": "Selection"
}, },
"volume": {
"file": "stock_shipment_measurements/stock.py",
"module": "stock_shipment_measurements",
"relation": null,
"required": null,
"settable": false,
"type": "Function[Float]"
},
"warehouse": { "warehouse": {
"file": "stock/shipment.py", "file": "stock/shipment.py",
"module": "stock", "module": "stock",
@@ -7156,6 +7390,14 @@
"required": null, "required": null,
"settable": false, "settable": false,
"type": "Function[Many2One]" "type": "Function[Many2One]"
},
"weight": {
"file": "stock_shipment_measurements/stock.py",
"module": "stock_shipment_measurements",
"relation": null,
"required": null,
"settable": false,
"type": "Function[Float]"
} }
}, },
"persisted": true "persisted": true
+40 -4
View File
@@ -409,6 +409,23 @@
- **timestamp_rounding**: Float required - **timestamp_rounding**: Float required
- **type**: Selection - **type**: Selection
## `ir.sequence.strict` _(Felder aus: company, ir, res)_
- **code**: Selection required
- **company**: Many2One -> `company.company` (aus Modul `company`)
- **groups**: Function[Many2Many] NICHT setzbar (Function ohne setter) (aus Modul `res`)
- **last_timestamp**: Integer
- **name**: Char required
- **number_increment**: Integer
- **number_next**: Function NICHT setzbar (Function ohne setter)
- **number_next_internal**: Integer
- **padding**: Integer
- **prefix**: Char
- **suffix**: Char
- **timestamp_offset**: Float required
- **timestamp_rounding**: Float required
- **type**: Selection
## `ir.sequence.type` _(Felder aus: ir, res)_ ## `ir.sequence.type` _(Felder aus: ir, res)_
- **code**: Char required - **code**: Char required
@@ -805,6 +822,15 @@
- **uom**: Many2One -> `product.uom` required - **uom**: Many2One -> `product.uom` required
- **uom_category**: Function[Many2One] NICHT setzbar (Function ohne setter) - **uom_category**: Function[Many2One] NICHT setzbar (Function ohne setter)
## `production.bom.output` _(Felder aus: production)_
- **bom**: Many2One -> `production.bom` required
- **product**: Many2One -> `product.product` required
- **quantity**: Float required
- **unit_digits**: Function[Integer] NICHT setzbar (Function ohne setter)
- **uom**: Many2One -> `product.uom` required
- **uom_category**: Function[Many2One] NICHT setzbar (Function ohne setter)
## `production.bom.tree` _(Wizard/Transient - nicht persistent)_ _(Felder aus: production)_ ## `production.bom.tree` _(Wizard/Transient - nicht persistent)_ _(Felder aus: production)_
- **childs**: One2Many -> `production.bom.tree` - **childs**: One2Many -> `production.bom.tree`
@@ -1062,6 +1088,8 @@
- **additional_weight**: Float (aus Modul `stock_shipment_measurements`) - **additional_weight**: Float (aus Modul `stock_shipment_measurements`)
- **total_volume**: Function[Float] NICHT setzbar (Function ohne setter) (aus Modul `stock_shipment_measurements`) - **total_volume**: Function[Float] NICHT setzbar (Function ohne setter) (aus Modul `stock_shipment_measurements`)
- **total_weight**: Function[Float] NICHT setzbar (Function ohne setter) (aus Modul `stock_shipment_measurements`) - **total_weight**: Function[Float] NICHT setzbar (Function ohne setter) (aus Modul `stock_shipment_measurements`)
- **volume**: Function[Float] NICHT setzbar (Function ohne setter) (aus Modul `stock_shipment_measurements`)
- **weight**: Function[Float] NICHT setzbar (Function ohne setter) (aus Modul `stock_shipment_measurements`)
## `stock.period` _(Felder aus: stock, stock_lot)_ ## `stock.period` _(Felder aus: stock, stock_lot)_
@@ -1105,7 +1133,7 @@
- **forecast_date**: Date - **forecast_date**: Date
- **stock_date_end**: Function[Date] NICHT setzbar (Function ohne setter) - **stock_date_end**: Function[Date] NICHT setzbar (Function ohne setter)
## `stock.shipment.in` _(Felder aus: stock)_ ## `stock.shipment.in` _(Felder aus: stock, stock_shipment_measurements)_
- **company**: Many2One -> `company.company` required - **company**: Many2One -> `company.company` required
- **contact_address**: Many2One -> `party.address` - **contact_address**: Many2One -> `party.address`
@@ -1120,11 +1148,13 @@
- **state**: Selection - **state**: Selection
- **supplier**: Many2One -> `party.party` required - **supplier**: Many2One -> `party.party` required
- **supplier_location**: Function[Many2One] NICHT setzbar (Function ohne setter) - **supplier_location**: Function[Many2One] NICHT setzbar (Function ohne setter)
- **volume**: Function[Float] NICHT setzbar (Function ohne setter) (aus Modul `stock_shipment_measurements`)
- **warehouse**: Many2One -> `stock.location` required - **warehouse**: Many2One -> `stock.location` required
- **warehouse_input**: Function[Many2One] NICHT setzbar (Function ohne setter) - **warehouse_input**: Function[Many2One] NICHT setzbar (Function ohne setter)
- **warehouse_storage**: Function[Many2One] NICHT setzbar (Function ohne setter) - **warehouse_storage**: Function[Many2One] NICHT setzbar (Function ohne setter)
- **weight**: Function[Float] NICHT setzbar (Function ohne setter) (aus Modul `stock_shipment_measurements`)
## `stock.shipment.in.return` _(Felder aus: stock)_ ## `stock.shipment.in.return` _(Felder aus: stock, stock_shipment_measurements)_
- **company**: Many2One -> `company.company` required - **company**: Many2One -> `company.company` required
- **delivery_address**: Many2One -> `party.address` - **delivery_address**: Many2One -> `party.address`
@@ -1138,6 +1168,8 @@
- **state**: Selection - **state**: Selection
- **supplier**: Many2One -> `party.party` required - **supplier**: Many2One -> `party.party` required
- **to_location**: Many2One -> `stock.location` required - **to_location**: Many2One -> `stock.location` required
- **volume**: Function[Float] NICHT setzbar (Function ohne setter) (aus Modul `stock_shipment_measurements`)
- **weight**: Function[Float] NICHT setzbar (Function ohne setter) (aus Modul `stock_shipment_measurements`)
## `stock.shipment.in.return.assign.failed` _(Wizard/Transient - nicht persistent)_ _(Felder aus: stock)_ ## `stock.shipment.in.return.assign.failed` _(Wizard/Transient - nicht persistent)_ _(Felder aus: stock)_
@@ -1164,7 +1196,7 @@
- **moves**: Many2Many -> `stock.move` - **moves**: Many2Many -> `stock.move`
## `stock.shipment.out` _(Felder aus: stock)_ ## `stock.shipment.out` _(Felder aus: stock, stock_shipment_measurements)_
- **company**: Many2One -> `company.company` required - **company**: Many2One -> `company.company` required
- **customer**: Many2One -> `party.party` required - **customer**: Many2One -> `party.party` required
@@ -1179,15 +1211,17 @@
- **planned_date**: Date - **planned_date**: Date
- **reference**: Char - **reference**: Char
- **state**: Selection - **state**: Selection
- **volume**: Function[Float] NICHT setzbar (Function ohne setter) (aus Modul `stock_shipment_measurements`)
- **warehouse**: Many2One -> `stock.location` required - **warehouse**: Many2One -> `stock.location` required
- **warehouse_output**: Function[Many2One] NICHT setzbar (Function ohne setter) - **warehouse_output**: Function[Many2One] NICHT setzbar (Function ohne setter)
- **warehouse_storage**: Function[Many2One] NICHT setzbar (Function ohne setter) - **warehouse_storage**: Function[Many2One] NICHT setzbar (Function ohne setter)
- **weight**: Function[Float] NICHT setzbar (Function ohne setter) (aus Modul `stock_shipment_measurements`)
## `stock.shipment.out.assign.failed` _(Wizard/Transient - nicht persistent)_ _(Felder aus: stock)_ ## `stock.shipment.out.assign.failed` _(Wizard/Transient - nicht persistent)_ _(Felder aus: stock)_
- **inventory_moves**: Many2Many -> `stock.move` - **inventory_moves**: Many2Many -> `stock.move`
## `stock.shipment.out.return` _(Felder aus: stock)_ ## `stock.shipment.out.return` _(Felder aus: stock, stock_shipment_measurements)_
- **company**: Many2One -> `company.company` required - **company**: Many2One -> `company.company` required
- **customer**: Many2One -> `party.party` required - **customer**: Many2One -> `party.party` required
@@ -1202,9 +1236,11 @@
- **planned_date**: Date - **planned_date**: Date
- **reference**: Char - **reference**: Char
- **state**: Selection - **state**: Selection
- **volume**: Function[Float] NICHT setzbar (Function ohne setter) (aus Modul `stock_shipment_measurements`)
- **warehouse**: Many2One -> `stock.location` required - **warehouse**: Many2One -> `stock.location` required
- **warehouse_input**: Function[Many2One] NICHT setzbar (Function ohne setter) - **warehouse_input**: Function[Many2One] NICHT setzbar (Function ohne setter)
- **warehouse_storage**: Function[Many2One] NICHT setzbar (Function ohne setter) - **warehouse_storage**: Function[Many2One] NICHT setzbar (Function ohne setter)
- **weight**: Function[Float] NICHT setzbar (Function ohne setter) (aus Modul `stock_shipment_measurements`)
## `stock.shipment.split.start` _(Wizard/Transient - nicht persistent)_ _(Felder aus: stock_split)_ ## `stock.shipment.split.start` _(Wizard/Transient - nicht persistent)_ _(Felder aus: stock_split)_
File diff suppressed because it is too large Load Diff
+242 -10
View File
@@ -141,6 +141,7 @@
- **act_window_domains**: One2Many -> `ir.action.act_window.domain` - **act_window_domains**: One2Many -> `ir.action.act_window.domain`
- **act_window_views**: One2Many -> `ir.action.act_window.view` - **act_window_views**: One2Many -> `ir.action.act_window.view`
- **action**: Many2One -> `ir.action` required
- **context**: Char - **context**: Char
- **context_domain**: Char - **context_domain**: Char
- **context_model**: Char - **context_model**: Char
@@ -178,6 +179,7 @@
## `ir.action.report` _(Felder aus: ir)_ ## `ir.action.report` _(Felder aus: ir)_
- **action**: Many2One -> `ir.action` required
- **direct_print**: Boolean - **direct_print**: Boolean
- **extension**: Selection - **extension**: Selection
- **is_custom**: Function[Boolean] NICHT setzbar (Function ohne setter) - **is_custom**: Function[Boolean] NICHT setzbar (Function ohne setter)
@@ -196,10 +198,12 @@
## `ir.action.url` _(Felder aus: ir)_ ## `ir.action.url` _(Felder aus: ir)_
- **action**: Many2One -> `ir.action` required
- **url**: Char required - **url**: Char required
## `ir.action.wizard` _(Felder aus: ir)_ ## `ir.action.wizard` _(Felder aus: ir)_
- **action**: Many2One -> `ir.action` required
- **model**: Char - **model**: Char
- **window**: Boolean - **window**: Boolean
- **wiz_name**: Char required - **wiz_name**: Char required
@@ -218,11 +222,15 @@
## `ir.avatar` _(Felder aus: ir)_ ## `ir.avatar` _(Felder aus: ir)_
- **cache**: One2Many -> `ir.avatar.cache` - **cache**: One2Many -> `ir.avatar.cache`
- **image**: Binary
- **image_id**: Char
- **uuid**: Char required - **uuid**: Char required
## `ir.avatar.cache` _(Felder aus: ir)_ ## `ir.avatar.cache` _(Felder aus: ir)_
- **avatar**: Many2One -> `ir.avatar` required - **avatar**: Many2One -> `ir.avatar` required
- **image**: Binary
- **image_id**: Char
- **size**: Integer required - **size**: Integer required
## `ir.cache` _(Felder aus: ir)_ ## `ir.cache` _(Felder aus: ir)_
@@ -230,6 +238,18 @@
- **name**: Char required - **name**: Char required
- **timestamp**: Timestamp - **timestamp**: Timestamp
## `ir.calendar.day` _(Felder aus: ir)_
- **abbreviation**: Char required
- **index**: Integer required
- **name**: Char required
## `ir.calendar.month` _(Felder aus: ir)_
- **abbreviation**: Char required
- **index**: Integer required
- **name**: Char required
## `ir.chat.channel` _(Felder aus: ir)_ ## `ir.chat.channel` _(Felder aus: ir)_
- **followers**: One2Many -> `ir.chat.follower` - **followers**: One2Many -> `ir.chat.follower`
@@ -238,15 +258,21 @@
## `ir.chat.follower` _(Felder aus: ir)_ ## `ir.chat.follower` _(Felder aus: ir)_
- **author**: Function[Char] NICHT setzbar (Function ohne setter)
- **channel**: Many2One -> `ir.chat.channel` required - **channel**: Many2One -> `ir.chat.channel` required
- **email**: Char
- **user**: Many2One -> `res.user`
## `ir.chat.message` _(Felder aus: ir)_ ## `ir.chat.message` _(Felder aus: ir)_
- **audience**: Selection required - **audience**: Selection required
- **author**: Function[Char] NICHT setzbar (Function ohne setter)
- **channel**: Many2One -> `ir.chat.channel` required - **channel**: Many2One -> `ir.chat.channel` required
- **content**: Text required - **content**: Text required
- **email**: Char
- **reference**: Char - **reference**: Char
- **summary**: Function[Char] NICHT setzbar (Function ohne setter) - **summary**: Function[Char] NICHT setzbar (Function ohne setter)
- **user**: Many2One -> `res.user`
## `ir.configuration` _(Felder aus: currency, ir, product)_ ## `ir.configuration` _(Felder aus: currency, ir, product)_
@@ -348,6 +374,11 @@
- **export**: Many2One -> `ir.export` required - **export**: Many2One -> `ir.export` required
- **group**: Many2One -> `res.group` required - **group**: Many2One -> `res.group` required
## `ir.export-write-res.group` _(Felder aus: ir)_
- **export**: Many2One -> `ir.export` required
- **group**: Many2One -> `res.group` required
## `ir.export.line` _(Felder aus: ir)_ ## `ir.export.line` _(Felder aus: ir)_
- **export**: Many2One -> `ir.export` required - **export**: Many2One -> `ir.export` required
@@ -588,6 +619,23 @@
- **timestamp_rounding**: Float required - **timestamp_rounding**: Float required
- **type**: Selection - **type**: Selection
## `ir.sequence.strict` _(Felder aus: company, ir)_
- **company**: Many2One -> `company.company` (aus Modul `company`)
- **last_timestamp**: Integer
- **name**: Char required
- **number_increment**: Integer
- **number_next**: Function NICHT setzbar (Function ohne setter)
- **number_next_internal**: Integer
- **padding**: Integer
- **prefix**: Char
- **preview**: Function[Char] NICHT setzbar (Function ohne setter)
- **sequence_type**: Many2One -> `ir.sequence.type` required
- **suffix**: Char
- **timestamp_offset**: Float required
- **timestamp_rounding**: Float required
- **type**: Selection
## `ir.sequence.type` _(Felder aus: ir)_ ## `ir.sequence.type` _(Felder aus: ir)_
- **groups**: Many2Many -> `ir.sequence.type-res.group` - **groups**: Many2Many -> `ir.sequence.type-res.group`
@@ -923,6 +971,8 @@
## `product.cost_price.revision` _(Felder aus: stock)_ ## `product.cost_price.revision` _(Felder aus: stock)_
- **company**: Many2One -> `company.company` required (aus Modul `stock`) - **company**: Many2One -> `company.company` required (aus Modul `stock`)
- **cost_price**: Char required (aus Modul `stock`)
- **date**: Date required (aus Modul `stock`)
- **product**: Many2One -> `product.product` (aus Modul `stock`) - **product**: Many2One -> `product.product` (aus Modul `stock`)
- **template**: Many2One -> `product.template` required (aus Modul `stock`) - **template**: Many2One -> `product.template` required (aus Modul `stock`)
@@ -1100,6 +1150,15 @@
- **unit**: Many2One -> `product.uom` required - **unit**: Many2One -> `product.uom` required
- **uom_category**: Function[Many2One] NICHT setzbar (Function ohne setter) - **uom_category**: Function[Many2One] NICHT setzbar (Function ohne setter)
## `production.bom.output` _(Felder aus: production)_
- **bom**: Many2One -> `production.bom` required
- **phantom_bom**: Many2One -> `production.bom`
- **product**: Many2One -> `product.product`
- **quantity**: Float required
- **unit**: Many2One -> `product.uom` required
- **uom_category**: Function[Many2One] NICHT setzbar (Function ohne setter)
## `production.bom.tree` _(Wizard/Transient - nicht persistent)_ _(Felder aus: production)_ ## `production.bom.tree` _(Wizard/Transient - nicht persistent)_ _(Felder aus: production)_
- **childs**: One2Many -> `production.bom.tree` - **childs**: One2Many -> `production.bom.tree`
@@ -1339,11 +1398,18 @@
- **location**: Many2One -> `stock.location` required - **location**: Many2One -> `stock.location` required
- **warehouse**: Many2One -> `stock.location` required - **warehouse**: Many2One -> `stock.location` required
## `stock.lot` _(Felder aus: stock_lot)_ ## `stock.lot` _(Felder aus: stock_lot, stock_lot_sled)_
- **default_uom**: Function[Many2One] NICHT setzbar (Function ohne setter) (aus Modul `stock_lot`) - **default_uom**: Function[Many2One] NICHT setzbar (Function ohne setter) (aus Modul `stock_lot`)
- **expiration_date**: Date (aus Modul `stock_lot_sled`)
- **expiration_state**: Function[Selection] NICHT setzbar (Function ohne setter) (aus Modul `stock_lot_sled`)
- **forecast_quantity**: Function[Float] NICHT setzbar (Function ohne setter) (aus Modul `stock_lot`) - **forecast_quantity**: Function[Float] NICHT setzbar (Function ohne setter) (aus Modul `stock_lot`)
- **has_sequence**: Function[Boolean] NICHT setzbar (Function ohne setter) (aus Modul `stock_lot`)
- **number**: Char required (aus Modul `stock_lot`)
- **product**: Many2One -> `product.product` required (aus Modul `stock_lot`)
- **quantity**: Function[Float] NICHT setzbar (Function ohne setter) (aus Modul `stock_lot`) - **quantity**: Function[Float] NICHT setzbar (Function ohne setter) (aus Modul `stock_lot`)
- **shelf_life_expiration_date**: Date (aus Modul `stock_lot_sled`)
- **shelf_life_expiration_state**: Function[Selection] NICHT setzbar (Function ohne setter) (aus Modul `stock_lot_sled`)
## `stock.lot.trace` _(Felder aus: production, stock_lot)_ ## `stock.lot.trace` _(Felder aus: production, stock_lot)_
@@ -1420,11 +1486,18 @@
- **quantity_remaining**: Function[Float] NICHT setzbar (Function ohne setter) (aus Modul `stock_lot`) - **quantity_remaining**: Function[Float] NICHT setzbar (Function ohne setter) (aus Modul `stock_lot`)
- **unit**: Many2One -> `product.uom` (aus Modul `stock_lot`) - **unit**: Many2One -> `product.uom` (aus Modul `stock_lot`)
## `stock.move.add.lots.start.lot` _(Wizard/Transient - nicht persistent)_ _(Felder aus: stock_lot)_ ## `stock.move.add.lots.start.lot` _(Wizard/Transient - nicht persistent)_ _(Felder aus: stock_lot, stock_lot_sled)_
- **expiration_date**: Date (aus Modul `stock_lot_sled`)
- **expiration_state**: Function[Selection] NICHT setzbar (Function ohne setter) (aus Modul `stock_lot_sled`)
- **has_sequence**: Function[Boolean] NICHT setzbar (Function ohne setter) (aus Modul `stock_lot`)
- **number**: Char required (aus Modul `stock_lot`)
- **parent**: Many2One -> `stock.move.add.lots.start` (aus Modul `stock_lot`) - **parent**: Many2One -> `stock.move.add.lots.start` (aus Modul `stock_lot`)
- **product**: Many2One -> `product.product` required (aus Modul `stock_lot`)
- **quantity**: Float required (aus Modul `stock_lot`) - **quantity**: Float required (aus Modul `stock_lot`)
- **quantity_unit**: Function[Many2One] NICHT setzbar (Function ohne setter) (aus Modul `stock_lot`) - **quantity_unit**: Function[Many2One] NICHT setzbar (Function ohne setter) (aus Modul `stock_lot`)
- **shelf_life_expiration_date**: Date (aus Modul `stock_lot_sled`)
- **shelf_life_expiration_state**: Function[Selection] NICHT setzbar (Function ohne setter) (aus Modul `stock_lot_sled`)
## `stock.move.split.start` _(Wizard/Transient - nicht persistent)_ _(Felder aus: stock_split)_ ## `stock.move.split.start` _(Wizard/Transient - nicht persistent)_ _(Felder aus: stock_split)_
@@ -1437,8 +1510,14 @@
- **additional_weight**: Float (aus Modul `stock_shipment_measurements`) - **additional_weight**: Float (aus Modul `stock_shipment_measurements`)
- **additional_weight_uom**: Many2One -> `product.uom` (aus Modul `stock_shipment_measurements`) - **additional_weight_uom**: Many2One -> `product.uom` (aus Modul `stock_shipment_measurements`)
- **internal_volume**: Float (aus Modul `stock_shipment_measurements`)
- **internal_weight**: Float (aus Modul `stock_shipment_measurements`)
- **total_volume**: Function[Float] NICHT setzbar (Function ohne setter) (aus Modul `stock_shipment_measurements`) - **total_volume**: Function[Float] NICHT setzbar (Function ohne setter) (aus Modul `stock_shipment_measurements`)
- **total_weight**: Function[Float] NICHT setzbar (Function ohne setter) (aus Modul `stock_shipment_measurements`) - **total_weight**: Function[Float] NICHT setzbar (Function ohne setter) (aus Modul `stock_shipment_measurements`)
- **volume**: Function[Float] NICHT setzbar (Function ohne setter) (aus Modul `stock_shipment_measurements`)
- **volume_uom**: Function[Many2One] NICHT setzbar (Function ohne setter) (aus Modul `stock_shipment_measurements`)
- **weight**: Function[Float] NICHT setzbar (Function ohne setter) (aus Modul `stock_shipment_measurements`)
- **weight_uom**: Function[Many2One] NICHT setzbar (Function ohne setter) (aus Modul `stock_shipment_measurements`)
## `stock.period` _(Felder aus: stock, stock_lot)_ ## `stock.period` _(Felder aus: stock, stock_lot)_
@@ -1510,22 +1589,58 @@
- **forecast_date**: Date - **forecast_date**: Date
- **stock_date_end**: Function[Date] NICHT setzbar (Function ohne setter) - **stock_date_end**: Function[Date] NICHT setzbar (Function ohne setter)
## `stock.reporting.inventory` _(Felder aus: stock)_
- **company**: Many2One -> `company.company`
- **input_quantity**: Float
- **output_quantity**: Float
- **product**: Many2One -> `product.product`
- **product_reference**: Reference -> `Product Reference`
- **product_template**: Many2One -> `product.template`
- **quantity**: Float
- **unit**: Function[Many2One] NICHT setzbar (Function ohne setter)
## `stock.reporting.inventory.context` _(Wizard/Transient - nicht persistent)_ _(Felder aus: stock)_ ## `stock.reporting.inventory.context` _(Wizard/Transient - nicht persistent)_ _(Felder aus: stock)_
- **company**: Many2One -> `company.company` required
- **date**: Date required - **date**: Date required
- **location**: Many2One -> `stock.location` required
- **product_type**: Selection required - **product_type**: Selection required
## `stock.reporting.inventory.daily` _(Felder aus: stock)_ ## `stock.reporting.inventory.daily` _(Felder aus: stock)_
- **company**: Many2One -> `company.company`
- **from_date**: Date - **from_date**: Date
- **input_quantity**: Float
- **output_quantity**: Float
- **product**: Many2One -> `product.product`
- **product_reference**: Reference -> `Product Reference`
- **product_template**: Many2One -> `product.template`
- **quantity**: Float
- **to_date**: Date - **to_date**: Date
- **unit**: Function[Many2One] NICHT setzbar (Function ohne setter)
## `stock.reporting.inventory.move` _(Felder aus: stock)_ ## `stock.reporting.inventory.move` _(Felder aus: stock)_
- **company**: Many2One -> `company.company`
- **date**: Date - **date**: Date
- **document**: Function[Reference] NICHT setzbar (Function ohne setter) - **document**: Function[Reference] NICHT setzbar (Function ohne setter)
- **input_quantity**: Float
- **move**: Many2One -> `stock.move` - **move**: Many2One -> `stock.move`
- **origin**: Reference -> `Origin` - **origin**: Reference -> `Origin`
- **output_quantity**: Float
- **product**: Many2One -> `product.product`
- **product_reference**: Reference -> `Product Reference`
- **product_template**: Many2One -> `product.template`
- **quantity**: Float
- **unit**: Function[Many2One] NICHT setzbar (Function ohne setter)
## `stock.reporting.inventory.range.context` _(Wizard/Transient - nicht persistent)_ _(Felder aus: stock)_
- **company**: Many2One -> `company.company` required
- **from_date**: Date required
- **location**: Many2One -> `stock.location` required
- **to_date**: Date required
## `stock.reporting.inventory.turnover` _(Felder aus: stock)_ ## `stock.reporting.inventory.turnover` _(Felder aus: stock)_
@@ -1540,12 +1655,30 @@
## `stock.reporting.inventory.turnover.context` _(Wizard/Transient - nicht persistent)_ _(Felder aus: stock)_ ## `stock.reporting.inventory.turnover.context` _(Wizard/Transient - nicht persistent)_ _(Felder aus: stock)_
- **company**: Many2One -> `company.company` required
- **from_date**: Date required
- **location**: Many2One -> `stock.location` required
- **product_type**: Selection required - **product_type**: Selection required
- **to_date**: Date required
## `stock.reporting.margin.category` _(Wizard/Transient - nicht persistent)_ _(Felder aus: stock)_ ## `stock.reporting.margin.category` _(Felder aus: stock)_
- **category**: Many2One -> `product.category`
- **company**: Many2One -> `company.company`
- **currency**: Many2One -> `currency.currency`
- **margin**: Numeric
- **margin_trend**: Function[Char] NICHT setzbar (Function ohne setter)
- **time_series**: One2Many -> `stock.reporting.margin.category.time_series` - **time_series**: One2Many -> `stock.reporting.margin.category.time_series`
## `stock.reporting.margin.category.time_series` _(Felder aus: stock)_
- **category**: Many2One -> `product.category`
- **company**: Many2One -> `company.company`
- **currency**: Many2One -> `currency.currency`
- **date**: Date
- **margin**: Numeric
- **margin_trend**: Function[Char] NICHT setzbar (Function ohne setter)
## `stock.reporting.margin.category.tree` _(Felder aus: stock)_ ## `stock.reporting.margin.category.tree` _(Felder aus: stock)_
- **children**: One2Many -> `stock.reporting.margin.category.tree` - **children**: One2Many -> `stock.reporting.margin.category.tree`
@@ -1565,90 +1698,189 @@
- **period**: Selection required - **period**: Selection required
- **to_date**: Date - **to_date**: Date
## `stock.reporting.margin.main` _(Wizard/Transient - nicht persistent)_ _(Felder aus: stock)_ ## `stock.reporting.margin.main` _(Felder aus: stock)_
- **company**: Many2One -> `company.company`
- **currency**: Many2One -> `currency.currency`
- **margin**: Numeric
- **margin_trend**: Function[Char] NICHT setzbar (Function ohne setter)
- **time_series**: Function[Many2Many] NICHT setzbar (Function ohne setter) - **time_series**: Function[Many2Many] NICHT setzbar (Function ohne setter)
## `stock.reporting.margin.product` _(Wizard/Transient - nicht persistent)_ _(Felder aus: stock)_ ## `stock.reporting.margin.main.time_series` _(Felder aus: stock)_
- **company**: Many2One -> `company.company`
- **currency**: Many2One -> `currency.currency`
- **date**: Date
- **margin**: Numeric
- **margin_trend**: Function[Char] NICHT setzbar (Function ohne setter)
## `stock.reporting.margin.product` _(Felder aus: stock)_
- **company**: Many2One -> `company.company`
- **currency**: Many2One -> `currency.currency`
- **internal_quantity**: Float
- **margin**: Numeric
- **margin_trend**: Function[Char] NICHT setzbar (Function ohne setter)
- **product**: Many2One -> `product.product`
- **quantity**: Function[Float] NICHT setzbar (Function ohne setter)
- **time_series**: One2Many -> `stock.reporting.margin.product.time_series` - **time_series**: One2Many -> `stock.reporting.margin.product.time_series`
- **unit**: Many2One -> `product.uom`
## `stock.reporting.margin.product.time_series` _(Felder aus: stock)_
- **company**: Many2One -> `company.company`
- **currency**: Many2One -> `currency.currency`
- **date**: Date
- **internal_quantity**: Float
- **margin**: Numeric
- **margin_trend**: Function[Char] NICHT setzbar (Function ohne setter)
- **product**: Many2One -> `product.product`
- **quantity**: Function[Float] NICHT setzbar (Function ohne setter)
- **unit**: Many2One -> `product.uom`
## `stock.shipment.assign.partial` _(Wizard/Transient - nicht persistent)_ _(Felder aus: stock)_ ## `stock.shipment.assign.partial` _(Wizard/Transient - nicht persistent)_ _(Felder aus: stock)_
- **moves**: Many2Many -> `stock.move` - **moves**: Many2Many -> `stock.move`
## `stock.shipment.in` _(Felder aus: stock)_ ## `stock.shipment.in` _(Felder aus: stock, stock_shipment_measurements)_
- **company**: Many2One -> `company.company` required - **company**: Many2One -> `company.company` required
- **contact_address**: Many2One -> `party.address` - **contact_address**: Many2One -> `party.address`
- **delay**: Function[TimeDelta] NICHT setzbar (Function ohne setter)
- **effective_date**: Date
- **incoming_moves**: Function[One2Many] - **incoming_moves**: Function[One2Many]
- **internal_volume**: Float (aus Modul `stock_shipment_measurements`)
- **internal_weight**: Float (aus Modul `stock_shipment_measurements`)
- **inventory_moves**: Function[One2Many] - **inventory_moves**: Function[One2Many]
- **moves**: One2Many -> `stock.move` - **moves**: One2Many -> `stock.move`
- **number**: Char
- **origin_planned_date**: Date
- **origins**: Function[Char] NICHT setzbar (Function ohne setter) - **origins**: Function[Char] NICHT setzbar (Function ohne setter)
- **planned_date**: Date
- **reference**: Char
- **state**: Selection - **state**: Selection
- **supplier**: Many2One -> `party.party` required - **supplier**: Many2One -> `party.party` required
- **supplier_location**: Function[Many2One] NICHT setzbar (Function ohne setter) - **supplier_location**: Function[Many2One] NICHT setzbar (Function ohne setter)
- **volume**: Function[Float] NICHT setzbar (Function ohne setter) (aus Modul `stock_shipment_measurements`)
- **volume_uom**: Function[Many2One] NICHT setzbar (Function ohne setter) (aus Modul `stock_shipment_measurements`)
- **warehouse**: Many2One -> `stock.location` required - **warehouse**: Many2One -> `stock.location` required
- **warehouse_input**: Many2One -> `stock.location` required - **warehouse_input**: Many2One -> `stock.location` required
- **warehouse_storage**: Many2One -> `stock.location` required - **warehouse_storage**: Many2One -> `stock.location` required
- **weight**: Function[Float] NICHT setzbar (Function ohne setter) (aus Modul `stock_shipment_measurements`)
- **weight_uom**: Function[Many2One] NICHT setzbar (Function ohne setter) (aus Modul `stock_shipment_measurements`)
## `stock.shipment.in.return` _(Felder aus: stock)_ ## `stock.shipment.in.return` _(Felder aus: stock, stock_shipment_measurements)_
- **company**: Many2One -> `company.company` required - **company**: Many2One -> `company.company` required
- **delay**: Function[TimeDelta] NICHT setzbar (Function ohne setter)
- **delivery_address**: Many2One -> `party.address` - **delivery_address**: Many2One -> `party.address`
- **effective_date**: Date
- **from_location**: Many2One -> `stock.location` required - **from_location**: Many2One -> `stock.location` required
- **internal_volume**: Float (aus Modul `stock_shipment_measurements`)
- **internal_weight**: Float (aus Modul `stock_shipment_measurements`)
- **moves**: One2Many -> `stock.move` - **moves**: One2Many -> `stock.move`
- **number**: Char
- **origin_planned_date**: Date
- **origins**: Function[Char] NICHT setzbar (Function ohne setter) - **origins**: Function[Char] NICHT setzbar (Function ohne setter)
- **packages_volume**: Function[Float] NICHT setzbar (Function ohne setter) (aus Modul `stock_shipment_measurements`)
- **packages_weight**: Function[Float] NICHT setzbar (Function ohne setter) (aus Modul `stock_shipment_measurements`)
- **partially_assigned**: Function[Boolean] NICHT setzbar (Function ohne setter)
- **planned_date**: Date
- **reference**: Char
- **state**: Selection - **state**: Selection
- **supplier**: Many2One -> `party.party` required - **supplier**: Many2One -> `party.party` required
- **to_location**: Many2One -> `stock.location` required - **to_location**: Many2One -> `stock.location` required
- **volume**: Function[Float] NICHT setzbar (Function ohne setter) (aus Modul `stock_shipment_measurements`)
- **volume_uom**: Function[Many2One] NICHT setzbar (Function ohne setter) (aus Modul `stock_shipment_measurements`)
- **warehouse**: Function[Many2One] NICHT setzbar (Function ohne setter) - **warehouse**: Function[Many2One] NICHT setzbar (Function ohne setter)
- **weight**: Function[Float] NICHT setzbar (Function ohne setter) (aus Modul `stock_shipment_measurements`)
- **weight_uom**: Function[Many2One] NICHT setzbar (Function ohne setter) (aus Modul `stock_shipment_measurements`)
## `stock.shipment.internal` _(Felder aus: stock)_ ## `stock.shipment.internal` _(Felder aus: stock, stock_shipment_measurements)_
- **company**: Many2One -> `company.company` required - **company**: Many2One -> `company.company` required
- **delay**: Function[TimeDelta] NICHT setzbar (Function ohne setter)
- **effective_date**: Date
- **effective_start_date**: Date - **effective_start_date**: Date
- **from_location**: Many2One -> `stock.location` required - **from_location**: Many2One -> `stock.location` required
- **incoming_moves**: Function[One2Many] - **incoming_moves**: Function[One2Many]
- **internal_transit_location**: Many2One -> `stock.location` - **internal_transit_location**: Many2One -> `stock.location`
- **internal_volume**: Float (aus Modul `stock_shipment_measurements`)
- **internal_weight**: Float (aus Modul `stock_shipment_measurements`)
- **moves**: One2Many -> `stock.move` - **moves**: One2Many -> `stock.move`
- **number**: Char
- **origin_planned_date**: Date
- **outgoing_moves**: Function[One2Many] - **outgoing_moves**: Function[One2Many]
- **partially_assigned**: Function[Boolean] NICHT setzbar (Function ohne setter)
- **planned_date**: Date
- **planned_start_date**: Date - **planned_start_date**: Date
- **reference**: Char
- **state**: Selection - **state**: Selection
- **to_location**: Many2One -> `stock.location` required - **to_location**: Many2One -> `stock.location` required
- **to_warehouse**: Function[Many2One] NICHT setzbar (Function ohne setter) - **to_warehouse**: Function[Many2One] NICHT setzbar (Function ohne setter)
- **transit_location**: Function[Many2One] NICHT setzbar (Function ohne setter) - **transit_location**: Function[Many2One] NICHT setzbar (Function ohne setter)
- **volume**: Function[Float] NICHT setzbar (Function ohne setter) (aus Modul `stock_shipment_measurements`)
- **volume_uom**: Function[Many2One] NICHT setzbar (Function ohne setter) (aus Modul `stock_shipment_measurements`)
- **warehouse**: Function[Many2One] NICHT setzbar (Function ohne setter) - **warehouse**: Function[Many2One] NICHT setzbar (Function ohne setter)
- **weight**: Function[Float] NICHT setzbar (Function ohne setter) (aus Modul `stock_shipment_measurements`)
- **weight_uom**: Function[Many2One] NICHT setzbar (Function ohne setter) (aus Modul `stock_shipment_measurements`)
## `stock.shipment.out` _(Felder aus: stock)_ ## `stock.shipment.out` _(Felder aus: stock, stock_shipment_measurements)_
- **company**: Many2One -> `company.company` required - **company**: Many2One -> `company.company` required
- **customer**: Many2One -> `party.party` required - **customer**: Many2One -> `party.party` required
- **customer_location**: Function[Many2One] NICHT setzbar (Function ohne setter) - **customer_location**: Function[Many2One] NICHT setzbar (Function ohne setter)
- **delay**: Function[TimeDelta] NICHT setzbar (Function ohne setter)
- **delivery_address**: Many2One -> `party.address` required - **delivery_address**: Many2One -> `party.address` required
- **effective_date**: Date
- **internal_volume**: Float (aus Modul `stock_shipment_measurements`)
- **internal_weight**: Float (aus Modul `stock_shipment_measurements`)
- **inventory_moves**: Function[One2Many] - **inventory_moves**: Function[One2Many]
- **moves**: One2Many -> `stock.move` - **moves**: One2Many -> `stock.move`
- **number**: Char
- **origin_planned_date**: Date
- **origins**: Function[Char] NICHT setzbar (Function ohne setter) - **origins**: Function[Char] NICHT setzbar (Function ohne setter)
- **outgoing_moves**: Function[One2Many] - **outgoing_moves**: Function[One2Many]
- **packages_volume**: Function[Float] NICHT setzbar (Function ohne setter) (aus Modul `stock_shipment_measurements`)
- **packages_weight**: Function[Float] NICHT setzbar (Function ohne setter) (aus Modul `stock_shipment_measurements`)
- **partially_assigned**: Function[Boolean] NICHT setzbar (Function ohne setter)
- **planned_date**: Date
- **reference**: Char
- **state**: Selection - **state**: Selection
- **volume**: Function[Float] NICHT setzbar (Function ohne setter) (aus Modul `stock_shipment_measurements`)
- **volume_uom**: Function[Many2One] NICHT setzbar (Function ohne setter) (aus Modul `stock_shipment_measurements`)
- **warehouse**: Many2One -> `stock.location` required - **warehouse**: Many2One -> `stock.location` required
- **warehouse_output**: Many2One -> `stock.location` required - **warehouse_output**: Many2One -> `stock.location` required
- **warehouse_storage**: Many2One -> `stock.location` required - **warehouse_storage**: Many2One -> `stock.location` required
- **weight**: Function[Float] NICHT setzbar (Function ohne setter) (aus Modul `stock_shipment_measurements`)
- **weight_uom**: Function[Many2One] NICHT setzbar (Function ohne setter) (aus Modul `stock_shipment_measurements`)
## `stock.shipment.out.return` _(Felder aus: stock)_ ## `stock.shipment.out.return` _(Felder aus: stock, stock_shipment_measurements)_
- **company**: Many2One -> `company.company` required - **company**: Many2One -> `company.company` required
- **contact_address**: Many2One -> `party.address` - **contact_address**: Many2One -> `party.address`
- **customer**: Many2One -> `party.party` required - **customer**: Many2One -> `party.party` required
- **customer_location**: Function[Many2One] NICHT setzbar (Function ohne setter) - **customer_location**: Function[Many2One] NICHT setzbar (Function ohne setter)
- **delay**: Function[TimeDelta] NICHT setzbar (Function ohne setter)
- **effective_date**: Date
- **incoming_moves**: Function[One2Many] - **incoming_moves**: Function[One2Many]
- **internal_volume**: Float (aus Modul `stock_shipment_measurements`)
- **internal_weight**: Float (aus Modul `stock_shipment_measurements`)
- **inventory_moves**: Function[One2Many] - **inventory_moves**: Function[One2Many]
- **moves**: One2Many -> `stock.move` - **moves**: One2Many -> `stock.move`
- **number**: Char
- **origin_planned_date**: Date
- **origins**: Function[Char] NICHT setzbar (Function ohne setter) - **origins**: Function[Char] NICHT setzbar (Function ohne setter)
- **planned_date**: Date
- **reference**: Char
- **state**: Selection - **state**: Selection
- **volume**: Function[Float] NICHT setzbar (Function ohne setter) (aus Modul `stock_shipment_measurements`)
- **volume_uom**: Function[Many2One] NICHT setzbar (Function ohne setter) (aus Modul `stock_shipment_measurements`)
- **warehouse**: Many2One -> `stock.location` required - **warehouse**: Many2One -> `stock.location` required
- **warehouse_input**: Many2One -> `stock.location` required - **warehouse_input**: Many2One -> `stock.location` required
- **warehouse_storage**: Many2One -> `stock.location` required - **warehouse_storage**: Many2One -> `stock.location` required
- **weight**: Function[Float] NICHT setzbar (Function ohne setter) (aus Modul `stock_shipment_measurements`)
- **weight_uom**: Function[Many2One] NICHT setzbar (Function ohne setter) (aus Modul `stock_shipment_measurements`)
## `stock.shipment.split.start` _(Wizard/Transient - nicht persistent)_ _(Felder aus: stock_split)_ ## `stock.shipment.split.start` _(Wizard/Transient - nicht persistent)_ _(Felder aus: stock_split)_
+42
View File
@@ -96,10 +96,35 @@ def resolve_model_get(node):
return node.args[0].value return node.args[0].value
def resolve_find_call(node, model_vars):
"""Erkennt `SomeModelVar.find([('feld', '=', 'wert'), ...])` bzw.
`.search(...)` - das Standardmuster, mit dem Tryton-Tests auf bereits
vorhandene Stammdaten zugreifen (Länder, Währungen, Einheiten,
Lagerorte, ... - i. d. R. per XML-Fixture beim Modul-Aktivieren
angelegt, nicht Teil des Testdatensatzes selbst). Liefert
(model_name, domain) oder None, wenn das Domain-Argument nicht rein
literal ist (z. B. eine Variable referenziert) oder das Muster nicht
passt."""
if not isinstance(node, ast.Call) or not isinstance(node.func, ast.Attribute):
return None
if node.func.attr not in ("find", "search"):
return None
if not isinstance(node.func.value, ast.Name) or node.func.value.id not in model_vars:
return None
if not node.args:
return None
try:
domain = ast.literal_eval(node.args[0])
except Exception:
return None
return model_vars[node.func.value.id], domain
class ScenarioParser: class ScenarioParser:
def __init__(self): def __init__(self):
self.model_vars = {} # Variable -> Modellname (aus Model.get(...)) self.model_vars = {} # Variable -> Modellname (aus Model.get(...))
self.record_vars = {} # Variable -> Record-Dict (key/model/fields) self.record_vars = {} # Variable -> Record-Dict (key/model/fields)
self.search_vars = {} # Variable -> {"model": ..., "domain": ...} (aus X.find(...)/.search(...))
self.finalized = set() # Variablen, deren erster .save()-Aufruf schon verarbeitet wurde self.finalized = set() # Variablen, deren erster .save()-Aufruf schon verarbeitet wurde
self.output = [] # fertige Record-Dicts in Reihenfolge self.output = [] # fertige Record-Dicts in Reihenfolge
self.manual_notes = [] # Zeilen, die nicht automatisch verstanden wurden self.manual_notes = [] # Zeilen, die nicht automatisch verstanden wurden
@@ -111,6 +136,8 @@ class ScenarioParser:
if isinstance(node, ast.Name): if isinstance(node, ast.Name):
if node.id in self.record_vars: if node.id in self.record_vars:
return {"$ref": self.record_vars[node.id]["key"]} return {"$ref": self.record_vars[node.id]["key"]}
if node.id in self.search_vars:
return {"$search": self.search_vars[node.id]}
return self._manual(node) return self._manual(node)
if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.USub): if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.USub):
@@ -163,6 +190,21 @@ class ScenarioParser:
return return
target = node.targets[0] target = node.targets[0]
# x, = SomeModelVar.find([('feld', '=', 'wert')]) (Zugriff auf
# vorhandene Stammdaten statt Anlegen eines neuen Records)
if (
isinstance(target, ast.Tuple)
and len(target.elts) == 1
and isinstance(target.elts[0], ast.Name)
):
found = resolve_find_call(node.value, self.model_vars)
if found:
model_name, domain = found
self.search_vars[target.elts[0].id] = {"model": model_name, "domain": domain}
return
self._manual(node)
return
# X = Model.get('some.model') # X = Model.get('some.model')
if isinstance(target, ast.Name): if isinstance(target, ast.Name):
model_name = resolve_model_get(node.value) model_name = resolve_model_get(node.value)
+268
View File
@@ -0,0 +1,268 @@
#!/usr/bin/env python3
"""
sql_dump_diff.py
Vergleicht zwei mit `pg_dump` (Klartext-Format, --format=plain) erzeugte
SQL-Dumps strukturell: welche Tabellen/Spalten gibt es in Version A, die
es in B nicht (mehr) gibt (und umgekehrt), welche Spaltentypen haben sich
geändert, und wie viele Zeilen stehen jeweils in einer gemeinsamen
Tabelle. Gedacht für zwei frisch initialisierte (oder später: mit
denselben Proteus-Testdaten befüllte) Datenbanken zweier Tryton-Versionen
- als dritte, von der Code-Analyse (extract_updates.py/model_diff.py)
unabhängige Quelle für "was hat sich am Schema wirklich geändert".
Mit --model-diff (Ausgabe von model_diff.py) wird zusätzlich pro
betroffener Tabelle geprüft, ob die rein statische Code-Analyse dieselbe
Änderung schon vorhergesagt hatte (Modellname -> Tabellenname: Punkte
durch Unterstriche ersetzen, wie es extract_updates.py/
compare_snapshots.py auch tun) - zeigt, wie gut beide Ansätze
übereinstimmen, und macht Abweichungen sichtbar (z. B. Python-seitige
Änderungen ohne SQL-Schemaauswirkung, oder Tabellenänderungen durch ein
Modul, das nicht im --modules-Set von extract_model_tree.py steckte).
Zeilen werden bereits vollständig geparst (als dict pro Zeile, Spalte ->
Wert) und im Rückgabewert von parse_dump() mitgeführt - für eine spätere
inhaltliche Vergleichs-Erweiterung (z. B. per --key-column concrete Werte
statt nur Zeilenzahlen gegenüberstellen). Der aktuelle Report nutzt davon
bisher nur die Zeilenzahl.
Bekannte Grenzen: erkennt keine Indizes/Constraints/Sequences/
Spaltenreihenfolge, nur Tabellen + Spalten(typen) + Zeilenzahl.
Nutzung:
python3 sql_dump_diff.py --dump-a data/leer5.sql --label-a 5.0 \\
--dump-b data/leer8.sql --label-b 8.0 \\
--model-diff reports/model_diff.json \\
--output reports/dump_diff_5.0_8.0.json \\
--report reports/dump_diff_5.0_8.0.md
"""
import argparse
import json
import re
from pathlib import Path
CREATE_TABLE_RE = re.compile(r'^CREATE TABLE public\.(\w+) \($')
COPY_RE = re.compile(r'^COPY public\.(\w+) \(([^)]*)\) FROM stdin;$')
def unescape_copy_value(raw):
"""pg_dump-COPY-Escaping rückgängig machen (\\N -> NULL, \\t/\\n/\\\\ -> Zeichen)."""
if raw == r'\N':
return None
out = []
i = 0
while i < len(raw):
c = raw[i]
if c == '\\' and i + 1 < len(raw):
out.append({'t': '\t', 'n': '\n', 'r': '\r', '\\': '\\'}.get(raw[i + 1], raw[i + 1]))
i += 2
else:
out.append(c)
i += 1
return ''.join(out)
def parse_column_line(line):
"""Eine Zeile aus dem CREATE-TABLE-Block -> (spaltenname, typ) oder (None, None)."""
text = line.strip()
if not text or text.startswith(('CONSTRAINT', 'PRIMARY KEY', 'UNIQUE')):
return None, None
text = text.rstrip(',')
for stopper in (' NOT NULL', ' DEFAULT ', ' COLLATE '):
idx = text.find(stopper)
if idx != -1:
text = text[:idx]
parts = text.split(None, 1)
if len(parts) != 2:
return None, None
name, coltype = parts
return name.strip('"'), coltype.strip()
def parse_dump(path):
"""Liefert {tabelle: {"columns": {name: typ}, "row_count": int, "rows": [dict, ...]}}."""
tables = {}
with open(path, encoding='utf-8', errors='replace') as f:
lines = iter(f)
for line in lines:
stripped = line.rstrip('\n')
m = CREATE_TABLE_RE.match(stripped)
if m:
table = m.group(1)
columns = {}
for col_line in lines:
col_line = col_line.rstrip('\n')
if col_line.startswith(');'):
break
name, coltype = parse_column_line(col_line)
if name:
columns[name] = coltype
entry = tables.setdefault(table, {"columns": {}, "row_count": 0, "rows": []})
entry["columns"] = columns
continue
m = COPY_RE.match(stripped)
if m:
table, col_csv = m.group(1), m.group(2)
col_names = [c.strip() for c in col_csv.split(',')]
rows = []
for data_line in lines:
data_line = data_line.rstrip('\n')
if data_line == '\\.':
break
values = [unescape_copy_value(v) for v in data_line.split('\t')]
rows.append(dict(zip(col_names, values)))
entry = tables.setdefault(table, {"columns": {}, "row_count": 0, "rows": []})
entry["row_count"] = len(rows)
entry["rows"] = rows
return tables
def diff_dumps(tables_a, tables_b):
tables_removed = sorted(set(tables_a) - set(tables_b))
tables_added = sorted(set(tables_b) - set(tables_a))
common = sorted(set(tables_a) & set(tables_b))
changed = {}
unchanged = []
for table in common:
cols_a, cols_b = tables_a[table]["columns"], tables_b[table]["columns"]
added_cols = sorted(set(cols_b) - set(cols_a))
removed_cols = sorted(set(cols_a) - set(cols_b))
type_changes = {
c: [cols_a[c], cols_b[c]]
for c in sorted(set(cols_a) & set(cols_b))
if cols_a[c] != cols_b[c]
}
row_count_a, row_count_b = tables_a[table]["row_count"], tables_b[table]["row_count"]
schema_changed = bool(added_cols or removed_cols or type_changes)
if schema_changed or row_count_a != row_count_b:
if removed_cols or type_changes:
expected_category = "geändert"
elif added_cols:
expected_category = "nur ergänzt"
else:
expected_category = None # nur Zeilenzahl anders - keine Code-Definitionsänderung
changed[table] = {
"added_columns": added_cols,
"removed_columns": removed_cols,
"type_changes": type_changes,
"row_count_a": row_count_a,
"row_count_b": row_count_b,
"schema_changed": schema_changed,
"expected_category": expected_category,
}
else:
unchanged.append(table)
return {
"tables_removed": tables_removed,
"tables_added": tables_added,
"tables_changed": changed,
"tables_unchanged": unchanged,
}
def build_table_to_models(model_diff):
mapping = {}
for model_name in model_diff:
mapping.setdefault(model_name.replace(".", "_"), []).append(model_name)
return mapping
def model_diff_note(table, model_diff, table_to_models, expected_category):
"""Vergleicht einen Tabellen-Fund mit der Vorhersage aus model_diff.py.
Liefert einen kurzen Text: Übereinstimmung, Abweichung, oder kein
passendes Modell gefunden (z. B. reine Relations-/Interntabelle)."""
if not model_diff:
return ""
models = table_to_models.get(table)
if not models:
return " _(kein passendes Modell in model_diff gefunden)_"
parts = []
for model in models:
category = model_diff[model]["category"]
flag = "übereinstimmung" if category == expected_category else "ABWEICHUNG"
parts.append(f"`{model}`={category} [{flag}]")
return " _(" + "; ".join(parts) + ")_"
def write_report(diff, out_path, label_a, label_b, model_diff, table_to_models):
lines = [f"# SQL-Dump-Diff: {label_a} -> {label_b}\n"]
lines.append(f"\n## Tabellen entfernt ({len(diff['tables_removed'])})\n")
for t in diff["tables_removed"]:
lines.append(f"- `{t}`{model_diff_note(t, model_diff, table_to_models, 'entfernt')}")
lines.append(f"\n\n## Tabellen neu ({len(diff['tables_added'])})\n")
for t in diff["tables_added"]:
lines.append(f"- `{t}`{model_diff_note(t, model_diff, table_to_models, 'neu')}")
schema_changed = {t: i for t, i in diff["tables_changed"].items() if i["schema_changed"]}
rows_only = {t: i for t, i in diff["tables_changed"].items() if not i["schema_changed"]}
lines.append(f"\n\n## Tabellen mit Schema-Änderung ({len(schema_changed)})\n")
lines.append(
"Spalten wurden hinzugefügt/entfernt oder haben einen anderen Typ - "
"vergleichbar mit einer model_diff.py-Kategorie.\n"
)
for t, info in sorted(schema_changed.items()):
note = model_diff_note(t, model_diff, table_to_models, info["expected_category"])
lines.append(f"\n### `{t}`{note}")
if info["added_columns"]:
lines.append(f"- neue Spalten: {', '.join(info['added_columns'])}")
if info["removed_columns"]:
lines.append(f"- entfernte Spalten: {', '.join(info['removed_columns'])}")
for col, (ta, tb) in info["type_changes"].items():
lines.append(f"- **{col}**: {ta} -> {tb}")
if info["row_count_a"] != info["row_count_b"]:
lines.append(f"- Zeilenzahl: {info['row_count_a']} -> {info['row_count_b']}")
lines.append(f"\n\n## Nur Zeilenzahl unterschiedlich, Schema identisch ({len(rows_only)})\n")
lines.append(
"Keine Code-Definitionsänderung zu erwarten (z. B. mehr XML-Fixture-Daten in "
"der neueren Version) - daher kein model_diff-Abgleich.\n"
)
for t, info in sorted(rows_only.items()):
lines.append(f"- `{t}`: {info['row_count_a']} -> {info['row_count_b']}")
lines.append(f"\n\n## Unverändert ({len(diff['tables_unchanged'])})\n")
lines.append(", ".join(f"`{t}`" for t in diff["tables_unchanged"]))
out_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
def main():
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("--dump-a", required=True)
parser.add_argument("--dump-b", required=True)
parser.add_argument("--label-a", default="A")
parser.add_argument("--label-b", default="B")
parser.add_argument("--model-diff", default=None, help="Optional: model_diff.json zum Abgleich")
parser.add_argument("--output", default="reports/dump_diff.json")
parser.add_argument("--report", default="reports/dump_diff.md")
args = parser.parse_args()
tables_a = parse_dump(args.dump_a)
tables_b = parse_dump(args.dump_b)
diff = diff_dumps(tables_a, tables_b)
model_diff = json.loads(Path(args.model_diff).read_text(encoding="utf-8")) if args.model_diff else {}
table_to_models = build_table_to_models(model_diff) if model_diff else {}
Path(args.output).write_text(json.dumps(diff, indent=2, ensure_ascii=False, sort_keys=True), encoding="utf-8")
write_report(diff, Path(args.report), args.label_a, args.label_b, model_diff, table_to_models)
print(
f"{len(diff['tables_removed'])} entfernt, {len(diff['tables_added'])} neu, "
f"{len(diff['tables_changed'])} geändert, {len(diff['tables_unchanged'])} unverändert "
f"-> {args.output}, {args.report}"
)
if __name__ == "__main__":
main()