Compare commits
113 Commits
18.0-targe
...
18.0
| Author | SHA1 | Date | |
|---|---|---|---|
| f2b5710178 | |||
| f834d70859 | |||
| 9b9b8d025e | |||
| d0b7a5737d | |||
| 8a414ed3ac | |||
| 5a27dc6a65 | |||
| c55b0e59d2 | |||
| 989720ed21 | |||
| 6676433d46 | |||
| c1df940daf | |||
| 92f9548d34 | |||
| b46fed0f8e | |||
| 90e3422e8b | |||
| f1b0c50fbf | |||
| 7f827cf7eb | |||
| dfb77411d9 | |||
| 59539e0201 | |||
| b6a0f0462d | |||
| 5fcaef0336 | |||
| bb3e492d30 | |||
| aeb8e5660b | |||
| 4c03959437 | |||
| 75d91984d1 | |||
| 77cd3f4595 | |||
| 4684d33241 | |||
| 85a90f90fd | |||
| 0d27ca36b0 | |||
| b85ec97d9c | |||
| f9cbcffe34 | |||
| fe09d1439f | |||
| 23b9867eec | |||
| 00df958985 | |||
| 98c96018d6 | |||
| b2f7a5edbf | |||
| d6c2986ebf | |||
| 67184e7e01 | |||
| ee0e6c8b98 | |||
| 1c9829a7aa | |||
| 1abeb97afa | |||
| daddb51327 | |||
| 5d1faa7b5b | |||
| 21b0d6305f | |||
| 988421a0c5 | |||
| b26e70dbe9 | |||
| 38d0becfad | |||
| 943d48af58 | |||
| 09f28c8070 | |||
| 40a5fe8b0f | |||
| 31a40e95d7 | |||
| 91209dead4 | |||
| 9cb6dc8ac5 | |||
| 93385abb0f | |||
| e443f8709d | |||
| b151894c39 | |||
| 0c4b58e0a6 | |||
| e460acddf3 | |||
| a1c6903893 | |||
| 72a46e3657 | |||
| 641bfb3ade | |||
| a73c0cb299 | |||
| 4626379d6e | |||
| 9be8f320f7 | |||
| d6a98cfbd0 | |||
| 0135035a54 | |||
| f79e126c8c | |||
| 253d289633 | |||
| de317f46e6 | |||
| 8ae586cca6 | |||
| e18f2880a4 | |||
| 43cf5754fd | |||
| 4c048b5ca9 | |||
| 686b201792 | |||
| bd235e3ca0 | |||
| a5f8fd32c3 | |||
| 3fa050f153 | |||
| e53c4028c9 | |||
| 5057985024 | |||
| 00f33faebe | |||
| 2eff81ce54 | |||
| 92490aeb9c | |||
| 1b203eeb10 | |||
| ed339b0e2e | |||
| e1feeb6d75 | |||
| 550cdac1eb | |||
| b68ac293c8 | |||
| 42db76a9c7 | |||
| f87755e7d0 | |||
| 8fb58c744e | |||
| ceb8af7e48 | |||
| 71c6ba56ed | |||
| 744b7b3234 | |||
| 3619526af0 | |||
| bb3d1bf7c9 | |||
| bf605539fa | |||
| 12d5902e3c | |||
| 977aa2d1b3 | |||
| 58c7e8f258 | |||
| 7cd458b72f | |||
| dff2de1755 | |||
| d56ae65b56 | |||
| bc0459ab9b | |||
| 7230bcb6f8 | |||
| f07f9dd8b3 | |||
| 5c7fd4330d | |||
| ab696db035 | |||
| 1069630e86 | |||
| 558dff276a | |||
| e0a9205fea | |||
| 24baff2a86 | |||
| 62dbf92b36 | |||
| bbb5181a74 | |||
| 33db478c72 | |||
| d4a835f178 |
15
.devcontainer/devcontainer.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"name": "Open Workshop (Odoo Dev)",
|
||||
"dockerComposeFile": ["${localWorkspaceFolder}/../../odoo/docker-compose.dev.yaml"],
|
||||
"service": "odoo-dev",
|
||||
"workspaceFolder": "/mnt/extra-addons/open_workshop",
|
||||
"runServices": ["odoo-dev", "db"],
|
||||
"shutdownAction": "stopCompose",
|
||||
"remoteUser": "root",
|
||||
"extensions": [
|
||||
"ms-python.python",
|
||||
"ms-python.vscode-pylance"
|
||||
],
|
||||
"forwardPorts": [4338],
|
||||
"postStartCommand": "echo 'Devcontainer started'"
|
||||
}
|
||||
8
.env
|
|
@ -1,8 +0,0 @@
|
|||
ODOO_VERSION=13.0
|
||||
CONTAINER_NAME_EXTENSION=13_dev
|
||||
ODOO_PORT=9013
|
||||
DB_HOST=hobbyhimmel_odoo_13_dev_db
|
||||
DB_PORT=5432
|
||||
DB_USER=odoo
|
||||
DB_PASSWORD=odoo
|
||||
DB_NAME=hobbyhimmel
|
||||
11
.gitignore
vendored
|
|
@ -1,3 +1,7 @@
|
|||
# Test Logs
|
||||
test_*.log
|
||||
|
||||
|
||||
# ---> Python
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
|
|
@ -15,7 +19,7 @@ dist/
|
|||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
#lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
|
|
@ -159,3 +163,8 @@ cython_debug/
|
|||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
# DokuWiki API Test Script (enthält Credentials)
|
||||
test_dokuwiki.py
|
||||
|
||||
# Python Virtual Environment
|
||||
venv/
|
||||
|
|
|
|||
289
.vscode/.jslintrc
vendored
Normal file
|
|
@ -0,0 +1,289 @@
|
|||
{
|
||||
"globals": {
|
||||
"$": false,
|
||||
"_": false,
|
||||
"fuzzy": false,
|
||||
"jQuery": false,
|
||||
"moment": false,
|
||||
"odoo": false,
|
||||
"openerp": false,
|
||||
"self": false
|
||||
},
|
||||
"env": {
|
||||
"browser": true
|
||||
},
|
||||
"rules": {
|
||||
"no-alert": "error",
|
||||
"no-array-constructor": "error",
|
||||
"no-bitwise": "off",
|
||||
"no-caller": "error",
|
||||
"no-case-declarations": "error",
|
||||
"no-catch-shadow": "error",
|
||||
"no-class-assign": "error",
|
||||
"no-cond-assign": "error",
|
||||
"no-confusing-arrow": "error",
|
||||
"no-console": "error",
|
||||
"no-const-assign": "error",
|
||||
"no-constant-condition": "error",
|
||||
"no-continue": "off",
|
||||
"no-control-regex": "error",
|
||||
"no-debugger": "error",
|
||||
"no-delete-var": "error",
|
||||
"no-div-regex": "error",
|
||||
"no-dupe-args": "error",
|
||||
"no-dupe-class-members": "error",
|
||||
"no-dupe-keys": "error",
|
||||
"no-duplicate-case": "error",
|
||||
"no-duplicate-imports": "error",
|
||||
"no-else-return": "error",
|
||||
"no-empty": "error",
|
||||
"no-empty-character-class": "error",
|
||||
"no-empty-function": "error",
|
||||
"no-empty-pattern": "error",
|
||||
"no-eq-null": "error",
|
||||
"no-eval": "error",
|
||||
"no-ex-assign": "error",
|
||||
"no-extend-native": "error",
|
||||
"no-extra-bind": "error",
|
||||
"no-extra-boolean-cast": "error",
|
||||
"no-extra-label": "error",
|
||||
"no-extra-parens": "error",
|
||||
"no-extra-semi": "error",
|
||||
"no-fallthrough": "error",
|
||||
"no-floating-decimal": "error",
|
||||
"no-func-assign": "error",
|
||||
"no-implicit-coercion": ["error", {
|
||||
"allow": ["~"]
|
||||
}],
|
||||
"no-implicit-globals": "error",
|
||||
"no-implied-eval": "error",
|
||||
"no-inline-comments": "error",
|
||||
"no-inner-declarations": "error",
|
||||
"no-invalid-regexp": "error",
|
||||
"no-invalid-this": "off",
|
||||
"no-irregular-whitespace": "error",
|
||||
"no-iterator": "error",
|
||||
"no-label-var": "error",
|
||||
"no-labels": "error",
|
||||
"no-lone-blocks": "error",
|
||||
"no-lonely-if": "error",
|
||||
"no-loop-func": "off",
|
||||
"no-magic-numbers": "off",
|
||||
"no-mixed-operators": "error",
|
||||
"no-mixed-requires": "error",
|
||||
"no-mixed-spaces-and-tabs": "error",
|
||||
"no-multi-spaces": "error",
|
||||
"no-multi-str": "error",
|
||||
"no-multiple-empty-lines": "error",
|
||||
"no-native-reassign": "error",
|
||||
"no-negated-condition": "error",
|
||||
"no-negated-in-lhs": "error",
|
||||
"no-nested-ternary": "off",
|
||||
"no-new": "error",
|
||||
"no-new-func": "error",
|
||||
"no-new-object": "error",
|
||||
"no-new-require": "error",
|
||||
"no-new-symbol": "error",
|
||||
"no-new-wrappers": "error",
|
||||
"no-obj-calls": "error",
|
||||
"no-octal": "error",
|
||||
"no-octal-escape": "error",
|
||||
"no-param-reassign": "error",
|
||||
"no-path-concat": "error",
|
||||
"no-plusplus": "off",
|
||||
"no-process-env": "error",
|
||||
"no-process-exit": "error",
|
||||
"no-proto": "error",
|
||||
"no-prototype-builtins": "error",
|
||||
"no-redeclare": "error",
|
||||
"no-regex-spaces": "error",
|
||||
"no-restricted-globals": "error",
|
||||
"no-restricted-imports": "error",
|
||||
"no-restricted-modules": "error",
|
||||
"no-restricted-syntax": "error",
|
||||
"no-return-assign": "error",
|
||||
"no-script-url": "error",
|
||||
"no-self-assign": "error",
|
||||
"no-self-compare": "error",
|
||||
"no-sequences": "error",
|
||||
"no-shadow": "error",
|
||||
"no-shadow-restricted-names": "error",
|
||||
"no-whitespace-before-property": "error",
|
||||
"no-spaced-func": "error",
|
||||
"no-sparse-arrays": "error",
|
||||
"no-sync": "error",
|
||||
"no-tabs": "error",
|
||||
"no-ternary": "off",
|
||||
"no-trailing-spaces": "error",
|
||||
"no-this-before-super": "error",
|
||||
"no-throw-literal": "error",
|
||||
"no-undef": "error",
|
||||
"no-undef-init": "error",
|
||||
"no-undefined": "off",
|
||||
"no-unexpected-multiline": "error",
|
||||
"no-underscore-dangle": "off",
|
||||
"no-unmodified-loop-condition": "error",
|
||||
"no-unneeded-ternary": "error",
|
||||
"no-unreachable": "error",
|
||||
"no-unsafe-finally": "error",
|
||||
"no-unused-expressions": "error",
|
||||
"no-unused-labels": "error",
|
||||
"no-unused-vars": "error",
|
||||
"no-use-before-define": "error",
|
||||
"no-useless-call": "error",
|
||||
"no-useless-computed-key": "error",
|
||||
"no-useless-concat": "error",
|
||||
"no-useless-constructor": "error",
|
||||
"no-useless-escape": "error",
|
||||
"no-useless-rename": "error",
|
||||
"no-void": "error",
|
||||
"no-var": "off",
|
||||
"no-warning-comments": "off",
|
||||
"no-with": "error",
|
||||
"array-bracket-spacing": "off",
|
||||
"array-callback-return": "error",
|
||||
"arrow-body-style": "error",
|
||||
"arrow-parens": "error",
|
||||
"arrow-spacing": "off",
|
||||
"accessor-pairs": "error",
|
||||
"block-scoped-var": "off",
|
||||
"block-spacing": ["error", "always"],
|
||||
"brace-style": "error",
|
||||
"callback-return": "error",
|
||||
"camelcase": "off",
|
||||
"capitalized-comments": ["error", "always", {
|
||||
"ignoreConsecutiveComments": true,
|
||||
"ignoreInlineComments": true
|
||||
}],
|
||||
"comma-dangle": ["error", "always-multiline"],
|
||||
"comma-spacing": ["error", {
|
||||
"before": false,
|
||||
"after": true
|
||||
}],
|
||||
"comma-style": "error",
|
||||
"complexity": [
|
||||
"error",
|
||||
15
|
||||
],
|
||||
"computed-property-spacing": "off",
|
||||
"consistent-return": "off",
|
||||
"consistent-this": "off",
|
||||
"constructor-super": "error",
|
||||
"curly": "error",
|
||||
"default-case": "off",
|
||||
"dot-location": ["error", "property"],
|
||||
"dot-notation": "error",
|
||||
"eol-last": "error",
|
||||
"eqeqeq": "error",
|
||||
"func-names": "off",
|
||||
"func-style": "off",
|
||||
"generator-star-spacing": "off",
|
||||
"global-require": "error",
|
||||
"guard-for-in": "off",
|
||||
"handle-callback-err": "error",
|
||||
"id-blacklist": "error",
|
||||
"id-length": "off",
|
||||
"id-match": "error",
|
||||
"indent": "error",
|
||||
"init-declarations": "error",
|
||||
"jsx-quotes": "error",
|
||||
"key-spacing": "off",
|
||||
"keyword-spacing": "error",
|
||||
"linebreak-style": [
|
||||
"error",
|
||||
"unix"
|
||||
],
|
||||
"lines-around-comment": "error",
|
||||
"max-depth": "error",
|
||||
"max-len": ["error", {
|
||||
"code": 80,
|
||||
"ignorePattern": "odoo\\.define\\(",
|
||||
"tabWidth": 4
|
||||
}],
|
||||
"max-lines": "off",
|
||||
"max-nested-callbacks": "error",
|
||||
"max-params": "off",
|
||||
"max-statements": "off",
|
||||
"max-statements-per-line": "error",
|
||||
"multiline-ternary": "off",
|
||||
"new-cap": "off",
|
||||
"new-parens": "error",
|
||||
"newline-after-var": "off",
|
||||
"newline-before-return": "off",
|
||||
"newline-per-chained-call": "off",
|
||||
"object-curly-newline": ["error", { "consistent": true }],
|
||||
"object-curly-spacing": ["error", "never"],
|
||||
"object-property-newline": ["error", {
|
||||
"allowAllPropertiesOnSameLine": true
|
||||
}],
|
||||
"object-shorthand": "off",
|
||||
"one-var": "off",
|
||||
"one-var-declaration-per-line": "off",
|
||||
"operator-assignment": "error",
|
||||
"operator-linebreak": "error",
|
||||
"padded-blocks": "off",
|
||||
"prefer-arrow-callback": "off",
|
||||
"prefer-const": "error",
|
||||
"prefer-reflect": "off",
|
||||
"prefer-rest-params": "off",
|
||||
"prefer-spread": "off",
|
||||
"prefer-template": "off",
|
||||
"quote-props": "off",
|
||||
"quotes": "off",
|
||||
"radix": "error",
|
||||
"require-yield": "error",
|
||||
"rest-spread-spacing": "off",
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"semi-spacing": "error",
|
||||
"sort-imports": "error",
|
||||
"sort-vars": "off",
|
||||
"space-before-blocks": "error",
|
||||
"space-before-function-paren": "error",
|
||||
"space-in-parens": "off",
|
||||
"space-infix-ops": "off",
|
||||
"space-unary-ops": "off",
|
||||
"spaced-comment": ["error", "always"],
|
||||
"strict": ["error", "function"],
|
||||
"template-curly-spacing": "off",
|
||||
"unicode-bom": "error",
|
||||
"use-isnan": "error",
|
||||
"valid-jsdoc": ["error", {
|
||||
"prefer": {
|
||||
"arg": "param",
|
||||
"argument": "param",
|
||||
"augments": "extends",
|
||||
"constructor": "class",
|
||||
"exception": "throws",
|
||||
"func": "function",
|
||||
"method": "function",
|
||||
"prop": "property",
|
||||
"return": "returns",
|
||||
"virtual": "abstract",
|
||||
"yield": "yields"
|
||||
},
|
||||
"preferType": {
|
||||
"array": "Array",
|
||||
"bool": "Boolean",
|
||||
"boolean": "Boolean",
|
||||
"number": "Number",
|
||||
"object": "Object",
|
||||
"str": "String",
|
||||
"string": "String"
|
||||
},
|
||||
"requireParamDescription": false,
|
||||
"requireReturn": false,
|
||||
"requireReturnDescription": false,
|
||||
"requireReturnType": false
|
||||
}],
|
||||
"valid-typeof": "error",
|
||||
"vars-on-top": "off",
|
||||
"wrap-iife": "error",
|
||||
"wrap-regex": "error",
|
||||
"yield-star-spacing": "off",
|
||||
"yoda": "error"
|
||||
},
|
||||
"parserOptions": {}
|
||||
}
|
||||
64
.vscode/README.md
vendored
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
# Quickstart: Debugging für open_workshop
|
||||
|
||||
Diese Datei hilft dir, das `open_workshop` Addon schnell in VS Code zu öffnen und sowohl das Addon als auch (optional) den Odoo‑Core zu debuggen.
|
||||
|
||||
Kurzfassung
|
||||
- Öffne in VS Code den Ordner `extra-addons/open_workshop`.
|
||||
- Starte die Development-Container/Composer Umgebung mit `./dev.sh` (Option 1 oder 2).
|
||||
- Verwende die DevContainer‑Funktion oder die Debug‑Konfigurationen unten.
|
||||
|
||||
1) Container starten
|
||||
|
||||
Empfohlen: im Projektstamm (`odoo`) ausführen:
|
||||
|
||||
```bash
|
||||
./dev.sh
|
||||
# Option 1: Normal starten (ODOO_DEV=1)
|
||||
# Option 2: Debug-Modus (ODOO_DEBUG=1) — der Container wartet auf eine Debug-Verbindung
|
||||
```
|
||||
|
||||
2) VS Code als DevContainer verbinden (empfohlen)
|
||||
|
||||
- Öffne VS Code im lokalen Ordner `extra-addons/open_workshop`.
|
||||
- Command Palette → `Dev Containers: Attach to Running Container...` → wähle `odoo-dev`.
|
||||
- VS Code öffnet den Container als Arbeitsumgebung (remoteUser ist `odoo`).
|
||||
- Öffne dann das Workspace‑Verzeichnis `/mnt/extra-addons/open_workshop`.
|
||||
|
||||
Vorteil: Du arbeitest direkt im Container (kein lokales Kopieren der Core‑Sourcen nötig) und Breakpoints funktionieren zuverlässig.
|
||||
|
||||
3) Debugging (Attach)
|
||||
|
||||
- Wenn du als DevContainer verbunden bist, verwende die Debug‑Konfiguration "Odoo Attach (container)" (port 5678).
|
||||
- Wenn du lokal arbeitest und den Host‑Port benutzt, verwende "Odoo Attach (host)" (port 4338).
|
||||
|
||||
4) Pfad‑Mapping
|
||||
|
||||
- Die `launch.json` enthält Pfadabbildungen:
|
||||
- `${workspaceFolder}` ↔ `/mnt/extra-addons/open_workshop` (dein Addon)
|
||||
- `${workspaceFolder}/../../odoo-source/odoo` ↔ `/usr/lib/python3/dist-packages/odoo` (falls du lokal eine Kopie des Odoo‑Cores hast)
|
||||
|
||||
Hinweis: Lokale `odoo-source` ist nicht erforderlich, wenn du per DevContainer arbeitest, weil VS Code die Dateien direkt im Container liest.
|
||||
|
||||
5) Troubleshooting
|
||||
|
||||
- Debug-Verbindung schlägt fehl: prüfe, ob der Container im Debug‑Modus läuft und der Port gemappt ist:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.dev.yaml ps
|
||||
docker compose -f docker-compose.dev.yaml logs -f odoo-dev
|
||||
```
|
||||
|
||||
- VS Code meldet, dass Breakpoints nicht aufgelöst werden: vergewissere dich, dass die `pathMappings` korrekt sind und die lokalen Dateien existieren (oder nutze DevContainer).
|
||||
|
||||
6) Image/Compose aktualisieren
|
||||
|
||||
- Wenn du `Dockerfile.Dev` geändert hast: neu bauen (Option 3 in `./dev.sh`), dann Container neu starten (Option 5 oder down/up).
|
||||
|
||||
7) Kurze Checklist für Mitentwickler
|
||||
|
||||
- `./dev.sh` → Option 3 (einmalig) wenn du das Dev‑Image bauen musst.
|
||||
- `./dev.sh` → Option 1 oder 2 zum Starten.
|
||||
- In VS Code: Öffne `extra-addons/open_workshop`, dann `Dev Containers: Attach to Running Container...`.
|
||||
- Starte Debug mit "Odoo Attach (container)" oder "Odoo Attach (host)".
|
||||
|
||||
|
||||
45
.vscode/launch.json
vendored
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Odoo Attach (host)",
|
||||
"type": "debugpy",
|
||||
"request": "attach",
|
||||
"connect": {
|
||||
"host": "localhost",
|
||||
"port": 4338
|
||||
},
|
||||
"pathMappings": [
|
||||
{
|
||||
"localRoot": "${workspaceFolder}",
|
||||
"remoteRoot": "/mnt/extra-addons/open_workshop"
|
||||
},
|
||||
{
|
||||
"localRoot": "${workspaceFolder}/../../odoo-source/odoo",
|
||||
"remoteRoot": "/usr/lib/python3/dist-packages/odoo"
|
||||
}
|
||||
],
|
||||
"justMyCode": false
|
||||
},
|
||||
{
|
||||
"name": "Odoo Attach (container)",
|
||||
"type": "debugpy",
|
||||
"request": "attach",
|
||||
"connect": {
|
||||
"host": "localhost",
|
||||
"port": 5678
|
||||
},
|
||||
"pathMappings": [
|
||||
{
|
||||
"localRoot": "${workspaceFolder}",
|
||||
"remoteRoot": "/mnt/extra-addons/open_workshop"
|
||||
},
|
||||
{
|
||||
"localRoot": "${workspaceFolder}/../../odoo-source/odoo",
|
||||
"remoteRoot": "/usr/lib/python3/dist-packages/odoo"
|
||||
}
|
||||
],
|
||||
"justMyCode": false
|
||||
}
|
||||
]
|
||||
}
|
||||
30
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"python.pythonPath": "/usr/bin/python3",
|
||||
"python.analysis.extraPaths": [
|
||||
"${workspaceFolder}",
|
||||
"${workspaceFolder}/../../odoo-source/odoo"
|
||||
],
|
||||
"python.analysis.typeCheckingMode": "off"
|
||||
}
|
||||
{
|
||||
"editor.rulers": [80, 100, 120],
|
||||
"files.eol": "\n",
|
||||
"[python]": {
|
||||
"editor.defaultFormatter": "ms-python.python",
|
||||
"editor.insertSpaces": true,
|
||||
"editor.tabSize": 4
|
||||
},
|
||||
"[javascript]": {
|
||||
"editor.insertSpaces": true,
|
||||
"editor.tabSize": 2
|
||||
},
|
||||
"python.analysis.extraPaths": [
|
||||
"../../odoo-source/odoo",
|
||||
],
|
||||
"python.testing.pytestArgs": [
|
||||
"--odoo-http",
|
||||
"."
|
||||
],
|
||||
"python.testing.pytestEnabled": false,
|
||||
"python.testing.unittestEnabled": false
|
||||
}
|
||||
68
.vscode/tasks.json
vendored
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "start-odoo-dev",
|
||||
"type": "shell",
|
||||
"command": "docker",
|
||||
"args": ["compose", "-f", "${workspaceFolder}/../../odoo/docker-compose.dev.yaml", "up", "-d"],
|
||||
"options": {
|
||||
"env": {
|
||||
"ODOO_DEV": "1"
|
||||
}
|
||||
},
|
||||
"group": { "kind": "build", "isDefault": true },
|
||||
"presentation": { "echo": true, "reveal": "always", "focus": false, "panel": "shared" },
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "start-odoo-debug",
|
||||
"type": "shell",
|
||||
"command": "docker",
|
||||
"args": ["compose", "-f", "${workspaceFolder}/../../odoo/docker-compose.dev.yaml", "up", "-d"],
|
||||
"options": {
|
||||
"env": {
|
||||
"ODOO_DEBUG": "1"
|
||||
}
|
||||
},
|
||||
"group": "build",
|
||||
"presentation": { "echo": true, "reveal": "always", "focus": false, "panel": "shared" },
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "rebuild-odoo-dev",
|
||||
"type": "shell",
|
||||
"command": "docker",
|
||||
"args": ["compose", "-f", "${workspaceFolder}/../../odoo/docker-compose.dev.yaml", "up", "--build", "-d"],
|
||||
"group": "build",
|
||||
"presentation": { "echo": true, "reveal": "always", "focus": false, "panel": "shared" },
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "stop-odoo-dev",
|
||||
"type": "shell",
|
||||
"command": "docker",
|
||||
"args": ["compose", "-f", "${workspaceFolder}/../..odoo/docker-compose.dev.yaml", "down"],
|
||||
"group": "build",
|
||||
"presentation": { "echo": true, "reveal": "always", "focus": false, "panel": "shared" },
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "odoo-logs",
|
||||
"type": "shell",
|
||||
"command": "docker",
|
||||
"args": ["compose", "-f", "${workspaceFolder}/../../odoo/docker-compose.dev.yaml", "logs", "-f", "odoo-dev"],
|
||||
"group": "test",
|
||||
"presentation": { "echo": true, "reveal": "always", "focus": false, "panel": "shared" },
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "shell-odoo",
|
||||
"type": "shell",
|
||||
"command": "docker",
|
||||
"args": ["compose", "-f", "${workspaceFolder}/../../odoo/docker-compose.dev.yaml", "exec", "odoo-dev", "bash"],
|
||||
"presentation": { "echo": true, "reveal": "always", "focus": true, "panel": "shared" },
|
||||
"problemMatcher": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,41 +1,43 @@
|
|||
# ✅ OpenWorkshop Test Checkliste
|
||||
|
||||
## 🔹 1. Migration
|
||||
- [ ] `migrate_existing_partners()` erzeugt zu jedem Partner genau einen `ows.user`.
|
||||
- [ ] `migrate_existing_partners()` übernimmt korrekt alte `vvow_*`-Felder.
|
||||
- [ ] `migrate_machine_access_from_old_fields()` erstellt korrekte Einträge in `ows.machine.access`.
|
||||
- [ ] `migrate_machine_access_from_old_fields()` übernimmt das Änderungsdatum aus `mail.tracking.value`.
|
||||
- [x] `migrate_existing_partners()` erzeugt zu jedem Partner genau einen `ows.user`.
|
||||
- [x] `migrate_existing_partners()` übernimmt korrekt alte `vvow_*`-Felder.
|
||||
- [x] `migrate_machine_access_from_old_fields()` erstellt korrekte Einträge in `ows.machine.access`.
|
||||
- [x] `migrate_machine_access_from_old_fields()` übernimmt das Änderungsdatum aus `mail.tracking.value`.
|
||||
|
||||
## 🔹 2. Kontakte Backend
|
||||
- [ ] Beim Anlegen eines neuen Partners wird automatisch ein `ows.user` angelegt.
|
||||
- [ ] Änderungen an Geburtstag, RFID, Haftung in Partner-Formular schreiben korrekt in `ows.user`.
|
||||
- [ ] Die Werte aus `ows.user` werden korrekt im Partnerformular angezeigt (via `compute`).
|
||||
- [ ] Das HTML-Widget mit Maschinenfreigaben (`machine_access_html`) wird korrekt dargestellt.
|
||||
- [x] Beim Anlegen eines neuen Partners wird automatisch ein `ows.user` angelegt.
|
||||
- [ ] Beim Speichern eines Partners wird überprüft, ob ein `ows.user` existiert, und ggf. angelegt.
|
||||
- [x] Änderungen an Geburtstag, RFID, Haftung in Partner-Formular schreiben korrekt in `ows.user`.
|
||||
- [x] Die Werte aus `ows.user` werden korrekt im Partnerformular angezeigt (via `compute`).
|
||||
- [x] Das HTML-Widget mit Maschinenfreigaben (`machine_access_html`) wird korrekt dargestellt.
|
||||
|
||||
## 🔹 3. POS-Integration
|
||||
- [ ] Felder aus `ows.user` (Geburtstag, RFID etc.) erscheinen im POS-Kunden-Popup.
|
||||
- [ ] Maschinenfreigaben erscheinen im POS-Layout korrekt gruppiert nach Bereichen.
|
||||
- [ ] Farben der Maschinenbereiche werden korrekt aus `color_hex` übernommen.
|
||||
- [x] Felder aus `ows.user` (Geburtstag, RFID etc.) erscheinen im POS-Kunden-Popup.
|
||||
- [x] Maschinenfreigaben erscheinen im POS-Layout korrekt gruppiert nach Bereichen.
|
||||
- [x] Farben der Maschinenbereiche werden korrekt aus `color_hex` übernommen.
|
||||
|
||||
## 🔹 4. Maschinenverwaltung
|
||||
- [ ] Maschinen-Formular zeigt Nutzungs- und Einweisungsprodukte korrekt an.
|
||||
- [ ] Drop-downs in den Produktlisten zeigen nur Produkte der richtigen Kategorie.
|
||||
- [ ] Neue Zuordnungen können direkt in den Tree-Ansichten editiert werden.
|
||||
- [ ] Filter greifen korrekt (Maschinennutzung / Einweisungen).
|
||||
- [x] Maschinen-Formular zeigt Nutzungs- und Einweisungsprodukte korrekt an.
|
||||
- [?] Drop-downs in den Produktlisten zeigen nur Produkte der richtigen Kategorie.
|
||||
- [?] Neue Zuordnungen können direkt in den Tree-Ansichten editiert werden.
|
||||
- [?] Filter greifen korrekt (Maschinennutzung / Einweisungen).
|
||||
- [ ] In der Ansicht Wartung - Ausrüstung - Tab Offene Werkstatt (hobbyhimmel) wird unter Nutzungsprodukte und Einweisungsprodukte nur die Auswahl des Produkts angezeigt, eine Zuordnung zu einer Maschine ist automatisch die aktelle ausgewählte Maschine des Formulars.
|
||||
|
||||
## 🔹 5. Menüstruktur
|
||||
- [ ] Menüeinträge "Nutzungsprodukte" und "Einweisungsprodukte" erscheinen unter Konfiguration > Maschinen.
|
||||
- [ ] Klick auf "Alle Maschinen" öffnet die erwartete Listenansicht.
|
||||
- [ ] Menüeinträge "Nutzungsprodukte" und "Einweisungsprodukte" erscheinen unter Wartung > Konfiguration > Zuordnungen.
|
||||
|
||||
|
||||
## 🔹 6. CSV/XML Demo-/Initialdaten
|
||||
- [ ] Maschinenbereiche (`ows.machine.area`) sind korrekt aus `data.xml` geladen.
|
||||
- [ ] Maschinen und ihre Produkt-Zuordnungen sind vollständig.
|
||||
- [ ] Kategorien und Produkte sind korrekt verknüpft (`product.category`, `product.product`).
|
||||
- [x] Maschinenbereiche (`ows.machine.area`) sind korrekt aus `data.xml` geladen.
|
||||
- [x] Maschinen und ihre Produkt-Zuordnungen sind vollständig.
|
||||
- [x] Kategorien und Produkte sind korrekt verknüpft (`product.category`, `product.product`).
|
||||
|
||||
## 🔹 7. Systemweite Konsistenz
|
||||
- [ ] Es gibt keine doppelten `ows.user`-Einträge.
|
||||
- [ ] Kein Partner existiert ohne zugehörigen `ows.user`.
|
||||
- [ ] `res.partner.ows_user_id` ist immer gefüllt.
|
||||
- [x] Es gibt keine doppelten `ows.user`-Einträge.
|
||||
- [bedingt] Kein Partner existiert ohne zugehörigen `ows.user`.
|
||||
- [bedingt] `res.partner.ows_user_id` ist immer gefüllt.
|
||||
|
||||
## 🔹 8. Technische Qualität
|
||||
- [ ] Kein `@api.depends('id')` mehr vorhanden.
|
||||
|
|
|
|||
178
FEATURE_REQUEST/odoo_wp_architektur_empfehlung.md
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
# Odoo vs WordPress vs Odoo.sh – Architektur- & Strategie-Empfehlung
|
||||
*Für HobbyHimmel / FabLab – Traffic: ~1000 Besucher, 2000 Views pro Tag*
|
||||
|
||||
---
|
||||
|
||||
# 🚀 Zusammenfassung (TL;DR)
|
||||
|
||||
| Bereich | Empfehlung |
|
||||
|--------|------------|
|
||||
| Öffentliche Website | **WordPress behalten** |
|
||||
| Odoo als öffentliche Website | **Nicht geeignet** (Performance + Security) |
|
||||
| Odoo.sh | **Nicht geeignet** (Benutzerkosten, Einschränkungen) |
|
||||
| Odoo self-hosted | **Optimal für interne Prozesse** |
|
||||
| Integration | **WordPress ↔ Odoo via REST API** |
|
||||
| Zielarchitektur | **WP Frontend + Odoo Backend** |
|
||||
|
||||
---
|
||||
|
||||
# 1. Zielsetzung
|
||||
|
||||
HobbyHimmel benötigt:
|
||||
|
||||
- Eine öffentliche Website (ca. 1000 Besucher / 2000 Views täglich)
|
||||
- Interne Verwaltungsprozesse (Maschinen, Einweisungen, Kurse, POS)
|
||||
- Möglichkeit, ausgewählte Daten (Maschinenstatus, Kurstermine…) öffentlich darzustellen
|
||||
- Möglichst geringe Kosten
|
||||
- Hohe Sicherheit (keine offene interne Datenbank)
|
||||
|
||||
---
|
||||
|
||||
# 2. Analyse der drei Optionen
|
||||
|
||||
## 2.1 WordPress (beste Lösung für öffentliche Webseite)
|
||||
|
||||
**Stärken:**
|
||||
- Sehr schneller Seitenaufbau, Caching, CDN
|
||||
- Perfekt für hohe Besucherzahlen
|
||||
- Themes, SEO, Plug-ins
|
||||
- Komplett getrennt vom internen FabLab-System
|
||||
|
||||
**Schwächen:**
|
||||
- Kein direkter Zugriff auf Odoo-Daten (muss über API geschehen)
|
||||
|
||||
**Fazit:**
|
||||
⭐⭐⭐⭐⭐ **Beste Lösung für alle öffentlichen Besucher.**
|
||||
|
||||
---
|
||||
|
||||
## 2.2 Odoo (self-hosted, intern)
|
||||
|
||||
**Stärken:**
|
||||
- Ideal für Maschinenverwaltung, POS, Maintenance, Events
|
||||
- Volle Freiheit, beliebige Module zu installieren
|
||||
- Keine Lizenzkosten für Ehrenamtliche, solange sie keinen Login benötigen
|
||||
- Perfekt für interne Daten & Prozesse
|
||||
|
||||
**Schwächen:**
|
||||
- Als öffentliche Website ungeeignet (Performance, kein CDN)
|
||||
- DSL-Upload wäre Flaschenhals
|
||||
|
||||
**Fazit:**
|
||||
⭐⭐⭐⭐⭐ **Beste Lösung für interne Prozesse.
|
||||
Nicht geeignet als öffentliche Website.**
|
||||
|
||||
---
|
||||
|
||||
## 2.3 Odoo.sh
|
||||
|
||||
**Vorteile:**
|
||||
- Kein DSL-Problem
|
||||
- Hosting, Deployment & Backups werden automatisiert
|
||||
|
||||
**ABER:**
|
||||
- **sehr hohe Kosten** bei >10 Ehrenamtlichen (pro Benutzer Lizenzpflicht)
|
||||
- **stark eingeschränkt** bei Custom-Code, Addons, APIs
|
||||
- Performance besser, aber **immer noch weit unter WordPress**
|
||||
- Website-Teil ist **nicht auf hohen Traffic optimiert**
|
||||
|
||||
**Fazit:**
|
||||
⭐⭐ **Für HobbyHimmel ungeeignet.**
|
||||
Zu teuer, zu eingeschränkt, keine Website-Performance wie WP.
|
||||
|
||||
---
|
||||
|
||||
# 3. Performance-Bewertung
|
||||
|
||||
| System | Performance für 1000 Besucher/Tag |
|
||||
|--------|-----------------------------------|
|
||||
| WordPress (mit CDN) | ⭐⭐⭐⭐⭐ |
|
||||
| Odoo.sh Website | ⭐⭐ |
|
||||
| Odoo self-hosted Website über DSL | ⭐ (Upload-Limit!) |
|
||||
|
||||
WordPress ist **um Größenordnungen** besser für Public Traffic.
|
||||
|
||||
---
|
||||
|
||||
# 4. Sicherheitsbewertung
|
||||
|
||||
| System | Risiko bei Public Exposure |
|
||||
|--------|----------------------------|
|
||||
| WordPress | gering (gefiltert, gehärtet, cached) |
|
||||
| Odoo Website | hoch (direkter Zugriff auf Business-Daten) |
|
||||
| Odoo API | gering (nur ausgewählte Daten, stark begrenzt) |
|
||||
|
||||
Odoo sollte **nicht** öffentlich zugänglich sein, außer über **stark eingeschränkte REST API**.
|
||||
|
||||
---
|
||||
|
||||
# 5. Kostenbewertung
|
||||
|
||||
| Option | Monatliche Kosten | Skalierbarkeit |
|
||||
|--------|-------------------|----------------|
|
||||
| WordPress gehostet | 5–20 € | sehr gut |
|
||||
| Odoo self-hosted | ~0–20 € | sehr gut |
|
||||
| **Odoo.sh** | **ab ~40 € pro Benutzer** | schlecht bei vielen Ehrenamtlichen |
|
||||
|
||||
**Odoo.sh wäre ruinös teuer für ein FabLab.**
|
||||
|
||||
---
|
||||
|
||||
# 6. Empfohlene Zielarchitektur
|
||||
|
||||
```
|
||||
Internet
|
||||
|
|
||||
| 1000 Besucher/Tag
|
||||
v
|
||||
WordPress Website (öffentlich)
|
||||
|
|
||||
| REST-API (lesend!)
|
||||
v
|
||||
Odoo API Gateway (open_workshop_api)
|
||||
|
|
||||
| internal-only
|
||||
v
|
||||
Odoo Backend (POS, Maintenance, Trainings, Events)
|
||||
```
|
||||
|
||||
**Vorteile:**
|
||||
- Odoo bleibt geschützt im LAN oder VPN
|
||||
- Kein DSL-Upload-Limit problematisch → API ist sehr leichtgewichtig
|
||||
- WordPress ist ultraschnell & SEO-stark
|
||||
- Odoo liefert Daten dynamisch an WP (z. B. Maschinenliste, Kurse)
|
||||
- Keine Benutzerkosten für Ehrenamtliche
|
||||
|
||||
---
|
||||
|
||||
# 7. Fazit
|
||||
|
||||
Die beste Lösung für HobbyHimmel besteht aus einer Kombination:
|
||||
|
||||
# 🟩 **WordPress für öffentliche Website**
|
||||
# 🟩 **Odoo (self-hosted) für Verwaltung & Prozesse**
|
||||
# 🟩 **API-Modul für WordPress-Anbindung**
|
||||
# 🟥 **Keine Odoo Website für Öffentlichkeit**
|
||||
# 🟥 **Kein Odoo.sh wegen hoher Lizenzkosten**
|
||||
|
||||
Diese Architektur ist:
|
||||
|
||||
- Sicher
|
||||
- Schnell
|
||||
- Zukunftssicher
|
||||
- Kosteneffizient
|
||||
- Flexibel
|
||||
- Perfekt für ein FabLab mit vielen Ehrenamtlichen
|
||||
|
||||
---
|
||||
|
||||
# 8. Nächste Schritte
|
||||
|
||||
Wenn gewünscht, kann ich liefern:
|
||||
|
||||
### ✔ Architekturdiagramm (grafisch)
|
||||
### ✔ open_workshop_api Modul als Odoo-Modul-Skeleton
|
||||
### ✔ WordPress-Shortcodes oder Plugin
|
||||
### ✔ Sicherheitsempfehlungen (CORS, Token, Firewalls)
|
||||
### ✔ Optimierungskonzept für den späteren Betrieb
|
||||
|
||||
323
FEATURE_REQUEST/open_workshop_IoT.md
Normal file
|
|
@ -0,0 +1,323 @@
|
|||
# Projektplan: MQTT-basierte IoT-Events in Odoo 18 Community (Simulation-first)
|
||||
|
||||
Stand: 2026-01-10
|
||||
Ziel: **Odoo-18-Community (self-hosted)** soll **Geräte-Events** (Timer/Maschinenlaufzeit, Waage, Zustandswechsel) über **MQTT** aufnehmen und in Odoo als **saubere, nachvollziehbare Sessions/Events** verarbeiten.
|
||||
Wichtig: **Hardware wird zunächst vollständig simuliert** (Software-Simulatoren), damit die Odoo-Schnittstelle stabil steht, bevor echte Geräte angebunden werden.
|
||||
|
||||
---
|
||||
|
||||
## 1. Ziele und Nicht-Ziele
|
||||
|
||||
### 1.1 Ziele (MVP)
|
||||
- Einheitliche **Device-Event-Schnittstelle** in Odoo (REST/Webhook) inkl. Authentifizierung
|
||||
- **Event-Log** in Odoo (persistente Rohereignisse + Normalisierung)
|
||||
- **Session-Logik** für Timer/Maschinenlaufzeit (Start/Stop/Heartbeat)
|
||||
- **Simulation** von:
|
||||
- Maschinenzustand (running/idle/fault)
|
||||
- Timer-Events (run_start/run_stop)
|
||||
- Waage (stable_weight/tare)
|
||||
- Reproduzierbare Tests (Unit/Integration) und eindeutige Fehlerdiagnostik (Logging)
|
||||
|
||||
### 1.2 Nicht-Ziele (für Phase 1)
|
||||
- Keine Enterprise-IoT-Box, keine Enterprise-Module
|
||||
- Keine echte Hardware-Treiberentwicklung in Phase 1
|
||||
- Kein POS-Frontend-Live-Widget (optional erst in späterer Phase)
|
||||
- Keine Abrechnungslogik/Preisregeln (kann vorbereitet, aber nicht umgesetzt werden)
|
||||
|
||||
---
|
||||
|
||||
## 2. Zielarchitektur (Simulation-first)
|
||||
|
||||
### 2.1 Komponenten
|
||||
1. **MQTT Broker** (z. B. Mosquitto in Docker)
|
||||
2. **Device Simulator(s)** (Python/Node)
|
||||
- veröffentlicht MQTT Topics wie echte Geräte
|
||||
3. **Gateway/Bridge (Software)**
|
||||
- abonniert MQTT Topics
|
||||
- validiert/normalisiert Payload
|
||||
- sendet Events via HTTPS an Odoo (Webhook)
|
||||
4. **Odoo Modul** `ows_iot_bridge`
|
||||
- REST Controller `/ows/iot/event`
|
||||
- Modelle für Devices, Events, Sessions
|
||||
- Business-Regeln für Session-Start/Stop/Hysterese (softwareseitig)
|
||||
5. Optional später: **Realtime-Feed** (WebSocket) für POS/Display
|
||||
|
||||
### 2.2 Datenfluss
|
||||
1. Simulator publiziert MQTT → `hobbyhimmel/machines/<machine_id>/state`
|
||||
2. Bridge konsumiert MQTT, mappt auf Event-Format
|
||||
3. Bridge POSTet JSON an Odoo Endpoint
|
||||
4. Odoo speichert Roh-Event + erzeugt ggf. Session-Updates
|
||||
|
||||
---
|
||||
|
||||
## 3. Schnittstelle zu Odoo (Kern des Projekts)
|
||||
|
||||
### 3.1 Authentifizierung
|
||||
- Pro Device oder pro Bridge ein **API-Token** (Bearer)
|
||||
- Header: `Authorization: Bearer <token>`
|
||||
- Token in Odoo in `ows.iot.device` oder `ir.config_parameter` hinterlegt
|
||||
- Optional: IP-Allowlist / Rate-Limit (in Reverse Proxy)
|
||||
|
||||
### 3.2 Endpoint (REST/Webhook)
|
||||
- `POST /ows/iot/event`
|
||||
- Content-Type: `application/json`
|
||||
- Antwort:
|
||||
- `200 OK` mit `{ "status": "ok", "event_id": "...", "session_id": "..." }`
|
||||
- Fehler: `400` (Schema), `401/403` (Auth), `409` (Duplikat), `500` (Server)
|
||||
|
||||
### 3.3 Idempotenz / Duplikaterkennung
|
||||
- Jedes Event enthält eine eindeutige `event_uid`
|
||||
- Odoo legt Unique-Constraint auf `event_uid` (pro device)
|
||||
- Wiederholte Events liefern `409 Conflict` oder `200 OK (duplicate=true)` (Designentscheidung)
|
||||
|
||||
---
|
||||
|
||||
## 4. Ereignis- und Topic-Standard (Versioniert)
|
||||
|
||||
### 4.1 MQTT Topics (v1)
|
||||
- Maschinenzustand:
|
||||
`hobbyhimmel/machines/<machine_id>/state`
|
||||
- Maschinenereignisse:
|
||||
`hobbyhimmel/machines/<machine_id>/event`
|
||||
- Waage:
|
||||
`hobbyhimmel/scales/<scale_id>/event`
|
||||
- Geräte-Status (optional):
|
||||
`hobbyhimmel/devices/<device_id>/status`
|
||||
|
||||
### 4.2 Gemeinsames JSON Event Schema (v1)
|
||||
Pflichtfelder:
|
||||
- `schema_version`: `"v1"`
|
||||
- `event_uid`: UUID/string
|
||||
- `ts`: ISO-8601 UTC (z. B. `"2026-01-10T12:34:56Z"`)
|
||||
- `source`: `"simulator" | "device" | "gateway"`
|
||||
- `device_id`: string
|
||||
- `entity_type`: `"machine" | "scale" | "sensor"`
|
||||
- `entity_id`: string (z. B. machine_id)
|
||||
- `event_type`: string (siehe unten)
|
||||
- `payload`: object
|
||||
- `confidence`: `"high" | "medium" | "low"` (für Sensorfusion)
|
||||
|
||||
### 4.3 Event-Typen (v1)
|
||||
**Maschine/Timer**
|
||||
- `run_start` (Start einer Laufphase)
|
||||
- `run_stop` (Ende einer Laufphase)
|
||||
- `heartbeat` (optional, laufend während running)
|
||||
- `state_changed` (idle/running/fault)
|
||||
- `fault` (Fehler mit Code/Severity)
|
||||
|
||||
**Waage**
|
||||
- `stable_weight` (stabiler Messwert)
|
||||
- `weight` (laufend)
|
||||
- `tare`
|
||||
- `zero`
|
||||
- `error`
|
||||
|
||||
---
|
||||
|
||||
## 5. Odoo Datenmodell (Vorschlag)
|
||||
|
||||
### 5.1 `ows.iot.device`
|
||||
- `name`
|
||||
- `device_id` (unique)
|
||||
- `token_hash` (oder Token in separater Tabelle)
|
||||
- `device_type` (machine/scale/...)
|
||||
- `active`
|
||||
- `last_seen`
|
||||
- `notes`
|
||||
|
||||
### 5.2 `ows.iot.event`
|
||||
- `event_uid` (unique)
|
||||
- `device_id` (m2o -> device)
|
||||
- `entity_type`, `entity_id`
|
||||
- `event_type`
|
||||
- `timestamp`
|
||||
- `payload_json` (Text/JSON)
|
||||
- `confidence`
|
||||
- `processing_state` (new/processed/error)
|
||||
- `session_id` (m2o optional)
|
||||
|
||||
### 5.3 `ows.machine.session` (Timer-Sessions)
|
||||
- `machine_id` (Char oder m2o auf bestehendes Maschinenmodell)
|
||||
- `start_ts`, `stop_ts`
|
||||
- `duration_s` (computed)
|
||||
- `state` (running/stopped/aborted)
|
||||
- `origin` (sensor/manual/sim)
|
||||
- `confidence_summary`
|
||||
- `event_ids` (o2m)
|
||||
|
||||
> Hinweis: Wenn du bereits `ows.machine` aus deinem open_workshop nutzt, referenziert `machine_id` direkt dieses Modell.
|
||||
|
||||
---
|
||||
|
||||
## 6. Verarbeitungslogik (Phase 1: minimal, robust)
|
||||
|
||||
### 6.1 Session-Automat (State Machine)
|
||||
- Eingang: Events `run_start`, `run_stop`, optional `heartbeat`
|
||||
- Regeln:
|
||||
- `run_start` erstellt neue Session, wenn keine läuft
|
||||
- `run_start` während laufender Session → nur Log, keine neue Session
|
||||
- `run_stop` schließt laufende Session
|
||||
- Timeout-Regel: wenn `heartbeat` ausbleibt (z. B. 10 min) → Session `aborted`
|
||||
|
||||
### 6.2 Hysterese (Simulation als Stellvertreter für Sensorik)
|
||||
In Simulation definierst du:
|
||||
- Start, wenn „running“ mindestens **2s stabil**
|
||||
- Stop, wenn „idle“ mindestens **15s stabil**
|
||||
Diese Parameter als Odoo Systemparameter, z. B.:
|
||||
- `ows_iot.start_debounce_s`
|
||||
- `ows_iot.stop_debounce_s`
|
||||
|
||||
---
|
||||
|
||||
## 7. Simulation (Software statt Hardware)
|
||||
|
||||
### 7.1 Device Simulator: Maschine
|
||||
- Konfigurierbar:
|
||||
- Muster: random, fixed schedule, manuell per CLI
|
||||
- Zustände: idle/running/fault
|
||||
- Optional: „power_w“ und „vibration“ als Felder im Payload
|
||||
- Publiziert MQTT in realistischen Intervallen
|
||||
|
||||
### 7.2 Device Simulator: Waage
|
||||
- Modi:
|
||||
- stream weight (mehrfach pro Sekunde)
|
||||
- stable_weight nur auf „stabil“
|
||||
- tare/zero Events per CLI
|
||||
|
||||
### 7.3 Bridge Simulator (MQTT → Odoo)
|
||||
- Abonniert alle relevanten Topics
|
||||
- Validiert Schema v1
|
||||
- POSTet Events an Odoo
|
||||
- Retry-Queue (lokal) bei Odoo-Ausfall
|
||||
- Metriken/Logs:
|
||||
- gesendete Events, Fehlerquoten, Latenz
|
||||
|
||||
---
|
||||
|
||||
## 8. Milestones & Deliverables
|
||||
|
||||
### M0 – Repo/Grundgerüst (0.5–1 Tag)
|
||||
**Deliverables**
|
||||
- Git-Repo Struktur
|
||||
- Docker Compose: mosquitto + simulator + bridge
|
||||
- Odoo Modul Skeleton `ows_iot_bridge`
|
||||
|
||||
### M1 – Odoo Endpoint & Modelle (1–2 Tage)
|
||||
**Deliverables**
|
||||
- `/ows/iot/event` Controller inkl. Token-Auth
|
||||
- Modelle `ows.iot.device`, `ows.iot.event`
|
||||
- Admin UI: Geräte anlegen, Token setzen, letzte Events anzeigen
|
||||
|
||||
### M2 – Session-Engine (1–2 Tage)
|
||||
**Deliverables**
|
||||
- Modell `ows.machine.session`
|
||||
- Event → Session Zuordnung
|
||||
- Timeout/Abbruch-Logik
|
||||
- Parameter für Debounce/Timeout
|
||||
|
||||
### M3 – Simulatoren & End-to-End Test (1–2 Tage)
|
||||
**Deliverables**
|
||||
- Machine Simulator + Scale Simulator
|
||||
- Bridge mit Retry-Queue
|
||||
- End-to-End: Simulator → MQTT → Bridge → Odoo → Session entsteht
|
||||
|
||||
### M4 – Qualität & Betrieb (1 Tag)
|
||||
**Deliverables**
|
||||
- Logging-Konzept (Odoo + Bridge)
|
||||
- Tests (Unit: Controller/Auth, Integration: Session Engine)
|
||||
- Doku: Event-Schema v1, Topics, Beispielpayloads
|
||||
|
||||
### Optional M5 – POS/Anzeige (später)
|
||||
- Realtime Anzeige im POS oder auf Display
|
||||
- Live-Weight in POS
|
||||
|
||||
---
|
||||
|
||||
## 9. Testplan (Simulation-first)
|
||||
|
||||
### 9.1 Unit Tests (Odoo)
|
||||
- Auth: gültiger Token → 200
|
||||
- ungültig/fehlend → 401/403
|
||||
- Schema-Validation → 400
|
||||
- Idempotenz: duplicate `event_uid` → 409 oder duplicate-flag
|
||||
|
||||
### 9.2 Integration Tests
|
||||
- Sequenz: start → heartbeat → stop → Session duration plausibel
|
||||
- stop ohne start → kein Crash, Event loggt Fehlerzustand
|
||||
- Timeout: start → keine heartbeat → Session aborted
|
||||
|
||||
### 9.3 Last-/Stabilitätstest (Simulator)
|
||||
- 20 Maschinen, je 1 Event/s, 1h Lauf
|
||||
- Ziel: Odoo bleibt stabil, Event-Insert performant, Queue läuft nicht über
|
||||
|
||||
---
|
||||
|
||||
## 10. Betriebs- und Sicherheitskonzept
|
||||
|
||||
- Token-Rotation möglich (neues Token, altes deaktivieren)
|
||||
- Reverse Proxy:
|
||||
- HTTPS
|
||||
- Rate limiting auf `/ows/iot/event`
|
||||
- optional Basic WAF Regeln
|
||||
- Payload-Größenlimit (z. B. 16–64 KB)
|
||||
- Bridge persistiert Retry-Queue auf Disk
|
||||
|
||||
---
|
||||
|
||||
## 11. Offene Entscheidungen (später, nicht blocker für MVP)
|
||||
|
||||
- Event-Consumer in Odoo vs. Bridge-only Webhook (empfohlen: Webhook)
|
||||
- Genaues Mapping zu bestehenden open_workshop Modellen (`ows.machine`, Nutzer/RFID)
|
||||
- Abrechnung: Preisregeln, Rundungen, POS-Integration
|
||||
- Realtime: Odoo Bus vs. eigener WebSocket Service
|
||||
|
||||
---
|
||||
|
||||
## 12. Nächste Schritte (konkret)
|
||||
|
||||
1. `ows_iot_bridge` Modul: Endpoint + Device/Event Modelle implementieren
|
||||
2. Docker Compose: mosquitto + bridge + simulator bereitstellen
|
||||
3. Simulatoren produzieren v1-Events; Bridge sendet an Odoo
|
||||
4. Session-Engine anschließen und End-to-End testen
|
||||
|
||||
---
|
||||
|
||||
## Anhang A: Beispiel-Event (Maschine run_start)
|
||||
```json
|
||||
{
|
||||
"schema_version": "v1",
|
||||
"event_uid": "c2a7d6f1-7d8d-4a63-9a7f-4e6d7b0d9e2a",
|
||||
"ts": "2026-01-10T12:34:56Z",
|
||||
"source": "simulator",
|
||||
"device_id": "esp32-fraser-01",
|
||||
"entity_type": "machine",
|
||||
"entity_id": "formatkreissaege",
|
||||
"event_type": "run_start",
|
||||
"confidence": "high",
|
||||
"payload": {
|
||||
"power_w": 820,
|
||||
"vibration": 0.73,
|
||||
"reason": "power_threshold"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Anhang B: Beispiel-Event (Waage stable_weight)
|
||||
```json
|
||||
{
|
||||
"schema_version": "v1",
|
||||
"event_uid": "b6d0a2c5-9b1f-4a0b-8b19-7f2e1b8f3d11",
|
||||
"ts": "2026-01-10T12:40:12Z",
|
||||
"source": "simulator",
|
||||
"device_id": "scale-sim-01",
|
||||
"entity_type": "scale",
|
||||
"entity_id": "waage-01",
|
||||
"event_type": "stable_weight",
|
||||
"confidence": "high",
|
||||
"payload": {
|
||||
"weight_g": 1532.4,
|
||||
"unit": "g",
|
||||
"stable_ms": 1200
|
||||
}
|
||||
}
|
||||
```
|
||||
538
FEATURE_REQUEST/open_workshop_feature_plan.md
Normal file
|
|
@ -0,0 +1,538 @@
|
|||
# 0. Migration: open_workshop → open_workshop_base ✅ ABGESCHLOSSEN
|
||||
|
||||
**Status: ERLEDIGT (08.12.2025)**
|
||||
|
||||
Die Migration wurde erfolgreich durchgeführt:
|
||||
|
||||
- ✅ SQL-Migration: Modul umbenannt in Datenbank
|
||||
- ✅ Pre-Migration: maintenance.equipment Integration vorbereitet
|
||||
- ✅ Post-Migration: 23 Maschinen zu maintenance.equipment migriert
|
||||
- ✅ _inherits Pattern implementiert (ows.machine → maintenance.equipment)
|
||||
- ✅ Automatische Migration in CI/CD Pipeline integriert
|
||||
- ✅ UX verbessert: Equipment wird automatisch erstellt
|
||||
- ✅ Location readonly und auto-sync von area
|
||||
- ✅ Menü umbenannt zu "Ausrüstung"
|
||||
|
||||
**Technische Details:**
|
||||
- Alte Felder entfernt: code, description, storage_location, purchase_price, purchase_date
|
||||
- Delegation an maintenance.equipment: name, serial_no, location, cost, effective_date
|
||||
- JSONB für mehrsprachige Namen (de_DE, en_US)
|
||||
- Area → Location Synchronisation automatisch
|
||||
|
||||
---
|
||||
|
||||
# Open Workshop – FEATURE PLAN
|
||||
## Vollständige Modularisierung + WordPress REST API Integration (API = Pflicht)
|
||||
|
||||
Dieses Dokument ist die **finale, vollständig korrigierte Version** des Feature-Plans.
|
||||
Es kombiniert alle Inhalte der vorherigen Dokumente, **konsolidiert, bereinigt und vereinheitlicht**,
|
||||
und stellt klar:
|
||||
|
||||
# ⭐ Die API ist ein **Pflichtbestandteil** der finalen Architektur.
|
||||
|
||||
Das Dokument ist **eigenständig** und enthält alles, was ein Entwickler oder KI-Agent benötigt,
|
||||
um das gesamte Open-Workshop-System korrekt modularisiert aufzubauen.
|
||||
|
||||
---
|
||||
|
||||
# 1. Gesamtziel
|
||||
|
||||
Das Projekt *Open Workshop* soll in eine moderne, klare und skalierbare Architektur überführt werden:
|
||||
|
||||
- **Odoo (intern)** als Verwaltungs-Backend für Maschinen, Einweisungen, POS, Maintenance, Events.
|
||||
- **WordPress (extern)** als öffentliche Website mit hoher Performance und SEO.
|
||||
- **REST API (Pflicht!)** als Kommunikationsschicht zwischen beiden Systemen.
|
||||
|
||||
Damit entsteht:
|
||||
|
||||
```
|
||||
WordPress (öffentlich)
|
||||
↑
|
||||
| REST API (nur lesend)
|
||||
↓
|
||||
Odoo (intern, geschützt)
|
||||
```
|
||||
|
||||
Diese Architektur vermeidet:
|
||||
|
||||
- Performance-Probleme durch DSL-Uplink
|
||||
- Sicherheitsrisiken von Odoo im Internet
|
||||
- Hohe Kosten für Odoo.sh Benutzerlizenzen
|
||||
|
||||
---
|
||||
|
||||
# 2. Aktuelle Modulstruktur (Stand: 08.12.2025)
|
||||
|
||||
```
|
||||
open_workshop/
|
||||
├── open_workshop/ # ✅ Alt-Modul (installable=False, Kompatibilität)
|
||||
├── open_workshop_base/ # ✅ FERTIG – Backend, _inherits maintenance.equipment
|
||||
├── open_workshop_pos/ # ✅ FERTIG – POS-Integration (JS, XML, UI)
|
||||
├── open_workshop_dokuwiki/ # ⏳ IN ENTWICKLUNG – DokuWiki-Integration
|
||||
└── open_workshop_api/ # 💤 OPTIONAL – REST API (zurückgestellt)
|
||||
```
|
||||
|
||||
**Entfernt:** open_workshop_maintenance (verworfen - Funktionalität direkt in open_workshop_base integriert)
|
||||
|
||||
---
|
||||
|
||||
# 3. Modul: open_workshop_base ✅ FERTIG
|
||||
|
||||
**Status: PRODUKTIV (18.0.1.0.4)**
|
||||
|
||||
Enthält:
|
||||
|
||||
- ✅ `ows.machine` mit _inherits zu `maintenance.equipment` (delegiert alle Felder von maintenance.equipment transparent an ows.machine)
|
||||
- ✅ `ows.machine.area` (Bereiche mit JSONB-Namen)
|
||||
- ✅ `ows.machine.product` (Nutzungsprodukte)
|
||||
- ✅ `ows.machine.training` (Einweisungsprodukte)
|
||||
- ✅ `ows.machine.access` (Zugriffsfreigaben)
|
||||
- ✅ Automatische Equipment-Erstellung bei Maschinenanlage
|
||||
- ✅ Area → Location Synchronisation
|
||||
- ✅ Sicherheitsregeln (ir.model.access)
|
||||
- ✅ Backend-UI: Form, Tree, Search/Filter
|
||||
- ✅ Smart Button zu Equipment-Details
|
||||
- ✅ Integration mit OCA maintenance Modulen
|
||||
|
||||
**Abhängigkeiten:**
|
||||
- base, account, hr, product, sale, contacts, **maintenance**
|
||||
|
||||
**Maintenance Integration:**
|
||||
- Statt separatem Modul: Direkte Integration via _inherits
|
||||
- Equipment-Felder: name, serial_no, location, cost, effective_date
|
||||
- OWS-Felder: category, area_id, product_ids, training_ids
|
||||
|
||||
---
|
||||
|
||||
# 4. Modul: open_workshop_pos ✅ FERTIG
|
||||
|
||||
**Status: PRODUKTIV**
|
||||
|
||||
Vollständig separiertes POS-Modul mit:
|
||||
|
||||
### JS-Module:
|
||||
- ✅ ows_machine_access_list.js
|
||||
- ✅ ows_pos_customer_sidebar.js
|
||||
- ✅ ows_pos_sidebar.js
|
||||
- ✅ ows_product_screen_template_patch.js
|
||||
|
||||
### XML-Templates:
|
||||
- ✅ QWeb Templates für POS-UI
|
||||
- ✅ Maschinenfreigaben-Widget
|
||||
- ✅ Customer Sidebar mit Zugriffsstatus
|
||||
|
||||
### Assets:
|
||||
```json
|
||||
'point_of_sale.assets': [
|
||||
'open_workshop_pos/static/src/js/**/*.js',
|
||||
'open_workshop_pos/static/src/xml/**/*.xml',
|
||||
'open_workshop_pos/static/src/css/**/*.css'
|
||||
]
|
||||
```
|
||||
|
||||
**Abhängigkeiten:**
|
||||
- open_workshop_base
|
||||
- point_of_sale
|
||||
|
||||
**Integration:**
|
||||
- Zeigt Maschinenzugriffe im POS
|
||||
- Filterung nach Bereichen
|
||||
- Live-Status der Freigaben
|
||||
|
||||
---
|
||||
|
||||
# 5. Modul: open_workshop_dokuwiki ⏳ IN ENTWICKLUNG
|
||||
|
||||
**Status: IN ENTWICKLUNG (Branch: feature/dokuwiki-integration)**
|
||||
|
||||
Dieses Modul integriert **DokuWiki** als Dokumentationssystem für Equipment.
|
||||
|
||||
## 5.1 Ziele
|
||||
|
||||
- Automatische Wiki-Seiten für jedes Equipment
|
||||
- **Invertierte Include-Architektur**:
|
||||
- Odoo generiert geschützte Hauptseite mit Equipment-Daten
|
||||
- Hauptseite inkludiert User-Dokumentation via Include Plugin
|
||||
- User editieren separate `:doku` Unterseite
|
||||
- **Klare Datentrennung**:
|
||||
- Odoo = Master für strukturierte Daten (Hauptseite, read-only für User)
|
||||
- User = Master für Freitext-Dokumentation (`:doku` Unterseite, editierbar)
|
||||
- **Generisches ACL-System**: Eine Regel für alle Equipment
|
||||
- Versionierung durch DokuWiki eingebaut
|
||||
|
||||
## 5.2 Architektur
|
||||
|
||||
```
|
||||
Odoo (maintenance.equipment)
|
||||
↓
|
||||
XML-RPC Client
|
||||
↓
|
||||
DokuWiki API (wiki.putPage)
|
||||
↓
|
||||
werkstatt:ausruestung:bereich:maschinenname
|
||||
```
|
||||
|
||||
## 5.3 DokuWiki-Seitenstruktur
|
||||
|
||||
**Zwei-Seiten-System pro Equipment:**
|
||||
|
||||
```
|
||||
werkstatt:ausruestung:holzwerkstatt:formatkreissaege ← Hauptseite (Odoo generiert)
|
||||
werkstatt:ausruestung:holzwerkstatt:formatkreissaege:doku ← Doku-Seite (User editieren)
|
||||
werkstatt:ausruestung:lasercutter:epilog_fusion_m2 ← Hauptseite
|
||||
werkstatt:ausruestung:lasercutter:epilog_fusion_m2:doku ← Doku-Seite
|
||||
```
|
||||
|
||||
**Namespace-Regeln:**
|
||||
- **Hauptnamespace**: `werkstatt:ausruestung:`
|
||||
- **Hauptseite**: `werkstatt:ausruestung:bereich:maschinenname`
|
||||
- Bereich: ows_area_id (normalisiert, lowercase)
|
||||
- Maschinenname: Equipment name (normalisiert, ohne Sonderzeichen)
|
||||
- **Komplett Odoo-generiert**, User können nicht editieren
|
||||
|
||||
- **Doku-Seite**: `werkstatt:ausruestung:bereich:maschinenname:doku`
|
||||
- **User editieren hier frei** (Anleitungen, Tipps, Bilder)
|
||||
- Wird via Include in Hauptseite eingebunden
|
||||
- User können weitere Unterseiten anlegen (`:doku:bilder`, `:doku:videos`)
|
||||
|
||||
## 5.4 Hauptseiten-Template (Odoo-generiert)
|
||||
|
||||
Odoo erstellt die komplette Hauptseite inklusive Include für User-Dokumentation:
|
||||
|
||||
```wiki
|
||||
====== Formatkreissäge ======
|
||||
|
||||
===== Grunddaten =====
|
||||
* **Seriennummer:** FKS-2024-001
|
||||
* **Bereich:** [[werkstatt:ausruestung:holzwerkstatt|Holzwerkstatt]]
|
||||
* **Kategorie:** 🔴 Kategorie 3 (Einweisung erforderlich)
|
||||
* **Standort:** Schrank A, Regal 2
|
||||
* **Hersteller:** Bosch
|
||||
|
||||
===== Sicherheit =====
|
||||
* **Einweisungspflichtig:** Ja
|
||||
* **Nutzungsprodukte:**
|
||||
* Holzwerkstatt Nutzung (5€/Tag)
|
||||
* **Einweisungsprodukte:**
|
||||
* Einweisung Formatkreissäge (einmalig 25€)
|
||||
|
||||
===== Wartung =====
|
||||
* **Letzte Wartung:** 01.12.2025
|
||||
* **Nächste Wartung:** 01.06.2026
|
||||
* **Wartungshistorie:** [[odoo:maintenance:request:42|Alle Wartungsaufträge]]
|
||||
|
||||
===== Dokumentation & Anleitungen =====
|
||||
{{page>.:formatkreissaege:doku&noheader}}
|
||||
|
||||
[[.:formatkreissaege:doku|✏️ Dokumentation bearbeiten]]
|
||||
```
|
||||
|
||||
**User-Doku-Seite** (`:doku`, User editieren hier):
|
||||
```wiki
|
||||
==== Bedienung ====
|
||||
1. Hauptschalter einschalten
|
||||
2. Sägeblatt-Drehzahl einstellen
|
||||
3. Anschlag positionieren
|
||||
...
|
||||
|
||||
==== Tipps & Tricks ====
|
||||
- Bei Hartholz: Vorschub langsamer
|
||||
- Sägeblatt nach 50h wechseln
|
||||
|
||||
==== Bilder ====
|
||||
{{.:doku:bild1.jpg?300}}
|
||||
```
|
||||
|
||||
## 5.5 Technische Umsetzung
|
||||
|
||||
**Python-Bibliothek:**
|
||||
- `dokuwiki` (Python XML-RPC Client für DokuWiki)
|
||||
- Installation: `pip install dokuwiki`
|
||||
|
||||
**API-Methoden:**
|
||||
```python
|
||||
# DokuWiki XML-RPC API
|
||||
wiki.putPage(page_id, content, summary, minor) # Seite erstellen/aktualisieren
|
||||
wiki.getPage(page_id) # Seite lesen
|
||||
wiki.getAllPages() # Alle Seiten auflisten
|
||||
```
|
||||
|
||||
**Odoo-Integration:**
|
||||
- Neues Model: `ows.dokuwiki.config` (DokuWiki-URL, Credentials)
|
||||
- Methode auf `maintenance.equipment`: `sync_to_dokuwiki()`
|
||||
- Automated Action: Bei Equipment-Änderung → Wiki aktualisieren
|
||||
- Smart Button: "Wiki öffnen" → Link zur DokuWiki-Seite
|
||||
|
||||
**Sync-Algorithmus (vereinfacht!):**
|
||||
```python
|
||||
def sync_to_dokuwiki(self):
|
||||
"""Generiert Hauptseite mit Include für User-Doku"""
|
||||
# 1. Generiere Hauptseite komplett (Odoo hat volle Kontrolle)
|
||||
content = self._generate_equipment_page()
|
||||
|
||||
# 2. Füge Include für User-Doku hinzu
|
||||
wiki_id = self._get_normalized_name()
|
||||
content += "\n\n===== Dokumentation & Anleitungen =====\n"
|
||||
content += f"{{{{page>.:{{wiki_id}}:doku&noheader}}}}\n\n"
|
||||
content += f"[[.:{{wiki_id}}:doku|✏️ Dokumentation bearbeiten]]\n"
|
||||
|
||||
# 3. Schreibe Hauptseite (überschreibt komplett)
|
||||
main_page = f"werkstatt:ausruestung:{self.ows_area_id.wiki_name}:{wiki_id}"
|
||||
dokuwiki_client.putPage(main_page, content, "Odoo Auto-Sync")
|
||||
|
||||
# 4. Erstelle leere Doku-Seite falls nicht vorhanden
|
||||
doku_page = f"{main_page}:doku"
|
||||
if not dokuwiki_client.getPage(doku_page):
|
||||
initial_doku = "==== Bedienung ====\n\n==== Tipps & Tricks ====\n"
|
||||
dokuwiki_client.putPage(doku_page, initial_doku, "Initial")
|
||||
|
||||
def _generate_equipment_page(self):
|
||||
"""Generiert komplette Hauptseite mit Equipment-Daten"""
|
||||
return f"""====== {self.name} ======
|
||||
|
||||
===== Grunddaten =====
|
||||
* **Seriennummer:** {self.serial_no or 'N/A'}
|
||||
* **Bereich:** [[werkstatt:ausruestung:{self.ows_area_id.wiki_name}|{self.ows_area_id.name}]]
|
||||
* **Kategorie:** {self._get_category_icon()} {self._get_category_label()}
|
||||
* **Standort:** {self.location or 'N/A'}
|
||||
* **Hersteller:** {self.partner_id.name or 'N/A'}
|
||||
|
||||
===== Sicherheit =====
|
||||
* **Einweisungspflichtig:** {self._is_training_required()}
|
||||
* **Nutzungsprodukte:**
|
||||
{self._format_product_list(self.ows_product_ids)}
|
||||
* **Einweisungsprodukte:**
|
||||
{self._format_product_list(self.ows_training_ids)}
|
||||
|
||||
===== Wartung =====
|
||||
* **Letzte Wartung:** {self.last_maintenance_date or 'N/A'}
|
||||
* **Nächste Wartung:** {self.next_action_date or 'N/A'}
|
||||
"""
|
||||
```
|
||||
|
||||
**Vorteile dieser Architektur:**
|
||||
- ✅ Keine Sync-Marker nötig (Hauptseite wird komplett überschrieben)
|
||||
- ✅ User können Odoo-Daten nicht versehentlich löschen
|
||||
- ✅ User-Content ist komplett geschützt (separate Seite)
|
||||
- ✅ Include funktioniert transparent (User sehen alles auf einer Seite)
|
||||
- ✅ Wartungsfreies ACL-System
|
||||
|
||||
## 5.6 ACL-System (Generisch & Wartungsfrei)
|
||||
|
||||
**Phase 1: Nur für Odoo sichtbar (Initial-Setup)**
|
||||
|
||||
```
|
||||
# Komplett gesperrt für alle außer Odoo
|
||||
werkstatt:ausruestung:* @odoo 8 # Voller Zugriff
|
||||
werkstatt:ausruestung:* @ALL 0 # Kein Zugriff
|
||||
```
|
||||
|
||||
**Phase 2: Werkstatt-Betreiber erhalten Zugriff (später)**
|
||||
|
||||
```
|
||||
# User-Doku-Seiten editierbar (spezifischere Regel zuerst!)
|
||||
werkstatt:ausruestung:*:doku @werkstatt 2 # Editieren erlaubt
|
||||
werkstatt:ausruestung:*:doku:* @werkstatt 2 # Auch Unterseiten (Bilder, Videos, etc.)
|
||||
|
||||
# Hauptseiten nur lesen
|
||||
werkstatt:ausruestung:* @werkstatt 1 # Nur Lesezugriff
|
||||
werkstatt:ausruestung:* @ALL 1 # Alle können lesen
|
||||
|
||||
# Odoo hat vollen Zugriff überall
|
||||
werkstatt:ausruestung:* @odoo 8 # Voller Zugriff
|
||||
werkstatt:ausruestung:*:doku @odoo 8 # Auch auf Doku-Seiten (für Initial-Erstellung)
|
||||
**Wie es funktioniert:**
|
||||
|
||||
| Seite | User @werkstatt | User @odoo | Ergebnis |
|
||||
|-------|-----------------|------------|----------|
|
||||
| `werkstatt:ausruestung:holzwerkstatt:formatkreissaege` | Matched: `werkstatt:ausruestung:*` → 1 | Matched: `werkstatt:ausruestung:*` → 8 | User: Lesen, Odoo: Schreiben |
|
||||
| `werkstatt:ausruestung:holzwerkstatt:formatkreissaege:doku` | Matched: `werkstatt:ausruestung:*:doku` → 2 | Matched: `werkstatt:ausruestung:*:doku` → 8 | User: Editieren, Odoo: Voll |
|
||||
- `16` = Admin (alle Rechte)
|
||||
**Vorteile:**
|
||||
- ✅ **Komplett generisch**: Gilt automatisch für alle Equipment
|
||||
- ✅ **Wartungsfrei**: Kein manuelles ACL-Setup pro Equipment
|
||||
- ✅ **Phasenweise Freischaltung**: Erst Odoo-Only, dann schrittweise für Werkstatt
|
||||
- ✅ **Selbsterklärend**: User sehen sofort wo sie editieren können
|
||||
- ✅ **Skalierbar**: Funktioniert auch für 1000+ Equipment-Seiten
|
||||
| `ausruestung:holzwerkstatt:formatkreissaege` | Matched: `ausruestung:*` → 1 | Matched: `ausruestung:*` → 8 | User: Lesen, Odoo: Schreiben |
|
||||
| `ausruestung:holzwerkstatt:formatkreissaege:doku` | Matched: `ausruestung:*:doku` → 2 | Matched: `ausruestung:*:doku` → 8 | User: Editieren, Odoo: Voll |
|
||||
|
||||
**Vorteile:**
|
||||
- ✅ **Komplett generisch**: Gilt automatisch für alle Equipment
|
||||
- ✅ **Wartungsfrei**: Kein manuelles ACL-Setup pro Equipment
|
||||
- ✅ **Selbsterklärend**: User sehen sofort wo sie editieren können
|
||||
- ✅ **Skalierbar**: Funktioniert auch für 1000+ Equipment-Seiten
|
||||
|
||||
**Odoo-Seite:**
|
||||
- Credentials in `ir.config_parameter`:
|
||||
- `dokuwiki.url` (z.B. `https://wiki.hobbyhimmel.de`)
|
||||
- `dokuwiki.user` (z.B. `odoo`)
|
||||
- `dokuwiki.password` (verschlüsselt gespeichert)
|
||||
- XML-RPC über HTTPS
|
||||
- Logging aller Wiki-Operationen in `mail.message`
|
||||
|
||||
## 5.7 Vorteile DokuWiki
|
||||
|
||||
✅ **Leichtgewichtig** – File-based, keine separate Datenbank
|
||||
✅ **Versionierung** – Eingebautes History-System
|
||||
✅ **Wiki-Syntax** – Perfekt für strukturierte Dokumentation
|
||||
✅ **ACL-System** – Granulare Berechtigungen
|
||||
✅ **Suchfunktion** – Volltext-Suche eingebaut
|
||||
✅ **Plugins** – Erweiterbar (Markdown, Galleries, etc.)
|
||||
✅ **Open Source** – Keine Lizenzkosten
|
||||
|
||||
---
|
||||
|
||||
# 6. Modul: open_workshop_api ⏳ GEPLANT
|
||||
|
||||
**Status: ZURÜCKGESTELLT (DokuWiki hat Priorität)**
|
||||
|
||||
REST API für externe Systeme (falls später WordPress/App-Integration gewünscht).
|
||||
|
||||
## 6.1 Potenzielle Endpunkte
|
||||
|
||||
```
|
||||
GET /api/v1/machines
|
||||
GET /api/v1/machine/<id>
|
||||
GET /api/v1/areas
|
||||
```
|
||||
|
||||
**Hinweis:** Mit DokuWiki kann die öffentliche Dokumentation direkt über das Wiki erfolgen.
|
||||
Eine separate REST API ist optional und wird nur bei Bedarf implementiert.
|
||||
|
||||
## 6.1 Warum DokuWiki statt WordPress?
|
||||
|
||||
- **Einfacher**: Keine separate Datenbank, file-based
|
||||
- **Wiki-Syntax**: Perfekt für technische Dokumentation
|
||||
- **Versionierung**: Eingebautes History-System
|
||||
- **ACL**: Feinere Berechtigungskontrolle
|
||||
- **Leichtgewichtig**: Weniger Overhead als WordPress
|
||||
- **Integration**: XML-RPC API für bidirektionale Sync
|
||||
|
||||
## 6.2 Zukunft: WordPress optional
|
||||
|
||||
Falls später gewünscht, kann WordPress **zusätzlich** die öffentliche Maschinenliste anzeigen:
|
||||
- DokuWiki für **Dokumentation** (intern/extern lesbar)
|
||||
- WordPress für **Präsentation** (extern mit Bildern, SEO)
|
||||
- Beide Systeme greifen auf Odoo-Daten zu
|
||||
|
||||
Aktuell hat DokuWiki-Integration **Priorität**.
|
||||
|
||||
---
|
||||
|
||||
# 7. Finaler Architektur-Plan
|
||||
|
||||
```
|
||||
Internet
|
||||
|
|
||||
DokuWiki (Dokumentation)
|
||||
WordPress (optional: Präsentation)
|
||||
|
|
||||
XML-RPC / REST API
|
||||
|
|
||||
----------------------------
|
||||
| Odoo Backend (LAN/VPN)
|
||||
| open_workshop_dokuwiki
|
||||
| maintenance.equipment
|
||||
----------------------------
|
||||
POS, Benutzer, Einweisungen
|
||||
```
|
||||
|
||||
**Fokus:** Equipment-Dokumentation über DokuWiki, WordPress später optional.
|
||||
|
||||
---
|
||||
|
||||
# 8. Sicherheit
|
||||
|
||||
**DokuWiki ACL:**
|
||||
- Namespace `ausruestung:*` lesbar für alle (@ALL = 1)
|
||||
- Namespace `ausruestung:*` schreibbar nur für Odoo (@odoo = 8)
|
||||
- Manuelle Edits durch Admins möglich
|
||||
|
||||
**Odoo:**
|
||||
- DokuWiki-Credentials in ir.config_parameter
|
||||
- XML-RPC über HTTPS
|
||||
- Logging aller Wiki-Operationen
|
||||
- Backup-System für Wiki-Dateien
|
||||
|
||||
**Optional (REST API):**
|
||||
- Reverse Proxy sperrt alle Odoo-URLs außer `/api/v1/*`
|
||||
- HTTPS erzwingen
|
||||
- Rate-Limit + Firewall
|
||||
- Token-Auth
|
||||
|
||||
---
|
||||
|
||||
# 9. Vorteile dieser Architektur
|
||||
|
||||
✔ **DokuWiki für Dokumentation** – Wiki-Syntax perfekt für Anleitungen
|
||||
✔ **Versionierung eingebaut** – History für alle Änderungen
|
||||
✔ **Odoo bleibt sicher** hinter Firewall
|
||||
✔ **Keine Benutzerkosten** – alle Ehrenamtlichen können intern mitarbeiten
|
||||
✔ **Modular, wartbar, zukunftssicher**
|
||||
✔ **Intelligente Sync** – Nur strukturierte Daten werden überschrieben, Freitext bleibt erhalten
|
||||
✔ **Öffentlich lesbar** – Anleitungen für alle verfügbar
|
||||
✔ **WordPress optional** – Kann später für Marketing/SEO ergänzt werden
|
||||
|
||||
---
|
||||
|
||||
# 10. Implementierungsstand (13.12.2025)
|
||||
|
||||
| Schritt | Status | Details |
|
||||
|---------|--------|---------|
|
||||
| 1. open_workshop_base | ✅ **FERTIG** | Version 18.0.1.0.4, produktiv |
|
||||
| 2. open_workshop_pos | ✅ **FERTIG** | POS-Integration komplett |
|
||||
| 3. ~~open_workshop_maintenance~~ | ❌ **VERWORFEN** | Direkt in Base integriert |
|
||||
| 4. Maintenance Integration | ✅ **FERTIG** | _inherits Pattern implementiert |
|
||||
| 5. Equipment-View Integration | ✅ **FERTIG** | Related fields, Menu-Migration |
|
||||
| 6. Migration Workflow | ✅ **FERTIG** | SQL + Python, CI/CD integriert |
|
||||
| 7. open_workshop_dokuwiki | ⏳ **IN ENTWICKLUNG** | Branch: feature/dokuwiki-integration |
|
||||
| 8. open_workshop_api | 💤 **OPTIONAL** | Zurückgestellt, bei Bedarf |
|
||||
|
||||
---
|
||||
|
||||
# 11. Nächste Schritte
|
||||
|
||||
1. **open_workshop_dokuwiki** entwickeln
|
||||
- DokuWiki XML-RPC Client implementieren
|
||||
- Template-System für Equipment-Seiten
|
||||
- Sync-Mechanismus (Odoo → Wiki)
|
||||
- Smart Button "Wiki öffnen"
|
||||
- ACL-Konfiguration dokumentieren
|
||||
|
||||
2. **DokuWiki aufsetzen**
|
||||
- Installation auf Server
|
||||
- Namespace `ausruestung:` anlegen
|
||||
- ACL konfigurieren (@odoo Gruppe)
|
||||
- XML-RPC API aktivieren
|
||||
|
||||
3. **Testing**
|
||||
- Equipment-Seiten automatisch generieren
|
||||
- Sync bei Änderungen testen
|
||||
- ACL-Rechte validieren
|
||||
- Performance testen
|
||||
|
||||
4. **Optional: WordPress Plugin**
|
||||
- Falls öffentliche Präsentation gewünscht
|
||||
- REST API dann implementieren
|
||||
- Caching-Layer hinzufügen
|
||||
|
||||
---
|
||||
|
||||
# 12. Endfazit
|
||||
|
||||
Diese Architektur vereint das Beste aus beiden Welten:
|
||||
|
||||
- ✅ **Technisch korrekt** – _inherits Pattern statt separatem Modul
|
||||
- ✅ **Performant** – Maintenance.equipment als Single Source of Truth
|
||||
- ✅ **Dokumentiert** – DokuWiki für strukturierte Anleitungen
|
||||
- ✅ **Versioniert** – History-System eingebaut
|
||||
- ✅ **Öffentlich zugänglich** – Wiki lesbar für alle
|
||||
- ✅ **Kostenoptimiert** – Keine zusätzlichen Lizenzen
|
||||
- ✅ **Langfristig erweiterbar** – Modularer Aufbau
|
||||
- ✅ **Produktiv im Einsatz** – Migration erfolgreich abgeschlossen
|
||||
|
||||
**DokuWiki-Integration ist der nächste logische Schritt** für bessere Equipment-Dokumentation.
|
||||
|
||||
**Letztes Update: 13.12.2025**
|
||||
|
||||
142
FEATURE_REQUEST/open_workshop_modulstruktur.md
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
# Empfohlene Modulstruktur für Open_Workshop + öffentliches Website-Modul
|
||||
|
||||
## 🎯 Ziel
|
||||
Du möchtest:
|
||||
1. Ein **öffentliches Website-Modul**, das Maschineninformationen aus dem Maintenance-Modul anzeigt.
|
||||
2. Dein gesamtes `open_workshop`-Repository sinnvoll modularisieren.
|
||||
|
||||
Diese Antwort beschreibt die optimale Struktur und die Gründe dafür.
|
||||
|
||||
---
|
||||
|
||||
# ✅ Öffentliches Modul ist sinnvoll
|
||||
|
||||
Du möchtest:
|
||||
- Maschinen anzeigen (Maintenance)
|
||||
- Zustand sichtbar machen
|
||||
- Bilder & PDFs anzeigen
|
||||
- ohne Login (auth="public")
|
||||
|
||||
➡️ Dafür ist ein eigenes Modul **open_workshop_website** ideal.
|
||||
|
||||
Dieses Website-Modul sollte *nur Anzeige* machen, keine Logik verwalten.
|
||||
|
||||
---
|
||||
|
||||
# ⭐ Empfohlene Modulstruktur (Best Practice)
|
||||
|
||||
```
|
||||
open_workshop/
|
||||
│
|
||||
├── open_workshop_pos/ # POS-Integration, Einweisungen, Freigaben
|
||||
│
|
||||
├── open_workshop_maintenance/ # Verbindung zu maintenance.equipment
|
||||
│ # z. B. Modelle, Felder, Extensions
|
||||
│
|
||||
└── open_workshop_website/ # Öffentliche Darstellung, QR-Codes, Website
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 🧩 Gründe für diese modulare Struktur
|
||||
|
||||
## 1️⃣ Trennung nach funktionalen Bereichen
|
||||
Odoo trennt:
|
||||
- POS
|
||||
- Maintenance
|
||||
- Website
|
||||
|
||||
Du solltest dasselbe tun.
|
||||
So bleibt alles klar, logisch und update-sicher.
|
||||
|
||||
---
|
||||
|
||||
## 2️⃣ Bessere Wartbarkeit
|
||||
Wenn Odoo 19/20 Änderungen bringt:
|
||||
|
||||
- POS ändert sich → nur `open_workshop_pos`
|
||||
- Maintenance ändert sich → nur `open_workshop_maintenance`
|
||||
- Website ändert sich → nur `open_workshop_website`
|
||||
|
||||
Ein großes Sammelmodul wäre schwer zu warten.
|
||||
|
||||
---
|
||||
|
||||
## 3️⃣ Nutzer können auswählen, was sie brauchen
|
||||
Beispiel:
|
||||
|
||||
Ein Kunde will nur Maschinen + Website, aber kein POS.
|
||||
|
||||
Dann installiert er:
|
||||
|
||||
- `open_workshop_maintenance`
|
||||
- `open_workshop_website`
|
||||
|
||||
🔧 Ohne unnötige POS-Abhängigkeit.
|
||||
|
||||
---
|
||||
|
||||
## 4️⃣ Klare Modulabhängigkeiten
|
||||
### `open_workshop_pos`
|
||||
- depends: `point_of_sale`
|
||||
|
||||
### `open_workshop_maintenance`
|
||||
- depends: `maintenance`
|
||||
|
||||
### `open_workshop_website`
|
||||
- depends: `website`
|
||||
- optional depends: `open_workshop_maintenance`
|
||||
|
||||
Alles sauber getrennt.
|
||||
|
||||
---
|
||||
|
||||
## 5️⃣ Erweiterbarkeit bleibt einfach
|
||||
Dein Projekt wächst schnell:
|
||||
|
||||
- QR-Codes
|
||||
- Maschinenstatus
|
||||
- API
|
||||
- Dashboards
|
||||
- Rollen mit Einweisungssichtbarkeit
|
||||
|
||||
Wenn alles in EINEM Modul wäre → unübersichtlich
|
||||
Wenn modular → perfekt strukturiert
|
||||
|
||||
---
|
||||
|
||||
# ❌ Warum alles in *einem einzigen Modul* schlecht wäre
|
||||
|
||||
- Mischmasch aus POS, Website und Maintenance-Code
|
||||
- schwerer zu debuggen
|
||||
- schlechtere Wiederverwendbarkeit
|
||||
- mehr Merge-Konflikte
|
||||
- Odoo selbst macht das nie so
|
||||
|
||||
---
|
||||
|
||||
# 🎉 Fazit
|
||||
|
||||
Die modulare Struktur ist optimal und entspricht OCA/Odoo-Best-Practices:
|
||||
|
||||
| Modul | Zweck |
|
||||
|-------|--------|
|
||||
| **open_workshop_pos** | Einweisung, Freigaben, POS GUI |
|
||||
| **open_workshop_maintenance** | Maschinenmodellierung, Maintenance-Anbindung |
|
||||
| **open_workshop_website** | öffentliche Darstellung, QR-Codes |
|
||||
|
||||
---
|
||||
|
||||
# 📦 Optional: Nächster Schritt
|
||||
|
||||
Ich kann dir komplette Modul-Gerüste erstellen:
|
||||
|
||||
- Manifest-Dateien
|
||||
- Controller
|
||||
- Views
|
||||
- Templates
|
||||
- QR-Code-Integration
|
||||
- Demo-Daten
|
||||
|
||||
Sag einfach Bescheid:
|
||||
👉 *„Bitte erstelle mir das Grundgerüst.“*
|
||||
161
FEATURE_REQUEST/workshop_maintenance_overview.md
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
# Maschinen- und Geräteverwaltung im FabLab mit Odoo Maintenance + Open_Workshop
|
||||
|
||||
## 🎯 Ausgangssituation
|
||||
|
||||
In der Werkstatt existieren viele verschiedene Maschinen und Geräte:
|
||||
|
||||
- stationär
|
||||
- mobil
|
||||
- elektrisch
|
||||
- mechanisch
|
||||
- verschiedene Sicherheits- und Einweisungsanforderungen
|
||||
|
||||
Der aktuelle Zustand, Bilder und Bedienungsanleitungen sind unstrukturiert im Wiki gespeichert.
|
||||
Das Ziel ist:
|
||||
|
||||
- eine zentrale, strukturierte Verwaltung in **Odoo**
|
||||
- Anzeige des Maschinenzustands
|
||||
- Zugriff für Nutzer auf Bedienungsanleitungen
|
||||
- ohne Backend-Zugang
|
||||
- am besten über Website/Portal + QR-Codes
|
||||
|
||||
---
|
||||
|
||||
## 💡 Die perfekte Lösung: Maintenance-Modul + Open_Workshop + Website
|
||||
|
||||
Die optimale Architektur besteht aus drei Komponenten:
|
||||
|
||||
1. **Maintenance-Modul**
|
||||
→ Zentrale Verwaltung aller Maschinen/Devices
|
||||
2. **Open_Workshop-Modul**
|
||||
→ Freigaben, Einweisungen, POS-Integration
|
||||
3. **Website-/Portal-Modul (Custom)**
|
||||
→ Anzeige für Nutzer: Maschine, Zustand, Anleitung, QR-Codes
|
||||
|
||||
Diese Kombination ist robust, klar strukturiert und update-sicher.
|
||||
|
||||
---
|
||||
|
||||
# ✅ Warum das Maintenance-Modul ideal ist
|
||||
|
||||
Das Modell `maintenance.equipment` bietet bereits alles, was du brauchst:
|
||||
|
||||
| Feld | Nutzen |
|
||||
|------|--------|
|
||||
| `name` | Maschinenname |
|
||||
| `category_id` | Maschinenbereich |
|
||||
| `maintenance_state` | Betriebszustand |
|
||||
| `note` | Beschreibung |
|
||||
| `image_1920` | Maschinenbild |
|
||||
| `message_attachment_count` | PDFs & Dateien |
|
||||
| Anhänge | Bedienungsanleitungen |
|
||||
|
||||
Diese Felder machen das Maintenance-Modul zur perfekten **Source of Truth**.
|
||||
|
||||
---
|
||||
|
||||
# 🧩 Wie du die Daten Nutzern sichtbar machst
|
||||
|
||||
Nicht das Backend öffnen.
|
||||
Nicht `website=True` setzen.
|
||||
Sondern:
|
||||
|
||||
### ✔ Eigener Website-/Portal-Controller
|
||||
|
||||
Beispiele:
|
||||
|
||||
### Maschinenliste
|
||||
```
|
||||
/workshop/machines
|
||||
```
|
||||
|
||||
### Maschinendetailansicht
|
||||
```
|
||||
/workshop/machine/<id>
|
||||
```
|
||||
|
||||
Dort sieht der Nutzer:
|
||||
|
||||
- Bild
|
||||
- Betriebszustand
|
||||
- Kategorie
|
||||
- Beschreibung
|
||||
- Bedienungsanleitung (PDF-Download)
|
||||
- optional: Sicherheitsinfos
|
||||
- optional: Einweisungsstatus (aus Open_Workshop)
|
||||
|
||||
Alles **read-only**, ohne Backend.
|
||||
|
||||
---
|
||||
|
||||
# 🔒 Zugriffsvarianten
|
||||
|
||||
Du entscheidest selbst:
|
||||
|
||||
## **Option A: Öffentlich**
|
||||
- Keine Anmeldung nötig
|
||||
- Zustand und Anleitung für alle sichtbar
|
||||
- Ideal für Tablets im FabLab oder QR-Codes an Maschinen
|
||||
|
||||
## **Option B: Portal-Zugang**
|
||||
- Nutzer müssen sich einloggen
|
||||
- Anleitungen nur für registrierte Mitglieder sichtbar
|
||||
|
||||
## **Option C: Hybrid**
|
||||
- Zustand öffentlich
|
||||
- Anleitungen nur für eingeloggte Benutzer mit Einweisung
|
||||
|
||||
Diese Variante passt besonders gut zu deinem Open_Workshop-Modell.
|
||||
|
||||
---
|
||||
|
||||
# 🧰 Integration mit Open_Workshop
|
||||
|
||||
Die ideale Struktur ist:
|
||||
|
||||
```
|
||||
maintenance.equipment ←→ ows.machine ←→ ows.machine.access
|
||||
```
|
||||
|
||||
Das bedeutet:
|
||||
|
||||
- Maintenance = technische Daten
|
||||
- Open_Workshop = Einweisungsprozesse
|
||||
- Website = Darstellung
|
||||
|
||||
---
|
||||
|
||||
# 📲 QR-Codes an den Maschinen
|
||||
|
||||
Odoo kann automatisch QR-Codes generieren.
|
||||
|
||||
Du platzierst auf jeder Maschine einen QR-Code, z. B.:
|
||||
|
||||
```
|
||||
/workshop/machine/42
|
||||
```
|
||||
|
||||
Der Nutzer scannt ihn und sieht:
|
||||
|
||||
- Bild
|
||||
- Zustand
|
||||
- Anleitung
|
||||
- ggf. "Einweisung vorhanden" / "Keine Einweisung"
|
||||
|
||||
---
|
||||
|
||||
# 🚀 Modularer Vorschlag für deine Architektur
|
||||
|
||||
### **1. Maintenance-Modul**
|
||||
Zentrale Maschinenverwaltung
|
||||
|
||||
### **2. Open_Workshop-Modul**
|
||||
Einweisung, Freigaben, POS
|
||||
|
||||
### **3. Custom Website-Modul**
|
||||
- Maschinenliste
|
||||
- Detailseiten
|
||||
- PDF-Downloads
|
||||
- Einweisungsstatus
|
||||
- optional QR-Code-Automatik
|
||||
|
||||
453
README.md
|
|
@ -1,48 +1,415 @@
|
|||
# Open Workshop (open_workshop ows)
|
||||
# Open Workshop
|
||||
|
||||
Dieses Odoo v18.0 Modul erweitert das POS- und Kontakt-Modul um Funktionen für offene Werkstätten (FabLabs, Makerspaces etc.) und dient der Verwaltung von Maschinen, Naschinen Einweisungen Produkten, Maschinen Nutzungsprodukten und Zugangsberechtigungen zu den Maschinen.
|
||||
**POS-basiertes Maschinenfreigabe- und Werkstattverwaltungssystem für Odoo**
|
||||
|
||||
## Funktionen
|
||||
|
||||
### Erweiterungen an Kontakten (res.partner)
|
||||
- Geburtstagsfeld, RFID-Karte, Haftungsausschluss usw. ausgelagert nach `ows.user`
|
||||
- Automatische Erstellung des `ows.user`-Eintrags beim Anlegen eines Kontakts
|
||||
- Übersichtliche Darstellung aller Maschinenfreigaben im Odoo Kontaktformular
|
||||
|
||||
### Maschinen und Bereiche
|
||||
- Modell `ows.machine` mit Gruppierung nach Bereichen (`ows.machine.area`)
|
||||
- Farblich kodierte Bereiche (Hex-Wert aus Datenbank) welche zur Darstellung im POS verwendet werden
|
||||
|
||||
### Einweisungen und Nutzungen
|
||||
- Modelle `ows.machine.training` und `ows.machine.product`
|
||||
- Konfigurierbare Produkte für Einweisung/Nutzung direkt im Backend
|
||||
- Zuweisung von Nutzungsprodukten zu Maschinen
|
||||
- Zuweisung von Einweisungsprodukten zu Maschinen
|
||||
|
||||
### Maschinenfreigaben
|
||||
- Modell `ows.machine.access` verknüpft Partner und Maschine
|
||||
- Darstellung im POS als tabellarische Übersicht mit Anzeige für eine bestehende Einweisung / Nutzungsberechtigung
|
||||
- Anzeige im POS-Kundendetailsansicht innerhalb der Kundendetailsansicht
|
||||
- Anzeige im Odoo Kontak Modul der Maschineneinweisungen
|
||||
|
||||
## Installation
|
||||
|
||||
1. Dieses Modul in den Custom-Addons-Ordner kopieren
|
||||
2. Im Odoo Backend unter Apps installieren
|
||||
|
||||
## Entwicklerhinweise
|
||||
|
||||
## ToDos
|
||||
- Bearbeitung der Maschinenfreigaben im Backend
|
||||
- Automatische Erstellung von `mail.message` bei manueller Freigabe
|
||||
- Integration von Fristen (z.B. Ablaufdatum Einweisung)
|
||||
|
||||
## Autoren
|
||||
- Matthias Lotz
|
||||
|
||||
## Lizenz
|
||||
AGPL-3.0 oder später
|
||||
Open Workshop ist eine modulare Erweiterung des Odoo POS-Systems zur Verwaltung von einweisungspflichtigen Maschinen und Geräten in Werkstätten, Makerspaces und FabLabs. Das System ermöglicht die Verwaltung von Maschinenfreigaben, Einweisungen und Equipment-Dokumentation.
|
||||
|
||||
---
|
||||
Letzte Aktualisierung: 06.04.2025
|
||||
|
||||
## Hauptfunktionen
|
||||
|
||||
- **Maschinenfreigaben**: Verwaltung von kundenspezifischen Freigaben für einweisungspflichtige Geräte
|
||||
- **Sicherheitskategorien**: Dreistufiges Kategoriesystem (grün/gelb/rot) für unterschiedliche Einweisungspflichten
|
||||
- **POS-Integration**: Verkauf von Einweisungen und Anzeige von Maschinenfreigaben direkt am Point of Sale
|
||||
- **Equipment-Verwaltung**: Vollständige Integration mit dem OCA Maintenance-Modul
|
||||
|
||||
- **Dokumentation**: Automatische Synchronisation von Equipment-Daten mit DokuWiki
|
||||
- **Mitarbeiter-Display**: Dynamische Anzeige von Mitarbeiterdaten im POS Customer Display
|
||||
|
||||
---
|
||||
|
||||
## Module
|
||||
|
||||
### open_workshop_base
|
||||
**Kernmodul** - Stellt die Basis-Funktionalität für das gesamte System bereit.
|
||||
|
||||
**Hauptfunktionen:**
|
||||
- Maschinenmodelle mit Sicherheitskategorien (grün/gelb/rot)
|
||||
- Bereiche (z.B. Holz, Metall, FabLab)
|
||||
- Freigabeverwaltung (wer darf welche Maschine nutzen)
|
||||
- Produktverknüpfungen für Einweisungen und Nutzung
|
||||
- Integration mit OCA maintenance.equipment als Single Source of Truth
|
||||
|
||||
**Datenmodelle:**
|
||||
- `ows.user` - Benutzerdaten (RFID, Geburtstag, Sicherheitsunterweisung)
|
||||
- `ows.machine.area` - Maschinenbereiche mit Farbkodierung
|
||||
- `ows.machine` - Maschinen (nutzt _inherits Pattern mit maintenance.equipment)
|
||||
- `ows.machine.access` - Freigaben (Partner ↔ Maschine)
|
||||
- `ows.machine.product` - Nutzungsprodukte für Maschinen
|
||||
- `ows.machine.training` - Einweisungsprodukte für Maschinen
|
||||
|
||||
**Abhängigkeiten:**
|
||||
- OCA `maintenance` (Basis-Equipment-Verwaltung)
|
||||
- OCA `maintenance_equipment_status` (Equipment-Statusverwaltung)
|
||||
|
||||
```bash
|
||||
git clone https://github.com/OCA/maintenance.git
|
||||
cd maintenance
|
||||
git reset --hard 5510275e
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### open_workshop_pos
|
||||
**POS-Integration** - Erweitert den Odoo Point of Sale um Maschinenfreigabe-Funktionalität.
|
||||
|
||||
**Hauptfunktionen:**
|
||||
- **Machine Access List**: Übersichtsanzeige aller verfügbaren Maschinen welche Einweisungspflichtig sind, mit Freigabestatus für den aktuellen Kunden (roter Punkt)
|
||||
- **Customer Sidebar**: Kundenspezifische Maschinenfreigaben am POS
|
||||
- **POS Sidebar**: Maschinenauswahl und Statusanzeige
|
||||
- **Automatische Freigabenvergabe**: Beim Verkauf eines Einweisungsprodukts wird dem Kunden automatisch die Freigabe für das zugehörige Gerät in der Datenbank erteilt
|
||||
|
||||
**Verwendungszweck:**
|
||||
Verkauf von Maschineneinweisungen und Anzeige bestehender Freigaben direkt am Kassensystem. Ermöglicht schnelle Prüfung, welcher Kunde auf welche einweisungspflichtigen Geräte zugreifen darf.
|
||||
|
||||
**Abhängigkeiten:**
|
||||
- `open_workshop_base`
|
||||
- `point_of_sale`
|
||||
|
||||
---
|
||||
|
||||
### open_workshop_pos_customer_display
|
||||
**Mitarbeiter-Display** - Zeigt dynamisches Mitarbeiter-Namensschild im POS Customer Display.
|
||||
|
||||
**Hauptfunktionen:**
|
||||
- Anzeige von Mitarbeiterfoto (generiert durch open_workshop_employee_imagegenerator)
|
||||
- Darstellung von Name und Schwerpunkten (job_focus)
|
||||
- Automatische Aktualisierung bei Kassiererwechsel
|
||||
- Responsive Design mit Fallback auf Company-Logo
|
||||
|
||||
**Verwendungszweck:**
|
||||
Darstellung der Mitarbeitenden am Kundendisplay, verbessert die Kundeninteraktion und Transparenz.
|
||||
|
||||
**Abhängigkeiten:**
|
||||
- `point_of_sale`
|
||||
- `hr`
|
||||
|
||||
---
|
||||
|
||||
### open_workshop_dokuwiki
|
||||
**DokuWiki-Integration** - Synchronisiert Equipment-Daten aus Odoo mit einem DokuWiki-System.
|
||||
|
||||
**Hauptfunktionen:**
|
||||
- Automatische Erstellung von Wiki-Seiten für Equipment
|
||||
- Übersichtstabelle aller Equipment (DataTables mit Sortierung/Filterung)
|
||||
- Status-Seiten für Include-Plugin (nur von Odoo generiert, read-only)
|
||||
- Smart Button "Wiki öffnen" im Equipment-Formular
|
||||
- Automatische Synchronisation bei Equipment-Änderungen
|
||||
|
||||
**Architektur-Prinzip:**
|
||||
- Odoo generiert **NUR** `odoo-status/` Seiten (maschinengeneriert, read-only)
|
||||
- Benutzer erstellen eigene Dokumentation in `{bereich}/` Namespaces
|
||||
- Einbindung der Odoo-Daten via DokuWiki Include-Plugin
|
||||
|
||||
**Verwendungszweck:**
|
||||
Zentrale Dokumentation aller Geräte mit automatisch aktualisiertem Status aus Odoo, kombiniert mit benutzergenerierten Anleitungen und Wartungshinweisen.
|
||||
|
||||
**Abhängigkeiten:**
|
||||
- `open_workshop_base`
|
||||
- `maintenance`
|
||||
- `maintenance_equipment_status`
|
||||
|
||||
---
|
||||
|
||||
### 🎨 open_workshop_employee_imagegenerator
|
||||
**Mitarbeiterfoto-Generator** - Upload und Zuschnitt von Mitarbeiterfotos direkt in Odoo.
|
||||
|
||||
**Hauptfunktionen:**
|
||||
- Upload und Zuschnitt von Fotos auf festes Format (369x492 Pixel)
|
||||
- Hinzufügen von Schwerpunktbereichen (job_focus)
|
||||
- Integrierter Cropper.js für professionelle Bildbearbeitung
|
||||
- Fester Crop-Frame (nur Bild bewegt sich, nicht der Rahmen)
|
||||
- Integration im Employee-Formular über Button "Namensschild erstellen"
|
||||
|
||||
**Verwendungszweck:**
|
||||
Erstellung von professionellen Mitarbeiterfotos für das POS Customer Display. Vereinfacht den Workflow durch direkten Upload in Odoo ohne externe Bildbearbeitung.
|
||||
|
||||
**Abhängigkeiten:**
|
||||
- `hr`
|
||||
- `web`
|
||||
|
||||
---
|
||||
|
||||
## Architektur & Datenfluss
|
||||
|
||||
### Datenmodell-Übersicht
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ OCA maintenance.equipment │
|
||||
│ (Single Source of Truth für name, serial_no, cost, etc.) │
|
||||
└──────────────────────┬──────────────────────────────────────┘
|
||||
│ _inherits (Delegation Pattern)
|
||||
│
|
||||
▼
|
||||
┌────────────────┐
|
||||
│ ows.machine │ ◄────────┐
|
||||
│ (OWS-Features) │ │
|
||||
└────┬───────────┘ │
|
||||
│ │
|
||||
┌─────────┼──────────────────────┤
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌──────────────────┐ │
|
||||
│ │ ows.machine.area │ │
|
||||
│ │ (Bereiche) │ │
|
||||
│ └──────────────────┘ │
|
||||
│ │
|
||||
▼ │
|
||||
┌──────────────────┐ ┌────────┴─────────┐
|
||||
│ ows.machine. │ │ ows.machine. │
|
||||
│ product │ │ training │
|
||||
│ (Nutzung) │ │ (Einweisung) │
|
||||
└──────────────────┘ └──────────────────┘
|
||||
|
||||
┌──────────────────┐ ┌──────────────────┐
|
||||
│ res.partner │ │ ows.user │
|
||||
│ (Kunde) │◄───────────│ (Benutzerdaten) │
|
||||
└────────┬─────────┘ 1:1 └──────────────────┘
|
||||
│
|
||||
│ M:N
|
||||
│
|
||||
▼
|
||||
┌──────────────────┐ ┌──────────────────┐
|
||||
│ ows.machine. │ │ ows.machine │
|
||||
│ access │───────────►│ │
|
||||
│ (Freigaben) │ │ │
|
||||
└──────────────────┘ └──────────────────┘
|
||||
```
|
||||
|
||||
### _inherits Pattern Erklärung
|
||||
|
||||
Das Modul nutzt das Odoo **_inherits Pattern** (Delegation/Prototypische Vererbung) für `ows.machine`:
|
||||
|
||||
**Wie es funktioniert:**
|
||||
- `ows.machine` delegiert Basis-Felder an `maintenance.equipment`
|
||||
- Beim Erstellen einer `ows.machine` wird automatisch ein `maintenance.equipment` erstellt
|
||||
- Felder wie `name`, `serial_no`, `cost`, `location` werden direkt von equipment übernommen
|
||||
- **Keine Datenduplizierung!** Ein Equipment-Datensatz, zwei Modell-Sichten
|
||||
|
||||
**Vorteile:**
|
||||
- `maintenance.equipment` bleibt die **Single Source of Truth** für Stammdaten
|
||||
- Kompatibilität mit allen OCA Maintenance-Modulen
|
||||
- `ows.machine` fügt nur OWS-spezifische Felder hinzu (category, area_id, etc.)
|
||||
- Nahtlose Integration in bestehendes Maintenance-System
|
||||
|
||||
**Beispiel:**
|
||||
```python
|
||||
class OwsMachine(models.Model):
|
||||
_name = 'ows.machine'
|
||||
_inherits = {'maintenance.equipment': 'equipment_id'}
|
||||
|
||||
equipment_id = fields.Many2one('maintenance.equipment', required=True, ondelete='cascade')
|
||||
category = fields.Selection(...) # OWS-spezifisch
|
||||
area_id = fields.Many2one(...) # OWS-spezifisch
|
||||
```
|
||||
|
||||
Beim Zugriff auf `machine.name` wird automatisch `machine.equipment_id.name` zurückgegeben.
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Modul-Zusammenhänge
|
||||
|
||||
### Installation & Abhängigkeiten
|
||||
|
||||
```
|
||||
OCA maintenance ─────┐
|
||||
OCA maintenance_ │
|
||||
equipment_status ──┤
|
||||
▼
|
||||
open_workshop_base ◄──────────┐
|
||||
│ │
|
||||
┌───────────┼─────────────┐ │
|
||||
│ │ │ │
|
||||
▼ ▼ ▼ │
|
||||
open_workshop open_workshop open_workshop
|
||||
_pos _dokuwiki _employee_
|
||||
imagegenerator
|
||||
│ │
|
||||
▼ │
|
||||
open_workshop_pos_ │
|
||||
customer_display ◄───────────────┘
|
||||
```
|
||||
|
||||
**Installation:**
|
||||
|
||||
Die OCA Module `maintenance` und `maintenance_equipment_status` müssen im `addons_path` verfügbar sein. Sie werden automatisch mit installiert, wenn `open_workshop_base` installiert wird (siehe `depends` in der `__manifest__.py`).
|
||||
|
||||
```bash
|
||||
git clone https://github.com/OCA/maintenance.git
|
||||
cd maintenance
|
||||
git reset --hard 5510275e
|
||||
```
|
||||
|
||||
**Installationsreihenfolge:**
|
||||
1. `open_workshop_base` (Kernmodul - installiert OCA-Abhängigkeiten automatisch)
|
||||
2. Optionale Module je nach Bedarf:
|
||||
- `open_workshop_pos` für POS-Integration
|
||||
- `open_workshop_dokuwiki` für Wiki-Dokumentation
|
||||
- `open_workshop_employee_imagegenerator` für Mitarbeiterfotos
|
||||
- `open_workshop_pos_customer_display` für Customer Display (benötigt imagegenerator)
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Entwicklungshinweise
|
||||
|
||||
### Wo finde ich was?
|
||||
|
||||
#### open_workshop_base
|
||||
- **Models**: `/models/ows_models.py` - Alle Kern-Datenmodelle
|
||||
- **Views**: `/views/` - Backend-Ansichten (Form, Tree, Kanban)
|
||||
- Erweiterte Views:
|
||||
- `res.partner` (Kontakte): Formular, Listenansicht - Maschinenfreigaben-Tab
|
||||
- `maintenance.equipment`: Formular, Listenansicht, Suchansicht - OWS-Felder
|
||||
- **Security**: `/security/ir.model.access.csv` - Zugriffsrechte
|
||||
- **Data**: `/data/` - Stammdaten (Equipment-Status, Bereiche)
|
||||
|
||||
#### open_workshop_pos
|
||||
- **JavaScript**: `/static/src/js/` - POS Frontend-Logik
|
||||
- `ows_pos_sidebar.js` - POS Maschinensidebar
|
||||
- `ows_pos_customer_sidebar.js` - Kundensidebar
|
||||
- `ows_machine_access_list.js` - Zugriffsliste
|
||||
- **Templates**: `/static/src/xml/` - OWL Templates für POS UI
|
||||
- **Models**: `/models/pos_order.py` - POS Order Extensions
|
||||
|
||||
#### open_workshop_dokuwiki
|
||||
- **Models**: `/models/`
|
||||
- `dokuwiki_client.py` - DokuWiki XML-RPC Client
|
||||
- `maintenance_equipment.py` - Equipment Extensions
|
||||
- **Views**: `/views/`
|
||||
- Erweiterte Views:
|
||||
- `maintenance.equipment`: Smart Button "Wiki öffnen"
|
||||
- `maintenance.equipment.status`: DokuWiki-Synchronisation
|
||||
- **Wizard**: `/wizard/` - Synchronisations-Assistenten
|
||||
|
||||
#### open_workshop_employee_imagegenerator
|
||||
- **JavaScript**: `/static/src/js/employee_image_widget.js` - Cropper Widget
|
||||
- **Templates**: `/static/src/xml/employee_image_widget.xml` - Widget UI
|
||||
- **Views**: `/views/`
|
||||
- Erweiterte Views:
|
||||
- `hr.employee`: Button "Namensschild erstellen" im Formular
|
||||
- **CSS**: `/static/src/css/` - Badge-Styling
|
||||
- **Library**: `/static/lib/cropperjs/` - Cropper.js Integration
|
||||
|
||||
#### open_workshop_pos_customer_display
|
||||
- **JavaScript**: `/static/src/js/customer_display.js` - Display-Logik
|
||||
- **CSS**: `/static/src/css/employee_badge.css` - Badge-Styling
|
||||
|
||||
### Wichtige Methoden & RPC Calls
|
||||
|
||||
#### open_workshop_base
|
||||
- `ows.machine.get_access_list_grouped(partner_id)` - Gruppierte Zugriffsliste für POS
|
||||
- Liefert alle Bereiche mit Maschinen und Zugriffssstatus für einen Partner
|
||||
- Wird vom POS Frontend verwendet
|
||||
|
||||
#### res.partner Extensions
|
||||
- `machine_access_ids` - One2many zu allen Freigaben
|
||||
- `machine_access_html` - Computed HTML-Tabelle für Backend-Ansicht im Res.partner / Kontakt Formular
|
||||
- `ows_user_id` - One2many zu ows.user (Benutzerdaten)
|
||||
|
||||
### Erweiterung des Systems
|
||||
|
||||
**Neue Sicherheitskategorie hinzufügen:**
|
||||
1. In `ows_models.py` → `OwsMachine.category` Selection erweitern
|
||||
2. Icon-Mapping in `_compute_category_icon()` aktualisieren
|
||||
3. CSS in `/static/src/css/category_color.css` ergänzen
|
||||
|
||||
**Neuen Maschinenbereich erstellen:**
|
||||
- Im Backend: **Wartung → Konfiguration → Bereiche**
|
||||
- Oder via Data-File in `/data/` für Standardbereiche
|
||||
|
||||
**POS-UI anpassen:**
|
||||
- OWL Templates in `/static/src/xml/` bearbeiten
|
||||
- JavaScript-Komponenten in `/static/src/js/` erweitern
|
||||
- CSS in `/static/src/css/` anpassen
|
||||
|
||||
---
|
||||
|
||||
## 📝 Technische Details
|
||||
|
||||
### Backend-Konfiguration
|
||||
|
||||
**Sicherheitskategorien zuweisen:**
|
||||
- Menü: **Wartung → Ausrüstung** → [Equipment auswählen]
|
||||
- Im Formular: Feld "Sicherheitskategorie" (🟢/🟡/🔴)
|
||||
- Die Kategorie ist direkt am `ows.machine` / `maintenance.equipment` gespeichert
|
||||
|
||||
**Einweisungsprodukte zuweisen:**
|
||||
- Menü: **Wartung → Ausrüstung** → [Equipment auswählen]
|
||||
- Im Formular: Tab "Einweisungsprodukte"
|
||||
- Button "Hinzufügen" → Produkt aus `product.product` auswählen
|
||||
- Speichert Verknüpfung in `ows.machine.training` (Many2many über One2many)
|
||||
- Beim Verkauf dieses Produkts am POS wird automatisch die Freigabe erteilt
|
||||
|
||||
**Nutzungsprodukte zuweisen:**
|
||||
- Gleicher Weg wie Einweisungsprodukte
|
||||
- Tab "Nutzungsprodukte" im Equipment-Formular
|
||||
- Speichert Verknüpfung in `ows.machine.product`
|
||||
- Diese Produkte können für Nutzungsgebühren verwendet werden
|
||||
|
||||
**Maschinenfreigaben manuell vergeben:**
|
||||
- Menü: **Kontakte** → [Partner auswählen]
|
||||
- Tab "Maschinenfreigaben" (von open_workshop_base hinzugefügt)
|
||||
- Übersicht aller Bereiche und Maschinen mit aktuellem Freigabestatus
|
||||
- Freigaben werden jedoch hauptsächlich automatisch über POS-Verkäufe vergeben
|
||||
|
||||
**Konfiguration:**
|
||||
- **Bereiche**: Wartung → Konfiguration → Bereiche
|
||||
- **Produkt-Zuordnungen**: Wartung → Konfiguration → Zuordnungen
|
||||
- Nutzungsprodukte (Übersicht aller Zuordnungen)
|
||||
- Einweisungsprodukte (Übersicht aller Zuordnungen)
|
||||
|
||||
---
|
||||
|
||||
### Sicherheitskategorien
|
||||
|
||||
- **🟢 Grün (Kategorie 1)**: Keine Einweisungspflicht, freier Zugang
|
||||
- **🟡 Gelb (Kategorie 2)**: Empfohlene Einweisung, optionale Freigabe
|
||||
- **🔴 Rot (Kategorie 3)**: Einweisung zwingend erforderlich, POS zeigt nur rote Maschinen
|
||||
|
||||
### Freigabenverwaltung
|
||||
|
||||
Freigaben werden in `ows.machine.access` gespeichert mit:
|
||||
- Partner (Kunde)
|
||||
- Maschine
|
||||
- Datum der Freigabe
|
||||
- Optional: Ablaufdatum
|
||||
- Herkunft (granted_by_pos)
|
||||
|
||||
### DokuWiki Namespace-Struktur
|
||||
|
||||
```
|
||||
{equipment_namespace}:
|
||||
├── overview # Übersichtstabelle (von Odoo)
|
||||
└── odoo-status/ # Maschinengeneriert (read-only)
|
||||
├── maschine_1 # Status-Seite für Include
|
||||
├── maschine_2
|
||||
└── c_template # Template für Status-Seiten
|
||||
|
||||
{bereich}: # Benutzer-Namespaces
|
||||
├── holz/
|
||||
│ ├── maschine_1 # Benutzer-Dokumentation
|
||||
│ └── maschine_2 # (inkludiert odoo-status via {{page>}})
|
||||
└── metall/
|
||||
└── ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📄 Lizenz
|
||||
|
||||
AGPL-3 (open_workshop_base)
|
||||
LGPL-3 (weitere Module)
|
||||
|
||||
---
|
||||
|
||||
## 👤 Autor
|
||||
|
||||
Matthias Lotz / Hobbyhimmel
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Verwandte Projekte
|
||||
|
||||
- [OCA Maintenance](https://github.com/OCA/maintenance) - Basis Equipment-Verwaltung
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
from . import models
|
||||
from . import controllers
|
||||
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
{
|
||||
'name': 'POS Open Workshop',
|
||||
'license': 'AGPL-3',
|
||||
'version': '18.0.1.0.0',
|
||||
'summary': 'Erstellt Maschinenfreigaben basierend auf POS-Einweisungsprodukten',
|
||||
'depends': ['base', 'account','product','sale','contacts','point_of_sale'],
|
||||
'author': 'matthias.lotz',
|
||||
'category': 'Point of Sale',
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'views/machine_product_training_views.xml',
|
||||
'views/menu_views.xml',
|
||||
'views/machine_area_views.xml',
|
||||
'views/machine_views.xml',
|
||||
'views/res_partner_view.xml',
|
||||
'data/data.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
'open_workshop/static/src/css/category_color.css',
|
||||
],
|
||||
'point_of_sale._assets_pos': [
|
||||
'open_workshop/static/src/css/pos.css',
|
||||
'open_workshop/static/src/js/ows_machine_access_list.js',
|
||||
'open_workshop/static/src/js/ows_pos_customer_sidebar.js',
|
||||
'open_workshop/static/src/js/ows_pos_sidebar.js',
|
||||
'open_workshop/static/src/js/ows_product_screen_template_patch.js',
|
||||
'open_workshop/static/src/xml/ows_machine_access_list.xml',
|
||||
'open_workshop/static/src/xml/ows_pos_customer_sidebar.xml',
|
||||
'open_workshop/static/src/xml/ows_pos_sidebar.xml',
|
||||
'open_workshop/static/src/xml/ows_product_screen_template_patch.xml',
|
||||
],
|
||||
},
|
||||
'description': """
|
||||
Diese App erstellt Maschinenfreigaben basierend auf POS-Einweisungsprodukten.
|
||||
Die App ist für den Einsatz in der Odoo-Version 18.0 konzipiert.
|
||||
""",
|
||||
}
|
||||
23
aspl_equipment_qrcode_generator/__init__.py
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import api, SUPERUSER_ID
|
||||
|
||||
from . import models
|
||||
from . import report
|
||||
from . import wizard
|
||||
|
||||
|
||||
# TODO: Generate Sequence For each company for Equipment
|
||||
def pre_init_hook(env):
|
||||
company_ids = env['res.company'].search([])
|
||||
for company_id in company_ids:
|
||||
sequence_id = env['ir.sequence'].search(
|
||||
[('name', '=', 'Equipment Company Sequence'), ('company_id', '=', company_id.id)])
|
||||
if not sequence_id:
|
||||
env['ir.sequence'].create({
|
||||
'name': 'Equipment Company Sequence',
|
||||
'prefix': company_id.id,
|
||||
'padding': 5,
|
||||
'number_increment': 1,
|
||||
'company_id': company_id.id
|
||||
})
|
||||
36
aspl_equipment_qrcode_generator/__manifest__.py
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
###############################################################################
|
||||
#
|
||||
# Aspire Softserv Pvt. Ltd.
|
||||
# Copyright (C) Aspire Softserv Pvt. Ltd.(<https://aspiresoftserv.com>).
|
||||
#
|
||||
###############################################################################
|
||||
{
|
||||
"name": "QR Code on Equipment",
|
||||
'category': '',
|
||||
"summary": "Add QR Code on equipment for managing equipment.",
|
||||
"version": "18.0.0.1.0",
|
||||
"license": "AGPL-3",
|
||||
"price": 00.00,
|
||||
'description': """
|
||||
The Equipment Management Module generates unique QR codes for each asset, offering instant details and direct Odoo profile access for seamless management.
|
||||
""",
|
||||
"author": "Aspire Softserv Pvt. Ltd",
|
||||
"website": "https://aspiresoftserv.com",
|
||||
"depends": ['account','maintenance'],
|
||||
"external_dependencies": {
|
||||
'python': ['qrcode']
|
||||
},
|
||||
"data": [
|
||||
'views/maintenance_equipment.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'report/custom_qrcode.xml',
|
||||
'wizard/equipment_label_layout_views.xml',
|
||||
],
|
||||
'pre_init_hook': 'pre_init_hook',
|
||||
"application": True,
|
||||
"installable": True,
|
||||
"maintainer": "Aspire Softserv Pvt. Ltd",
|
||||
"support": "odoo@aspiresoftserv.com",
|
||||
'images': ['static/description/banner.gif'],
|
||||
}
|
||||
4
aspl_equipment_qrcode_generator/models/__init__.py
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import maintenance_equipment
|
||||
from . import res_company
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import models, fields
|
||||
|
||||
|
||||
class MaintenanceEquipment(models.Model):
|
||||
_inherit = 'maintenance.equipment'
|
||||
|
||||
qr_code = fields.Binary("QR Code", attachment=True)
|
||||
comp_serial_no = fields.Char("Inventory Serial No", tracking=True)
|
||||
serial_no = fields.Char('Mfg. Serial Number', copy=False)
|
||||
|
||||
def action_print_qrcode_layout(self):
|
||||
action = self.env['ir.actions.act_window']._for_xml_id('aspl_equipment_qrcode_generator.action_open_label_layout_equipment')
|
||||
action['context'] = {'default_equipment_ids': self.ids}
|
||||
return action
|
||||
|
||||
def generate_serial_no(self):
|
||||
for equipment_id in self:
|
||||
if not equipment_id.comp_serial_no:
|
||||
company_id = equipment_id.company_id.id
|
||||
sequence_id = self.env['ir.sequence'].search(
|
||||
[('name', '=', 'Equipment Company Sequence'), ('company_id', '=', company_id)])
|
||||
if sequence_id:
|
||||
data = sequence_id._next()
|
||||
equipment_id.write({
|
||||
'comp_serial_no': data
|
||||
})
|
||||
22
aspl_equipment_qrcode_generator/models/res_company.py
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import models, api
|
||||
|
||||
|
||||
class ResCompany(models.Model):
|
||||
_inherit = 'res.company'
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
result = super(ResCompany, self).create(vals)
|
||||
sequence_id = self.env['ir.sequence'].search(
|
||||
[('name', '=', 'Equipment Company Sequence'), ('company_id', '=', result.id)])
|
||||
if not sequence_id:
|
||||
self.env['ir.sequence'].create({
|
||||
'name': 'Equipment Company Sequence',
|
||||
'prefix': result.id,
|
||||
'padding': 5,
|
||||
'number_increment': 1,
|
||||
'company_id': result.id
|
||||
})
|
||||
return result
|
||||
3
aspl_equipment_qrcode_generator/report/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import custom_qrcode_generator
|
||||
190
aspl_equipment_qrcode_generator/report/custom_qrcode.xml
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<template id="report_equipment_simple_label2x7">
|
||||
<t t-set="qrcode_size" t-value="'height:14mm'"/>
|
||||
<t t-set="table_style" t-value="'width:97mm;height:37.1mm;margin:inherit;' + table_style"/>
|
||||
<td t-att-style="make_invisible and 'visibility:hidden;'" >
|
||||
<div class="o_label_full" t-att-style="table_style">
|
||||
<div class="o_label_name">
|
||||
<strong t-field="equipment.name"/>
|
||||
</div>
|
||||
<div class="o_label_data">
|
||||
<div class="text-center o_label_right_column">
|
||||
<t t-if="equipment.qr_code">
|
||||
<img t-att-src="'data:image/png;base64,' + equipment.qr_code.decode('utf-8')" style="width: 130px;margin-top: -40px;"/>
|
||||
</t>
|
||||
</div>
|
||||
<div class="text-left" style="line-height:normal;word-wrap: break-word;">
|
||||
<span class="text-nowrap" t-field="equipment.serial_no"/>
|
||||
<div class="o_label_extra_data">
|
||||
<span t-field="equipment.comp_serial_no"/>
|
||||
</div>
|
||||
<t t-if="equipment.warranty_date">
|
||||
<strong t-field="equipment.warranty_date"/>
|
||||
</t>
|
||||
</div>
|
||||
<div class="o_label_clear"></div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</template>
|
||||
|
||||
<template id="report_equipment_simple_label4x7">
|
||||
<t t-set="barcode_size" t-value="'width:80px;'"/>
|
||||
<t t-set="table_style" t-value="'width:47mm;height:37.1mm;margin:inherit;' + table_style"/>
|
||||
<td t-att-style="make_invisible and 'visibility:hidden;'" >
|
||||
<div class="o_label_full" t-att-style="table_style">
|
||||
<div class="o_label_name">
|
||||
<strong t-field="equipment.name"/>
|
||||
</div>
|
||||
<div class= "text-center o_label_right_column">
|
||||
<t t-if="equipment.qr_code">
|
||||
<img t-att-src="'data:image/png;base64,' + equipment.qr_code.decode('utf-8')" style="width:95px;padding:0px;margin-top:-10px"/>
|
||||
</t>
|
||||
</div>
|
||||
<div class="text-left o_label_left_column" style="line-height:normal;word-wrap: break-word;">
|
||||
<div class="o_label_data">
|
||||
<strong t-field="equipment.serial_no"/>
|
||||
<span t-field="equipment.comp_serial_no"/>
|
||||
<t t-if="equipment.warranty_date">
|
||||
<strong t-field="equipment.warranty_date"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</template>
|
||||
|
||||
<template id="report_equipment_simple_label2x5">
|
||||
<t t-set="barcode_size" t-value="'width:38mm;height:14mm;margin-top:50px;'"/>
|
||||
<t t-set="table_style"
|
||||
t-value="'width:97mm;height:50mm;font-size: 12px;border: 2px solid black;table-layout: fixed;'"/>
|
||||
<td>
|
||||
<table t-att-style="table_style">
|
||||
<tr>
|
||||
<td style="width:25%;">Name</td>
|
||||
<td style="width:5%">:</td>
|
||||
<td colspan="2" style="width:70%;word-wrap: break-word;">
|
||||
<span t-field="equipment.name"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="width:25%">Model</td>
|
||||
<td style="width:5%">:</td>
|
||||
<td style="width:35%;word-wrap: break-word;">
|
||||
<span t-field="equipment.model"/>
|
||||
</td>
|
||||
<td rowspan="5" style="width:35%">
|
||||
<t t-if="equipment.qr_code">
|
||||
<img t-att-src="'data:image/png;base64,' + equipment.qr_code.decode('utf-8')"
|
||||
style="width:30mm;height:30mm;"/>
|
||||
</t>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td style="width:25%">Mfg Serial</td>
|
||||
<td style="width:5%">:</td>
|
||||
<td style="width:35%;font-size:10px;word-wrap: break-word;">
|
||||
<span t-field="equipment.serial_no"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="width:25%">Serial</td>
|
||||
<td style="width:5%">:</td>
|
||||
<td style="width:35%;word-wrap: break-word;">
|
||||
<span t-field="equipment.comp_serial_no"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="width:25%">Warranty Date</td>
|
||||
<td style="width:5%">:</td>
|
||||
<td style="width:35%;word-wrap: break-word;">
|
||||
<span t-field="equipment.warranty_date"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="width:25%"></td>
|
||||
<td style="width:5%"></td>
|
||||
<td style="width:35%;word-wrap: break-word;"></td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</template>
|
||||
|
||||
<template id="report_quipmentlabel">
|
||||
<t t-call="web.html_container">
|
||||
<t t-if="columns and rows">
|
||||
<t t-if="columns == 2 and rows == 7">
|
||||
<t t-set="padding_page" t-value="'padding: 14mm 3mm'"/>
|
||||
<t t-set="report_to_call" t-value="'aspl_equipment_qrcode_generator.report_equipment_simple_label2x7'"/>
|
||||
</t>
|
||||
<t t-if="columns == 4 and rows == 7">
|
||||
<t t-set="padding_page" t-value="'padding: 14mm 3mm'"/>
|
||||
<t t-set="report_to_call" t-value="'aspl_equipment_qrcode_generator.report_equipment_simple_label4x7'"/>
|
||||
</t>
|
||||
<t t-if="columns == 2 and rows == 5">
|
||||
<t t-set="padding_page" t-value="'padding: 14mm 3mm'"/>
|
||||
<t t-set="report_to_call" t-value="'aspl_equipment_qrcode_generator.report_equipment_simple_label2x5'"/>
|
||||
</t>
|
||||
<t t-foreach="range(page_numbers)" t-as="page">
|
||||
<div class="o_label_sheet" t-att-style="padding_page">
|
||||
<table class="my-0 table table-sm table-borderless">
|
||||
<t t-foreach="range(rows)" t-as="row">
|
||||
<tr>
|
||||
<t t-foreach="range(columns)" t-as="column">
|
||||
<t t-if="equipment_data">
|
||||
<t t-set="current_data" t-value="equipment_data.popitem()"/>
|
||||
<t t-set="equipment" t-value="current_data[0]"/>
|
||||
<t t-set="table_style" t-value="'border: 1px solid black'"/>
|
||||
<t t-call="{{report_to_call}}"/>
|
||||
</t>
|
||||
</t>
|
||||
</tr>
|
||||
</t>
|
||||
</table>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="maintenance_quip">
|
||||
<t t-call="web.basic_layout">
|
||||
<div class="page">
|
||||
<t t-call="aspl_equipment_qrcode_generator.report_quipmentlabel">
|
||||
<t t-set="products" t-value="products"/>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<record id="paperformat_label_sheet_qrcode" model="report.paperformat">
|
||||
<field name="name">A4 Label Sheet</field>
|
||||
<field name="default" eval="True"/>
|
||||
<field name="format">A4</field>
|
||||
<field name="page_height">0</field>
|
||||
<field name="page_width">0</field>
|
||||
<field name="orientation">Portrait</field>
|
||||
<field name="margin_top">0</field>
|
||||
<field name="margin_bottom">0</field>
|
||||
<field name="margin_left">0</field>
|
||||
<field name="margin_right">0</field>
|
||||
<field name="disable_shrinking" eval="True"/>
|
||||
<field name="dpi">96</field>
|
||||
</record>
|
||||
|
||||
<record id="report_equipment_label" model="ir.actions.report">
|
||||
<field name="name">Equipment QR-code (PDF)</field>
|
||||
<field name="model">maintenance.equipment</field>
|
||||
<field name="report_type">qweb-pdf</field>
|
||||
<field name="report_name">aspl_equipment_qrcode_generator.maintenance_quip</field>
|
||||
<field name="report_file">aspl_equipment_qrcode_generator.maintenance_quip</field>
|
||||
<field name="paperformat_id" ref="aspl_equipment_qrcode_generator.paperformat_label_sheet_qrcode"/>
|
||||
<field name="print_report_name">'Products Labels - %s' % (object.name)</field>
|
||||
<field name="binding_model_id" eval="False"/>
|
||||
<field name="binding_type">report</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class ReportProductTemplateLabel(models.AbstractModel):
|
||||
_name = 'report.aspl_equipment_qrcode_generator.maintenance_quip'
|
||||
_description = 'Equipment QR-code Report'
|
||||
|
||||
def _get_report_values(self, docids, data=None):
|
||||
"""
|
||||
QR-Code-Generierung erfolgt im Wizard (equipment_label_layout.py).
|
||||
Dieser Report rendert nur die bereits generierten QR-Codes.
|
||||
"""
|
||||
if not data:
|
||||
return {}
|
||||
|
||||
equipment_label_layout_id = self.env['equipment.label.layout'].browse(data.get('equipment_label_layout_id'))
|
||||
|
||||
equipment_dict = {}
|
||||
for equipment in equipment_label_layout_id.equipment_ids:
|
||||
equipment_dict[equipment] = 1
|
||||
|
||||
page_numbers = (len(equipment_label_layout_id.equipment_ids) - 1) // (
|
||||
equipment_label_layout_id.rows * equipment_label_layout_id.columns
|
||||
) + 1
|
||||
|
||||
return {
|
||||
'rows': equipment_label_layout_id.rows,
|
||||
'columns': equipment_label_layout_id.columns,
|
||||
'page_numbers': page_numbers,
|
||||
'equipment_data': equipment_dict
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_equipment_label_layout,access.equipment_label_layout,model_equipment_label_layout,,1,1,1,1
|
||||
|
BIN
aspl_equipment_qrcode_generator/static/description/banner.gif
Normal file
|
After Width: | Height: | Size: 78 KiB |
BIN
aspl_equipment_qrcode_generator/static/description/icon.png
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 563 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 362 KiB |
165
aspl_equipment_qrcode_generator/static/description/index.html
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
<section class="oe_container" style="border-left:1px solid grey;border-right :1px solid grey;font-family: Urbanist ,'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;" >
|
||||
<div class="oe_row oe_spaced">
|
||||
<div style="display:flex;margin-top:5rem;padding : 0rem 5rem;">
|
||||
<div><img src="./images/logo.png" style="width:200px;"/></div>
|
||||
<div style="width: 100%;text-align: end;"><a href="https://hrms.aspiresoftserv.com/contactus" class="btn" target="_blank" style="background-color:#011D45;color:white;border-radius:5px;"><i class="fa fa-envelope" style="margin-right:0.5rem"></i>Request A Demo</a></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="oe_row oe_spaced" style="margin-top:5rem;text-align:center;width:100%;">
|
||||
<h3 style="color:#011D45;">Euipment Qr-code</h3>
|
||||
<h6 style="color:#011D45;width: 50%;margin: auto;text-align: left;">
|
||||
The Equipment Management Module streamlines the intricate process of equipment management by generating unique QR codes for each asset. Scanning these QR codes instantly provides comprehensive details about the equipment. Additionally, the QR code features a direct link to the equipment's profile in Odoo, facilitating seamless management and updates.</h6>
|
||||
<br/>
|
||||
<h4 style="color:#011D45;width: 50%;margin: auto;text-align: left;margin-bottom:2px;">Prerequisite:-</h4>
|
||||
|
||||
<h6 style="color:#011D45;width: 50%;margin: auto;text-align: left;">
|
||||
pip install qrcode==6.1
|
||||
</h6>
|
||||
</div>
|
||||
<div class="oe_row oe_spaced" style="display:flex;margin-top:3rem;">
|
||||
<div style="width:750px;margin: auto;">
|
||||
<img src="./banner.gif" style="width:100%">
|
||||
</div>
|
||||
</div>
|
||||
<div class="oe_row oe_spaced" style="display:flex;">
|
||||
<h3 style="color:#011D45;margin: auto;">Screenshots</h3>
|
||||
</div>
|
||||
<div class="oe_row oe_spaced" style="display:flex;">
|
||||
<div style="margin: auto;">
|
||||
<div style="width:800px;padding: 2rem;">
|
||||
<div style="width:100%;text-align:left;">
|
||||
<h4 style="color:#011D45;">Sequence Configuration</h4>
|
||||
<h6 style="color:#011D45;">Once you install the module, It will automatically create maintenance sequence for available company.</h6>
|
||||
</div>
|
||||
<div style="width:80%;margin:auto;margin-top: 1rem;">
|
||||
<img src="./screenshots/1_sequence.png" style="width:100%;">
|
||||
</div>
|
||||
</div>
|
||||
<div style="width:800px;background-color:#F4F6F6;padding: 2rem;">
|
||||
<div style="width:100%;text-align:left;">
|
||||
<h4 style="color:#011D45;">Serial number Generation</h4>
|
||||
<h6 style=" color:#011D45;">In the equipment, you'll find a "Generate Serial Number" button. When you click this button, it automatically creates a serial number based on the company's predefined sequence, which is set up when the module is installed (check above screenshot related to sequence configuration).
|
||||
</h6>
|
||||
</div>
|
||||
<div style="width:80%;margin:auto;margin-top: 1rem;">
|
||||
<img src="./screenshots/2_equipment_screen.png" style="width:100%">
|
||||
</div>
|
||||
</div>
|
||||
<div style="width:800px;padding: 2rem;">
|
||||
<div style="width:100%;text-align:left;">
|
||||
<h4 style="color:#011D45;">Print QR-Code View</h4>
|
||||
<h6 style="color:#011D45;">In the equipment tree view, When you select an item, It will display a "QR Code" button.</h6>
|
||||
</div>
|
||||
<div style="width:80%;margin:auto;margin-top: 1rem;">
|
||||
<img src="./screenshots/3_equipment_tree.png" style="width:100%;">
|
||||
</div>
|
||||
</div>
|
||||
<div style="width:800px;background-color:#F4F6F6;padding: 2rem;">
|
||||
<div style="width:100%;text-align:left;">
|
||||
<h4 style="color:#011D45;">QR Code Size Options</h4>
|
||||
<h6 style="color:#011D45;">The module will display all available options for QR code sizes, allowing you to choose the appropriate dimensions for generating your QR codes.</h6>
|
||||
</div>
|
||||
<div style="width:80%;margin:auto;margin-top: 1rem;">
|
||||
<img src="./screenshots/4_qrcode_size_selection.png" style="width:100%;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="oe_row oe_spaced" style="display:flex;margin-top:3rem;">
|
||||
<h3 style="color:#011D45;margin: auto;">Suggested Apps</h3>
|
||||
</div>
|
||||
<div id="slides" class="row carousel slide mt64 mb32" style="width:800px;margin:auto;" data-ride="carousel">
|
||||
<div class="carousel-inner">
|
||||
<div class="carousel-item active" style="min-height: 0px;">
|
||||
<div class="row no-lg-gutters">
|
||||
<div class="col-6 col-sm-3 text-center">
|
||||
<a class="d-block"
|
||||
href="https://apps.odoo.com/apps/modules/15.0/aspl_project_timesheet_activity/"
|
||||
style="text-decoration: none;" target="_blank">
|
||||
<img alt="Project Timesheet Activity" src="./images/aspl_project_timesheet_activity.png" style="width:60%;">
|
||||
<div class="card-body text-center p-0 mt-3 mb-3 mb-lg-0">
|
||||
<h6 class="card-text m-0" style="font-size: 16px; color:#011D45;font-weight: 600">
|
||||
Project Timesheet<br>Activity</h6>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-6 col-sm-3 text-center">
|
||||
<a class="d-block"
|
||||
href="https://apps.odoo.com/apps/modules/15.0/aspl_save_view_to_favourite/"
|
||||
style="text-decoration: none;" target="_blank">
|
||||
<img alt="Project Timesheet Activity" src="./images/aspl_save_view_to_favourite.png" style="width:60%;">
|
||||
<div class="card-body text-center p-0 mt-3 mb-3 mb-lg-0">
|
||||
<h6 class="card-text m-0" style="font-size: 16px; color:#011D45;font-weight: 600">
|
||||
Project Timesheet<br>Activity</h6>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-6 col-sm-3 text-center">
|
||||
<a class="d-block"
|
||||
href="https://apps.odoo.com/apps/modules/15.0/aspl_select_multi_company/"
|
||||
style="text-decoration: none;" target="_blank">
|
||||
<img alt="Select Multi Company" src="./images/aspl_select_multi_company.png" style="width:60%;">
|
||||
<div class="card-body text-center p-0 mt-3 mb-3 mb-lg-0">
|
||||
<h6 class="card-text m-0" style="font-size: 16px; color:#011D45;font-weight: 600">
|
||||
Select Multi<br>Company</h6>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-6 col-sm-3 text-center">
|
||||
<a class="d-block"
|
||||
href="https://apps.odoo.com/apps/modules/15.0/aspl_project_task_milestone/"
|
||||
style="text-decoration: none;" target="_blank">
|
||||
<img alt="Project Task Milestone" src="./images/project_task_milestone.png" style="width:60%;">
|
||||
<div class="card-body text-center p-0 mt-3 mb-3 mb-lg-0">
|
||||
<h6 class="card-text m-0" style="font-size: 16px;color:#011D45; font-weight: 600">
|
||||
Project Task <br>Milestone</h6>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-6 col-sm-3 text-center">
|
||||
<a class="d-block"
|
||||
href="https://apps.odoo.com/apps/modules/15.0/aspl_equipment_qrcode_generator/"
|
||||
style="text-decoration: none;" target="_blank">
|
||||
<img alt="Qr Code" src="./icon.png" style="width:60%;">
|
||||
<div class="card-body text-center p-0 mt-3 mb-3 mb-lg-0">
|
||||
<h6 class="card-text m-0" style="font-size: 16px;color:#011D45; font-weight: 600">
|
||||
Qr Code</h6>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="oe_row oe_spaced" style="display:flex;margin-top:1rem;">
|
||||
<div style="width:750px;margin: auto;">
|
||||
<img src="./images/service_banner.png" style="width:100%">
|
||||
</div>
|
||||
</div>
|
||||
<div class="oe_row oe_spaced" style="display:flex;margin-top:4rem;">
|
||||
<div style="width:800px;height:140px;margin: auto;">
|
||||
<img src="./images/bg_image.png" width="100%;">
|
||||
<div style="margin-left: 2rem;margin-top:-110px;display: flex;">
|
||||
<div>
|
||||
<h5 style="color:white;">Free Support</h5>
|
||||
<p style="color:white;">We will provide free support for any issues,queries and bug fixing<br/>upto
|
||||
90 days from the date of purchase of this application.</p>
|
||||
</div>
|
||||
<div style="margin-left: auto;margin-right: 2rem;margin-top: 2rem;">
|
||||
<div style="background-color:white;color:#011D45;border-radius:5px;padding: 4px;width:205px;text-align:center;"><a
|
||||
href="https://hrms.aspiresoftserv.com/contactus" style="text-decoration:none;color:#011D45;"
|
||||
target="_blank">Request Free Support</a></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="oe_row oe_spaced" style="display:flex;margin-top:-35px;">
|
||||
<div style="width:600px;display:flex;margin: auto;">
|
||||
<div style="display: flex;"><i class="fa fa-envelope" style="margin-right:0.4rem;margin-top:0.2rem;color:#D81A58;"></i><a href="mailto:sales@aspiresoftserv.com" style="text-decoration:none;color:#D81A58;">sales@aspiresoftserv.com</a></div>
|
||||
<div style="display: flex;margin: 0 3rem;"><i class="fa fa-skype" style="margin-right:0.4rem;color:#D81A58;margin-top:0.2rem;"></i><a href="skype:aspire.software?chat" style="text-decoration:none;color:#D81A58;">aspire.software</a></div>
|
||||
<div style="display: flex;"><i class="fa fa-phone" style="margin-right:0.4rem;color:#D81A58;margin-top:0.2rem;"></i><span style="text-decoration:none;color:#D81A58;">+916351895006</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 196 KiB |
|
|
@ -0,0 +1,56 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="view_extend_equipment_tree" model="ir.ui.view">
|
||||
<field name="name">maintenance.equipment.tree.inherit</field>
|
||||
<field name="model">maintenance.equipment</field>
|
||||
<field name="priority" eval="16"/>
|
||||
<field name="inherit_id" ref="maintenance.hr_equipment_view_tree"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//list" position="inside">
|
||||
<header>
|
||||
<button string="Print QR-code" type="object" name="action_print_qrcode_layout"/>
|
||||
</header>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_extend_equipment_form" model="ir.ui.view">
|
||||
<field name="name">maintenance.equipment.form.inherit</field>
|
||||
<field name="model">maintenance.equipment</field>
|
||||
<field name="priority" eval="16"/>
|
||||
<field name="inherit_id" ref="maintenance.hr_equipment_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//page[@name='product_information']" position="inside">
|
||||
<group>
|
||||
<field name="comp_serial_no"/>
|
||||
</group>
|
||||
</xpath>
|
||||
<xpath expr="//sheet" position="before">
|
||||
<header>
|
||||
<button string="Generate Serial Number" type="object" name="generate_serial_no" class="oe_highlight"/>
|
||||
</header>
|
||||
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="generate_serial_no_action" model="ir.actions.server">
|
||||
<field name="name">Generate Serial Number</field>
|
||||
<field name="model_id" ref="model_maintenance_equipment"/>
|
||||
<field name="binding_model_id" ref="model_maintenance_equipment"/>
|
||||
<field name="binding_view_types">list</field>
|
||||
<field name="state">code</field>
|
||||
<field name="code">action = records.generate_serial_no()</field>
|
||||
</record>
|
||||
|
||||
<record id="generate_qrcode_no_action" model="ir.actions.server">
|
||||
<field name="name">Print QR-Code</field>
|
||||
<field name="model_id" ref="model_maintenance_equipment"/>
|
||||
<field name="binding_model_id" ref="model_maintenance_equipment"/>
|
||||
<field name="binding_view_types">form</field>
|
||||
<field name="state">code</field>
|
||||
<field name="code">action = records.action_print_qrcode_layout()</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
3
aspl_equipment_qrcode_generator/wizard/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import equipment_label_layout
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
|
||||
|
||||
class EquipmentLabelLayout(models.TransientModel):
|
||||
_name = 'equipment.label.layout'
|
||||
_description = 'Choose the sheet layout to print the labels'
|
||||
|
||||
print_format = fields.Selection([
|
||||
('2x5', '2 x 5'),
|
||||
('2x7', '2 x 7'),
|
||||
('4x7', '4 x 7')], string="Format", default='2x5', required=True)
|
||||
equipment_ids = fields.Many2many('maintenance.equipment')
|
||||
rows = fields.Integer(compute='_compute_dimensions')
|
||||
columns = fields.Integer(compute='_compute_dimensions')
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields_list):
|
||||
"""Override to properly set equipment_ids from context"""
|
||||
res = super().default_get(fields_list)
|
||||
if self.env.context.get('active_ids'):
|
||||
res['equipment_ids'] = [(6, 0, self.env.context.get('active_ids', []))]
|
||||
elif self.env.context.get('default_equipment_ids'):
|
||||
res['equipment_ids'] = [(6, 0, self.env.context.get('default_equipment_ids', []))]
|
||||
return res
|
||||
|
||||
@api.depends('print_format')
|
||||
def _compute_dimensions(self):
|
||||
for wizard in self:
|
||||
if 'x' in wizard.print_format:
|
||||
columns, rows = wizard.print_format.split('x')[:2]
|
||||
wizard.columns = int(columns)
|
||||
wizard.rows = int(rows)
|
||||
else:
|
||||
wizard.columns, wizard.rows = 1, 1
|
||||
|
||||
|
||||
def process_label(self):
|
||||
# Generiere QR-Codes für alle Equipment VOR dem Report
|
||||
self._generate_qr_codes()
|
||||
|
||||
xml_id = 'aspl_equipment_qrcode_generator.report_equipment_label'
|
||||
data = {
|
||||
'equipment_label_layout_id': self.id
|
||||
}
|
||||
|
||||
# report_action benötigt die Equipment IDs als docids
|
||||
return self.env.ref(xml_id).report_action(self.equipment_ids.ids, data=data)
|
||||
|
||||
def _generate_qr_codes(self):
|
||||
"""Generiert QR-Codes für alle ausgewählten Equipment"""
|
||||
import base64
|
||||
from io import BytesIO
|
||||
import qrcode
|
||||
|
||||
# Hole die base_url für die Equipment-Links
|
||||
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
||||
menu_id = self.env.ref('maintenance.menu_equipment_form').sudo().id
|
||||
action_id = self.env.ref('maintenance.hr_equipment_action').sudo().id
|
||||
|
||||
for equipment in self.equipment_ids:
|
||||
# Extrahiere Namen aus JSONB falls nötig
|
||||
equipment_name = equipment.name
|
||||
if isinstance(equipment_name, dict):
|
||||
equipment_name = equipment_name.get('de_DE') or equipment_name.get('en_US') or str(equipment_name)
|
||||
|
||||
category_name = equipment.category_id.name if equipment.category_id else ""
|
||||
if isinstance(category_name, dict):
|
||||
category_name = category_name.get('de_DE') or category_name.get('en_US') or str(category_name)
|
||||
|
||||
# Erstelle Equipment-Link
|
||||
equipment_link = f"{base_url}/web#id={equipment.id}&menu_id={menu_id}&action={action_id}&model=maintenance.equipment&view_type=form"
|
||||
|
||||
# Erstelle Equipment-Details für QR-Code
|
||||
qr_data = f"Name: {equipment_name}\n"
|
||||
qr_data += f"Model: {equipment.model or ''}\n"
|
||||
qr_data += f"Mfg serial no: {equipment.serial_no or ''}\n"
|
||||
qr_data += f"Warranty Exp. Date: {equipment.warranty_date or ''}\n"
|
||||
qr_data += f"Category: {category_name}\n\n"
|
||||
qr_data += equipment_link
|
||||
|
||||
# Generiere QR-Code
|
||||
qr = qrcode.QRCode(
|
||||
version=1,
|
||||
error_correction=qrcode.constants.ERROR_CORRECT_L,
|
||||
box_size=20,
|
||||
border=4,
|
||||
)
|
||||
qr.add_data(qr_data)
|
||||
qr.make(fit=True)
|
||||
img = qr.make_image()
|
||||
temp = BytesIO()
|
||||
img.save(temp, format="PNG")
|
||||
qr_image = base64.b64encode(temp.getvalue())
|
||||
|
||||
# Speichere QR-Code im Equipment
|
||||
equipment.write({'qr_code': qr_image})
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="equipment_label_layout_form" model="ir.ui.view">
|
||||
<field name="name">equipment.label.layout.form</field>
|
||||
<field name="model">equipment.label.layout</field>
|
||||
<field name="mode">primary</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<group>
|
||||
<field name="equipment_ids" invisible="1"/>
|
||||
<group>
|
||||
<field name="print_format" widget="radio"/>
|
||||
</group>
|
||||
</group>
|
||||
<footer>
|
||||
<button name="process_label" string="Confirm" type="object" class="btn-primary"/>
|
||||
<button string="Discard" class="btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_open_label_layout_equipment" model="ir.actions.act_window">
|
||||
<field name="name">Choose Labels Layout</field>
|
||||
<field name="res_model">equipment.label.layout</field>
|
||||
<field name="view_ids"
|
||||
eval="[(5, 0, 0),
|
||||
(0, 0, {'view_mode': 'form', 'view_id': ref('equipment_label_layout_form')})]" />
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
</odoo>
|
||||
48
open_workshop_base/README.md
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# Open Workshop (open_workshop ows)
|
||||
|
||||
Dieses Odoo v18.0 Modul erweitert das POS- und Kontakt-Modul um Funktionen für offene Werkstätten (FabLabs, Makerspaces etc.) und dient der Verwaltung von Maschinen, Naschinen Einweisungen Produkten, Maschinen Nutzungsprodukten und Zugangsberechtigungen zu den Maschinen.
|
||||
|
||||
## Funktionen
|
||||
|
||||
### Erweiterungen an Kontakten (res.partner)
|
||||
- Geburtstagsfeld, RFID-Karte, Haftungsausschluss usw. ausgelagert nach `ows.user`
|
||||
- Automatische Erstellung des `ows.user`-Eintrags beim Anlegen eines Kontakts
|
||||
- Übersichtliche Darstellung aller Maschinenfreigaben im Odoo Kontaktformular
|
||||
|
||||
### Maschinen und Bereiche
|
||||
- Modell `ows.machine` mit Gruppierung nach Bereichen (`ows.machine.area`)
|
||||
- Farblich kodierte Bereiche (Hex-Wert aus Datenbank) welche zur Darstellung im POS verwendet werden
|
||||
|
||||
### Einweisungen und Nutzungen
|
||||
- Modelle `ows.machine.training` und `ows.machine.product`
|
||||
- Konfigurierbare Produkte für Einweisung/Nutzung direkt im Backend
|
||||
- Zuweisung von Nutzungsprodukten zu Maschinen
|
||||
- Zuweisung von Einweisungsprodukten zu Maschinen
|
||||
|
||||
### Maschinenfreigaben
|
||||
- Modell `ows.machine.access` verknüpft Partner und Maschine
|
||||
- Darstellung im POS als tabellarische Übersicht mit Anzeige für eine bestehende Einweisung / Nutzungsberechtigung
|
||||
- Anzeige im POS-Kundendetailsansicht innerhalb der Kundendetailsansicht
|
||||
- Anzeige im Odoo Kontak Modul der Maschineneinweisungen
|
||||
|
||||
## Installation
|
||||
|
||||
1. Dieses Modul in den Custom-Addons-Ordner kopieren
|
||||
2. Im Odoo Backend unter Apps installieren
|
||||
|
||||
## Entwicklerhinweise
|
||||
|
||||
## ToDos
|
||||
- Bearbeitung der Maschinenfreigaben im Backend
|
||||
- Automatische Erstellung von `mail.message` bei manueller Freigabe
|
||||
- Integration von Fristen (z.B. Ablaufdatum Einweisung)
|
||||
|
||||
## Autoren
|
||||
- Matthias Lotz
|
||||
|
||||
## Lizenz
|
||||
AGPL-3.0 oder später
|
||||
|
||||
---
|
||||
Letzte Aktualisierung: 06.04.2025
|
||||
|
||||
56
open_workshop_base/__init__.py
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
from . import models
|
||||
from . import controllers
|
||||
|
||||
|
||||
def post_init_hook(env):
|
||||
"""
|
||||
Migration Hook: open_workshop → open_workshop_base
|
||||
Wird automatisch nach der Installation/Update des Moduls ausgeführt
|
||||
"""
|
||||
cr = env.cr
|
||||
|
||||
# Prüfen ob altes Modul 'open_workshop' in DB existiert
|
||||
cr.execute("""
|
||||
SELECT id FROM ir_module_module
|
||||
WHERE name = 'open_workshop' AND id != (
|
||||
SELECT id FROM ir_module_module WHERE name = 'open_workshop_base'
|
||||
)
|
||||
""")
|
||||
|
||||
if cr.fetchone():
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
_logger.info("=" * 70)
|
||||
_logger.info("MIGRATION: Renaming module 'open_workshop' to 'open_workshop_base'")
|
||||
_logger.info("=" * 70)
|
||||
|
||||
# Modulnamen aktualisieren
|
||||
cr.execute("""
|
||||
UPDATE ir_module_module
|
||||
SET name = 'open_workshop_base'
|
||||
WHERE name = 'open_workshop'
|
||||
""")
|
||||
_logger.info(f"✓ Updated ir_module_module: {cr.rowcount} row(s)")
|
||||
|
||||
# ir_model_data Referenzen aktualisieren
|
||||
cr.execute("""
|
||||
UPDATE ir_model_data
|
||||
SET module = 'open_workshop_base'
|
||||
WHERE module = 'open_workshop'
|
||||
""")
|
||||
_logger.info(f"✓ Updated ir_model_data: {cr.rowcount} row(s)")
|
||||
|
||||
# Abhängigkeiten aktualisieren
|
||||
cr.execute("""
|
||||
UPDATE ir_module_module_dependency
|
||||
SET name = 'open_workshop_base'
|
||||
WHERE name = 'open_workshop'
|
||||
""")
|
||||
if cr.rowcount > 0:
|
||||
_logger.info(f"✓ Updated dependencies: {cr.rowcount} row(s)")
|
||||
|
||||
_logger.info("=" * 70)
|
||||
_logger.info("MIGRATION COMPLETED")
|
||||
_logger.info("=" * 70)
|
||||
|
||||
44
open_workshop_base/__manifest__.py
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
{
|
||||
'name': 'Open Workshop Base',
|
||||
'license': 'AGPL-3',
|
||||
'version': '18.0.1.0.5', # Migration läuft bei 18.0.1.0.4
|
||||
'summary': 'Kern-Modul für Maschinenfreigaben - vereinfachte Equipment-Integration',
|
||||
'depends': ['base', 'account', 'hr', 'product', 'sale', 'contacts', 'maintenance', 'maintenance_equipment_status'],
|
||||
'author': 'matthias.lotz',
|
||||
'category': 'Manufacturing',
|
||||
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'data/maintenance_equipment_status_data.xml',
|
||||
'views/machine_product_training_views.xml',
|
||||
'views/machine_area_views.xml',
|
||||
'views/machine_views.xml',
|
||||
'views/maintenance_equipment_views.xml',
|
||||
'views/res_partner_view.xml',
|
||||
'views/menu_views.xml',
|
||||
'data/data.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
'open_workshop_base/static/src/css/category_color.css',
|
||||
],
|
||||
},
|
||||
'description': """
|
||||
Open Workshop Base - Kernmodul
|
||||
================================
|
||||
|
||||
Dieses Modul stellt die Basis-Funktionalität für Open Workshop bereit:
|
||||
|
||||
* Maschinenmodelle (ows.machine) → nutzt maintenance.equipment als Single Source of Truth
|
||||
* Bereiche (ows.machine.area)
|
||||
* Einweisungslogik und Freigaben
|
||||
* Produktverknüpfungen für Einweisungen
|
||||
* Backend-UI (Form, Tree, Kanban)
|
||||
* Automatische Migration bestehender Maschinen zu maintenance.equipment
|
||||
|
||||
Für POS-Integration installiere: open_workshop_pos
|
||||
Für API-Zugriff installiere: open_workshop_api (erforderlich für WordPress Frontend)
|
||||
""",
|
||||
'post_init_hook': 'post_init_hook',
|
||||
}
|
||||
|
|
@ -23,130 +23,130 @@
|
|||
<!-- Maschinen im Fablab -->
|
||||
<record id="machine_sabako_laser" model="ows.machine">
|
||||
<field name="name">Sabako Laser</field>
|
||||
<field name="code">sabako_laser</field>
|
||||
<field name="serial_no">sabako_laser</field>
|
||||
<field name="area_id" ref="area_fablab"/>
|
||||
</record>
|
||||
|
||||
<record id="machine_prusa" model="ows.machine">
|
||||
<field name="name">Prusa</field>
|
||||
<field name="code">prusa</field>
|
||||
<field name="serial_no">prusa</field>
|
||||
<field name="area_id" ref="area_fablab"/>
|
||||
</record>
|
||||
|
||||
<record id="machine_prusa_mmu" model="ows.machine">
|
||||
<field name="name">Prusa MMU</field>
|
||||
<field name="code">prusa_mmu</field>
|
||||
<field name="serial_no">prusa_mmu</field>
|
||||
<field name="area_id" ref="area_fablab"/>
|
||||
</record>
|
||||
|
||||
<record id="machine_3d_delta" model="ows.machine">
|
||||
<field name="name">3D Delta</field>
|
||||
<field name="code">3d_delta</field>
|
||||
<field name="serial_no">3d_delta</field>
|
||||
<field name="area_id" ref="area_fablab"/>
|
||||
</record>
|
||||
|
||||
<record id="machine_cnc_beamicon" model="ows.machine">
|
||||
<field name="name">CNC Beamicon</field>
|
||||
<field name="code">cnc_beamicon</field>
|
||||
<field name="serial_no">cnc_beamicon</field>
|
||||
<field name="area_id" ref="area_fablab"/>
|
||||
</record>
|
||||
|
||||
<!-- Maschinen im Holzbereich -->
|
||||
<record id="machine_formatkreissaege" model="ows.machine">
|
||||
<field name="name">Formatkreissäge</field>
|
||||
<field name="code">formatkreissaege</field>
|
||||
<field name="serial_no">formatkreissaege</field>
|
||||
<field name="area_id" ref="area_holz"/>
|
||||
</record>
|
||||
|
||||
<record id="machine_bandsaege_holz" model="ows.machine">
|
||||
<field name="name">Bandsäge</field>
|
||||
<field name="code">bandsaege_holz</field>
|
||||
<field name="serial_no">bandsaege_holz</field>
|
||||
<field name="area_id" ref="area_holz"/>
|
||||
</record>
|
||||
|
||||
<record id="machine_abrichte" model="ows.machine">
|
||||
<field name="name">Abricht Dickenhobel</field>
|
||||
<field name="code">dickenhobel</field>
|
||||
<field name="serial_no">dickenhobel</field>
|
||||
<field name="area_id" ref="area_holz"/>
|
||||
</record>
|
||||
|
||||
<record id="machine_drechselbank" model="ows.machine">
|
||||
<field name="name">Drechselbank</field>
|
||||
<field name="code">drechselbank</field>
|
||||
<field name="serial_no">drechselbank</field>
|
||||
<field name="area_id" ref="area_holz"/>
|
||||
</record>
|
||||
|
||||
<record id="machine_festool_domino" model="ows.machine">
|
||||
<field name="name">Festool Domino Fräse</field>
|
||||
<field name="code">festool_domino</field>
|
||||
<field name="serial_no">festool_domino</field>
|
||||
<field name="area_id" ref="area_holz"/>
|
||||
</record>
|
||||
|
||||
<record id="machine_maffel_duo" model="ows.machine">
|
||||
<field name="name">Maffel Duo Dübler</field>
|
||||
<field name="code">maffel_duo</field>
|
||||
<field name="serial_no">maffel_duo</field>
|
||||
<field name="area_id" ref="area_holz"/>
|
||||
</record>
|
||||
|
||||
<record id="machine_lamello" model="ows.machine">
|
||||
<field name="name">Lamello Zeta P2</field>
|
||||
<field name="code">lamello_zeta_p2</field>
|
||||
<field name="serial_no">lamello_zeta_p2</field>
|
||||
<field name="area_id" ref="area_holz"/>
|
||||
</record>
|
||||
|
||||
<!-- Maschinen im Metallbereich -->
|
||||
<record id="machine_kreissaege_metall" model="ows.machine">
|
||||
<field name="name">Kreissäge</field>
|
||||
<field name="code">kreissaege_metall</field>
|
||||
<field name="serial_no">kreissaege_metall</field>
|
||||
<field name="area_id" ref="area_metall"/>
|
||||
</record>
|
||||
|
||||
<record id="machine_bandsaege_metall" model="ows.machine">
|
||||
<field name="name">Bandsäge</field>
|
||||
<field name="code">bandsaege_metall</field>
|
||||
<field name="serial_no">bandsaege_metall</field>
|
||||
<field name="area_id" ref="area_metall"/>
|
||||
</record>
|
||||
|
||||
<record id="machine_mig_mag" model="ows.machine">
|
||||
<field name="name">MIG/MAG Schweißgeräte</field>
|
||||
<field name="code">mig_mag</field>
|
||||
<field name="serial_no">mig_mag</field>
|
||||
<field name="area_id" ref="area_metall"/>
|
||||
</record>
|
||||
|
||||
<record id="machine_wig" model="ows.machine">
|
||||
<field name="name">WIG Schweißgerät</field>
|
||||
<field name="code">wig</field>
|
||||
<field name="serial_no">wig</field>
|
||||
<field name="area_id" ref="area_metall"/>
|
||||
</record>
|
||||
|
||||
<record id="machine_schweissen" model="ows.machine">
|
||||
<field name="name">Schweißen allgemein</field>
|
||||
<field name="code">schweissen_allgemein</field>
|
||||
<field name="serial_no">schweissen_allgemein</field>
|
||||
<field name="area_id" ref="area_metall"/>
|
||||
</record>
|
||||
|
||||
<record id="machine_drehbank" model="ows.machine">
|
||||
<field name="name">Drehbank</field>
|
||||
<field name="code">drehbank</field>
|
||||
<field name="serial_no">drehbank</field>
|
||||
<field name="area_id" ref="area_metall"/>
|
||||
</record>
|
||||
|
||||
<record id="machine_fraese" model="ows.machine">
|
||||
<field name="name">Fräse</field>
|
||||
<field name="code">fraese</field>
|
||||
<field name="serial_no">fraese</field>
|
||||
<field name="area_id" ref="area_metall"/>
|
||||
</record>
|
||||
|
||||
<record id="machine_abkantbank" model="ows.machine">
|
||||
<field name="name">Abkantbank</field>
|
||||
<field name="code">abkantbank</field>
|
||||
<field name="serial_no">abkantbank</field>
|
||||
<field name="area_id" ref="area_metall"/>
|
||||
</record>
|
||||
|
||||
<!-- Maschine im Elektronikbereich -->
|
||||
<record id="machine_loetkolben" model="ows.machine">
|
||||
<field name="name">Lötkolben</field>
|
||||
<field name="code">loetkolben</field>
|
||||
<field name="serial_no">loetkolben</field>
|
||||
<field name="area_id" ref="area_elektronik"/>
|
||||
</record>
|
||||
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo noupdate="1">
|
||||
<!-- Equipment Status für Open Workshop -->
|
||||
|
||||
<record id="equipment_status_inbetrieb" model="maintenance.equipment.status">
|
||||
<field name="name">In Betrieb</field>
|
||||
<field name="sequence" eval="10"/>
|
||||
</record>
|
||||
|
||||
<record id="equipment_status_defekt" model="maintenance.equipment.status">
|
||||
<field name="name">Defekt</field>
|
||||
<field name="sequence" eval="20"/>
|
||||
</record>
|
||||
|
||||
<record id="equipment_status_wartung" model="maintenance.equipment.status">
|
||||
<field name="name">Wartung</field>
|
||||
<field name="sequence" eval="30"/>
|
||||
</record>
|
||||
|
||||
<record id="equipment_status_ausgemustert" model="maintenance.equipment.status">
|
||||
<field name="name">Ausgemustert</field>
|
||||
<field name="sequence" eval="40"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
160
open_workshop_base/migrations/18.0.1.0.4/post-migration.py
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import logging
|
||||
from datetime import date
|
||||
from odoo import SUPERUSER_ID, api
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def migrate(cr, version):
|
||||
"""
|
||||
Post-Migration: Migriere bestehende ows.machine Daten zu maintenance.equipment
|
||||
Area wird in equipment.location gespeichert (nicht mehr in category!)
|
||||
"""
|
||||
_logger.info("=== Post-Migration: Daten-Migration zu equipment ===")
|
||||
|
||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||
|
||||
# 1. Zähle zu migrierende Maschinen
|
||||
cr.execute("SELECT COUNT(*) FROM ows_machine WHERE equipment_id IS NULL")
|
||||
machines_to_migrate = cr.fetchone()[0]
|
||||
_logger.info(f"Found {machines_to_migrate} machines to migrate")
|
||||
|
||||
if machines_to_migrate == 0:
|
||||
_logger.info("No machines to migrate - skipping")
|
||||
return
|
||||
|
||||
_logger.info("Creating maintenance.equipment entries...")
|
||||
|
||||
# 2. Migriere alle Maschinen die noch kein equipment haben
|
||||
cr.execute("""
|
||||
SELECT
|
||||
id, name, area_id,
|
||||
active, create_uid, write_uid, create_date, write_date
|
||||
FROM ows_machine
|
||||
WHERE equipment_id IS NULL
|
||||
""")
|
||||
|
||||
machines = cr.fetchall()
|
||||
migrated_count = 0
|
||||
|
||||
# Default Company ermitteln
|
||||
cr.execute("SELECT id FROM res_company ORDER BY id LIMIT 1")
|
||||
default_company_id = cr.fetchone()[0] if cr.rowcount > 0 else None
|
||||
|
||||
for machine in machines:
|
||||
(m_id, m_name_jsonb, m_area_id,
|
||||
m_active, m_create_uid, m_write_uid, m_create_date, m_write_date) = machine
|
||||
|
||||
# Name aus JSONB extrahieren (de_DE oder en_US)
|
||||
m_name = None
|
||||
if m_name_jsonb:
|
||||
m_name = m_name_jsonb.get('de_DE') or m_name_jsonb.get('en_US')
|
||||
|
||||
# Serial number generieren
|
||||
m_serial_no = f"OWS-{m_id}"
|
||||
|
||||
# Location aus Area-Name ermitteln
|
||||
location = None
|
||||
if m_area_id:
|
||||
# Hole Area-Name (JSONB!) und nutze als location
|
||||
cr.execute("SELECT name FROM ows_machine_area WHERE id = %s", (m_area_id,))
|
||||
area_result = cr.fetchone()
|
||||
if area_result and area_result[0]:
|
||||
area_name_jsonb = area_result[0]
|
||||
if area_name_jsonb:
|
||||
location = area_name_jsonb.get('de_DE') or area_name_jsonb.get('en_US')
|
||||
|
||||
# Equipment erstellen (name als JSONB!)
|
||||
# Fallback: Wenn m_name leer, nutze serial_no
|
||||
display_name = m_name if m_name and m_name.strip() else m_serial_no
|
||||
|
||||
# Verwende SQL direkt für korrektes JSONB handling
|
||||
try:
|
||||
cr.execute("""
|
||||
INSERT INTO maintenance_equipment
|
||||
(name, serial_no, location, cost, effective_date, equipment_assign_to,
|
||||
company_id, active, create_uid, write_uid, create_date, write_date)
|
||||
VALUES
|
||||
(jsonb_build_object('de_DE', %s, 'en_US', %s), %s, %s, %s, %s, 'other',
|
||||
%s, %s, %s, %s, %s, %s)
|
||||
RETURNING id
|
||||
""", (display_name, display_name, m_serial_no,
|
||||
location, 0.0, date.today(),
|
||||
default_company_id,
|
||||
m_active if m_active is not None else True,
|
||||
m_create_uid, m_write_uid, m_create_date, m_write_date))
|
||||
|
||||
equipment_id = cr.fetchone()[0]
|
||||
|
||||
# equipment_id in ows_machine setzen
|
||||
cr.execute("""
|
||||
UPDATE ows_machine
|
||||
SET equipment_id = %s
|
||||
WHERE id = %s
|
||||
""", (equipment_id, m_id))
|
||||
|
||||
migrated_count += 1
|
||||
|
||||
except Exception as e:
|
||||
_logger.error(f"Failed to migrate machine {m_id}: {e}")
|
||||
|
||||
# 3. Synchronisiere related fields von ows.machine → maintenance.equipment
|
||||
# Nötig weil related fields mit store=True nicht automatisch bei SQL-INSERT synchronisiert werden
|
||||
_logger.info("Synchronizing related fields (area_id, category) to maintenance.equipment...")
|
||||
|
||||
cr.execute("""
|
||||
UPDATE maintenance_equipment me
|
||||
SET ows_area_id = om.area_id
|
||||
FROM ows_machine om
|
||||
WHERE om.equipment_id = me.id
|
||||
AND om.area_id IS NOT NULL
|
||||
""")
|
||||
synced_areas = cr.rowcount
|
||||
_logger.info(f"✅ Synchronized {synced_areas} area_id values")
|
||||
|
||||
cr.execute("""
|
||||
UPDATE maintenance_equipment me
|
||||
SET ows_category = om.category
|
||||
FROM ows_machine om
|
||||
WHERE om.equipment_id = me.id
|
||||
AND om.category IS NOT NULL
|
||||
""")
|
||||
synced_categories = cr.rowcount
|
||||
_logger.info(f"✅ Synchronized {synced_categories} category values")
|
||||
|
||||
# 4. Setze Default-Status "In Betrieb" für migrierte Equipment ohne Status
|
||||
# (Status-Records werden automatisch über XML-Datei erstellt)
|
||||
_logger.info("Setting default status for equipment without status...")
|
||||
|
||||
# Prüfe ob Status "In Betrieb" existiert
|
||||
cr.execute("SELECT id FROM maintenance_equipment_status WHERE name = 'In Betrieb' LIMIT 1")
|
||||
status_result = cr.fetchone()
|
||||
|
||||
if status_result:
|
||||
status_id = status_result[0]
|
||||
cr.execute("""
|
||||
UPDATE maintenance_equipment me
|
||||
SET status_id = %s
|
||||
WHERE me.status_id IS NULL
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM ows_machine om
|
||||
WHERE om.equipment_id = me.id
|
||||
)
|
||||
""", (status_id,))
|
||||
status_set = cr.rowcount
|
||||
_logger.info(f"✅ Set default status for {status_set} equipment records")
|
||||
else:
|
||||
_logger.warning("⚠️ Status 'In Betrieb' not found - skipping status assignment")
|
||||
|
||||
# 5. Validierung
|
||||
cr.execute("SELECT COUNT(*) FROM ows_machine WHERE equipment_id IS NULL")
|
||||
remaining = cr.fetchone()[0]
|
||||
|
||||
if remaining > 0:
|
||||
_logger.warning(f"⚠️ {remaining} machines could not be migrated!")
|
||||
else:
|
||||
_logger.info(f"✅ Successfully migrated {migrated_count} machines to equipment")
|
||||
|
||||
_logger.info("Post-migration completed")
|
||||
|
||||
59
open_workshop_base/migrations/18.0.1.0.4/pre-migration.py
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import logging
|
||||
from odoo import SUPERUSER_ID, api
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def migrate(cr, version):
|
||||
"""
|
||||
Pre-Migration: Bereite equipment_id Spalte vor
|
||||
Installiert maintenance-Modul falls nötig
|
||||
"""
|
||||
_logger.info("=== Pre-Migration: ows.machine → maintenance.equipment ===")
|
||||
|
||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||
|
||||
# 1. Prüfe ob maintenance installiert ist, falls nicht: installieren
|
||||
cr.execute("""
|
||||
SELECT state FROM ir_module_module
|
||||
WHERE name = 'maintenance'
|
||||
""")
|
||||
result = cr.fetchone()
|
||||
|
||||
if not result or result[0] != 'installed':
|
||||
_logger.info("Installing maintenance module...")
|
||||
maintenance_module = env['ir.module.module'].search([('name', '=', 'maintenance')])
|
||||
if maintenance_module:
|
||||
maintenance_module.button_immediate_install()
|
||||
_logger.info("✅ Maintenance module installed")
|
||||
else:
|
||||
raise Exception("Maintenance module not found in addons_path!")
|
||||
else:
|
||||
_logger.info("✓ Maintenance module already installed")
|
||||
|
||||
# 2. Spalte equipment_id hinzufügen (falls nicht vorhanden)
|
||||
cr.execute("""
|
||||
SELECT column_name
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'ows_machine' AND column_name = 'equipment_id'
|
||||
""")
|
||||
|
||||
if not cr.fetchone():
|
||||
_logger.info("Adding equipment_id column to ows_machine")
|
||||
cr.execute("""
|
||||
ALTER TABLE ows_machine
|
||||
ADD COLUMN equipment_id INTEGER;
|
||||
""")
|
||||
cr.execute("""
|
||||
ALTER TABLE ows_machine
|
||||
ADD CONSTRAINT ows_machine_equipment_id_fkey
|
||||
FOREIGN KEY (equipment_id)
|
||||
REFERENCES maintenance_equipment(id)
|
||||
ON DELETE CASCADE;
|
||||
""")
|
||||
_logger.info("✅ equipment_id column added")
|
||||
else:
|
||||
_logger.info("✓ equipment_id column already exists")
|
||||
|
||||
_logger.info("Pre-migration completed")
|
||||
105
open_workshop_base/migrations/migrate_open_workshop_to_base.sql
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
-- ============================================================================
|
||||
-- Pre-Deployment Migration Script: open_workshop → open_workshop_base
|
||||
-- ============================================================================
|
||||
-- Dieses SQL-Skript MUSS vor dem Odoo-Update in der Gitea Action ausgeführt werden
|
||||
--
|
||||
-- Verwendung in Gitea Action:
|
||||
-- docker exec <db-container> psql -U odoo -d <dbname> < migrate_open_workshop_to_base.sql
|
||||
--
|
||||
-- Oder als Deployment-Hook vor "odoo -u open_workshop_base"
|
||||
-- ============================================================================
|
||||
|
||||
\echo '=========================================='
|
||||
\echo 'Migration: open_workshop → open_workshop_base'
|
||||
\echo '=========================================='
|
||||
|
||||
-- Prüfen ob Migration nötig ist
|
||||
DO $$
|
||||
DECLARE
|
||||
old_module_exists BOOLEAN;
|
||||
new_module_exists BOOLEAN;
|
||||
BEGIN
|
||||
-- Prüfe ob altes Modul existiert
|
||||
SELECT EXISTS(
|
||||
SELECT 1 FROM ir_module_module WHERE name = 'open_workshop'
|
||||
) INTO old_module_exists;
|
||||
|
||||
-- Prüfe ob neues Modul bereits existiert
|
||||
SELECT EXISTS(
|
||||
SELECT 1 FROM ir_module_module WHERE name = 'open_workshop_base'
|
||||
) INTO new_module_exists;
|
||||
|
||||
IF old_module_exists THEN
|
||||
RAISE NOTICE '✓ Found module "open_workshop" - starting migration';
|
||||
|
||||
IF new_module_exists THEN
|
||||
RAISE NOTICE '⚠ Module "open_workshop_base" already exists - cleaning up';
|
||||
-- Aufräumen falls Testinstallation vorhanden
|
||||
DELETE FROM ir_model_data WHERE module = 'base' AND name = 'module_open_workshop_base';
|
||||
DELETE FROM ir_module_module WHERE name = 'open_workshop_base';
|
||||
END IF;
|
||||
|
||||
-- ===== MIGRATION DURCHFÜHREN =====
|
||||
|
||||
-- 1. Modulnamen und Version aktualisieren
|
||||
UPDATE ir_module_module
|
||||
SET name = 'open_workshop_base',
|
||||
latest_version = '18.0.1.0.3' -- Auf 18.0.1.0.3 setzen, damit 18.0.1.0.4 Migration läuft
|
||||
WHERE name = 'open_workshop';
|
||||
|
||||
RAISE NOTICE '✓ Updated ir_module_module: % row(s)', (SELECT 1);
|
||||
|
||||
-- 2. Alle ir_model_data Referenzen aktualisieren
|
||||
UPDATE ir_model_data
|
||||
SET module = 'open_workshop_base'
|
||||
WHERE module = 'open_workshop';
|
||||
|
||||
RAISE NOTICE '✓ Updated ir_model_data: % row(s)', (
|
||||
SELECT COUNT(*) FROM ir_model_data WHERE module = 'open_workshop_base'
|
||||
);
|
||||
|
||||
-- 3. Alten ir_model_data Eintrag für module_open_workshop löschen
|
||||
DELETE FROM ir_model_data
|
||||
WHERE module = 'base' AND name = 'module_open_workshop';
|
||||
|
||||
RAISE NOTICE '✓ Cleaned up old module_open_workshop entry';
|
||||
|
||||
-- 4. Abhängigkeiten in anderen Modulen aktualisieren (falls vorhanden)
|
||||
UPDATE ir_module_module_dependency
|
||||
SET name = 'open_workshop_base'
|
||||
WHERE name = 'open_workshop';
|
||||
|
||||
IF FOUND THEN
|
||||
RAISE NOTICE '✓ Updated dependencies in other modules';
|
||||
END IF;
|
||||
|
||||
RAISE NOTICE '==========================================';
|
||||
RAISE NOTICE '✓ MIGRATION COMPLETED SUCCESSFULLY';
|
||||
RAISE NOTICE '==========================================';
|
||||
RAISE NOTICE 'Next step: Run "odoo -u open_workshop_base -d <dbname>"';
|
||||
|
||||
ELSE
|
||||
RAISE NOTICE 'ℹ Module "open_workshop" not found - migration not needed';
|
||||
RAISE NOTICE 'This is normal for new installations';
|
||||
END IF;
|
||||
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
RAISE EXCEPTION 'Migration failed: %', SQLERRM;
|
||||
END
|
||||
$$;
|
||||
|
||||
-- Prüfung: Zeige aktuellen Status
|
||||
\echo ''
|
||||
\echo 'Current module status:'
|
||||
SELECT name, state, latest_version
|
||||
FROM ir_module_module
|
||||
WHERE name LIKE 'open_workshop%'
|
||||
ORDER BY name;
|
||||
|
||||
\echo ''
|
||||
\echo 'Model data count:'
|
||||
SELECT module, COUNT(*) as entries
|
||||
FROM ir_model_data
|
||||
WHERE module LIKE 'open_workshop%'
|
||||
GROUP BY module;
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
from . import ows_models
|
||||
from . import pos_order
|
||||
|
||||
|
||||
|
||||
|
|
@ -15,22 +15,31 @@ class HREmployee(models.Model):
|
|||
def anonymize_for_testsystem(self):
|
||||
"""Benennt Admin-Angestellten um und archiviert alle anderen für das Testsystem."""
|
||||
admin_user = self.env['res.users'].search([('name', '=', 'Administrator')], limit=1)
|
||||
if not admin_user:
|
||||
_logger.error("[OWS] Administrator-Benutzer nicht gefunden!")
|
||||
return
|
||||
|
||||
_logger.info(f"[OWS] Administrator-Benutzer gefunden: {admin_user.name} (ID: {admin_user.id})")
|
||||
admin_employee = self.search([('user_id', '=', admin_user.id)], limit=1)
|
||||
|
||||
# Suche auch archivierte Employees
|
||||
admin_employee = self.with_context(active_test=False).search([('user_id', '=', admin_user.id)], limit=1)
|
||||
|
||||
if admin_employee:
|
||||
# Administrator-Employee reaktivieren und umbenennen
|
||||
admin_employee.write({
|
||||
'name': 'TESTSYSTEM',
|
||||
'job_title': 'Testumgebung',
|
||||
'work_email': False,
|
||||
'work_email': 'office@hobbyhimmel.de',
|
||||
'work_phone': False,
|
||||
'active': True, # Reaktivieren falls archiviert
|
||||
})
|
||||
_logger.info("[OWS] Admin-Angestellter wurde umbenannt.")
|
||||
_logger.info(f"[OWS] Admin-Angestellter reaktiviert und umbenannt: {admin_employee.name} (ID: {admin_employee.id})")
|
||||
else:
|
||||
_logger.warning("[OWS] Kein Angestellter für user_admin gefunden.")
|
||||
_logger.warning("[OWS] Kein Angestellter für Administrator gefunden.")
|
||||
return
|
||||
|
||||
# Alle anderen Angestellten archivieren
|
||||
other_employees = self.search([('id', '!=', admin_employee.id)])
|
||||
# Alle anderen Angestellten archivieren (auch bereits archivierte berücksichtigen)
|
||||
other_employees = self.with_context(active_test=False).search([('id', '!=', admin_employee.id)])
|
||||
other_employees.write({'active': False})
|
||||
_logger.info("[OWS] %d Angestellte archiviert.", len(other_employees))
|
||||
|
||||
|
|
@ -391,6 +400,128 @@ AVAILABLE_COLORS = [
|
|||
('#ffffff', 'Weiss'),
|
||||
]
|
||||
|
||||
class MaintenanceEquipment(models.Model):
|
||||
"""Erweitere maintenance.equipment mit OWS-Feldern für direkte Bearbeitung"""
|
||||
_inherit = 'maintenance.equipment'
|
||||
|
||||
# Inverse Relation zu ows.machine
|
||||
ows_machine_id = fields.One2many('ows.machine', 'equipment_id', string='OWS Maschine', readonly=True)
|
||||
|
||||
# OWS-Felder direkt auf Equipment (via related für einfache Bearbeitung)
|
||||
# store=True für Performance beim Filtern/Suchen
|
||||
ows_category = fields.Selection(
|
||||
related='ows_machine_id.category',
|
||||
string='Sicherheitskategorie',
|
||||
readonly=False,
|
||||
store=True
|
||||
)
|
||||
ows_category_icon = fields.Char(
|
||||
related='ows_machine_id.category_icon',
|
||||
string='⚙'
|
||||
)
|
||||
ows_area_id = fields.Many2one(
|
||||
'ows.machine.area',
|
||||
related='ows_machine_id.area_id',
|
||||
string='Bereich',
|
||||
readonly=False,
|
||||
store=True
|
||||
)
|
||||
ows_product_ids = fields.One2many(
|
||||
'ows.machine.product',
|
||||
related='ows_machine_id.product_ids',
|
||||
string='Nutzungsprodukte',
|
||||
readonly=False
|
||||
)
|
||||
ows_training_ids = fields.One2many(
|
||||
'ows.machine.training',
|
||||
related='ows_machine_id.training_ids',
|
||||
string='Einweisungsprodukte',
|
||||
readonly=False
|
||||
)
|
||||
ows_usage_product_ids = fields.Many2many(
|
||||
'product.product',
|
||||
compute='_compute_usage_products',
|
||||
inverse='_inverse_usage_products',
|
||||
string='Nutzungsprodukte',
|
||||
store=False
|
||||
)
|
||||
ows_training_product_ids = fields.Many2many(
|
||||
'product.product',
|
||||
compute='_compute_training_products',
|
||||
inverse='_inverse_training_products',
|
||||
string='Einweisungsprodukte',
|
||||
store=False
|
||||
)
|
||||
|
||||
@api.depends('ows_product_ids.product_id')
|
||||
def _compute_usage_products(self):
|
||||
for record in self:
|
||||
record.ows_usage_product_ids = record.ows_product_ids.mapped('product_id')
|
||||
|
||||
def _inverse_usage_products(self):
|
||||
for record in self:
|
||||
if record.ows_machine_id:
|
||||
# Entferne alle bisherigen
|
||||
record.ows_machine_id.product_ids.unlink()
|
||||
# Füge neue hinzu
|
||||
for product in record.ows_usage_product_ids:
|
||||
self.env['ows.machine.product'].create({
|
||||
'machine_id': record.ows_machine_id.id,
|
||||
'product_id': product.id
|
||||
})
|
||||
|
||||
@api.depends('ows_training_ids.training_id')
|
||||
def _compute_training_products(self):
|
||||
for record in self:
|
||||
record.ows_training_product_ids = record.ows_training_ids.mapped('training_id')
|
||||
|
||||
def _inverse_training_products(self):
|
||||
for record in self:
|
||||
if record.ows_machine_id:
|
||||
# Entferne alle bisherigen
|
||||
record.ows_machine_id.training_ids.unlink()
|
||||
# Füge neue hinzu
|
||||
for product in record.ows_training_product_ids:
|
||||
self.env['ows.machine.training'].create({
|
||||
'machine_id': record.ows_machine_id.id,
|
||||
'training_id': product.id
|
||||
})
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
"""Erstelle automatisch ows.machine für jedes neue Equipment (außer wenn vom ows.machine.create aufgerufen)"""
|
||||
records = super(MaintenanceEquipment, self).create(vals_list)
|
||||
records._create_missing_ows_machines()
|
||||
return records
|
||||
|
||||
def _create_missing_ows_machines(self):
|
||||
"""Erstelle fehlende ows.machine Datensätze für Equipment ohne ows.machine"""
|
||||
if self.env.context.get('skip_ows_machine_creation'):
|
||||
return
|
||||
|
||||
for record in self:
|
||||
if not record.ows_machine_id:
|
||||
self.env['ows.machine'].with_context(skip_ows_machine_creation=True).create({
|
||||
'equipment_id': record.id,
|
||||
'category': record.ows_category or 'red',
|
||||
'area_id': record.ows_area_id.id if record.ows_area_id else False,
|
||||
})
|
||||
|
||||
@api.model
|
||||
def load(self, fields, data):
|
||||
"""Override load() um sicherzustellen, dass ows.machine nach dem Import erstellt wird"""
|
||||
result = super(MaintenanceEquipment, self).load(fields, data)
|
||||
|
||||
# Nach erfolgreichem Import: Erstelle fehlende ows.machine Einträge
|
||||
if result.get('ids'):
|
||||
equipment_records = self.browse(result['ids'])
|
||||
equipment_records._create_missing_ows_machines()
|
||||
|
||||
return result
|
||||
|
||||
|
||||
|
||||
|
||||
class OwsMachineArea(models.Model):
|
||||
_name = 'ows.machine.area'
|
||||
_table = 'ows_machine_area'
|
||||
|
|
@ -431,12 +562,38 @@ class OwsMachineArea(models.Model):
|
|||
|
||||
|
||||
class OwsMachine(models.Model):
|
||||
"""
|
||||
Open Workshop Maschine - nutzt maintenance.equipment als Single Source of Truth.
|
||||
|
||||
Delegation Pattern (_inherits):
|
||||
- Beim Erstellen wird automatisch ein maintenance.equipment erstellt
|
||||
- Felder wie name, serial_no, cost etc. werden direkt von equipment übernommen
|
||||
- Keine Datenduplizierung!
|
||||
"""
|
||||
_name = 'ows.machine'
|
||||
_table = 'ows_machine'
|
||||
_description = 'OWS: Maschine'
|
||||
_inherits = {'maintenance.equipment': 'equipment_id'}
|
||||
|
||||
# PFLICHT: Verknüpfung zu maintenance.equipment (Single Source of Truth)
|
||||
equipment_id = fields.Many2one(
|
||||
'maintenance.equipment',
|
||||
required=True,
|
||||
ondelete='cascade',
|
||||
string='Equipment',
|
||||
help='Verknüpfung zum Maintenance Equipment (Single Source of Truth für name, code, Preis, etc.)'
|
||||
)
|
||||
|
||||
name = fields.Char(required=True, translate=True)
|
||||
code = fields.Char(required=True, help="Eindeutiger Kurzcode, z.B. 'lasercutter'")
|
||||
# Delegierte Felder (kommen automatisch von equipment via _inherits):
|
||||
# - name (equipment.name)
|
||||
# - serial_no → wird als 'code' verwendet (siehe @api.depends unten)
|
||||
# - cost → purchase_price
|
||||
# - effective_date → purchase_date
|
||||
# - location → storage_location (wird von area_id synchronisiert, readonly via View)
|
||||
# - note → description
|
||||
# - category_id → Wird mit area_id synchronisiert
|
||||
|
||||
# OWS-spezifische Felder (nur in ows.machine!)
|
||||
category = fields.Selection([
|
||||
('green', 'Kategorie 1: grün'),
|
||||
('yellow', 'Kategorie 2: gelb'),
|
||||
|
|
@ -458,16 +615,11 @@ class OwsMachine(models.Model):
|
|||
}
|
||||
rec.category_icon = icon_map.get(rec.category, '⚪')
|
||||
|
||||
description = fields.Text()
|
||||
active = fields.Boolean(default=True)
|
||||
area_id = fields.Many2one('ows.machine.area', string='Bereich', help="Bereich, in dem die Maschine oder das Gerät steht.")
|
||||
product_ids = fields.One2many('ows.machine.product', 'machine_id', string="Nutzungsprodukte")
|
||||
product_names = fields.Char(string="Liste der Nutzungsprodukte", compute="_compute_product_using_names", store=False,)
|
||||
training_ids = fields.One2many('ows.machine.training', 'machine_id', string="Einweisungsprodukte")
|
||||
training_names = fields.Char(string="Liste der Einweisungsprodukte", compute="_compute_product_training_names", store=False,)
|
||||
storage_location = fields.Char(string="Lagerort", help="Lagerort der Maschine oder des Geräts.")
|
||||
purchase_price = fields.Float(string="Kaufpreis", help="Kaufpreis der Maschine oder des Geräts.")
|
||||
purchase_date = fields.Date(string="Kaufdatum", help="Kaufdatum der Maschine oder des Geräts.")
|
||||
|
||||
@api.depends('product_ids.product_id.name')
|
||||
def _compute_product_using_names(self):
|
||||
|
|
@ -481,12 +633,67 @@ class OwsMachine(models.Model):
|
|||
names = machine.training_ids.mapped('training_id.name')
|
||||
machine.training_names = ", ".join(names)
|
||||
|
||||
_sql_constraints = [
|
||||
('code_unique', 'unique(code)', 'Maschinencode muss eindeutig sein.')
|
||||
]
|
||||
# Keine eigenen SQL Constraints - Equipment hat bereits unique constraint für serial_no
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
"""
|
||||
Beim Erstellen einer ows.machine:
|
||||
1. Equipment nur erstellen wenn equipment_id NICHT angegeben wurde
|
||||
2. Area → Location synchronisieren
|
||||
3. serial_no und name vom User übernehmen
|
||||
"""
|
||||
# Equipment nur erstellen wenn noch nicht vorhanden
|
||||
for vals in vals_list:
|
||||
# Wenn equipment_id bereits gesetzt ist (z.B. bei Import), KEIN neues Equipment erstellen!
|
||||
if 'equipment_id' not in vals:
|
||||
equipment_vals = {
|
||||
'name': vals.get('name', 'Neue Maschine'),
|
||||
'serial_no': vals.get('serial_no', False),
|
||||
}
|
||||
|
||||
equipment = self.env['maintenance.equipment'].with_context(skip_ows_machine_creation=True).create(equipment_vals)
|
||||
vals['equipment_id'] = equipment.id
|
||||
|
||||
return super(OwsMachine, self).create(vals_list)
|
||||
|
||||
def write(self, vals):
|
||||
"""
|
||||
Bei Updates:
|
||||
1. Name/Serial_no → Equipment synchronisieren
|
||||
"""
|
||||
# Name/Serial_no an Equipment weiterleiten
|
||||
equipment_vals = {}
|
||||
if 'name' in vals:
|
||||
equipment_vals['name'] = vals['name']
|
||||
if 'serial_no' in vals:
|
||||
equipment_vals['serial_no'] = vals['serial_no']
|
||||
|
||||
if equipment_vals and self.equipment_id:
|
||||
self.equipment_id.write(equipment_vals)
|
||||
|
||||
return super(OwsMachine, self).write(vals)
|
||||
|
||||
def name_get(self):
|
||||
return [(rec.id, f"{rec.name} ({rec.code})") for rec in self]
|
||||
result = []
|
||||
for rec in self:
|
||||
name = rec.name or 'Unbenannt'
|
||||
if rec.serial_no: # Kommt direkt von equipment via _inherits
|
||||
name = f"[{rec.serial_no}] {name}"
|
||||
result.append((rec.id, name))
|
||||
return result
|
||||
|
||||
def action_open_equipment(self):
|
||||
"""Smart Button: Öffnet die Equipment-Detailansicht"""
|
||||
self.ensure_one()
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': 'Equipment Details',
|
||||
'res_model': 'maintenance.equipment',
|
||||
'res_id': self.equipment_id.id,
|
||||
'view_mode': 'form',
|
||||
'target': 'current',
|
||||
}
|
||||
|
||||
@api.model
|
||||
def get_access_list_grouped(self, partner_id):
|
||||
|
|
@ -571,6 +778,14 @@ class OwsMachineProduct(models.Model):
|
|||
product_id = fields.Many2one('product.product', required=True, domain=[('available_in_pos', '=', True)], ondelete='cascade')
|
||||
machine_id = fields.Many2one('ows.machine', required=True, ondelete='cascade')
|
||||
|
||||
def name_get(self):
|
||||
"""Zeigt den Produktnamen statt ows.machine.product,ID"""
|
||||
result = []
|
||||
for record in self:
|
||||
name = record.product_id.name if record.product_id else 'Unbenannt'
|
||||
result.append((record.id, name))
|
||||
return result
|
||||
|
||||
class OwsMachineTraining(models.Model):
|
||||
_name = 'ows.machine.training'
|
||||
_table = 'ows_machine_training'
|
||||
|
|
@ -578,3 +793,11 @@ class OwsMachineTraining(models.Model):
|
|||
|
||||
training_id = fields.Many2one('product.product', required=True, domain=[('available_in_pos', '=', True)], ondelete='cascade')
|
||||
machine_id = fields.Many2one('ows.machine', required=True, ondelete='cascade')
|
||||
|
||||
def name_get(self):
|
||||
"""Zeigt den Produktnamen statt ows.machine.training,ID"""
|
||||
result = []
|
||||
for record in self:
|
||||
name = record.training_id.name if record.training_id else 'Unbenannt'
|
||||
result.append((record.id, name))
|
||||
return result
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
<odoo>
|
||||
<template id="assets_open_workshop" inherit_id="point_of_sale._assets_pos">
|
||||
<xpath expr="." position="inside">
|
||||
<script type="text/javascript" src="/open_workshop/static/src/js/machine_access_sidebar.js"/>
|
||||
<link rel="stylesheet" type="text/css" href="/open_workshop/static/src/css/pos.css"/>
|
||||
<script type="text/javascript" src="/open_workshop_base/static/src/js/machine_access_sidebar.js"/>
|
||||
<link rel="stylesheet" type="text/css" href="/open_workshop_base/static/src/css/pos.css"/>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||
|
|
@ -7,9 +7,6 @@
|
|||
<field name="view_mode">list,form</field>
|
||||
</record>
|
||||
|
||||
<!-- Menüpunkt unter Maschinen > Konfiguration -->
|
||||
<menuitem id="menu_machine_area" name="Bereiche" parent="menu_machine_config" action="open_workshop.action_machine_area_list" sequence="30"/>
|
||||
|
||||
<!-- Listenansicht -->
|
||||
<record id="view_machine_area_tree" model="ir.ui.view">
|
||||
<field name="name">ows.machine.area.tree</field>
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
<odoo>
|
||||
<!-- Tree View: Nutzungsprodukte -->
|
||||
<!-- Tree View: Nutzungsprodukte (normale Ansicht) -->
|
||||
<record id="view_machine_product_tree" model="ir.ui.view">
|
||||
<field name="name">ows.machine.product.tree</field>
|
||||
<field name="model">ows.machine.product</field>
|
||||
|
|
@ -10,8 +10,20 @@
|
|||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Tree View: Nutzungsprodukte (vereinfacht für Equipment) -->
|
||||
<record id="view_ows_machine_product_tree_simple" model="ir.ui.view">
|
||||
<field name="name">ows.machine.product.tree.simple</field>
|
||||
<field name="model">ows.machine.product</field>
|
||||
<field name="arch" type="xml">
|
||||
<list editable="bottom">
|
||||
<field name="machine_id" column_invisible="1"/>
|
||||
<field name="product_id" domain="[('categ_id.name', '=', 'Maschinennutzung')]"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Tree View: Einweisungsprodukte -->
|
||||
<!-- Tree View: Einweisungsprodukte (normale Ansicht) -->
|
||||
<record id="view_machine_training_tree" model="ir.ui.view">
|
||||
<field name="name">ows.machine.training.tree</field>
|
||||
<field name="model">ows.machine.training</field>
|
||||
|
|
@ -22,6 +34,18 @@
|
|||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Tree View: Einweisungsprodukte (vereinfacht für Equipment) -->
|
||||
<record id="view_ows_machine_training_tree_simple" model="ir.ui.view">
|
||||
<field name="name">ows.machine.training.tree.simple</field>
|
||||
<field name="model">ows.machine.training</field>
|
||||
<field name="arch" type="xml">
|
||||
<list editable="bottom">
|
||||
<field name="machine_id" column_invisible="1"/>
|
||||
<field name="training_id" domain="[('categ_id.name', 'in', ['Einweisungen', 'Kurse'])]"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action: Nutzungsprodukte -->
|
||||
<record id="action_machine_product" model="ir.actions.act_window">
|
||||
105
open_workshop_base/views/machine_views.xml
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
<!-- machine_views.xml -->
|
||||
<odoo>
|
||||
<!-- Maschinen Such- und Filteransicht -->
|
||||
<record id="view_machine_search" model="ir.ui.view">
|
||||
<field name="name">ows.machine.search</field>
|
||||
<field name="model">ows.machine</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Ausrüstung suchen">
|
||||
<!-- OWS Felder -->
|
||||
<field name="name" string="Name"/>
|
||||
<field name="serial_no" string="Code"/>
|
||||
<field name="area_id" string="Bereich"/>
|
||||
<!-- maintenance.equipment Felder (via _inherits) -->
|
||||
<field name="model" string="Modell"/>
|
||||
<field name="partner_id" string="Hersteller"/>
|
||||
<field name="location" string="Standort"/>
|
||||
<field name="company_id" string="Unternehmen"/>
|
||||
<separator/>
|
||||
<filter string="Kategorie grün" name="filter_green" domain="[('category', '=', 'green')]"/>
|
||||
<filter string="Kategorie gelb" name="filter_yellow" domain="[('category', '=', 'yellow')]"/>
|
||||
<filter string="Kategorie rot" name="filter_red" domain="[('category', '=', 'red')]"/>
|
||||
<separator/>
|
||||
<filter string="Aktiv" name="filter_active" domain="[('active', '=', True)]"/>
|
||||
<filter string="Archiviert" name="filter_inactive" domain="[('active', '=', False)]"/>
|
||||
<separator/>
|
||||
<group expand="1" string="Gruppieren nach">
|
||||
<filter string="Bereich" name="group_area" context="{'group_by': 'area_id'}"/>
|
||||
<filter string="Kategorie" name="group_category" context="{'group_by': 'category'}"/>
|
||||
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Maschinen Listenansicht -->
|
||||
<record id="view_machine_tree" model="ir.ui.view">
|
||||
<field name="name">ows.machine.tree</field>
|
||||
<field name="model">ows.machine</field>
|
||||
<field name="arch" type="xml">
|
||||
<list sample="1" multi_edit="1">
|
||||
<field name="category_icon" string="⚙" readonly="1"/>
|
||||
<field name="name" optional="show"/>
|
||||
<!--field name="serial_no" string="Code" optional="show"/-->
|
||||
<field name="category" optional="show"/>
|
||||
<field name="area_id" widget="many2one" optional="show"/>
|
||||
<field name="location" optional="hide"/>
|
||||
<field name="product_names" optional="show"/>
|
||||
<field name="training_names" optional="show"/>
|
||||
<field name="active" optional="show"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Maschinen Formularansicht -->
|
||||
<record id="view_machine_form" model="ir.ui.view">
|
||||
<field name="name">ows.machine.form</field>
|
||||
<field name="model">ows.machine</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Maschine">
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="action_open_equipment" type="object" class="oe_stat_button" icon="fa-wrench">
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_text">Equipment</span>
|
||||
<span class="o_stat_text">Details</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<group>
|
||||
<group string="Maschine" name="ows_group">
|
||||
<field name="equipment_id" invisible="1"/>
|
||||
<field name="name" placeholder="z.B. Sabako Laser"/>
|
||||
<field name="serial_no" string="Code/Seriennummer" placeholder="z.B. sabako_laser"/>
|
||||
<field name="category_icon" string="⚙" readonly="1"/>
|
||||
<field name="category"/>
|
||||
<field name="area_id" required="1"/>
|
||||
</group>
|
||||
<group name="info_group">
|
||||
<field name="location" readonly="1" string="Standort (von Bereich)"/>
|
||||
<field name="active"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<!-- Notebook für Produkte und Einweisungen -->
|
||||
<notebook>
|
||||
<page string="Nutzungsprodukte">
|
||||
<field name="product_ids" context="{'default_machine_id': id}">
|
||||
<list editable="bottom">
|
||||
<field name="product_id" domain="[('categ_id.name', '=', 'Maschinennutzung')]" />
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Einweisungsprodukte">
|
||||
<field name="training_ids" context="{'default_machine_id': id}">
|
||||
<list editable="bottom">
|
||||
<field name="training_id" domain="[('categ_id.name', 'in', ['Einweisungen', 'Kurse'])]" />
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
80
open_workshop_base/views/maintenance_equipment_views.xml
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Erweitere Equipment Form mit OWS-Feldern -->
|
||||
<record id="view_equipment_form_ows_extension" model="ir.ui.view">
|
||||
<field name="name">maintenance.equipment.form.ows</field>
|
||||
<field name="model">maintenance.equipment</field>
|
||||
<field name="inherit_id" ref="maintenance.hr_equipment_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<!-- Füge OWS-Felder in die Details-Gruppe ein -->
|
||||
<xpath expr="//field[@name='location']" position="after">
|
||||
<field name="ows_area_id" string="Bereich"/>
|
||||
<field name="ows_category" string="Sicherheitskategorie"/>
|
||||
</xpath>
|
||||
|
||||
<!-- Füge Open Workshop Notebook-Page hinzu -->
|
||||
<xpath expr="//notebook" position="inside">
|
||||
<page string="Offene Werkstatt (Hobbyhimmel)" name="open_workshop" invisible="not ows_machine_id">
|
||||
<group>
|
||||
<group string="Sicherheit">
|
||||
<field name="ows_machine_id" invisible="1"/>
|
||||
<field name="ows_category_icon" readonly="1"/>
|
||||
<field name="ows_category"/>
|
||||
<field name="ows_area_id"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Nutzungsprodukte">
|
||||
<field name="ows_usage_product_ids" nolabel="1" widget="many2many_tags" options="{'no_create': True}" domain="[('categ_id.name', '=', 'Maschinennutzung')]"/>
|
||||
</group>
|
||||
<group string="Einweisungsprodukte">
|
||||
<field name="ows_training_product_ids" nolabel="1" widget="many2many_tags" options="{'no_create': True}" domain="[('categ_id.name', 'in', ['Einweisungen', 'Kurse'])]"/>
|
||||
</group>
|
||||
</page>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Erweitere Equipment List-View mit OWS-Spalten -->
|
||||
<record id="view_equipment_tree_ows_extension" model="ir.ui.view">
|
||||
<field name="name">maintenance.equipment.tree.ows</field>
|
||||
<field name="model">maintenance.equipment</field>
|
||||
<field name="inherit_id" ref="maintenance.hr_equipment_view_tree"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='name']" position="after">
|
||||
<field name="ows_category_icon" string="⚙" optional="show"/>
|
||||
<field name="ows_area_id" optional="show"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Erweitere Equipment Search-View mit OWS-Filtern -->
|
||||
<record id="view_equipment_search_ows_extension" model="ir.ui.view">
|
||||
<field name="name">maintenance.equipment.search.ows</field>
|
||||
<field name="model">maintenance.equipment</field>
|
||||
<field name="inherit_id" ref="maintenance.hr_equipment_view_search"/>
|
||||
<field name="arch" type="xml">
|
||||
<!-- Füge OWS-Felder zur Suche hinzu -->
|
||||
<xpath expr="//field[@name='name']" position="after">
|
||||
<field name="ows_area_id" string="Bereich"/>
|
||||
<field name="ows_category" string="Kategorie"/>
|
||||
</xpath>
|
||||
|
||||
<!-- Füge OWS-Filter hinzu -->
|
||||
<xpath expr="//filter[@name='inactive']" position="after">
|
||||
<separator/>
|
||||
<filter string="Kategorie 1 (grün)" name="filter_green" domain="[('ows_category', '=', 'green')]"/>
|
||||
<filter string="Kategorie 2 (gelb)" name="filter_yellow" domain="[('ows_category', '=', 'yellow')]"/>
|
||||
<filter string="Kategorie 3 (rot)" name="filter_red" domain="[('ows_category', '=', 'red')]"/>
|
||||
</xpath>
|
||||
|
||||
<!-- Füge OWS-Gruppierungen hinzu (erstelle group wenn nicht vorhanden) -->
|
||||
<xpath expr="//search" position="inside">
|
||||
<group expand="0" string="Gruppieren nach">
|
||||
<filter string="Bereich" name="group_by_area" context="{'group_by': 'ows_area_id'}"/>
|
||||
<filter string="Kategorie" name="group_by_category" context="{'group_by': 'ows_category'}"/>
|
||||
</group>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
|
|
@ -1,10 +1,15 @@
|
|||
<!-- menu_views.xml -->
|
||||
<odoo>
|
||||
<!-- Maschinenliste -->
|
||||
<record id="action_machine_list" model="ir.actions.act_window">
|
||||
<field name="name">Maschinen</field>
|
||||
<field name="res_model">ows.machine</field>
|
||||
<!-- Equipment-Liste mit OWS-Filter (ersetzt Standard Equipment Action) -->
|
||||
<record id="action_equipment_ows" model="ir.actions.act_window">
|
||||
<field name="name">Ausrüstung</field>
|
||||
<field name="res_model">maintenance.equipment</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="domain">[('ows_machine_id', '!=', False)]</field>
|
||||
<field name="context">{
|
||||
'default_ows_category': 'red',
|
||||
'search_default_group_by_area': 1
|
||||
}</field>
|
||||
</record>
|
||||
|
||||
<!-- Trainingsprodukt-Liste -->
|
||||
|
|
@ -14,42 +19,32 @@
|
|||
<field name="view_mode">list,form</field>
|
||||
</record>
|
||||
|
||||
<!-- Menüstruktur -->
|
||||
<!-- Oberstes Menü -->
|
||||
<menuitem id="menu_machine_root"
|
||||
name="Maschinen"
|
||||
sequence="10"/>
|
||||
<!-- Integration in Maintenance Menü -->
|
||||
|
||||
<!-- Bereiche unter Maintenance → Configuration -->
|
||||
<menuitem id="menu_maintenance_ows_areas"
|
||||
name="Bereiche"
|
||||
parent="maintenance.menu_maintenance_config"
|
||||
action="action_machine_area_list"
|
||||
sequence="50"/>
|
||||
|
||||
<!-- Konfigurationsebene -->
|
||||
<menuitem id="menu_machine_config"
|
||||
name="Konfiguration"
|
||||
parent="menu_machine_root"
|
||||
sequence="10"/>
|
||||
|
||||
<!-- Menüpunkt: Maschinenliste (klickbar) -->
|
||||
<menuitem id="menu_machine_list_action"
|
||||
name="Alle Maschinen"
|
||||
parent="menu_machine_config"
|
||||
action="open_workshop.action_machine_list"
|
||||
sequence="10"/>
|
||||
|
||||
<!-- Menücontainer: Zuordnungen -->
|
||||
<menuitem id="menu_machine_list"
|
||||
<!-- Zuordnungen Container unter Maintenance → Configuration -->
|
||||
<menuitem id="menu_maintenance_ows_assignments"
|
||||
name="Zuordnungen"
|
||||
parent="menu_machine_config"
|
||||
sequence="20"/>
|
||||
parent="maintenance.menu_maintenance_config"
|
||||
sequence="51"/>
|
||||
|
||||
<!-- Untermenü: Nutzungsprodukte -->
|
||||
<menuitem id="menu_machine_product"
|
||||
<!-- Nutzungsprodukte unter Zuordnungen -->
|
||||
<menuitem id="menu_maintenance_machine_product"
|
||||
name="Nutzungsprodukte"
|
||||
parent="menu_machine_list"
|
||||
parent="menu_maintenance_ows_assignments"
|
||||
action="action_machine_product"
|
||||
sequence="10"/>
|
||||
|
||||
<!-- Untermenü: Einweisungsprodukte -->
|
||||
<menuitem id="menu_machine_training"
|
||||
<!-- Einweisungsprodukte unter Zuordnungen -->
|
||||
<menuitem id="menu_maintenance_machine_training"
|
||||
name="Einweisungsprodukte"
|
||||
parent="menu_machine_list"
|
||||
parent="menu_maintenance_ows_assignments"
|
||||
action="action_machine_training"
|
||||
sequence="20"/>
|
||||
|
||||
|
|
@ -1,29 +1,25 @@
|
|||
<odoo>
|
||||
<data>
|
||||
<!-- Entfernt die Partner-Warnung (Duplicate Bank Accounts) in res.partner
|
||||
<record id="patch_res_partner_duplicate_warning" model="ir.ui.view">
|
||||
<field name="name">res.partner.remove.duplicate.bank.warning</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="inherit_id" ref="account.view_partner_property_form"/>
|
||||
<field name="priority" eval="99"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@name='warning_tax' and @class='alert alert-warning oe_edit_only']" position="replace"/>
|
||||
</field>
|
||||
</record>-->
|
||||
|
||||
<!-- Entfernt die Partner-Warnung (Duplicate Bank Accounts) in res.partner -->
|
||||
<record id="patch_res_partner_duplicate_warning" model="ir.ui.view">
|
||||
<field name="name">res.partner.remove.duplicate.bank.warning</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="inherit_id" ref="account.view_partner_property_form"/>
|
||||
<field name="priority" eval="99"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@class='alert alert-warning']" position="replace"/>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Entfernt die Bankkonto-Warnung in res.partner.bank -->
|
||||
<record id="patch_res_partner_bank_duplicate_warning" model="ir.ui.view">
|
||||
<field name="name">res.partner.bank.remove.duplicate.warning</field>
|
||||
<field name="model">res.partner.bank</field>
|
||||
<field name="inherit_id" ref="account.view_partner_bank_form_inherit_account"/>
|
||||
<field name="priority" eval="99"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@class='alert alert-warning']" position="replace"/>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
<!-- Entfernt die Bankkonto-Warnung in res.partner.bank
|
||||
<record id="patch_res_partner_bank_duplicate_warning" model="ir.ui.view">
|
||||
<field name="name">res.partner.bank.remove.duplicate.warning</field>
|
||||
<field name="model">res.partner.bank</field>
|
||||
<field name="inherit_id" ref="account.view_partner_bank_form_inherit_account"/>
|
||||
<field name="priority" eval="99"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@class='alert alert-warning']" position="replace"/>
|
||||
</field>
|
||||
</record> -->
|
||||
|
||||
<!-- Zentrale View für alle drei Tabs in garantierter Reihenfolge -->
|
||||
|
||||
456
open_workshop_dokuwiki/README.md
Normal file
|
|
@ -0,0 +1,456 @@
|
|||
# Equipment Übersichtstabelle - Setup
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
Das **datatables Plugin** muss in DokuWiki installiert sein:
|
||||
```bash
|
||||
# Als DokuWiki Admin: Administration → Erweiterungsverwaltung
|
||||
# Suche: "datatables"
|
||||
# Installiere: "DataTables Plugin" von Matthias Schulte
|
||||
```
|
||||
|
||||
Das **include Plugin** muss in DokuWiki installiert sein (für Einbindung von Odoo-Status-Seiten):
|
||||
```bash
|
||||
# Als DokuWiki Admin: Administration → Erweiterungsverwaltung
|
||||
# Suche: "include"
|
||||
# Installiere: "Include Plugin" - sollte standardmäßig vorhanden sein
|
||||
```
|
||||
|
||||
## Setup-Schritte
|
||||
|
||||
### 1. Odoo Konfiguration
|
||||
|
||||
Die Übersichtstabelle wird vollständig aus Odoo heraus konfiguriert - **kein DokuWiki Template nötig!**
|
||||
|
||||
Gehe zu: **Einstellungen → Technisch → Parameter → Systemparameter**
|
||||
|
||||
Standardwerte (automatisch gesetzt beim Modul-Install):
|
||||
|
||||
```python
|
||||
# Basis-Namespace für Equipment-Dokumentation (konfigurierbar)
|
||||
dokuwiki.equipment_namespace = werkstatt:ausstattung
|
||||
|
||||
# Seiten-ID wo die Übersicht erstellt wird (konfigurierbar)
|
||||
dokuwiki.overview_page_id = werkstatt:ausstattung:uebersicht
|
||||
|
||||
# Titel der Übersichtsseite
|
||||
dokuwiki.overview_title = Geräte & Maschinen - Übersicht
|
||||
|
||||
# Spaltenüberschriften (Pipe-separiert)
|
||||
dokuwiki.overview_columns = Status|Sicherheits-Kategorie|Hersteller|Bemerkung|Typ|Bild|Standort|Dokumentation
|
||||
|
||||
# Spaltendaten mit Platzhaltern (Pipe-separiert)
|
||||
dokuwiki.overview_column_data = {status_smiley}|{ows_machine_id.category_icon}|{partner_id}|{note}|{model}|{image}|{ows_machine_id.location}|{wiki_doku_link}
|
||||
```
|
||||
|
||||
### 2. Erste Synchronisation
|
||||
|
||||
1. In Odoo: **Wartung → Konfiguration → Wiki-Synchronisation**
|
||||
2. Wähle: **"Übersichtstabelle aktualisieren"**
|
||||
3. Klicke: **"Synchronisieren"**
|
||||
|
||||
Die Übersichtsseite wird erstellt unter: `werkstatt:ausstattung:uebersicht` (konfigurierbar über Systemparameter)
|
||||
|
||||
## Features
|
||||
|
||||
### DataTables Funktionen
|
||||
- ✅ Sortieren nach allen Spalten (Klick auf Header)
|
||||
- ✅ Filter/Suche über alle Felder
|
||||
- ✅ Pagination bei vielen Einträgen
|
||||
- ✅ Responsive Design
|
||||
|
||||
### Flexible Spalten
|
||||
- ✅ Spalten in Odoo konfigurierbar (kein DokuWiki Template)
|
||||
- ✅ Alle Platzhalter verfügbar (siehe unten)
|
||||
- ✅ Spalten hinzufügen/entfernen ohne Code-Änderung
|
||||
- ✅ Änderungen sofort beim nächsten Sync wirksam
|
||||
|
||||
### Standard-Spalten
|
||||
| Spalte | Platzhalter | Ausgabe |
|
||||
|--------|-------------|---------|
|
||||
| Status | `{status_smiley}` | 😊 / 😐 / ☹️ |
|
||||
| Sicherheits-Kategorie | `{ows_machine_id.category_icon}` | 🟢 / 🟡 / 🔴 |
|
||||
| Hersteller | `{partner_id}` | BOSCH, Festool, ... |
|
||||
| Bemerkung | `{note}` | Notizen |
|
||||
| Typ | `{model}` | Modellbezeichnung |
|
||||
| Bild | `{image}` | Thumbnail 100px |
|
||||
| Standort | `{ows_machine_id.location}` | Regal A3, ... |
|
||||
| Dokumentation | `{wiki_doku_link}` | Link zur Detail-Seite |
|
||||
|
||||
## Spalten anpassen
|
||||
|
||||
### Beispiel 1: Seriennummer hinzufügen
|
||||
|
||||
```python
|
||||
# Spaltenüberschriften erweitern
|
||||
dokuwiki.overview_columns = Status|Typ|Seriennummer|Hersteller|Standort|Dokumentation
|
||||
|
||||
# Spaltendaten erweitern
|
||||
dokuwiki.overview_column_data = {status_smiley}|{model}|{serial_no}|{partner_id}|{ows_machine_id.location}|{wiki_doku_link}
|
||||
```
|
||||
|
||||
### Beispiel 2: Minimale Ansicht
|
||||
|
||||
```python
|
||||
dokuwiki.overview_columns = Name|Status|Kategorie|Dokumentation
|
||||
dokuwiki.overview_column_data = {name}|{status_smiley}|{ows_machine_id.category_icon}|{wiki_doku_link}
|
||||
```
|
||||
|
||||
### Beispiel 3: Detaillierte Ansicht mit Kosten
|
||||
|
||||
```python
|
||||
dokuwiki.overview_columns = Name|Hersteller|Modell|Seriennummer|Kosten|Garantie|Standort|Kategorie
|
||||
dokuwiki.overview_column_data = {name}|{partner_id}|{model}|{serial_no}|{cost}|{warranty_date}|{location}|{ows_machine_id.category_icon}
|
||||
```
|
||||
|
||||
### Beispiel 4: Mit Tags
|
||||
|
||||
```python
|
||||
dokuwiki.overview_columns = Status|Name|Typ|Tags|Standort|Dokumentation
|
||||
dokuwiki.overview_column_data = {status_smiley}|{name}|{model}|{tags_list}|{location}|{wiki_doku_link}
|
||||
```
|
||||
|
||||
**Wichtig:** Anzahl der Pipes muss übereinstimmen!
|
||||
- 3 Pipes = 4 Spalten
|
||||
- Spalten und Daten müssen gleiche Anzahl haben
|
||||
|
||||
## Performance
|
||||
|
||||
- ⚡ Schneller Sync: Keine DB-Iteration über alle Einträge
|
||||
- ⚡ Generiert nur eine Seite (statt 156 einzelne)
|
||||
- ⚡ DokuWiki cached die Seite automatisch
|
||||
- ⚡ Spalten-Änderung ohne Code-Deploy
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### DataTables funktioniert nicht
|
||||
→ Installiere DataTables Plugin in DokuWiki
|
||||
|
||||
### Bilder werden nicht angezeigt
|
||||
→ Prüfe ob Equipment `image_1920` Feld gesetzt hat
|
||||
→ Bilder müssen zuerst via "Alle Equipment" Sync hochgeladen werden
|
||||
|
||||
### Equipment fehlen in Tabelle
|
||||
→ Nur Equipment mit gesetztem `ows_area_id` werden angezeigt
|
||||
→ Prüfe Filter in `action_sync_overview_table()`
|
||||
|
||||
### Spalten werden nicht korrekt dargestellt
|
||||
→ Prüfe ob Anzahl Pipes in `overview_columns` und `overview_column_data` übereinstimmt
|
||||
→ Beispiel: 3 Pipes = 4 Spalten
|
||||
|
||||
### Platzhalter wird nicht ersetzt (z.B. `{status}` bleibt stehen)
|
||||
→ Prüfe Schreibweise (case-sensitive!)
|
||||
→ Siehe Liste der verfügbaren Platzhalter unten
|
||||
→ Bei `ows.machine` Feldern: `{ows_machine_id.feldname}` verwenden
|
||||
|
||||
## Workflow
|
||||
|
||||
```
|
||||
┌─────────────────┐
|
||||
│ Odoo Systempar. │
|
||||
│ - Spalten │
|
||||
│ - Platzhalter │
|
||||
└────────┬────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────┐
|
||||
│ Odoo Equipment │
|
||||
│ (156 Einträge) │
|
||||
└────────┬────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────┐
|
||||
│ _generate_overview_ │
|
||||
│ table_row() │ ← Pro Equipment eine Zeile
|
||||
│ (Platzhalter ersetzen) │ mit Platzhalter-Engine
|
||||
└────────┬────────────────┘
|
||||
│ 156 Zeilen
|
||||
▼
|
||||
┌─────────────────────────┐
|
||||
│ Seite zusammenbauen │
|
||||
│ - Titel │
|
||||
│ - DataTables Header │
|
||||
│ - Zeilen │
|
||||
│ - Footer │
|
||||
└────────┬────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────┐
|
||||
│ Seite speichern │
|
||||
│ werkstatt:ausstattung: │
|
||||
│ uebersicht │
|
||||
└─────────────────────────┘
|
||||
```
|
||||
|
||||
## Automatische Updates
|
||||
|
||||
Die Übersichtstabelle kann optional automatisch bei Equipment-Änderungen aktualisiert werden.
|
||||
|
||||
### Manueller Sync (empfohlen)
|
||||
|
||||
**Manueller Sync empfohlen für:**
|
||||
- Nach Massen-Importen
|
||||
- Einmal täglich/wöchentlich
|
||||
- Bei strukturellen Änderungen (neue Bereiche, etc.)
|
||||
|
||||
**Vorteil:** Performanter, keine zusätzliche Last bei jeder Equipment-Änderung
|
||||
|
||||
### Automatischer Sync bei Equipment-Änderungen
|
||||
|
||||
**Was passiert beim Speichern eines Equipment?**
|
||||
|
||||
1. **Equipment-Detailseite wird immer aktualisiert** wenn:
|
||||
- Eines der überwachten Felder geändert wurde (siehe unten)
|
||||
- `wiki_auto_sync` für das Equipment aktiviert ist (Standard: `True`)
|
||||
- Equipment bereits synchronisiert wurde (`wiki_synced = True`)
|
||||
|
||||
2. **Übersichtstabelle wird zusätzlich aktualisiert** wenn:
|
||||
- Alle Bedingungen von (1) erfüllt sind UND
|
||||
- Systemparameter `dokuwiki.auto_update_overview_table = True` gesetzt ist
|
||||
|
||||
**Überwachte Felder (lösen Sync aus):**
|
||||
- `name`, `serial_no`, `ows_area_id`, `category_id`, `status_id`
|
||||
- `model`, `partner_id`, `location`, `note`, `image_1920`, `tag_ids`
|
||||
|
||||
**Aktivierung Übersichtstabellen-Sync:**
|
||||
```python
|
||||
# In Odoo: Einstellungen → Technisch → Parameter → Systemparameter
|
||||
dokuwiki.auto_update_overview_table = True
|
||||
```
|
||||
|
||||
**Achtung:**
|
||||
- Bei vielen gleichzeitigen Equipment-Änderungen kann dies Performance-Probleme verursachen!
|
||||
- Die komplette Übersichtstabelle wird bei jeder Änderung neu generiert (ca. 156 Zeilen)
|
||||
|
||||
### Cronjob (Alternative)
|
||||
```python
|
||||
# In Odoo: Einstellungen → Technisch → Automatisierung → Geplante Aktionen
|
||||
|
||||
Name: Wiki Übersichtstabelle Update
|
||||
Modell: maintenance.equipment
|
||||
Aktion: action_sync_overview_table()
|
||||
Intervall: Täglich um 02:00 Uhr
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Verfügbare Platzhalter
|
||||
|
||||
Alle Platzhalter aus dem Detail-Template sind verfügbar!
|
||||
|
||||
## Basis-Felder (maintenance.equipment)
|
||||
|
||||
- `{name}` - Equipment-Name
|
||||
- `{serial_no}` - Seriennummer
|
||||
- `{model}` - Modell
|
||||
- `{category}` - Kategoriename
|
||||
- `{status}` - Status (aus status_id)
|
||||
- `{status_smiley}` - Status als Smiley (aus status_id.smiley Feld, z.B. 😊 oder ☹️)
|
||||
- `{location}` - Standort
|
||||
- `{ows_area}` - Bereichsname
|
||||
- `{assign_date}` - Zuweisungsdatum (formatiert)
|
||||
- `{cost}` - Kosten
|
||||
- `{warranty_date}` - Garantiedatum (formatiert)
|
||||
- `{note}` - Notizen
|
||||
- `{partner_id}` - Lieferant (Name)
|
||||
- `{partner_ref}` - Lieferanten-Referenz
|
||||
|
||||
## Tags (falls maintenance_equipment_tags installiert)
|
||||
|
||||
- `{tags}` - Komma-separierte Liste aller Tags (z.B. "Holz, CNC, Einweisung erforderlich")
|
||||
- `{tags_list}` - DokuWiki Bullet-Liste mit Zeilenumbrüchen (ideal für Tabellenzellen)
|
||||
- `{tags_count}` - Anzahl der zugewiesenen Tags
|
||||
|
||||
## Spezial-Felder
|
||||
|
||||
- `{view_type}` - "Bereich" oder "Einsatzzweck"
|
||||
- `{view_name}` - Name des Bereichs/Einsatzzwecks
|
||||
- `{wiki_doku_page}` - ID der zentralen Doku-Seite
|
||||
- `{wiki_doku_link}` - Fertiger Link zur zentralen Doku-Seite
|
||||
- `{odoo_link}` - Fertiger Link zur Odoo Equipment-Seite
|
||||
- `{odoo_url}` - URL zur Odoo Equipment-Seite (ohne Link-Markup)
|
||||
- `{sync_datetime}` - Aktuelles Datum/Zeit
|
||||
- `{image}` - Equipment-Bild (100px Breite) - Format: `{{:media_id?100}}`
|
||||
- `{image_large}` - Equipment-Bild (Originalgröße) - Format: `{{:media_id}}`
|
||||
- `{image_id}` - Media-ID des Bildes (z.B. werkstatt:ausruestung:media:sabako-laser.jpg)
|
||||
|
||||
## ows.machine Felder (falls verknüpft)
|
||||
|
||||
- `{ows_machine_id.name}` - Name
|
||||
- `{ows_machine_id.model}` - Modell
|
||||
- `{ows_machine_id.serial_no}` - Seriennummer
|
||||
- `{ows_machine_id.location}` - Standort
|
||||
- `{ows_machine_id.note}` - Notizen
|
||||
- `{ows_machine_id.category}` - Sicherheitskategorie (red/yellow/green)
|
||||
- `{ows_machine_id.category_icon}` - Kategorie-Icon als Emoji (🔴/🟡/🟢)
|
||||
|
||||
## Tipps für Platzhalter
|
||||
|
||||
- **Case-sensitive**: `{Name}` funktioniert nicht, nur `{name}`
|
||||
- **Pipe als Trenner**: Zwischen Spalten `|` verwenden
|
||||
- **Leere Werte**: Werden automatisch durch `-` ersetzt
|
||||
- **Links**: `{wiki_doku_link}` und `{odoo_link}` enthalten bereits DokuWiki Link-Syntax
|
||||
- **Bilder**: `{image}` ist bereits formatiert mit `{{:...?100}}`
|
||||
- **ows.machine**: Immer mit Präfix `ows_machine_id.` (z.B. `{ows_machine_id.location}`)
|
||||
|
||||
## Beispiel-Konfigurationen
|
||||
|
||||
### Minimalistische Ansicht
|
||||
```
|
||||
Spalten: Name|Status|Kategorie
|
||||
Daten: {name}|{status_smiley}|{ows_machine_id.category_icon}
|
||||
```
|
||||
|
||||
### Standard-Ansicht (Default)
|
||||
```
|
||||
Spalten: Status|Sicherheits-Kategorie|Hersteller|Bemerkung|Typ|Bild|Standort|Dokumentation
|
||||
Daten: {status_smiley}|{ows_machine_id.category_icon}|{partner_id}|{note}|{model}|{image}|{ows_machine_id.location}|{wiki_doku_link}
|
||||
```
|
||||
|
||||
### Vollständige Ansicht
|
||||
```
|
||||
Spalten: Name|Status|Kategorie|Hersteller|Modell|S/N|Kosten|Garantie|Standort|Bereich|Bild|Doku
|
||||
Daten: {name}|{status_smiley}|{ows_machine_id.category_icon}|{partner_id}|{model}|{serial_no}|{cost}|{warranty_date}|{location}|{ows_area}|{image}|{wiki_doku_link}
|
||||
```
|
||||
|
||||
### Mit Tags
|
||||
```
|
||||
Spalten: Name|Status|Tags|Hersteller|Standort|Doku
|
||||
Daten: {name}|{status_smiley}|{tags_list}|{partner_id}|{location}|{wiki_doku_link}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Best Practices: DokuWiki Seitenstruktur
|
||||
|
||||
## Erweiterbare Dokumentation ohne Umbenennung
|
||||
|
||||
### Problem
|
||||
Benutzer möchten zu Equipment-Seiten eigene Unterseiten hinzufügen, ohne die von Odoo generierten Seiten umbenennen zu müssen.
|
||||
|
||||
### Lösung: Parallele Seite + Namespace
|
||||
|
||||
DokuWiki erlaubt **gleichzeitig** eine Seite und einen gleichnamigen Namespace:
|
||||
- Datei: `equipment.txt` (von Odoo generiert)
|
||||
- Verzeichnis: `equipment/` (für Benutzer-Unterseiten)
|
||||
|
||||
**Die Seite `equipment.txt` hat Vorrang und bleibt die Haupt-Equipment-Seite!**
|
||||
|
||||
### Empfohlene Struktur
|
||||
|
||||
```
|
||||
werkstatt/
|
||||
└── ausstattung/
|
||||
├── uebersicht # VON ODOO GENERIERT: Übersichtstabelle
|
||||
├── odoo-status/ # VON ODOO GENERIERT: Nur-Lese Seiten (nur odoo.odoo schreibt)
|
||||
│ ├── c_template.txt # Template für Equipment-Status-Seiten
|
||||
│ ├── analog-oscilloscope-hm303-6.txt # Status-Seite für include-Plugin
|
||||
│ ├── cnc-fraese-xyz.txt # Status-Seite für include-Plugin
|
||||
│ └── ...
|
||||
└── {Bereich Name}/ # VON BENUTZER ERSTELLT: Namespace wird von Odoo festgelegt
|
||||
├── analog-oscilloscope-hm303-6.txt # Benutzer erstellt diese Seite manuell (Klick auf Link in Übersicht)
|
||||
├── analog-oscilloscope-hm303-6/ # Benutzer-Unterseiten (optional)
|
||||
│ ├── kalibrierung.txt
|
||||
│ ├── messungen.txt
|
||||
│ └── bilder/
|
||||
│ └── oszillogramme.txt
|
||||
├── cnc-fraese-xyz.txt # Benutzer erstellt diese Seite manuell (Klick auf Link in Übersicht)
|
||||
└── cnc-fraese-xyz/ # Benutzer-Unterseiten (optional)
|
||||
├── programme.txt
|
||||
└── werkzeuge.txt
|
||||
```
|
||||
|
||||
**Wichtig:**
|
||||
- **Odoo erstellt NUR:** `{overview_page_id}` (Übersichtstabelle) und `odoo-status/*.txt` (Status-Seiten)
|
||||
- **Odoo erstellt NIEMALS:** Seiten in `{Bereich Name}/` - diese werden ausschließlich von Benutzern erstellt
|
||||
- **Workflow:** Übersichtstabelle verlinkt auf `{Bereich Name}/equipment-name.txt` → Link ist rot (Seite existiert nicht) → Benutzer klickt darauf → DokuWiki bietet "Seite erstellen" an → Benutzer fügt `{{page>odoo-status:equipment-name}}` ein
|
||||
- **Berechtigungen:** `odoo-status/` Namespace hat spezielle Berechtigungen (nur odoo.odoo kann schreiben)
|
||||
|
||||
### Wichtige Regeln
|
||||
|
||||
#### ✅ DO: Unterverzeichnis OHNE start.txt
|
||||
|
||||
```
|
||||
equipment/
|
||||
├── subpage1.txt
|
||||
├── subpage2.txt
|
||||
└── weitere/
|
||||
└── details.txt
|
||||
```
|
||||
|
||||
**Vorteile:**
|
||||
- Breadcrumb zeigt: `Home » Werkstatt » Ausrüstung » Equipment » Subpage1`
|
||||
- Keine Duplikation oder Verwirrung
|
||||
- Equipment-Hauptseite bleibt `equipment.txt`
|
||||
|
||||
#### ❌ DON'T: Unterverzeichnis MIT start.txt
|
||||
|
||||
```
|
||||
equipment/
|
||||
├── start.txt # ❌ NICHT erstellen!
|
||||
├── subpage1.txt
|
||||
└── subpage2.txt
|
||||
```
|
||||
|
||||
**Probleme:**
|
||||
- Breadcrumb wird verwirrend: `Equipment` könnte auf zwei Seiten verweisen
|
||||
- `equipment` und `equipment:start` zeigen unterschiedliche Inhalte
|
||||
- Benutzer wissen nicht, welche Seite die "richtige" ist
|
||||
|
||||
### Include-Plugin für Odoo-Status einbinden
|
||||
|
||||
**Wichtig:** Diese Seite wird NICHT von Odoo erstellt! Der Benutzer erstellt sie manuell nach Klick auf den Link in der Übersichtstabelle.
|
||||
|
||||
In der Benutzer-Dokumentationsseite wird der Odoo-generierte Status eingebunden:
|
||||
|
||||
```dokuwiki
|
||||
===== Analog Oscilloscope HM303-6 =====
|
||||
|
||||
==== Odoo Status ====
|
||||
|
||||
{{page>werkstatt:ausstattung:odoo-status:analog-oscilloscope-hm303-6}}
|
||||
|
||||
==== Eigene Dokumentation ====
|
||||
|
||||
Hier kann der Benutzer seine eigenen Inhalte hinzufügen...
|
||||
|
||||
==== Weitere Dokumentation ====
|
||||
|
||||
<catlist .:analog-oscilloscope-hm303-6 -noNSInBold -sortByTitle>
|
||||
```
|
||||
|
||||
**Syntax-Erklärung:**
|
||||
- `{{page>...}}` → Bindet Odoo-Status-Seite ein (nur lesbar für Benutzer)
|
||||
- `.:namespace` → Relativer Namespace für catlist (Punkt + Doppelpunkt!)
|
||||
- `-noNSInBold` → Namespace-Präfix nicht fett darstellen
|
||||
- `-sortByTitle` → Alphabetisch sortieren
|
||||
- `-exclude:{ns1 ns2}` → Optional: Namespaces ausschließen
|
||||
|
||||
**Ergebnis:**
|
||||
- Odoo-Status wird direkt in Benutzer-Seite eingebettet
|
||||
- Benutzer sehen aktuelle Odoo-Daten ohne direkt auf odoo-status zuzugreifen
|
||||
- Catlist zeigt alle Benutzer-Unterseiten im `equipment/` Verzeichnis
|
||||
- Automatisch aktualisiert wenn Benutzer neue Seiten erstellen
|
||||
- Kein manuelles Pflegen von Links nötig
|
||||
|
||||
### Workflow
|
||||
|
||||
1. **Odoo synchronisiert** → Erstellt/aktualisiert `{overview_page_id}` (Übersichtstabelle) und `odoo-status/equipment.txt` (Status-Seiten)
|
||||
2. **Benutzer sieht Übersichtstabelle** → Equipment sind verlinkt, aber Links sind rot (Seiten existieren nicht)
|
||||
3. **Benutzer klickt roten Link** → DokuWiki zeigt "Diese Seite existiert noch nicht - erstellen?"
|
||||
4. **Benutzer erstellt Seite** → Fügt `{{page>werkstatt:ausstattung:odoo-status:equipment-name}}` ein und eigene Inhalte
|
||||
5. **Benutzer erstellt optional Unterseiten** → Erstellt Verzeichnis `equipment/` für weitere Dokumentation
|
||||
6. **Catlist zeigt automatisch Unterseiten** → Wenn in Hauptseite eingebunden
|
||||
7. **Keine Konflikte** → Odoo schreibt NIEMALS in `{Bereich Name}/`, nur in `start` und `odoo-status/`
|
||||
|
||||
### Vorteile dieser Struktur
|
||||
|
||||
- ✅ **Klare Trennung:** `odoo-status/` (nur Odoo schreibt) vs. `{Bereich Name}/` (nur Benutzer schreiben)
|
||||
- ✅ **Berechtigungsschutz:** Benutzer können Odoo-Daten nicht versehentlich überschreiben
|
||||
- ✅ **Keine Überschreibgefahr:** Odoo erstellt niemals Seiten in Benutzer-Namespaces
|
||||
- ✅ **Einfaches Einbinden:** Ein `{{page>...}}` in Benutzer-Seite, fertig
|
||||
- ✅ **Keine Namenskollisionen:** Odoo und Benutzer haben komplett getrennte Namespaces
|
||||
- ✅ **Benutzer entscheiden:** Nur wer dokumentieren will, erstellt eine Seite (über Link in Übersichtstabelle)
|
||||
- ✅ **Flexibel erweiterbar:** Benutzer können beliebig viele Unterseiten erstellen
|
||||
- ✅ **Automatische Updates:** Odoo-Status wird in alle einbindenden Seiten propagiert
|
||||
- ✅ **Konfigurierbarer Namespace:** Basis-Pfad über Systemparameter `dokuwiki.equipment_namespace` anpassbar
|
||||
4
open_workshop_dokuwiki/__init__.py
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from . import models
|
||||
from . import wizard
|
||||
from .hooks import post_init_hook, pre_init_hook
|
||||
68
open_workshop_dokuwiki/__manifest__.py
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
'name': 'Open Workshop - DokuWiki Integration',
|
||||
'version': '18.0.2.1.0',
|
||||
'category': 'Maintenance',
|
||||
'summary': 'Synchronisiert Equipment-Daten mit DokuWiki für Dokumentation',
|
||||
'description': """
|
||||
Open Workshop DokuWiki Integration
|
||||
===================================
|
||||
|
||||
Dieses Modul synchronisiert Equipment-Daten aus Odoo mit einem DokuWiki System.
|
||||
|
||||
Features:
|
||||
---------
|
||||
* Automatische Erstellung von Wiki-Seiten für Equipment
|
||||
* Übersichtstabelle aller Equipment (DataTables mit Sortierung/Filterung)
|
||||
* Status-Seiten für Include-Plugin (nur von Odoo generiert, nur lesbar)
|
||||
* Benutzer erstellen eigene Dokumentationsseiten und binden Odoo-Status ein
|
||||
* Smart Button "Wiki öffnen" im Equipment-Formular
|
||||
* Automatische Synchronisation bei Equipment-Änderungen (optional)
|
||||
|
||||
Architektur:
|
||||
------------
|
||||
* Odoo generiert NUR: {overview_page_id} (Übersichtstabelle) und odoo-status/ Seiten
|
||||
* Odoo generiert NIEMALS: Benutzer-Dokumentationsseiten in {bereich}/ Namespaces
|
||||
* Namespace-Struktur (konfigurierbar über dokuwiki.equipment_namespace):
|
||||
- {equipment_namespace}:{overview_page_id} - Übersichtstabelle (von Odoo)
|
||||
- {equipment_namespace}:odoo-status:{equipment_id} - Status-Seiten (von Odoo, nur lesbar)
|
||||
- {equipment_namespace}:odoo-status:c_template - Template für Status-Seiten
|
||||
- {equipment_namespace}:{bereich}:{equipment_id} - Benutzer-Dokumentation (von Benutzern erstellt!)
|
||||
|
||||
Workflow:
|
||||
---------
|
||||
1. Odoo synchronisiert Equipment → erstellt odoo-status/ Seiten
|
||||
2. Übersichtstabelle verlinkt auf {bereich}/{equipment_id} → Link ist rot (existiert nicht)
|
||||
3. Benutzer klickt roten Link → DokuWiki: "Seite erstellen?"
|
||||
4. Benutzer erstellt Seite und fügt {{page>odoo-status:equipment_id}} ein
|
||||
|
||||
ACL-Empfehlung:
|
||||
---------------
|
||||
* odoo-status/ Namespace: Nur odoo.odoo kann schreiben, alle anderen nur lesen
|
||||
* {bereich}/ Namespaces: Benutzer haben volle Schreibrechte
|
||||
""",
|
||||
'author': 'Hobbyhimmel',
|
||||
'website': 'https://hobbyhimmel.de',
|
||||
'license': 'LGPL-3',
|
||||
'depends': [
|
||||
'open_workshop_base',
|
||||
'maintenance',
|
||||
'maintenance_equipment_status',
|
||||
],
|
||||
'external_dependencies': {
|
||||
'python': ['dokuwiki'],
|
||||
},
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'data/ir_config_parameter.xml',
|
||||
'data/maintenance_equipment_status_data.xml',
|
||||
'views/maintenance_equipment_views.xml',
|
||||
'views/maintenance_equipment_status_views.xml',
|
||||
'wizard/equipment_wiki_sync_wizard_views.xml',
|
||||
],
|
||||
'post_init_hook': 'post_init_hook',
|
||||
'pre_init_hook': 'pre_init_hook',
|
||||
'installable': True,
|
||||
'application': False,
|
||||
'auto_install': False,
|
||||
}
|
||||
8
open_workshop_dokuwiki/data/ir_config_parameter.xml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!--
|
||||
DokuWiki System Parameters werden über post_init_hook initialisiert
|
||||
Dadurch werden sie bei Install und Update korrekt gehandhabt
|
||||
und User-Änderungen bleiben erhalten
|
||||
-->
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo noupdate="1">
|
||||
<!-- DokuWiki Smiley-Werte für Status aus open_workshop_base -->
|
||||
|
||||
<record id="open_workshop_base.equipment_status_inbetrieb" model="maintenance.equipment.status">
|
||||
<field name="smiley">😊</field>
|
||||
</record>
|
||||
|
||||
<record id="open_workshop_base.equipment_status_defekt" model="maintenance.equipment.status">
|
||||
<field name="smiley">☹️</field>
|
||||
</record>
|
||||
|
||||
<record id="open_workshop_base.equipment_status_wartung" model="maintenance.equipment.status">
|
||||
<field name="smiley">🛠️</field>
|
||||
</record>
|
||||
|
||||
<record id="open_workshop_base.equipment_status_ausgemustert" model="maintenance.equipment.status">
|
||||
<field name="smiley">❌</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
15
open_workshop_dokuwiki/hooks.py
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
def pre_init_hook(env):
|
||||
"""
|
||||
Hook der VOR der Modul-Installation/Update ausgeführt wird.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def post_init_hook(env):
|
||||
"""
|
||||
Hook der nach der Modul-Installation/Update ausgeführt wird.
|
||||
Initialisiert die DokuWiki-Parameter falls sie nicht existieren.
|
||||
"""
|
||||
env['res.config.settings']._init_dokuwiki_parameters()
|
||||
101
open_workshop_dokuwiki/migrations/18.0.2.1.0/pre-migration.py
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import logging
|
||||
from psycopg2 import sql
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def migrate(cr, version):
|
||||
"""
|
||||
Fügt die neuen Wiki-ID-Felder zur maintenance_equipment Tabelle hinzu.
|
||||
|
||||
Args:
|
||||
cr: Database cursor
|
||||
version: Aktuelle Version vor dem Upgrade (sollte 18.0.2.0.0 sein)
|
||||
"""
|
||||
_logger.info("=" * 80)
|
||||
_logger.info("PRE-MIGRATION SCRIPT: open_workshop_dokuwiki 18.0.2.1.0")
|
||||
_logger.info("Aktuelle Version: %s", version)
|
||||
_logger.info("=" * 80)
|
||||
|
||||
try:
|
||||
# Prüfe ob die Tabelle maintenance_equipment existiert
|
||||
cr.execute("""
|
||||
SELECT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'maintenance_equipment'
|
||||
)
|
||||
""")
|
||||
table_exists = cr.fetchone()[0]
|
||||
|
||||
if not table_exists:
|
||||
_logger.warning("Tabelle maintenance_equipment existiert nicht - überspringe Migration")
|
||||
return
|
||||
|
||||
_logger.info("Tabelle maintenance_equipment gefunden")
|
||||
|
||||
# Prüfe ob die Felder bereits existieren
|
||||
cr.execute("""
|
||||
SELECT column_name
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'maintenance_equipment'
|
||||
AND column_name IN ('wiki_status_id', 'wiki_doku_id')
|
||||
""")
|
||||
existing_fields = [row[0] for row in cr.fetchall()]
|
||||
_logger.info("Bereits existierende Felder: %s", existing_fields)
|
||||
|
||||
# Füge wiki_status_id hinzu falls nicht vorhanden
|
||||
if 'wiki_status_id' not in existing_fields:
|
||||
_logger.info("Füge Spalte wiki_status_id hinzu...")
|
||||
cr.execute("""
|
||||
ALTER TABLE maintenance_equipment
|
||||
ADD COLUMN wiki_status_id VARCHAR
|
||||
""")
|
||||
cr.commit() # Wichtig: Commit nach jeder Änderung
|
||||
_logger.info("✓ Spalte wiki_status_id erfolgreich hinzugefügt")
|
||||
else:
|
||||
_logger.info("→ Spalte wiki_status_id existiert bereits, überspringe")
|
||||
|
||||
# Füge wiki_doku_id hinzu falls nicht vorhanden
|
||||
if 'wiki_doku_id' not in existing_fields:
|
||||
_logger.info("Füge Spalte wiki_doku_id hinzu...")
|
||||
cr.execute("""
|
||||
ALTER TABLE maintenance_equipment
|
||||
ADD COLUMN wiki_doku_id VARCHAR
|
||||
""")
|
||||
cr.commit() # Wichtig: Commit nach jeder Änderung
|
||||
_logger.info("✓ Spalte wiki_doku_id erfolgreich hinzugefügt")
|
||||
else:
|
||||
_logger.info("→ Spalte wiki_doku_id existiert bereits, überspringe")
|
||||
|
||||
# Verifiziere dass beide Felder jetzt existieren
|
||||
cr.execute("""
|
||||
SELECT column_name
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'maintenance_equipment'
|
||||
AND column_name IN ('wiki_status_id', 'wiki_doku_id')
|
||||
ORDER BY column_name
|
||||
""")
|
||||
final_fields = [row[0] for row in cr.fetchall()]
|
||||
_logger.info("Felder nach Migration: %s", final_fields)
|
||||
|
||||
if len(final_fields) == 2:
|
||||
_logger.info("=" * 80)
|
||||
_logger.info("✓ PRE-MIGRATION ERFOLGREICH ABGESCHLOSSEN")
|
||||
_logger.info("=" * 80)
|
||||
else:
|
||||
_logger.error("!" * 80)
|
||||
_logger.error("FEHLER: Nicht alle Felder wurden hinzugefügt!")
|
||||
_logger.error("Erwartet: ['wiki_doku_id', 'wiki_status_id']")
|
||||
_logger.error("Gefunden: %s", final_fields)
|
||||
_logger.error("!" * 80)
|
||||
|
||||
except Exception as e:
|
||||
_logger.error("!" * 80)
|
||||
_logger.error("FEHLER IM PRE-MIGRATION SCRIPT!")
|
||||
_logger.error("Exception: %s", str(e))
|
||||
_logger.error("!" * 80)
|
||||
raise
|
||||
5
open_workshop_dokuwiki/models/__init__.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from . import dokuwiki_client
|
||||
from . import maintenance_equipment
|
||||
from . import maintenance_equipment_status
|
||||
from . import res_config_settings
|
||||
392
open_workshop_dokuwiki/models/dokuwiki_client.py
Normal file
|
|
@ -0,0 +1,392 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import logging
|
||||
from dokuwiki import DokuWiki, DokuWikiError
|
||||
from odoo import api, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
# Connection Cache (pro Request wiederverwendet)
|
||||
_connection_cache = {}
|
||||
|
||||
|
||||
class DokuWikiClient(models.AbstractModel):
|
||||
"""
|
||||
Wrapper für DokuWiki XML-RPC API Client.
|
||||
Managed die Verbindung zum DokuWiki und stellt Methoden für Seitenoperationen bereit.
|
||||
|
||||
Performance-Optimierung: Verbindungen werden gecached und wiederverwendet
|
||||
um den Overhead von XML-RPC Verbindungsaufbauten zu minimieren.
|
||||
"""
|
||||
_name = 'dokuwiki.client'
|
||||
_description = 'DokuWiki API Client'
|
||||
|
||||
@api.model
|
||||
def _get_cache_key(self):
|
||||
"""
|
||||
Generiert einen Cache-Key basierend auf den Verbindungsparametern.
|
||||
|
||||
Returns:
|
||||
str: Cache-Key (URL + User)
|
||||
"""
|
||||
IrConfigParameter = self.env['ir.config_parameter'].sudo()
|
||||
wiki_url = IrConfigParameter.get_param('dokuwiki.url', '')
|
||||
wiki_user = IrConfigParameter.get_param('dokuwiki.user', '')
|
||||
return f"{wiki_url}:{wiki_user}"
|
||||
|
||||
@api.model
|
||||
def _get_wiki_connection(self, use_cache=True):
|
||||
"""
|
||||
Erstellt eine DokuWiki-Verbindung mit Credentials aus System-Parametern.
|
||||
Verbindungen werden gecached und wiederverwendet für bessere Performance.
|
||||
|
||||
Args:
|
||||
use_cache (bool): Wenn True, wird eine gecachte Verbindung wiederverwendet
|
||||
|
||||
Returns:
|
||||
DokuWiki: Verbundenes DokuWiki-Client-Objekt
|
||||
|
||||
Raises:
|
||||
UserError: Wenn Konfiguration fehlt oder Verbindung fehlschlägt
|
||||
"""
|
||||
IrConfigParameter = self.env['ir.config_parameter'].sudo()
|
||||
|
||||
# Credentials aus System-Parametern holen
|
||||
wiki_url = IrConfigParameter.get_param('dokuwiki.url')
|
||||
wiki_user = IrConfigParameter.get_param('dokuwiki.user')
|
||||
wiki_password = IrConfigParameter.get_param('dokuwiki.password')
|
||||
|
||||
if not all([wiki_url, wiki_user, wiki_password]):
|
||||
raise UserError(
|
||||
"DokuWiki-Konfiguration unvollständig. "
|
||||
"Bitte unter Einstellungen → Technisch → Parameter → Systemparameter konfigurieren:\n"
|
||||
"- dokuwiki.url\n"
|
||||
"- dokuwiki.user\n"
|
||||
"- dokuwiki.password"
|
||||
)
|
||||
|
||||
# URL validieren/normalisieren
|
||||
if not wiki_url.startswith(('http://', 'https://')):
|
||||
raise UserError(f"DokuWiki URL muss mit http:// oder https:// beginnen: {wiki_url}")
|
||||
|
||||
# Cache-Key generieren
|
||||
cache_key = self._get_cache_key()
|
||||
|
||||
# Gecachte Verbindung wiederverwenden wenn möglich
|
||||
if use_cache and cache_key in _connection_cache:
|
||||
try:
|
||||
# Teste ob Verbindung noch lebt
|
||||
cached_wiki = _connection_cache[cache_key]
|
||||
_ = cached_wiki.version # Test-Call
|
||||
_logger.debug(f"Wiederverwendung gecachter DokuWiki-Verbindung: {wiki_url}")
|
||||
return cached_wiki
|
||||
except Exception as e:
|
||||
_logger.warning(f"Gecachte Verbindung ungültig, erstelle neue: {e}")
|
||||
del _connection_cache[cache_key]
|
||||
|
||||
# Neue Verbindung erstellen
|
||||
try:
|
||||
_logger.info(f"Neue DokuWiki-Verbindung: {wiki_url} (User: {wiki_user})")
|
||||
wiki = DokuWiki(wiki_url, wiki_user, wiki_password)
|
||||
# Test-Verbindung
|
||||
version = wiki.version
|
||||
_logger.info(f"DokuWiki-Verbindung erfolgreich: Version {version}")
|
||||
|
||||
# In Cache speichern
|
||||
if use_cache:
|
||||
_connection_cache[cache_key] = wiki
|
||||
_logger.debug(f"Verbindung im Cache gespeichert: {cache_key}")
|
||||
|
||||
return wiki
|
||||
except DokuWikiError as e:
|
||||
error_details = f"URL: {wiki_url}, User: {wiki_user}, Fehler: {e}"
|
||||
_logger.error(f"DokuWiki-Verbindung fehlgeschlagen: {error_details}")
|
||||
raise UserError(
|
||||
f"Verbindung zu DokuWiki fehlgeschlagen.\n\n"
|
||||
f"Details:\n"
|
||||
f"- URL: {wiki_url}\n"
|
||||
f"- User: {wiki_user}\n"
|
||||
f"- Fehler: {e}\n\n"
|
||||
f"Hinweise:\n"
|
||||
f"- Prüfen Sie ob die URL korrekt ist (sollte auf /lib/exe/xmlrpc.php zeigen)\n"
|
||||
f"- Stellen Sie sicher, dass XML-RPC in DokuWiki aktiviert ist\n"
|
||||
f"- Prüfen Sie die Benutzer-Zugangsdaten"
|
||||
)
|
||||
except Exception as e:
|
||||
_logger.error(f"Unerwarteter Fehler bei DokuWiki-Verbindung: {e}", exc_info=True)
|
||||
raise UserError(f"Unerwarteter Fehler: {e}")
|
||||
|
||||
@api.model
|
||||
def clear_connection_cache(self):
|
||||
"""
|
||||
Löscht den Verbindungs-Cache (z.B. nach Konfigurationsänderungen).
|
||||
"""
|
||||
global _connection_cache
|
||||
_connection_cache.clear()
|
||||
_logger.info("DokuWiki Verbindungs-Cache geleert")
|
||||
|
||||
@api.model
|
||||
def create_page(self, page_id, content, summary="Erstellt von Odoo"):
|
||||
"""
|
||||
Erstellt oder aktualisiert eine DokuWiki-Seite.
|
||||
|
||||
Args:
|
||||
page_id (str): DokuWiki Page-ID (z.B. "werkstatt:ausruestung:holzwerkstatt:maschine1")
|
||||
content (str): Wiki-Markup-Inhalt der Seite
|
||||
summary (str): Änderungskommentar für die Wiki-History
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg
|
||||
|
||||
Raises:
|
||||
UserError: Bei API-Fehlern
|
||||
"""
|
||||
wiki = self._get_wiki_connection()
|
||||
|
||||
try:
|
||||
_logger.info(f"Erstelle/Aktualisiere Wiki-Seite: {page_id} ({len(content)} Zeichen)")
|
||||
wiki.pages.set(page_id, content, summary=summary)
|
||||
_logger.info(f"Wiki-Seite erfolgreich erstellt/aktualisiert: {page_id}")
|
||||
return True
|
||||
except DokuWikiError as e:
|
||||
_logger.error(f"DokuWiki API Fehler bei Seite {page_id}: {e}", exc_info=True)
|
||||
raise UserError(
|
||||
f"Fehler beim Erstellen der Wiki-Seite '{page_id}':\n\n"
|
||||
f"{e}\n\n"
|
||||
f"Mögliche Ursachen:\n"
|
||||
f"- XML-RPC nicht aktiviert in DokuWiki\n"
|
||||
f"- Keine Schreibrechte für den Namespace\n"
|
||||
f"- Ungültige Zeichen in der Page-ID\n"
|
||||
f"- DokuWiki gibt Fehler/Warnings aus (prüfen Sie die DokuWiki-Logs)"
|
||||
)
|
||||
except Exception as e:
|
||||
_logger.error(f"Unerwarteter Fehler beim Erstellen der Seite {page_id}: {e}", exc_info=True)
|
||||
raise UserError(f"Unerwarteter Fehler: {e}")
|
||||
|
||||
@api.model
|
||||
def get_page(self, page_id):
|
||||
"""
|
||||
Liest den Inhalt einer DokuWiki-Seite.
|
||||
|
||||
Args:
|
||||
page_id (str): DokuWiki Page-ID
|
||||
|
||||
Returns:
|
||||
str: Wiki-Markup-Inhalt der Seite (leer wenn Seite nicht existiert)
|
||||
"""
|
||||
wiki = self._get_wiki_connection()
|
||||
|
||||
try:
|
||||
content = wiki.pages.get(page_id)
|
||||
return content or ""
|
||||
except DokuWikiError as e:
|
||||
_logger.warning(f"Fehler beim Lesen der Wiki-Seite {page_id}: {e}")
|
||||
return ""
|
||||
|
||||
@api.model
|
||||
def page_exists(self, page_id):
|
||||
"""
|
||||
Prüft ob eine DokuWiki-Seite existiert.
|
||||
|
||||
Args:
|
||||
page_id (str): DokuWiki Page-ID
|
||||
|
||||
Returns:
|
||||
bool: True wenn Seite existiert
|
||||
"""
|
||||
content = self.get_page(page_id)
|
||||
return bool(content.strip())
|
||||
|
||||
@api.model
|
||||
def delete_page(self, page_id, summary="Gelöscht von Odoo"):
|
||||
"""
|
||||
Löscht eine DokuWiki-Seite (setzt Inhalt auf leer).
|
||||
|
||||
Args:
|
||||
page_id (str): DokuWiki Page-ID
|
||||
summary (str): Änderungskommentar für die Wiki-History
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg
|
||||
"""
|
||||
wiki = self._get_wiki_connection()
|
||||
|
||||
try:
|
||||
wiki.pages.set(page_id, "", summary=summary)
|
||||
_logger.info(f"Wiki-Seite gelöscht: {page_id}")
|
||||
return True
|
||||
except DokuWikiError as e:
|
||||
_logger.error(f"Fehler beim Löschen der Wiki-Seite {page_id}: {e}")
|
||||
raise UserError(f"Fehler beim Löschen der Wiki-Seite: {e}")
|
||||
|
||||
@api.model
|
||||
def list_pages(self, namespace):
|
||||
"""
|
||||
Listet alle Seiten in einem DokuWiki-Namespace auf.
|
||||
|
||||
Args:
|
||||
namespace (str): DokuWiki-Namespace (z.B. "werkstatt:ausruestung")
|
||||
|
||||
Returns:
|
||||
list: Liste von Dictionaries mit Page-Informationen
|
||||
"""
|
||||
wiki = self._get_wiki_connection()
|
||||
|
||||
try:
|
||||
pages = wiki.pages.list(namespace)
|
||||
return pages
|
||||
except DokuWikiError as e:
|
||||
_logger.error(f"Fehler beim Auflisten des Namespace {namespace}: {e}")
|
||||
return []
|
||||
|
||||
@api.model
|
||||
def get_wiki_url(self, page_id):
|
||||
"""
|
||||
Generiert die vollständige URL zu einer DokuWiki-Seite.
|
||||
|
||||
Args:
|
||||
page_id (str): DokuWiki Page-ID
|
||||
|
||||
Returns:
|
||||
str: Vollständige URL zur Wiki-Seite
|
||||
"""
|
||||
IrConfigParameter = self.env['ir.config_parameter'].sudo()
|
||||
wiki_url = IrConfigParameter.get_param('dokuwiki.url', '')
|
||||
|
||||
if not wiki_url:
|
||||
return ""
|
||||
|
||||
# Entferne trailing slash falls vorhanden
|
||||
wiki_url = wiki_url.rstrip('/')
|
||||
|
||||
return f"{wiki_url}/doku.php?id={page_id}"
|
||||
|
||||
@api.model
|
||||
def upload_media(self, media_id, file_content, overwrite=True):
|
||||
"""
|
||||
Lädt eine Mediendatei (Bild) ins DokuWiki hoch.
|
||||
|
||||
Args:
|
||||
media_id (str): DokuWiki Media-ID (z.B. "werkstatt:ausruestung:equipment_3.jpg")
|
||||
file_content (bytes): Binärer Dateiinhalt
|
||||
overwrite (bool): Existierende Datei überschreiben
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg
|
||||
|
||||
Raises:
|
||||
UserError: Bei Fehler beim Upload
|
||||
"""
|
||||
wiki = self._get_wiki_connection()
|
||||
|
||||
import tempfile
|
||||
import os
|
||||
import base64
|
||||
import imghdr
|
||||
|
||||
tmp_path = None
|
||||
|
||||
# Unterstütze mehrere Eingabetypen:
|
||||
# - ir.attachment record (Recordset)
|
||||
# - attachment id (int)
|
||||
# - rohe Bytes
|
||||
# - base64-codierter String
|
||||
try:
|
||||
content_bytes = None
|
||||
ext = ''
|
||||
|
||||
# ir.attachment Record
|
||||
if hasattr(file_content, '_name') and getattr(file_content, '_name') == 'ir.attachment':
|
||||
att = file_content.sudo()
|
||||
if not att.datas:
|
||||
raise UserError(f"Attachment {att.id} enthält keine Daten")
|
||||
content_bytes = base64.b64decode(att.datas)
|
||||
fname = att.datas_fname or ''
|
||||
ext = os.path.splitext(fname)[1]
|
||||
|
||||
# attachment id
|
||||
elif isinstance(file_content, int):
|
||||
att = self.env['ir.attachment'].sudo().browse(file_content)
|
||||
if not att.exists() or not att.datas:
|
||||
raise UserError(f"Attachment {file_content} nicht gefunden oder leer")
|
||||
content_bytes = base64.b64decode(att.datas)
|
||||
fname = att.datas_fname or ''
|
||||
ext = os.path.splitext(fname)[1]
|
||||
|
||||
# base64 string
|
||||
elif isinstance(file_content, str):
|
||||
try:
|
||||
content_bytes = base64.b64decode(file_content)
|
||||
except Exception:
|
||||
# Fallback: treat as empty
|
||||
raise UserError("Übergebener String konnte nicht als base64 decodiert werden")
|
||||
ext = os.path.splitext(media_id)[1]
|
||||
|
||||
# rohe bytes
|
||||
else:
|
||||
# assume bytes-like
|
||||
content_bytes = file_content
|
||||
# try extension from media_id
|
||||
ext = os.path.splitext(media_id)[1]
|
||||
|
||||
# Wenn noch keine Extension, versuche Bildtyp zu erkennen
|
||||
if not ext:
|
||||
try:
|
||||
img_type = imghdr.what(None, content_bytes)
|
||||
if img_type:
|
||||
ext = f'.{img_type}'
|
||||
except Exception:
|
||||
ext = ''
|
||||
|
||||
if not ext:
|
||||
ext = '.bin'
|
||||
|
||||
# Sicherstellen, dass ext mit Punkt beginnt
|
||||
if not ext.startswith('.'):
|
||||
ext = f'.{ext}'
|
||||
|
||||
# Temporäre Datei erstellen (dokuwiki.py erwartet Datei-Pfad)
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix=ext) as tmp_file:
|
||||
tmp_file.write(content_bytes)
|
||||
tmp_path = tmp_file.name
|
||||
|
||||
try:
|
||||
wiki.medias.add(media_id, tmp_path, overwrite=overwrite)
|
||||
_logger.info(f"Mediendatei hochgeladen: {media_id} (tmp={tmp_path})")
|
||||
return True
|
||||
except DokuWikiError as e:
|
||||
error_msg = f"Fehler beim Hochladen der Mediendatei {media_id}: {e}"
|
||||
_logger.error(error_msg)
|
||||
raise UserError(error_msg)
|
||||
finally:
|
||||
# Temporäre Datei löschen
|
||||
if tmp_path and os.path.exists(tmp_path):
|
||||
try:
|
||||
os.unlink(tmp_path)
|
||||
except Exception:
|
||||
_logger.warning(f"Konnte temporäre Datei nicht löschen: {tmp_path}")
|
||||
|
||||
@api.model
|
||||
def get_media_url(self, media_id):
|
||||
"""
|
||||
Generiert die vollständige URL zu einer DokuWiki-Mediendatei.
|
||||
|
||||
Args:
|
||||
media_id (str): DokuWiki Media-ID
|
||||
|
||||
Returns:
|
||||
str: Vollständige URL zur Mediendatei
|
||||
"""
|
||||
IrConfigParameter = self.env['ir.config_parameter'].sudo()
|
||||
wiki_url = IrConfigParameter.get_param('dokuwiki.url', '')
|
||||
|
||||
if not wiki_url:
|
||||
return ""
|
||||
|
||||
# Entferne trailing slash falls vorhanden
|
||||
wiki_url = wiki_url.rstrip('/')
|
||||
|
||||
# Ersetze : durch / für Media-Pfad
|
||||
media_path = media_id.replace(':', '/')
|
||||
|
||||
return f"{wiki_url}/lib/exe/fetch.php?media={media_id}"
|
||||
974
open_workshop_dokuwiki/models/maintenance_equipment.py
Normal file
|
|
@ -0,0 +1,974 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import logging
|
||||
import re
|
||||
from datetime import datetime
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
# Cache für Wiki-Templates (wird beim Server-Neustart geleert)
|
||||
_template_cache = {}
|
||||
|
||||
class MaintenanceEquipment(models.Model):
|
||||
"""
|
||||
Erweitert das Maintenance Equipment Modell um DokuWiki-Integration.
|
||||
Diese Klasse ermöglicht die Synchronisation von Werkstatt-Equipment-Daten mit einem
|
||||
DokuWiki-System. Jedes Equipment erhält eine zentrale Dokumentationsseite sowie
|
||||
verschiedene Ansichtsseiten (nach Bereich, später auch nach Einsatzzweck).
|
||||
Hauptfunktionen:
|
||||
- Automatische Generierung von Wiki-Seiten aus Equipment-Daten
|
||||
- Template-basierte Wiki-Seiten-Erstellung (verwendet c_template aus DokuWiki)
|
||||
- Bild-Upload und Einbindung in Wiki-Seiten
|
||||
- Zentrale Dokumentationsseite pro Equipment (manuell erweiterbar)
|
||||
- Bereichs-spezifische Übersichtsseiten (automatisch aktualisiert)
|
||||
- Automatische Übersichtstabelle aller Equipment (DataTable mit Sortierung/Filterung)
|
||||
- Automatische Synchronisation bei Feldänderungen (optional)
|
||||
Wiki-Seitenstruktur:
|
||||
- {equipment_namespace}:{overview_page_id} - Übersichtstabelle (von Odoo generiert)
|
||||
- {equipment_namespace}:odoo-status:{wiki_doku_id} - Status-Seite (von Odoo generiert, nur lesbar)
|
||||
- {equipment_namespace}:odoo-status:c_template - Template für Status-Seiten
|
||||
- {equipment_namespace}:{bereich}:{wiki_doku_id} - Benutzer-Dokumentation (NICHT von Odoo erstellt!)
|
||||
Neue Felder:
|
||||
- image_1920: Bild des Equipment (wird automatisch ins Wiki hochgeladen)
|
||||
- wiki_doku_id: Eindeutige ID für Wiki-Dokumentation (aus Equipment-Name generiert)
|
||||
- wiki_page_url: Berechnete URL zur Wiki-Seite
|
||||
- wiki_synced: Status der Synchronisation
|
||||
- wiki_last_sync: Zeitstempel der letzten Synchronisation
|
||||
- wiki_auto_sync: Schalter für automatische Synchronisation bei Änderungen
|
||||
Template-System:
|
||||
Die Wiki-Seiten werden aus einem Template ({equipment_namespace}:odoo-status:c_template)
|
||||
generiert, das folgende Platzhalter unterstützt:
|
||||
- {feldname} - Direkte Equipment-Felder (z.B. {name}, {serial_no})
|
||||
- {ows_machine_id.feldname} - Maschinenfelder (z.B. {ows_machine_id.power})
|
||||
- {wiki_status_page} - ID der Odoo-Status-Seite
|
||||
- {wiki_status_link} - DokuWiki-Link zur Odoo-Status-Seite
|
||||
- {image} - Bild-Einbindung (wird automatisch hochgeladen)
|
||||
- {tags} - Komma-separierte Tag-Liste
|
||||
- {sync_datetime} - Zeitstempel der Synchronisation
|
||||
Übersichtstabelle:
|
||||
Generiert eine zentrale Übersichtstabelle aller Equipment mit konfigurierbaren
|
||||
Spalten. Die Tabelle verwendet das DokuWiki DataTable-Plugin für interaktive
|
||||
Sortierung und Filterung. Konfiguration über Systemparameter:
|
||||
- dokuwiki.overview_page_id - Seiten-ID der Übersicht
|
||||
- dokuwiki.overview_title - Titel der Seite
|
||||
- dokuwiki.overview_columns - Spaltenüberschriften (pipe-separiert)
|
||||
- dokuwiki.overview_column_data - Spaltendaten mit Platzhaltern
|
||||
Verwendung:
|
||||
1. Equipment anlegen/bearbeiten in Odoo
|
||||
2. Bereich (ows_area_id) setzen (erforderlich für Wiki-Sync)
|
||||
3. Optional: Bild hochladen (wird automatisch ins Wiki übertragen)
|
||||
4. "Zum Wiki synchronisieren" klicken (oder automatisch bei Änderungen)
|
||||
5. Wiki-Seiten werden erstellt/aktualisiert
|
||||
6. "Wiki-Seite öffnen" für direkten Browser-Zugriff
|
||||
Notes:
|
||||
- Zentrale Dokumentationsseite wird nur beim ersten Sync erstellt
|
||||
- Bereichsansichten werden bei jedem Sync aktualisiert
|
||||
- Bilder werden als {equipment_namespace}:{bereich}:{wiki_doku_id}.jpg gespeichert (parallele Struktur zu Pages)
|
||||
- Wiki-Namen werden normalisiert (Umlaute, Sonderzeichen, Kleinschreibung)
|
||||
- Bei fehlendem Template wird Fallback-Inhalt verwendet
|
||||
- Übersichtstabelle kann manuell oder automatisch aktualisiert werden
|
||||
"""
|
||||
_inherit = 'maintenance.equipment'
|
||||
|
||||
# Bild-Feld
|
||||
image_1920 = fields.Image(
|
||||
string='Bild',
|
||||
max_width=1920,
|
||||
max_height=1920,
|
||||
help='Equipment-Bild (wird beim Wiki-Sync hochgeladen)'
|
||||
)
|
||||
|
||||
# Wiki-Felder
|
||||
wiki_status_id = fields.Char(
|
||||
string='Wiki Status-ID',
|
||||
readonly=True,
|
||||
help='Eindeutige ID für die Odoo-Status-Seite (automatisch generiert aus Name + Seriennummer, z.B. "formatkreissaege-eq001")'
|
||||
)
|
||||
wiki_doku_id = fields.Char(
|
||||
string='Wiki Dokumentations-ID',
|
||||
readonly=False,
|
||||
help='ID für die Benutzer-Dokumentationsseite (editierbar, initial aus Equipment-Namen generiert, z.B. "formatkreissaege"). Mehrere Equipment können die gleiche ID nutzen.'
|
||||
)
|
||||
wiki_page_url = fields.Char(
|
||||
string='Wiki-Seiten URL',
|
||||
compute='_compute_wiki_page_url',
|
||||
help='URL zur Wiki-Seite im Browser'
|
||||
)
|
||||
wiki_synced = fields.Boolean(
|
||||
string='Wiki synchronisiert',
|
||||
default=False,
|
||||
help='Gibt an, ob die Wiki-Seite aktuell ist'
|
||||
)
|
||||
wiki_last_sync = fields.Datetime(
|
||||
string='Letzte Wiki-Synchronisation',
|
||||
readonly=True,
|
||||
help='Zeitpunkt der letzten erfolgreichen Wiki-Synchronisation'
|
||||
)
|
||||
wiki_auto_sync = fields.Boolean(
|
||||
string='Automatische Wiki-Synchronisation',
|
||||
default=True,
|
||||
help='Bei Änderungen automatisch zum Wiki synchronisieren'
|
||||
)
|
||||
wiki_doku_link = fields.Html(
|
||||
string='Wiki Doku',
|
||||
compute='_compute_wiki_doku_link',
|
||||
sanitize=False,
|
||||
help='Klickbarer Link zur Wiki-Dokumentationsseite'
|
||||
)
|
||||
wiki_status_link = fields.Html(
|
||||
string='Wiki Status',
|
||||
compute='_compute_wiki_status_link',
|
||||
sanitize=False,
|
||||
help='Klickbarer Link zur Wiki-Status-Seite (Odoo-Daten)'
|
||||
)
|
||||
|
||||
@api.depends('wiki_doku_id', 'wiki_page_url')
|
||||
def _compute_wiki_doku_link(self):
|
||||
"""
|
||||
Generiert einen HTML-Link mit wiki_doku_id als Text und wiki_page_url als Ziel.
|
||||
"""
|
||||
for record in self:
|
||||
if record.wiki_doku_id and record.wiki_page_url:
|
||||
record.wiki_doku_link = f'<a href="{record.wiki_page_url}" target="_blank" style="color: #007bff;">{record.wiki_doku_id}</a>'
|
||||
else:
|
||||
record.wiki_doku_link = ''
|
||||
|
||||
@api.depends('wiki_status_id', 'name', 'comp_serial_no', 'ows_area_id')
|
||||
def _compute_wiki_status_link(self):
|
||||
"""
|
||||
Generiert einen HTML-Link mit wiki_status_id als Text zur Odoo-Status-Seite.
|
||||
"""
|
||||
dokuwiki_client = self.env['dokuwiki.client']
|
||||
for record in self:
|
||||
# wiki_status_id dynamisch berechnen falls noch nicht gesetzt
|
||||
wiki_status_id = record.wiki_status_id or record._get_wiki_status_id()
|
||||
if wiki_status_id and record.ows_area_id:
|
||||
status_page_id = record._get_wiki_status_page_id()
|
||||
if status_page_id:
|
||||
status_url = dokuwiki_client.get_wiki_url(status_page_id)
|
||||
record.wiki_status_link = f'<a href="{status_url}" target="_blank" style="color: #007bff;">{wiki_status_id}</a>'
|
||||
else:
|
||||
record.wiki_status_link = ''
|
||||
else:
|
||||
record.wiki_status_link = ''
|
||||
|
||||
@api.depends('ows_area_id')
|
||||
def _compute_wiki_page_url(self):
|
||||
"""
|
||||
Berechnet die URL zur Haupt-Wiki-Seite (nach Bereich).
|
||||
"""
|
||||
dokuwiki_client = self.env['dokuwiki.client']
|
||||
|
||||
for record in self:
|
||||
wiki_doku_id = record._get_wiki_doku_id()
|
||||
if wiki_doku_id and record.ows_area_id:
|
||||
page_id = record._get_wiki_page_id_by_area()
|
||||
record.wiki_page_url = dokuwiki_client.get_wiki_url(page_id)
|
||||
else:
|
||||
record.wiki_page_url = False
|
||||
|
||||
def _get_wiki_status_id(self):
|
||||
"""
|
||||
Generiert die Wiki-Status-ID aus Equipment-Namen und comp_serial_no.
|
||||
Format: normalisierter_name-comp_serial_no (z.B. "formatkreissaege-eq001")
|
||||
|
||||
Diese ID ist eindeutig und wird für die Odoo-Status-Seite und Media-Dateien verwendet.
|
||||
Die comp_serial_no wird automatisch generiert falls noch nicht vorhanden.
|
||||
|
||||
Returns:
|
||||
str: Wiki-Status-ID oder False wenn kein Name vorhanden
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
# Verwende wiki_status_id falls bereits gesetzt
|
||||
if self.wiki_status_id:
|
||||
return self.wiki_status_id
|
||||
|
||||
# Name ist erforderlich
|
||||
if not self.name:
|
||||
return False
|
||||
|
||||
# comp_serial_no automatisch generieren falls nicht vorhanden
|
||||
if not self.comp_serial_no:
|
||||
self.generate_serial_no()
|
||||
# Refresh nach generate_serial_no
|
||||
self.refresh()
|
||||
|
||||
# wiki_status_id aus Name + comp_serial_no generieren
|
||||
normalized_name = self._normalize_wiki_name(self.name)
|
||||
normalized_serial = self._normalize_wiki_name(self.comp_serial_no)
|
||||
return f"{normalized_name}-{normalized_serial}"
|
||||
|
||||
def _get_wiki_doku_id(self):
|
||||
"""
|
||||
Generiert die Wiki-Dokumentations-ID aus Equipment-Namen (ohne Seriennummer).
|
||||
Format: normalisierter_name (z.B. "formatkreissaege")
|
||||
|
||||
Diese ID ist editierbar und wird für die Benutzer-Dokumentationsseite verwendet.
|
||||
Mehrere Equipment können die gleiche wiki_doku_id haben (z.B. alle Akkuschrauber).
|
||||
|
||||
Returns:
|
||||
str: Wiki-Doku-ID oder False wenn kein Name vorhanden
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
# Verwende wiki_doku_id falls bereits gesetzt
|
||||
if self.wiki_doku_id:
|
||||
return self.wiki_doku_id
|
||||
|
||||
# Name ist erforderlich
|
||||
if not self.name:
|
||||
return False
|
||||
|
||||
# wiki_doku_id nur aus Name generieren (Duplikate sind erwünscht)
|
||||
normalized_name = self._normalize_wiki_name(self.name)
|
||||
return normalized_name
|
||||
|
||||
def _get_wiki_page_id_by_area(self):
|
||||
"""
|
||||
Generiert die Wiki-Page-ID für die Benutzer-Dokumentationsseite (nach Bereich).
|
||||
Format: {equipment_namespace}:{area_name}:{wiki_doku_id}
|
||||
|
||||
WICHTIG: Diese Seite wird NICHT von Odoo erstellt! Sie wird nur in der Übersichtstabelle
|
||||
verlinkt. Benutzer erstellen sie manuell durch Klick auf den Link.
|
||||
|
||||
Returns:
|
||||
str: Page-ID für Benutzer-Dokumentation
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
if not self.ows_area_id:
|
||||
return False
|
||||
|
||||
# Equipment-Namespace aus Systemparameter laden
|
||||
IrConfigParameter = self.env['ir.config_parameter'].sudo()
|
||||
equipment_namespace = IrConfigParameter.get_param(
|
||||
'dokuwiki.equipment_namespace',
|
||||
default='werkstatt:ausstattung'
|
||||
)
|
||||
|
||||
# Area-Name normalisieren (Umlaute, Leerzeichen, Sonderzeichen)
|
||||
area_name = self._normalize_wiki_name(self.ows_area_id.name)
|
||||
|
||||
wiki_doku_id = self._get_wiki_doku_id()
|
||||
return f"{equipment_namespace}:{area_name}:{wiki_doku_id}"
|
||||
|
||||
def _get_wiki_status_page_id(self):
|
||||
"""
|
||||
Generiert die Wiki-Page-ID für die Odoo-Status-Seite (nur lesbar für Benutzer).
|
||||
Format: {equipment_namespace}:odoo-status:{wiki_status_id}
|
||||
Diese Seite wird von Odoo generiert und kann mit dem include-Plugin eingebunden werden.
|
||||
|
||||
Returns:
|
||||
str: Page-ID der Odoo-Status-Seite
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
# Namespace aus Systemparameter laden (verwendet jetzt central_documentation_namespace)
|
||||
IrConfigParameter = self.env['ir.config_parameter'].sudo()
|
||||
status_namespace = IrConfigParameter.get_param(
|
||||
'dokuwiki.central_documentation_namespace',
|
||||
default='werkstatt:ausstattung:odoo-status'
|
||||
)
|
||||
|
||||
wiki_status_id = self._get_wiki_status_id()
|
||||
return f"{status_namespace}:{wiki_status_id}"
|
||||
|
||||
def _normalize_wiki_name(self, name):
|
||||
"""
|
||||
Normalisiert einen Namen für DokuWiki-Page-IDs.
|
||||
- Umlaute ersetzen
|
||||
- Leerzeichen und Sonderzeichen durch Bindestriche ersetzen
|
||||
- Kleinbuchstaben
|
||||
|
||||
Args:
|
||||
name (str): Original-Name
|
||||
|
||||
Returns:
|
||||
str: Normalisierter Name
|
||||
"""
|
||||
if not name:
|
||||
return "unnamed"
|
||||
|
||||
# Umlaute ersetzen
|
||||
replacements = {
|
||||
'ä': 'ae', 'ö': 'oe', 'ü': 'ue',
|
||||
'Ä': 'ae', 'Ö': 'oe', 'Ü': 'ue',
|
||||
'ß': 'ss'
|
||||
}
|
||||
for old, new in replacements.items():
|
||||
name = name.replace(old, new)
|
||||
|
||||
# Nur alphanumerische Zeichen und Bindestriche
|
||||
name = re.sub(r'[^a-zA-Z0-9\-_]', '-', name)
|
||||
|
||||
# Mehrfache Bindestriche durch einen ersetzen
|
||||
name = re.sub(r'-+', '-', name)
|
||||
|
||||
# Führende/Trailing Bindestriche entfernen
|
||||
name = name.strip('-')
|
||||
|
||||
# Kleinbuchstaben
|
||||
name = name.lower()
|
||||
|
||||
return name or "unnamed"
|
||||
|
||||
def _render_template_from_wiki(self, view_type='area'):
|
||||
"""
|
||||
Lädt c_template aus DokuWiki und ersetzt Platzhalter mit Odoo-Feldwerten.
|
||||
Template-Pfad wird dynamisch aus dem Systemparameter 'dokuwiki.central_documentation_namespace' generiert.
|
||||
Standard: werkstatt:ausstattung:odoo-status:c_template
|
||||
|
||||
Platzhalter-Format:
|
||||
- {feldname} für maintenance.equipment Felder, z.B. {name}, {serial_no}
|
||||
- {ows_machine_id.feldname} für ows.machine Felder, z.B. {ows_machine_id.power}
|
||||
- {wiki_status_page} für die Odoo-Status-Seite ID
|
||||
- {wiki_status_link} für Link zur Odoo-Status-Seite
|
||||
- {ows_area} für Bereichsname
|
||||
- {category} für Kategoriename
|
||||
- {sync_datetime} für aktuelles Datum/Zeit
|
||||
|
||||
Args:
|
||||
view_type (str): 'area' oder 'purpose'
|
||||
|
||||
Returns:
|
||||
str: Gerenderter Wiki-Markup-Inhalt
|
||||
"""
|
||||
self.ensure_one()
|
||||
global _template_cache
|
||||
dokuwiki_client = self.env['dokuwiki.client']
|
||||
|
||||
# Template-Pfad aus Systemparameter generieren
|
||||
IrConfigParameter = self.env['ir.config_parameter'].sudo()
|
||||
status_namespace = IrConfigParameter.get_param(
|
||||
'dokuwiki.central_documentation_namespace',
|
||||
default='werkstatt:ausstattung:odoo-status'
|
||||
)
|
||||
template_page_id = f"{status_namespace}:c_template"
|
||||
|
||||
try:
|
||||
# Template aus Cache oder Wiki laden
|
||||
if template_page_id not in _template_cache:
|
||||
template_content = dokuwiki_client.get_page(template_page_id)
|
||||
if not template_content:
|
||||
_logger.warning(f"Template {template_page_id} nicht gefunden, verwende Fallback")
|
||||
return self._generate_wiki_main_page_content_fallback(view_type)
|
||||
|
||||
_template_cache[template_page_id] = template_content
|
||||
_logger.info(f"Template geladen und gecacht: {template_page_id} ({len(template_content)} Zeichen)")
|
||||
else:
|
||||
_logger.debug(f"Template aus Cache verwendet: {template_page_id}")
|
||||
|
||||
template_content = _template_cache[template_page_id]
|
||||
|
||||
# Werte-Dictionary vorbereiten
|
||||
values = self._prepare_template_values(view_type)
|
||||
|
||||
# Debug-Logging für partner_ref
|
||||
if 'partner_ref' in values:
|
||||
_logger.info(f"partner_ref Wert für {self.name}: '{values['partner_ref']}'")
|
||||
|
||||
# Platzhalter ersetzen
|
||||
rendered_content = template_content
|
||||
for key, value in values.items():
|
||||
placeholder = '{' + key + '}'
|
||||
rendered_content = rendered_content.replace(placeholder, str(value or ''))
|
||||
|
||||
return rendered_content
|
||||
|
||||
except Exception as e:
|
||||
_logger.warning(f"Fehler beim Laden des Templates {template_page_id}: {e}")
|
||||
return self._generate_wiki_main_page_content_fallback(view_type)
|
||||
|
||||
def _prepare_template_values(self, view_type='area'):
|
||||
"""
|
||||
Bereitet Dictionary mit allen verfügbaren Feldwerten für Template-Rendering vor.
|
||||
|
||||
Args:
|
||||
view_type (str): 'area' oder 'purpose'
|
||||
|
||||
Returns:
|
||||
dict: Dictionary mit Feldnamen als Keys und Werten
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
# Odoo-Status-Seite
|
||||
status_page_id = self._get_wiki_status_page_id()
|
||||
|
||||
# Odoo Base URL
|
||||
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url', 'http://localhost:8069')
|
||||
odoo_equipment_url = f"{base_url}/web#id={self.id}&model=maintenance.equipment&view_type=form"
|
||||
|
||||
# Wiki Page IDs
|
||||
wiki_page_id = self._get_wiki_page_id_by_area() if self.ows_area_id else ''
|
||||
|
||||
# Basis-Werte für maintenance.equipment
|
||||
values = {
|
||||
# Spezielle Werte
|
||||
'wiki_status_page': status_page_id,
|
||||
'wiki_status_link': f"[[{status_page_id}|📊 Odoo Status]]",
|
||||
'wiki_page_id': wiki_page_id, # Page-ID der Hauptseite (nach Bereich)
|
||||
'wiki_page_url': self.wiki_page_url or '', # Externe URL zur Hauptseite
|
||||
'odoo_link': f"[[{odoo_equipment_url}|🔗 In Odoo öffnen]]",
|
||||
'odoo_url': odoo_equipment_url,
|
||||
'sync_datetime': datetime.now().strftime('%d.%m.%Y %H:%M'),
|
||||
|
||||
# Standard Equipment-Felder
|
||||
'name': self.name or '',
|
||||
'serial_no': self.serial_no or '',
|
||||
'model': self.model or '',
|
||||
'ows_area': self.ows_area_id.name if self.ows_area_id else '',
|
||||
'category': self.category_id.name if self.category_id else '',
|
||||
'status': self.status_id.name if self.status_id else '',
|
||||
'status_smiley': self.status_id.smiley if self.status_id and self.status_id.smiley else '',
|
||||
'location': self.location or '',
|
||||
'assign_date': self.assign_date.strftime('%d.%m.%Y') if self.assign_date else '',
|
||||
'cost': str(self.cost) if self.cost else '',
|
||||
'warranty_date': self.warranty_date.strftime('%d.%m.%Y') if self.warranty_date else '',
|
||||
'color': str(self.color) if self.color else '',
|
||||
'note': self.note or '',
|
||||
'partner_id': self.partner_id.name if self.partner_id else '',
|
||||
'partner_ref': self.partner_ref or '',
|
||||
}
|
||||
|
||||
# Tags hinzufügen (falls vorhanden)
|
||||
if hasattr(self, 'tag_ids') and self.tag_ids:
|
||||
# Komma-separierte Liste für Template
|
||||
values['tags'] = ', '.join(self.tag_ids.mapped('name'))
|
||||
# DokuWiki Bullet-Liste für Tabellenzellen
|
||||
values['tags_list'] = ' '.join(self.tag_ids.mapped('name')) if self.tag_ids else ''
|
||||
# Anzahl der Tags
|
||||
values['tags_count'] = str(len(self.tag_ids))
|
||||
else:
|
||||
values['tags'] = ''
|
||||
values['tags_list'] = ''
|
||||
values['tags_count'] = '0'
|
||||
|
||||
# ows.machine Felder hinzufügen (falls verknüpft)
|
||||
if self.ows_machine_id:
|
||||
machine = self.ows_machine_id
|
||||
# Nur existierende Felder hinzufügen
|
||||
ows_machine_fields = {}
|
||||
|
||||
# Standard-Felder von ows.machine
|
||||
if hasattr(machine, 'name') and machine.name:
|
||||
ows_machine_fields['ows_machine_id.name'] = machine.name
|
||||
if hasattr(machine, 'model') and machine.model:
|
||||
ows_machine_fields['ows_machine_id.model'] = machine.model
|
||||
if hasattr(machine, 'serial_no') and machine.serial_no:
|
||||
ows_machine_fields['ows_machine_id.serial_no'] = machine.serial_no
|
||||
if hasattr(machine, 'location') and machine.location:
|
||||
ows_machine_fields['ows_machine_id.location'] = machine.location
|
||||
if hasattr(machine, 'note'):
|
||||
ows_machine_fields['ows_machine_id.note'] = machine.note or ''
|
||||
if hasattr(machine, 'ows_category') and machine.ows_category:
|
||||
ows_machine_fields['ows_machine_id.category'] = machine.ows_category
|
||||
if hasattr(machine, 'ows_category_icon') and machine.ows_category_icon:
|
||||
ows_machine_fields['ows_machine_id.category_icon'] = machine.ows_category_icon
|
||||
|
||||
values.update(ows_machine_fields)
|
||||
|
||||
# Bild-Upload und Referenz (falls vorhanden)
|
||||
if self.image_1920:
|
||||
wiki_status_id = self._get_wiki_status_id()
|
||||
# Media-ID mit gleicher Namespace-Struktur wie Pages (DokuWiki Best Practice)
|
||||
IrConfigParameter = self.env['ir.config_parameter'].sudo()
|
||||
equipment_namespace = IrConfigParameter.get_param(
|
||||
'dokuwiki.equipment_namespace',
|
||||
default='werkstatt:ausstattung'
|
||||
)
|
||||
# Area-Name normalisieren (wie bei Pages)
|
||||
area_name = self._normalize_wiki_name(self.ows_area_id.name) if self.ows_area_id else 'unbekannt'
|
||||
# Bild ins Wiki hochladen (erkenne Extension aus Bytes)
|
||||
try:
|
||||
import base64
|
||||
import imghdr
|
||||
|
||||
# image_1920 ist base64-kodiert, in bytes umwandeln für XML-RPC
|
||||
image_bytes = base64.b64decode(self.image_1920)
|
||||
|
||||
# Typ/Extension erkennen (jpeg -> .jpg)
|
||||
img_type = None
|
||||
try:
|
||||
img_type = imghdr.what(None, image_bytes)
|
||||
except Exception:
|
||||
img_type = None
|
||||
|
||||
if img_type == 'jpeg':
|
||||
ext = '.jpg'
|
||||
elif img_type:
|
||||
ext = f'.{img_type}'
|
||||
elif image_bytes.startswith(b'%PDF'):
|
||||
ext = '.pdf'
|
||||
else:
|
||||
ext = '.bin'
|
||||
|
||||
media_id = f"{equipment_namespace}:{area_name}:{wiki_status_id}{ext}"
|
||||
|
||||
dokuwiki_client = self.env['dokuwiki.client']
|
||||
dokuwiki_client.upload_media(media_id, image_bytes, overwrite=True)
|
||||
|
||||
# DokuWiki Image-Syntax: {{namespace:file.jpg?300}}
|
||||
values['image'] = f"{{{{:{media_id}?100}}}}"
|
||||
values['image_large'] = f"{{{{:{media_id}}}}}"
|
||||
values['image_id'] = media_id
|
||||
_logger.info(f"Bild hochgeladen: {media_id}")
|
||||
except Exception as e:
|
||||
_logger.error(f"Fehler beim Bild-Upload: {e}", exc_info=True)
|
||||
values['image'] = ''
|
||||
values['image_large'] = ''
|
||||
values['image_id'] = ''
|
||||
else:
|
||||
values['image'] = ''
|
||||
values['image_large'] = ''
|
||||
values['image_id'] = ''
|
||||
|
||||
# View-Typ spezifische Werte
|
||||
if view_type == 'area':
|
||||
values['view_type'] = 'Bereich'
|
||||
values['view_name'] = self.ows_area_id.name if self.ows_area_id else 'Unbekannt'
|
||||
else:
|
||||
values['view_type'] = 'Einsatzzweck'
|
||||
values['view_name'] = 'TODO: Einsatzzweck'
|
||||
|
||||
return values
|
||||
|
||||
def _generate_wiki_main_page_content_fallback(self, view_type='area'):
|
||||
"""
|
||||
Fallback-Methode: Generiert hart-codierten Wiki-Markup-Inhalt,
|
||||
wenn c_template.txt nicht verfügbar ist.
|
||||
Dieser Content wird in odoo-status/ gespeichert und ist nur lesbar.
|
||||
|
||||
Args:
|
||||
view_type (str): 'area' oder 'purpose'
|
||||
|
||||
Returns:
|
||||
str: Wiki-Markup-Inhalt für odoo-status Seite
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
# Header je nach View-Typ
|
||||
if view_type == 'area':
|
||||
view_name = self.ows_area_id.name if self.ows_area_id else "Unbekannt"
|
||||
view_label = "Bereich"
|
||||
else:
|
||||
# Später für Einsatzzweck
|
||||
view_name = "TODO: Einsatzzweck"
|
||||
view_label = "Einsatzzweck"
|
||||
|
||||
content = f"""====== {self.name} - Odoo Status ======
|
||||
|
||||
**{view_label}:** {view_name}
|
||||
**Kategorie:** {self.category_id.name if self.category_id else 'Keine'}
|
||||
**Seriennummer:** {self.serial_no or 'Keine'}
|
||||
**Modell:** {self.model or 'Keine'}
|
||||
**Status:** {self.status_id.name if self.status_id else 'Unbekannt'} {self.status_id.smiley if self.status_id and self.status_id.smiley else ''}
|
||||
|
||||
===== Technische Daten =====
|
||||
|
||||
**Hersteller:** {self.partner_id.name if self.partner_id else 'Unbekannt'}
|
||||
**Standort:** {self.location or 'Nicht angegeben'}
|
||||
**Kosten:** {self.cost if self.cost else 'Nicht angegeben'}
|
||||
**Garantie bis:** {self.warranty_date.strftime('%d.%m.%Y') if self.warranty_date else 'Keine'}
|
||||
|
||||
===== Notizen =====
|
||||
|
||||
{self.note or 'Keine Notizen'}
|
||||
|
||||
----
|
||||
//Diese Seite wird automatisch von Odoo generiert und ist nur lesbar.//
|
||||
//Letzte Synchronisation: {datetime.now().strftime('%d.%m.%Y %H:%M')} //
|
||||
"""
|
||||
return content
|
||||
|
||||
def _generate_wiki_main_page_content(self, view_type='area'):
|
||||
"""
|
||||
Generiert den Wiki-Markup-Inhalt für die Odoo-Status-Seite.
|
||||
Verwendet c_template.txt aus DokuWiki falls verfügbar, sonst Fallback.
|
||||
Diese Seite wird in odoo-status/ gespeichert und ist nur lesbar.
|
||||
|
||||
Args:
|
||||
view_type (str): 'area' oder 'purpose'
|
||||
|
||||
Returns:
|
||||
str: Wiki-Markup-Inhalt für odoo-status Seite
|
||||
"""
|
||||
return self._render_template_from_wiki(view_type)
|
||||
|
||||
def _generate_wiki_doku_page_content(self):
|
||||
"""
|
||||
DEPRECATED: Diese Methode wird nicht mehr verwendet.
|
||||
Odoo erstellt keine Benutzer-Dokumentationsseiten mehr.
|
||||
Benutzer erstellen diese Seiten manuell durch Klick auf den Link in der Übersichtstabelle.
|
||||
"""
|
||||
_logger.warning("_generate_wiki_doku_page_content() wurde aufgerufen - diese Methode ist deprecated!")
|
||||
return ""
|
||||
|
||||
def sync_to_dokuwiki(self):
|
||||
"""
|
||||
Synchronisiert Equipment-Daten zum DokuWiki.
|
||||
|
||||
WICHTIG: Erstellt NUR die Odoo-Status-Seite in odoo-status/ Namespace!
|
||||
Benutzer-Dokumentationsseiten werden NICHT erstellt - diese erstellen Benutzer
|
||||
manuell durch Klick auf den Link in der Übersichtstabelle.
|
||||
|
||||
Erstellt/aktualisiert:
|
||||
- wiki_doku_id beim ersten Sync (falls noch nicht gesetzt)
|
||||
- Odoo-Status-Seite (odoo-status:wiki_doku_id) - wird immer aktualisiert
|
||||
|
||||
Returns:
|
||||
bool: True bei Erfolg
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
if not self.ows_area_id:
|
||||
raise UserError("Bereich muss gesetzt sein für Wiki-Synchronisation!")
|
||||
|
||||
if not self.name:
|
||||
raise UserError("Equipment-Name muss gesetzt sein für Wiki-Synchronisation!")
|
||||
|
||||
# wiki_status_id beim ersten Sync setzen (aus Equipment-Namen und comp_serial_no generieren)
|
||||
if not self.wiki_status_id:
|
||||
generated_status_id = self._get_wiki_status_id()
|
||||
if not generated_status_id:
|
||||
raise UserError("wiki_status_id konnte nicht generiert werden!")
|
||||
self.write({'wiki_status_id': generated_status_id})
|
||||
_logger.info(f"Wiki-Status-ID für {self.name} gesetzt: {generated_status_id}")
|
||||
|
||||
# wiki_doku_id beim ersten Sync setzen (aus Equipment-Namen generieren, editierbar)
|
||||
if not self.wiki_doku_id:
|
||||
generated_doku_id = self._get_wiki_doku_id()
|
||||
if not generated_doku_id:
|
||||
generated_doku_id = self._normalize_wiki_name(self.name)
|
||||
self.write({'wiki_doku_id': generated_doku_id})
|
||||
_logger.info(f"Wiki-Doku-ID für {self.name} gesetzt: {generated_doku_id}")
|
||||
|
||||
dokuwiki_client = self.env['dokuwiki.client']
|
||||
|
||||
try:
|
||||
# Odoo-Status-Seite erstellen/aktualisieren (immer beim Sync)
|
||||
status_page_id = self._get_wiki_status_page_id()
|
||||
status_content = self._generate_wiki_main_page_content(view_type='area')
|
||||
|
||||
dokuwiki_client.create_page(
|
||||
status_page_id,
|
||||
status_content,
|
||||
f"Synchronisiert von Odoo: {self.name}"
|
||||
)
|
||||
_logger.info(f"Odoo-Status-Seite für {self.name} aktualisiert: {status_page_id}")
|
||||
|
||||
# WICHTIG: Benutzer-Dokumentationsseiten werden NICHT erstellt!
|
||||
# Diese werden nur in der Übersichtstabelle verlinkt und von Benutzern
|
||||
# manuell erstellt durch Klick auf den roten Link.
|
||||
|
||||
# Sync-Status aktualisieren
|
||||
self.write({
|
||||
'wiki_synced': True,
|
||||
'wiki_last_sync': fields.Datetime.now(),
|
||||
})
|
||||
|
||||
_logger.info(f"Equipment {self.name} erfolgreich zu DokuWiki synchronisiert")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
_logger.error(f"Fehler bei Wiki-Synchronisation für {self.name}: {e}")
|
||||
self.write({'wiki_synced': False})
|
||||
raise UserError(f"Wiki-Synchronisation fehlgeschlagen: {e}")
|
||||
|
||||
def action_sync_to_dokuwiki(self):
|
||||
"""
|
||||
Button-Action für manuelle Wiki-Synchronisation.
|
||||
"""
|
||||
for record in self:
|
||||
record.sync_to_dokuwiki()
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': _('Wiki-Synchronisation'),
|
||||
'message': _('Equipment wurde erfolgreich zum Wiki synchronisiert.'),
|
||||
'type': 'success',
|
||||
'sticky': False,
|
||||
}
|
||||
}
|
||||
|
||||
def action_open_wiki(self):
|
||||
"""
|
||||
Button-Action zum Öffnen der Wiki-Seite im Browser.
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
if not self.wiki_page_url:
|
||||
raise UserError("Wiki-URL nicht verfügbar. Bitte erst zum Wiki synchronisieren.")
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.act_url',
|
||||
'url': self.wiki_page_url,
|
||||
'target': 'new',
|
||||
}
|
||||
|
||||
def write(self, vals):
|
||||
"""
|
||||
Override write um automatische Synchronisation zu triggern.
|
||||
"""
|
||||
result = super().write(vals)
|
||||
|
||||
# Felder die eine Synchronisation auslösen
|
||||
sync_fields = {'name', 'serial_no', 'ows_area_id', 'category_id', 'status_id',
|
||||
'model', 'partner_id', 'partner_ref', 'location', 'note', 'image_1920', 'tag_ids'}
|
||||
|
||||
# Flag ob Übersichtstabelle aktualisiert werden soll
|
||||
should_update_overview = False
|
||||
|
||||
if sync_fields & set(vals.keys()):
|
||||
# Nur synchronisieren wenn auto_sync aktiviert und bereits synced
|
||||
for record in self:
|
||||
if record.wiki_auto_sync and record.wiki_synced:
|
||||
try:
|
||||
record.sync_to_dokuwiki()
|
||||
should_update_overview = True
|
||||
except Exception as e:
|
||||
_logger.warning(f"Automatische Wiki-Sync fehlgeschlagen: {e}")
|
||||
# Nicht abbrechen, nur loggen
|
||||
else:
|
||||
# Sync-Status zurücksetzen
|
||||
record.write({'wiki_synced': False})
|
||||
|
||||
# Optional: Übersichtstabelle aktualisieren
|
||||
if should_update_overview:
|
||||
IrConfigParameter = self.env['ir.config_parameter'].sudo()
|
||||
auto_update_overview = IrConfigParameter.get_param(
|
||||
'dokuwiki.auto_update_overview_table',
|
||||
default='False'
|
||||
)
|
||||
|
||||
if auto_update_overview.lower() == 'true':
|
||||
try:
|
||||
self.env['maintenance.equipment'].action_sync_overview_table()
|
||||
_logger.info("Übersichtstabelle automatisch aktualisiert")
|
||||
except Exception as e:
|
||||
_logger.warning(f"Automatische Übersichtstabellen-Sync fehlgeschlagen: {e}")
|
||||
|
||||
return result
|
||||
|
||||
# ==========================================
|
||||
# Übersichtstabelle (DataTable)
|
||||
# ==========================================
|
||||
|
||||
def _generate_overview_table_row(self, column_data_template):
|
||||
"""
|
||||
Generiert eine DokuWiki DataTable Zeile für dieses Equipment mit konfigurierbaren Spalten.
|
||||
|
||||
Args:
|
||||
column_data_template (str): Template mit Platzhaltern, z.B. "{status_smiley}|{partner_id}|{model}"
|
||||
|
||||
Returns:
|
||||
str: Wiki-Markup für eine Tabellenzeile
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
# Template-Werte vorbereiten (nutzt bestehende Platzhalter-Engine)
|
||||
values = self._prepare_template_values(view_type='area')
|
||||
|
||||
# Platzhalter im Template ersetzen
|
||||
rendered_data = column_data_template
|
||||
for key, value in values.items():
|
||||
placeholder = '{' + key + '}'
|
||||
rendered_data = rendered_data.replace(placeholder, str(value or '-'))
|
||||
|
||||
# Pipe-separierte Werte in DokuWiki-Tabellenzeile umwandeln
|
||||
columns = rendered_data.split('|')
|
||||
row = '| ' + ' | '.join(columns) + ' |'
|
||||
|
||||
return row
|
||||
|
||||
@api.model
|
||||
def action_sync_overview_table(self):
|
||||
"""
|
||||
Aktualisiert die Übersichtstabelle in DokuWiki mit allen Equipment-Einträgen.
|
||||
Verwendet konfigurierbare Spalten aus Systemparametern (kein Template mehr nötig).
|
||||
|
||||
Returns:
|
||||
dict: Ergebnis-Dictionary mit 'total', 'success', 'error_messages'
|
||||
"""
|
||||
IrConfigParameter = self.env['ir.config_parameter'].sudo()
|
||||
dokuwiki_client = self.env['dokuwiki.client']
|
||||
|
||||
# Config Parameter (Defaults werden via _register_hook automatisch gesetzt)
|
||||
overview_page_id = IrConfigParameter.get_param('dokuwiki.overview_page_id')
|
||||
overview_title = IrConfigParameter.get_param('dokuwiki.overview_title')
|
||||
overview_columns = IrConfigParameter.get_param('dokuwiki.overview_columns')
|
||||
overview_column_data = IrConfigParameter.get_param('dokuwiki.overview_column_data')
|
||||
|
||||
# Validierung
|
||||
if not all([overview_page_id, overview_title, overview_columns, overview_column_data]):
|
||||
raise UserError("DokuWiki Übersichtstabellen-Parameter nicht konfiguriert! Bitte Modul neu installieren.")
|
||||
|
||||
try:
|
||||
# Alle Equipment mit ows_area_id laden (sortiert nach Bereich, dann Name)
|
||||
equipment_records = self.search([
|
||||
('ows_area_id', '!=', False)
|
||||
], order='ows_area_id, name')
|
||||
|
||||
if not equipment_records:
|
||||
raise UserError("Keine Equipment-Einträge mit Bereich gefunden!")
|
||||
|
||||
# Tabellenzeilen generieren
|
||||
table_rows = []
|
||||
for equipment in equipment_records:
|
||||
try:
|
||||
row = equipment._generate_overview_table_row(overview_column_data)
|
||||
table_rows.append(row)
|
||||
except Exception as e:
|
||||
_logger.warning(f"Fehler beim Generieren der Zeile für {equipment.name}: {e}")
|
||||
continue
|
||||
|
||||
# Rows zusammenfügen
|
||||
table_rows_markup = '\n'.join(table_rows)
|
||||
|
||||
# Spalten-Header generieren (aus overview_columns)
|
||||
column_headers = overview_columns.split('|')
|
||||
header_row = '^ ' + ' ^ '.join(column_headers) + ' ^'
|
||||
|
||||
# Sync-Zeitstempel
|
||||
sync_datetime = datetime.now().strftime('%d.%m.%Y %H:%M:%S')
|
||||
|
||||
# Komplette Seite zusammenbauen (ohne Template)
|
||||
overview_content = f"""====== {overview_title} ======
|
||||
|
||||
Diese Seite wird automatisch aktualisiert.
|
||||
|
||||
**Letztes Update:** {sync_datetime}
|
||||
|
||||
<datatable Equipment-Übersicht>
|
||||
{header_row}
|
||||
{table_rows_markup}
|
||||
</datatable>
|
||||
|
||||
----
|
||||
|
||||
**Hinweis:** Diese Tabelle ist interaktiv:
|
||||
* Klicke auf die Spaltenköpfe zum Sortieren
|
||||
* Nutze das Suchfeld zum Filtern
|
||||
* Klicke auf die Links in der Spalte "Name" für Details
|
||||
|
||||
**Status:** 🙂 Maschine/Gerät in gutem Zustand, 🙁 - Maschine/Gerät ist Defekt, ❌ Maschine/Gerät wurde ausgemustert
|
||||
|
||||
**Sicherheit:** 🟢 **keine** Einweisungspflicht, 🟡 keine **explizite** Einweisungspflicht, 🔴 explizite Einweisungspflicht
|
||||
|
||||
(Für Einweisungstermine bitte den Thekendienst befragen oder trage dich [[https://hobbyhimmel.de/so-gehts/einweisungen/|hier]] im Kalender für einen Termin ein)
|
||||
|
||||
|
||||
"""
|
||||
|
||||
# Seite in DokuWiki speichern
|
||||
dokuwiki_client.create_page(
|
||||
overview_page_id,
|
||||
overview_content,
|
||||
f"Automatisches Update der Übersichtstabelle ({len(table_rows)} Equipment)"
|
||||
)
|
||||
|
||||
_logger.info(f"✓ Übersichtstabelle aktualisiert: {len(table_rows)} Equipment in {overview_page_id}")
|
||||
|
||||
return {
|
||||
'total': len(equipment_records),
|
||||
'success': len(table_rows),
|
||||
'error_messages': '',
|
||||
'overview_url': dokuwiki_client.get_wiki_url(overview_page_id),
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Fehler beim Aktualisieren der Übersichtstabelle: {str(e)}"
|
||||
_logger.error(error_msg)
|
||||
return {
|
||||
'total': 0,
|
||||
'success': 0,
|
||||
'error_messages': error_msg,
|
||||
'overview_url': '',
|
||||
}
|
||||
|
||||
@api.model
|
||||
def action_clear_template_cache(self):
|
||||
"""
|
||||
Leert den Template-Cache und erzwingt ein Neuladen aller Templates aus DokuWiki.
|
||||
Nützlich nach Template-Änderungen im Wiki.
|
||||
|
||||
Returns:
|
||||
dict: Notification mit Anzahl geleerte Cache-Einträge
|
||||
"""
|
||||
global _template_cache
|
||||
|
||||
count = len(_template_cache)
|
||||
cache_keys = list(_template_cache.keys())
|
||||
_template_cache.clear()
|
||||
|
||||
_logger.info(f"Template-Cache geleert: {count} Einträge ({', '.join(cache_keys)})")
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': _('Template-Cache geleert'),
|
||||
'message': _(f'{count} Template(s) aus dem Cache entfernt. Nächster Sync lädt Templates neu.'),
|
||||
'type': 'success',
|
||||
'sticky': False,
|
||||
}
|
||||
}
|
||||
|
||||
@api.model
|
||||
def action_reset_wiki_sync_status(self):
|
||||
"""
|
||||
Setzt den Wiki-Synchronisationsstatus für alle Equipment zurück.
|
||||
Nützlich nach Namespace-Änderungen oder wenn alle Equipment neu synchronisiert werden sollen.
|
||||
|
||||
Setzt zurück:
|
||||
- wiki_synced auf False
|
||||
- wiki_last_sync auf NULL
|
||||
|
||||
Optional (auskommentiert):
|
||||
- wiki_doku_id auf NULL (nur wenn IDs komplett neu generiert werden sollen)
|
||||
|
||||
Returns:
|
||||
dict: Notification mit Anzahl zurückgesetzter Equipment
|
||||
"""
|
||||
equipment_records = self.search([
|
||||
('wiki_synced', '=', True)
|
||||
])
|
||||
|
||||
count = len(equipment_records)
|
||||
|
||||
if count == 0:
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': _('Wiki-Sync Reset'),
|
||||
'message': _('Keine synchronisierten Equipment gefunden.'),
|
||||
'type': 'warning',
|
||||
'sticky': False,
|
||||
}
|
||||
}
|
||||
|
||||
# Reset durchführen
|
||||
equipment_records.write({
|
||||
'wiki_synced': False,
|
||||
'wiki_last_sync': False,
|
||||
'wiki_status_id': False, # Status-ID neu generieren
|
||||
# wiki_doku_id wird NICHT zurückgesetzt - ist manuell editierbar!
|
||||
})
|
||||
|
||||
_logger.info(f"Wiki-Sync-Status für {count} Equipment zurückgesetzt")
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': _('Wiki-Sync Reset'),
|
||||
'message': _(f'{count} Equipment wurden zurückgesetzt und müssen neu synchronisiert werden.'),
|
||||
'type': 'success',
|
||||
'sticky': False,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class MaintenanceEquipmentStatus(models.Model):
|
||||
"""
|
||||
Extension of the maintenance.equipment.status model to add DokuWiki smiley support.
|
||||
|
||||
This class extends the standard Odoo maintenance equipment status model to include
|
||||
a smiley field that can be used for DokuWiki integration. The smiley field allows
|
||||
administrators to associate DokuWiki emoticon syntax with each maintenance equipment
|
||||
status, enabling visual representation of equipment status in DokuWiki documentation.
|
||||
|
||||
The smiley field accepts DokuWiki emoticon syntax (e.g., ':-)', ':-(', ':-D', etc.)
|
||||
which can be used to display appropriate emoticons when exporting or displaying
|
||||
equipment status information in DokuWiki format.
|
||||
|
||||
This extension is particularly useful for workshops or maintenance departments that
|
||||
use DokuWiki as their documentation platform and want to have a visual representation
|
||||
of equipment status alongside textual information.
|
||||
"""
|
||||
_inherit = 'maintenance.equipment.status'
|
||||
|
||||
smiley = fields.Char(
|
||||
string='Smiley',
|
||||
help='DokuWiki Smiley für diesen Status (z.B. :-) oder :-()'
|
||||
)
|
||||
54
open_workshop_dokuwiki/models/res_config_settings.py
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import logging
|
||||
from odoo import api, models
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
@api.model
|
||||
def _register_hook(self):
|
||||
"""
|
||||
Hook der bei jedem Modul-Load (auch bei Update) ausgeführt wird.
|
||||
Initialisiert fehlende DokuWiki-Parameter.
|
||||
"""
|
||||
super()._register_hook()
|
||||
self._init_dokuwiki_parameters()
|
||||
|
||||
@api.model
|
||||
def _init_dokuwiki_parameters(self):
|
||||
"""
|
||||
Initialisiert DokuWiki-Parameter beim Modul-Install/-Update falls sie nicht existieren.
|
||||
Wird automatisch beim Laden des Moduls aufgerufen.
|
||||
"""
|
||||
IrConfigParameter = self.env['ir.config_parameter'].sudo()
|
||||
|
||||
# Default-Parameter
|
||||
defaults = {
|
||||
'dokuwiki.url': 'https://wiki.hobbyhimmel.de',
|
||||
'dokuwiki.user': 'odoo.odoo',
|
||||
'dokuwiki.password': 'CHANGE_ME',
|
||||
'dokuwiki.equipment_namespace': 'werkstatt:ausstattung', # NEU: Basis-Namespace für Equipment
|
||||
'dokuwiki.central_documentation_namespace': 'werkstatt:ausstattung:odoo-status', # NEU: Namespace für Odoo-generierte Status-Seiten
|
||||
'dokuwiki.overview_page_id': 'werkstatt:ausstattung:uebersicht', # Seiten-ID für Übersichtstabelle
|
||||
'dokuwiki.overview_title': 'Geräte & Maschinen - Übersicht',
|
||||
'dokuwiki.overview_columns': 'Name|Zustand|Sicherheit|Bereich|Standort|Bild',
|
||||
'dokuwiki.overview_column_data': '[[{wiki_page_id}|{name}]]|{status_smiley}|{ows_machine_id.category_icon}|{ows_area}|{location}|{image}',
|
||||
'dokuwiki.auto_update_overview_table': 'False', # Automatische Übersichtstabellen-Aktualisierung bei Equipment-Änderungen
|
||||
}
|
||||
|
||||
# Nur fehlende Parameter anlegen
|
||||
count_created = 0
|
||||
count_existing = 0
|
||||
for key, value in defaults.items():
|
||||
if not IrConfigParameter.get_param(key):
|
||||
IrConfigParameter.set_param(key, value)
|
||||
count_created += 1
|
||||
_logger.info(f"DokuWiki parameter '{key}' initialized with default value")
|
||||
else:
|
||||
count_existing += 1
|
||||
|
||||
if count_created > 0:
|
||||
_logger.info(f"DokuWiki parameters: {count_created} created, {count_existing} already existing")
|
||||
2
open_workshop_dokuwiki/security/ir.model.access.csv
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_equipment_wiki_sync_wizard,equipment.wiki.sync.wizard,model_equipment_wiki_sync_wizard,base.group_user,1,1,1,1
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="maintenance_equipment_status_form_view" model="ir.ui.view">
|
||||
<field name="name">maintenance.equipment.status.form.dokuwiki</field>
|
||||
<field name="model">maintenance.equipment.status</field>
|
||||
<field name="inherit_id" ref="maintenance_equipment_status.maintenance_equipment_status_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="name" position="after">
|
||||
<field name="smiley" placeholder="z.B. :-) oder :-("/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="maintenance_equipment_status_tree_view" model="ir.ui.view">
|
||||
<field name="name">maintenance.equipment.status.tree.dokuwiki</field>
|
||||
<field name="model">maintenance.equipment.status</field>
|
||||
<field name="inherit_id" ref="maintenance_equipment_status.maintenance_equipment_status_view_tree"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="name" position="after">
|
||||
<field name="smiley" optional="show"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
120
open_workshop_dokuwiki/views/maintenance_equipment_views.xml
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- Equipment Tree View Extension - Wiki Link -->
|
||||
<record id="maintenance_equipment_view_tree_dokuwiki" model="ir.ui.view">
|
||||
<field name="name">maintenance.equipment.tree.dokuwiki</field>
|
||||
<field name="model">maintenance.equipment</field>
|
||||
<field name="inherit_id" ref="maintenance.hr_equipment_view_tree"/>
|
||||
<field name="arch" type="xml">
|
||||
<!-- Wiki Doku-Link nach dem Namen -->
|
||||
<field name="name" position="after">
|
||||
<field name="wiki_doku_link" widget="html" string="Wiki Doku" optional="show"/>
|
||||
<field name="wiki_status_link" widget="html" string="Wiki Status" optional="hide"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Equipment Form View Extension -->
|
||||
<record id="maintenance_equipment_view_form_dokuwiki" model="ir.ui.view">
|
||||
<field name="name">maintenance.equipment.form.dokuwiki</field>
|
||||
<field name="model">maintenance.equipment</field>
|
||||
<field name="inherit_id" ref="maintenance.hr_equipment_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
|
||||
<!-- Bild-Feld oben rechts neben den Smart Buttons -->
|
||||
<xpath expr="//sheet/div[@name='button_box']" position="before">
|
||||
<field name="image_1920" widget="image" class="oe_avatar" options="{'preview_image': 'image_1920'}"/>
|
||||
</xpath>
|
||||
|
||||
<!-- Smart Button im Header -->
|
||||
<xpath expr="//div[@name='button_box']" position="inside">
|
||||
<button name="action_open_wiki"
|
||||
type="object"
|
||||
class="oe_stat_button"
|
||||
icon="fa-book"
|
||||
invisible="not wiki_page_url">
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_text">Wiki</span>
|
||||
<span class="o_stat_text text-success" invisible="not wiki_synced">Synchronisiert</span>
|
||||
<span class="o_stat_text text-warning" invisible="wiki_synced">Nicht synchronisiert</span>
|
||||
</div>
|
||||
</button>
|
||||
</xpath>
|
||||
|
||||
<!-- Wiki-Felder in neuem Tab -->
|
||||
<xpath expr="//notebook" position="inside">
|
||||
<page string="Wiki" name="wiki">
|
||||
<group>
|
||||
<group string="Wiki Dokumentations-Seite (Benutzer-Doku)" colspan="2">
|
||||
<field name="wiki_doku_id" string="Seitenname"/>
|
||||
<field name="wiki_page_url" widget="url" string="🔗 Zur Doku-Seite" class="oe_link"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<group string="Wiki Status-Seite (Odoo-Daten, automatisch)">
|
||||
<field name="wiki_status_id" string="Seitenname" readonly="1"/>
|
||||
</group>
|
||||
<group string="Synchronisations-Status">
|
||||
<field name="wiki_synced" readonly="1"/>
|
||||
<field name="wiki_last_sync" readonly="1"/>
|
||||
<field name="wiki_auto_sync"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<group>
|
||||
<group string="Aktionen">
|
||||
<button name="action_sync_to_dokuwiki"
|
||||
string="Zum Wiki synchronisieren"
|
||||
type="object"
|
||||
class="btn-primary"
|
||||
icon="fa-refresh"/>
|
||||
<button name="action_open_wiki"
|
||||
string="Wiki im Browser öffnen"
|
||||
type="object"
|
||||
class="btn-secondary"
|
||||
icon="fa-external-link"
|
||||
invisible="not wiki_page_url"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<group string="Hilfe">
|
||||
<div class="alert alert-info" role="alert">
|
||||
<h4 class="alert-heading">📚 DokuWiki-Integration</h4>
|
||||
<p>
|
||||
Dieses Equipment ist mit DokuWiki verbunden. Die Synchronisation erstellt <strong>zwei unterschiedliche Seiten</strong>:
|
||||
</p>
|
||||
<hr/>
|
||||
<h5>📊 Wiki Status Seite (automatisch, nur lesbar)</h5>
|
||||
<ul>
|
||||
<li><strong>Zweck:</strong> Enthält aktuelle Odoo-Daten (Status, Standort, Hersteller etc.)</li>
|
||||
<li><strong>Format:</strong> werkstatt:ausstattung:odoo-status:[name-seriennummer]</li>
|
||||
<li><strong>Eindeutigkeit:</strong> Jedes Equipment hat eine eigene Status-Seite</li>
|
||||
<li><strong>Update:</strong> Wird bei jeder Synchronisation automatisch aktualisiert</li>
|
||||
</ul>
|
||||
|
||||
<h5>📖 Wiki Dokumentations Seite (manuell, editierbar)</h5>
|
||||
<ul>
|
||||
<li><strong>Zweck:</strong> Benutzer-Dokumentation, Anleitungen, Tipps</li>
|
||||
<li><strong>Format:</strong> werkstatt:ausstattung:[bereich]:[doku-name]</li>
|
||||
<li><strong>Besonderheit:</strong> Kann von mehreren Equipment geteilt werden (z.B. "akkuschrauber")</li>
|
||||
<li><strong>Erstellung:</strong> Wird NICHT von Odoo erstellt - Benutzer erstellen diese manuell</li>
|
||||
</ul>
|
||||
<hr/>
|
||||
<p class="mb-0">
|
||||
<strong>💡 Tipp:</strong> Die Status-Seite kann mit dem include-Plugin in die Dokumentations-Seite eingebunden werden:<br/>
|
||||
<code>{{page>werkstatt:ausstattung:odoo-status:[name-seriennummer]}}</code>
|
||||
</p>
|
||||
<hr/>
|
||||
<p class="mb-0">
|
||||
<strong>⚙️ Automatische Synchronisation:</strong> Wenn aktiviert, werden Änderungen automatisch zur Status-Seite übertragen.
|
||||
</p>
|
||||
</div>
|
||||
</group>
|
||||
</page>
|
||||
</xpath>
|
||||
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
2
open_workshop_dokuwiki/wizard/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from . import equipment_wiki_sync_wizard
|
||||
157
open_workshop_dokuwiki/wizard/equipment_wiki_sync_wizard.py
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import logging
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EquipmentWikiSyncWizard(models.TransientModel):
|
||||
_name = 'equipment.wiki.sync.wizard'
|
||||
_description = 'Massen-Synchronisation von Equipment zu DokuWiki'
|
||||
|
||||
sync_mode = fields.Selection([
|
||||
('all', 'Alle Equipment'),
|
||||
('area', 'Nach Bereich filtern'),
|
||||
('unsynced', 'Nur nicht synchronisierte'),
|
||||
('overview_table', 'Übersichtstabelle aktualisieren'),
|
||||
('reset_sync', 'Sync-Status zurücksetzen'),
|
||||
], string='Synchronisations-Modus', default='all', required=True)
|
||||
|
||||
ows_area_id = fields.Many2one(
|
||||
'ows.machine.area',
|
||||
string='Bereich',
|
||||
help='Nur Equipment aus diesem Bereich synchronisieren'
|
||||
)
|
||||
|
||||
# Statistik-Felder (nach Sync)
|
||||
total_count = fields.Integer(string='Gefunden', readonly=True)
|
||||
success_count = fields.Integer(string='Erfolgreich', readonly=True)
|
||||
error_count = fields.Integer(string='Fehler', readonly=True)
|
||||
error_messages = fields.Text(string='Fehlermeldungen', readonly=True)
|
||||
overview_url = fields.Char(string='Übersichtsseite URL', readonly=True)
|
||||
|
||||
@api.onchange('sync_mode')
|
||||
def _onchange_sync_mode(self):
|
||||
"""Bereich-Feld nur bei 'area' Modus anzeigen"""
|
||||
if self.sync_mode != 'area':
|
||||
self.ows_area_id = False
|
||||
|
||||
def action_sync_equipment(self):
|
||||
"""
|
||||
Hauptaktion: Equipment synchronisieren basierend auf gewähltem Modus
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
# Übersichtstabelle-Modus: Schneller Update ohne Equipment-Iteration
|
||||
if self.sync_mode == 'overview_table':
|
||||
return self._sync_overview_table()
|
||||
|
||||
# Reset-Modus: Sync-Status zurücksetzen
|
||||
if self.sync_mode == 'reset_sync':
|
||||
return self.env['maintenance.equipment'].action_reset_wiki_sync_status()
|
||||
|
||||
# Equipment-Liste basierend auf Modus ermitteln
|
||||
domain = []
|
||||
|
||||
if self.sync_mode == 'area':
|
||||
if not self.ows_area_id:
|
||||
raise UserError("Bitte einen Bereich auswählen!")
|
||||
domain.append(('ows_area_id', '=', self.ows_area_id.id))
|
||||
elif self.sync_mode == 'unsynced':
|
||||
domain.append(('wiki_synced', '=', False))
|
||||
|
||||
# Equipment finden
|
||||
equipment_records = self.env['maintenance.equipment'].search(domain)
|
||||
|
||||
if not equipment_records:
|
||||
raise UserError("Keine Equipment-Einträge gefunden, die synchronisiert werden können!")
|
||||
|
||||
total = len(equipment_records)
|
||||
success = 0
|
||||
errors = 0
|
||||
error_list = []
|
||||
|
||||
_logger.info(f"Starte Wiki-Synchronisation für {total} Equipment-Einträge")
|
||||
|
||||
# Jeden Equipment-Eintrag synchronisieren
|
||||
for equipment in equipment_records:
|
||||
try:
|
||||
# Prüfen ob Bereich gesetzt ist
|
||||
if not equipment.ows_area_id:
|
||||
error_msg = f"{equipment.name}: Kein Bereich gesetzt"
|
||||
error_list.append(error_msg)
|
||||
errors += 1
|
||||
_logger.warning(error_msg)
|
||||
continue
|
||||
|
||||
# Standard-Synchronisation (erstellt nur Odoo-Status-Seiten)
|
||||
equipment.sync_to_dokuwiki()
|
||||
success += 1
|
||||
_logger.info(f"✓ {equipment.name} synchronisiert")
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"{equipment.name}: {str(e)}"
|
||||
error_list.append(error_msg)
|
||||
errors += 1
|
||||
_logger.error(f"✗ {error_msg}")
|
||||
|
||||
# Statistik speichern
|
||||
self.write({
|
||||
'total_count': total,
|
||||
'success_count': success,
|
||||
'error_count': errors,
|
||||
'error_messages': '\n'.join(error_list) if error_list else 'Keine Fehler',
|
||||
})
|
||||
|
||||
# Ergebnis-View anzeigen
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': 'Wiki-Synchronisation Ergebnis',
|
||||
'res_model': 'equipment.wiki.sync.wizard',
|
||||
'view_mode': 'form',
|
||||
'res_id': self.id,
|
||||
'target': 'new',
|
||||
'context': {'show_result': True},
|
||||
}
|
||||
|
||||
def _sync_overview_table(self):
|
||||
"""
|
||||
Aktualisiert die Übersichtstabelle in DokuWiki.
|
||||
Ruft die Methode im maintenance.equipment Model auf.
|
||||
|
||||
Returns:
|
||||
dict: Action zum Anzeigen des Ergebnisses
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
_logger.info("Starte Übersichtstabellen-Synchronisation")
|
||||
|
||||
# Equipment Model aufrufen
|
||||
result = self.env['maintenance.equipment'].action_sync_overview_table()
|
||||
|
||||
# Statistik speichern
|
||||
self.write({
|
||||
'total_count': result.get('total', 0),
|
||||
'success_count': result.get('success', 0),
|
||||
'error_count': result.get('total', 0) - result.get('success', 0),
|
||||
'error_messages': result.get('error_messages', 'Keine Fehler'),
|
||||
'overview_url': result.get('overview_url', ''),
|
||||
})
|
||||
|
||||
# Ergebnis-View anzeigen
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': 'Übersichtstabelle aktualisiert',
|
||||
'res_model': 'equipment.wiki.sync.wizard',
|
||||
'view_mode': 'form',
|
||||
'res_id': self.id,
|
||||
'target': 'new',
|
||||
'context': {'show_result': True},
|
||||
}
|
||||
|
||||
def action_clear_template_cache(self):
|
||||
"""
|
||||
Leert den Template-Cache (delegiert an maintenance.equipment).
|
||||
"""
|
||||
return self.env['maintenance.equipment'].action_clear_template_cache()
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Wizard Form View -->
|
||||
<record id="equipment_wiki_sync_wizard_form" model="ir.ui.view">
|
||||
<field name="name">equipment.wiki.sync.wizard.form</field>
|
||||
<field name="model">equipment.wiki.sync.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Wiki-Synchronisation">
|
||||
<group invisible="context.get('show_result', False)">
|
||||
<group>
|
||||
<field name="sync_mode" widget="radio"/>
|
||||
<field name="ows_area_id" invisible="sync_mode != 'area'" required="sync_mode == 'area'"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<!-- Ergebnis-Anzeige -->
|
||||
<group invisible="not context.get('show_result', False)">
|
||||
<group>
|
||||
<field name="total_count"/>
|
||||
<field name="success_count"/>
|
||||
<field name="error_count"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="overview_url" widget="url" string="Übersichtsseite" invisible="not overview_url"/>
|
||||
<field name="error_messages" widget="text" colspan="2" invisible="error_count == 0"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<footer>
|
||||
<button string="Synchronisieren"
|
||||
name="action_sync_equipment"
|
||||
type="object"
|
||||
class="btn-primary"
|
||||
invisible="context.get('show_result', False)"/>
|
||||
<button string="Template-Cache leeren"
|
||||
name="action_clear_template_cache"
|
||||
type="object"
|
||||
class="btn-warning"
|
||||
invisible="context.get('show_result', False)"
|
||||
help="Leert den Template-Cache und erzwingt Neuladen beim nächsten Sync"/>
|
||||
<button string="Schließen"
|
||||
special="cancel"
|
||||
class="btn-secondary"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Menu-Action -->
|
||||
<record id="action_equipment_wiki_sync_wizard" model="ir.actions.act_window">
|
||||
<field name="name">Wiki-Synchronisation</field>
|
||||
<field name="res_model">equipment.wiki.sync.wizard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
<field name="context">{}</field>
|
||||
</record>
|
||||
|
||||
<!-- Menu-Eintrag unter Wartung → Konfiguration -->
|
||||
<menuitem id="menu_equipment_wiki_sync"
|
||||
name="Wiki-Synchronisation"
|
||||
parent="maintenance.menu_maintenance_configuration"
|
||||
action="action_equipment_wiki_sync_wizard"
|
||||
sequence="99"/>
|
||||
</odoo>
|
||||
139
open_workshop_employee_imagegenerator/README.md
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
# Open Workshop - Employee Image Generator
|
||||
|
||||
Erstelle professionelle Namensschilder (Thekenschilder) für Mitarbeiter direkt in Odoo.
|
||||
|
||||
## Beschreibung
|
||||
|
||||
Dieses Modul ersetzt die externe "thekenheld" WebApp und integriert die Funktionalität direkt in Odoo's HR-Modul. Mitarbeiter-Namensschilder können nun bequem aus dem Employee-Formular heraus erstellt werden.
|
||||
|
||||
## Features
|
||||
|
||||
- ✅ **Foto-Upload & Zuschnitt**: Cropper.js Integration für professionelle Bildbearbeitung
|
||||
- ✅ **Festes Seitenverhältnis**: 369x492 Pixel (optimiert für Thekenschilder)
|
||||
- ✅ **Text-Overlay**: Name und Schwerpunkte werden automatisch hinzugefügt
|
||||
- ✅ **Direkte Integration**: Button im Employee-Formular
|
||||
- ✅ **Sofortige Verfügbarkeit**: Generiertes Bild wird direkt als Employee Avatar gespeichert
|
||||
- ✅ **POS Integration**: Bilder werden automatisch im Customer Display angezeigt (via open_workshop_pos_customer_display)
|
||||
|
||||
## Installation
|
||||
|
||||
1. Modul in Odoo installieren
|
||||
2. Berechtigungen: HR / Mitarbeiter Module erforderlich
|
||||
|
||||
## Verwendung
|
||||
|
||||
### Namensschild erstellen:
|
||||
|
||||
1. Öffne einen Mitarbeiter-Datensatz (HR → Mitarbeiter)
|
||||
2. Klicke auf den Button **"Namensschild erstellen"** im Header
|
||||
3. **Schritt 1**: Wähle ein Foto aus (mindestens 369x492 Pixel empfohlen)
|
||||
4. **Schritt 2**: Schneide das Foto zu - bewege und zoome für den perfekten Ausschnitt
|
||||
5. **Schritt 3**: Überprüfe Name und Schwerpunkte, passe bei Bedarf an
|
||||
6. **Vorschau**: Sieh dir das finale Namensschild an
|
||||
7. Klicke **"Speichern"** - das Bild wird als Employee Avatar gesetzt
|
||||
|
||||
### Schwerpunkte definieren:
|
||||
|
||||
Im Employee-Formular gibt es ein neues Feld **"Schwerpunkte"** (job_focus):
|
||||
- Trage hier die Tätigkeitsschwerpunkte ein
|
||||
- Z.B. "Fahrrad-Reparatur, E-Bikes, Werkstatt"
|
||||
- Max. 2 Zeilen für optimale Darstellung
|
||||
- Falls leer, wird alternativ das job_title Feld verwendet
|
||||
|
||||
## Technische Details
|
||||
|
||||
### Architektur
|
||||
|
||||
**Frontend-only Lösung** mit:
|
||||
- **OWL Component**: Client Action für Dialog
|
||||
- **Cropper.js**: Professionelles Bild-Cropping
|
||||
- **Canvas API**: Text-Overlay Generation
|
||||
- **Base64 Upload**: Direktes Speichern als image_1920
|
||||
|
||||
### Dateien
|
||||
|
||||
```
|
||||
open_workshop_employee_imagegenerator/
|
||||
├── __init__.py
|
||||
├── __manifest__.py
|
||||
├── models/
|
||||
│ ├── __init__.py
|
||||
│ └── hr_employee.py # Erweiterung von hr.employee
|
||||
├── views/
|
||||
│ └── hr_employee_views.xml # Button im Form + job_focus Feld
|
||||
├── static/
|
||||
│ ├── src/
|
||||
│ │ ├── js/
|
||||
│ │ │ └── employee_image_widget.js # OWL Component
|
||||
│ │ ├── xml/
|
||||
│ │ │ └── employee_image_widget.xml # Template
|
||||
│ │ └── css/
|
||||
│ │ └── employee_image_widget.css # Styles
|
||||
│ └── lib/
|
||||
│ └── cropperjs/
|
||||
│ ├── cropper.js # Cropper.js Library
|
||||
│ └── cropper.css # Cropper.js Styles
|
||||
```
|
||||
|
||||
### Workflow
|
||||
|
||||
1. User klickt "Namensschild erstellen"
|
||||
2. Python Action öffnet Client Action mit Context (employee_id, name, job_focus)
|
||||
3. JavaScript Dialog (3 Schritte):
|
||||
- Upload → FileReader lädt Bild als Data URL
|
||||
- Crop → Cropper.js erzeugt zugeschnittenes Bild (369x492)
|
||||
- Text → Canvas API zeichnet Text-Overlay
|
||||
4. Base64 Image wird zu hr.employee.image_1920 gespeichert
|
||||
5. Page Reload zeigt neues Bild
|
||||
|
||||
### Canvas Text-Overlay
|
||||
|
||||
Das generierte Namensschild hat:
|
||||
- **Foto**: 369x492 Pixel (Vollbild)
|
||||
- **Text-Overlay**: 120px Höhe am unteren Rand
|
||||
- **Hintergrund**: Halbtransparent weiß (92% Opazität)
|
||||
- **Name**: Fett, 24px, zentriert
|
||||
- **Schwerpunkte**: Normal, 16px, zentriert, max. 2 Zeilen
|
||||
|
||||
## Integration
|
||||
|
||||
### Mit POS Customer Display
|
||||
|
||||
Dieses Modul arbeitet nahtlos mit `open_workshop_pos_customer_display` zusammen:
|
||||
- Generierte Bilder werden automatisch im Customer Display angezeigt
|
||||
- Verwendung der hr.employee.public URL
|
||||
- Keine zusätzliche Konfiguration nötig
|
||||
|
||||
### Ersetzt thekenheld WebApp
|
||||
|
||||
Die externe Docker-Container WebApp ist nicht mehr nötig:
|
||||
- ✅ Keine separate Instanz erforderlich
|
||||
- ✅ Alle Daten in Odoo
|
||||
- ✅ Berechtigungsverwaltung über Odoo
|
||||
- ✅ Besserer Workflow
|
||||
|
||||
## Migration von thekenheld
|
||||
|
||||
Falls bereits Bilder in der alten WebApp erstellt wurden:
|
||||
1. Bilder aus `/var/www/html/uploads/` kopieren
|
||||
2. Manuell in Odoo Employee-Datensätze hochladen
|
||||
3. Oder: Neues Namensschild mit diesem Tool erstellen
|
||||
|
||||
## Abhängigkeiten
|
||||
|
||||
- `hr` - Human Resources Module
|
||||
- `web` - Odoo Web Framework
|
||||
- Cropper.js v1.6.1 (inkludiert)
|
||||
|
||||
## Version
|
||||
|
||||
18.0.1.0.0
|
||||
|
||||
## Lizenz
|
||||
|
||||
LGPL-3
|
||||
|
||||
## Credits
|
||||
|
||||
Entwickelt für Open Workshop / Hobbyhimmel
|
||||
Basierend auf der thekenheld WebApp
|
||||
2
open_workshop_employee_imagegenerator/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from . import models
|
||||
53
open_workshop_employee_imagegenerator/__manifest__.py
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
'name': 'Open Workshop - Employee Image Generator',
|
||||
'version': '18.0.1.0.0',
|
||||
'category': 'Human Resources',
|
||||
'summary': 'Upload and crop employee photos (369x492) with job focus',
|
||||
'description': """
|
||||
Employee Image Generator
|
||||
=========================
|
||||
|
||||
Upload and crop employee photos directly in Odoo.
|
||||
|
||||
Features:
|
||||
---------
|
||||
* Upload and crop employee photos to fixed 369x492 pixels
|
||||
* Add job focus areas (Schwerpunkte)
|
||||
* Integrated Cropper.js for professional image editing
|
||||
* Fixed crop frame - only image moves and zooms
|
||||
* Direct integration in Employee form
|
||||
* Saved photo can be used in POS Customer Display
|
||||
|
||||
Usage:
|
||||
------
|
||||
1. Go to Employee form
|
||||
2. Click "Namensschild erstellen" button
|
||||
3. Upload photo, zoom and position it in fixed crop frame
|
||||
4. Enter job focus areas (optional)
|
||||
5. Save - photo and job_focus are saved to employee
|
||||
""",
|
||||
'author': 'Open Workshop',
|
||||
'website': 'https://www.open-workshop.de',
|
||||
'license': 'LGPL-3',
|
||||
'depends': ['hr', 'web'],
|
||||
'data': [
|
||||
'views/hr_employee_views.xml',
|
||||
],
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
# Cropper.js CSS
|
||||
'open_workshop_employee_imagegenerator/static/lib/cropperjs/cropper.css',
|
||||
'open_workshop_employee_imagegenerator/static/src/css/employee_image_widget.css',
|
||||
'open_workshop_employee_imagegenerator/static/src/css/badge_template.css',
|
||||
# Cropper.js Library
|
||||
'open_workshop_employee_imagegenerator/static/lib/cropperjs/cropper.js',
|
||||
# Our custom widgets
|
||||
'open_workshop_employee_imagegenerator/static/src/js/employee_image_widget.js',
|
||||
'open_workshop_employee_imagegenerator/static/src/xml/employee_image_widget.xml',
|
||||
],
|
||||
},
|
||||
'installable': True,
|
||||
'application': False,
|
||||
'auto_install': False,
|
||||
}
|
||||
2
open_workshop_employee_imagegenerator/models/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from . import hr_employee
|
||||
32
open_workshop_employee_imagegenerator/models/hr_employee.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields, api
|
||||
|
||||
|
||||
class HrEmployee(models.Model):
|
||||
_inherit = 'hr.employee'
|
||||
|
||||
# Zusätzliches Feld für Schwerpunkte (falls nicht job_title verwendet wird)
|
||||
job_focus = fields.Char(
|
||||
string='Schwerpunkte',
|
||||
help='Tätigkeitsschwerpunkte für Namensschild'
|
||||
)
|
||||
|
||||
def action_open_image_generator(self):
|
||||
"""Öffnet den Image Generator Dialog"""
|
||||
self.ensure_one()
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'employee_image_generator',
|
||||
'name': 'Namensschild erstellen',
|
||||
'target': 'new',
|
||||
'params': {
|
||||
'employee_id': self.id,
|
||||
'employee_name': self.name,
|
||||
'job_focus': self.job_focus or self.job_title or '',
|
||||
},
|
||||
'context': {
|
||||
'employee_id': self.id,
|
||||
'employee_name': self.name,
|
||||
'job_focus': self.job_focus or self.job_title or '',
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
/*!
|
||||
* Cropper.js v1.6.1
|
||||
* https://fengyuanchen.github.io/cropperjs
|
||||
*
|
||||
* Copyright 2015-present Chen Fengyuan
|
||||
* Released under the MIT license
|
||||
*
|
||||
* Date: 2023-09-17T03:44:17.565Z
|
||||
*/.cropper-container{direction:ltr;font-size:0;line-height:0;position:relative;-ms-touch-action:none;touch-action:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.cropper-container img{backface-visibility:hidden;display:block;height:100%;image-orientation:0deg;max-height:none!important;max-width:none!important;min-height:0!important;min-width:0!important;width:100%}.cropper-canvas,.cropper-crop-box,.cropper-drag-box,.cropper-modal,.cropper-wrap-box{bottom:0;left:0;position:absolute;right:0;top:0}.cropper-canvas,.cropper-wrap-box{overflow:hidden}.cropper-drag-box{background-color:#fff;opacity:0}.cropper-modal{background-color:#000;opacity:.5}.cropper-view-box{display:block;height:100%;outline:1px solid #39f;outline-color:rgba(51,153,255,.75);overflow:hidden;width:100%}.cropper-dashed{border:0 dashed #eee;display:block;opacity:.5;position:absolute}.cropper-dashed.dashed-h{border-bottom-width:1px;border-top-width:1px;height:33.33333%;left:0;top:33.33333%;width:100%}.cropper-dashed.dashed-v{border-left-width:1px;border-right-width:1px;height:100%;left:33.33333%;top:0;width:33.33333%}.cropper-center{display:block;height:0;left:50%;opacity:.75;position:absolute;top:50%;width:0}.cropper-center:after,.cropper-center:before{background-color:#eee;content:" ";display:block;position:absolute}.cropper-center:before{height:1px;left:-3px;top:0;width:7px}.cropper-center:after{height:7px;left:0;top:-3px;width:1px}.cropper-face,.cropper-line,.cropper-point{display:block;height:100%;opacity:.1;position:absolute;width:100%}.cropper-face{background-color:#fff;left:0;top:0}.cropper-line{background-color:#39f}.cropper-line.line-e{cursor:ew-resize;right:-3px;top:0;width:5px}.cropper-line.line-n{cursor:ns-resize;height:5px;left:0;top:-3px}.cropper-line.line-w{cursor:ew-resize;left:-3px;top:0;width:5px}.cropper-line.line-s{bottom:-3px;cursor:ns-resize;height:5px;left:0}.cropper-point{background-color:#39f;height:5px;opacity:.75;width:5px}.cropper-point.point-e{cursor:ew-resize;margin-top:-3px;right:-3px;top:50%}.cropper-point.point-n{cursor:ns-resize;left:50%;margin-left:-3px;top:-3px}.cropper-point.point-w{cursor:ew-resize;left:-3px;margin-top:-3px;top:50%}.cropper-point.point-s{bottom:-3px;cursor:s-resize;left:50%;margin-left:-3px}.cropper-point.point-ne{cursor:nesw-resize;right:-3px;top:-3px}.cropper-point.point-nw{cursor:nwse-resize;left:-3px;top:-3px}.cropper-point.point-sw{bottom:-3px;cursor:nesw-resize;left:-3px}.cropper-point.point-se{bottom:-3px;cursor:nwse-resize;height:20px;opacity:1;right:-3px;width:20px}@media (min-width:768px){.cropper-point.point-se{height:15px;width:15px}}@media (min-width:992px){.cropper-point.point-se{height:10px;width:10px}}@media (min-width:1200px){.cropper-point.point-se{height:5px;opacity:.75;width:5px}}.cropper-point.point-se:before{background-color:#39f;bottom:-50%;content:" ";display:block;height:200%;opacity:0;position:absolute;right:-50%;width:200%}.cropper-invisible{opacity:0}.cropper-bg{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC")}.cropper-hide{display:block;height:0;position:absolute;width:0}.cropper-hidden{display:none!important}.cropper-move{cursor:move}.cropper-crop{cursor:crosshair}.cropper-disabled .cropper-drag-box,.cropper-disabled .cropper-face,.cropper-disabled .cropper-line,.cropper-disabled .cropper-point{cursor:not-allowed}
|
||||
20
open_workshop_employee_imagegenerator/static/lib/html2canvas.min.js
vendored
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
/* Badge Template CSS - 1:1 von thekenheld print.html */
|
||||
|
||||
.badge-template {
|
||||
position: relative;
|
||||
background: white;
|
||||
width: 21cm;
|
||||
height: 29.7cm;
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.badge-template header,
|
||||
.badge-template footer {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: rgb(255, 255, 255);
|
||||
padding-right: 1.5cm;
|
||||
padding-left: 1.5cm;
|
||||
height: 30mm;
|
||||
}
|
||||
|
||||
.badge-template header {
|
||||
top: 0;
|
||||
padding-top: 5mm;
|
||||
padding-bottom: 3mm;
|
||||
}
|
||||
|
||||
.badge-template footer {
|
||||
bottom: 0;
|
||||
color: rgb(241, 245, 247);
|
||||
padding-top: 3mm;
|
||||
padding-bottom: 5mm;
|
||||
}
|
||||
|
||||
.badge-template .content {
|
||||
padding-top: 40mm;
|
||||
padding-bottom: 40mm;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.badge-template .logo {
|
||||
width: 43mm;
|
||||
height: 20mm;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.badge-template .headline1 {
|
||||
height: 13mm;
|
||||
font-size: 10mm;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.badge-template .headline2 {
|
||||
height: 9mm;
|
||||
font-size: 6mm;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.badge-template .headline3 {
|
||||
height: 10mm;
|
||||
font-size: 7mm;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.badge-template .centered-image {
|
||||
height: 130mm;
|
||||
width: 100mm;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
padding: 5mm 0 0 0;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
}
|
||||
|
||||
.badge-template .your-name {
|
||||
height: 22mm;
|
||||
font-size: 19mm;
|
||||
padding: 0;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.badge-template .topics {
|
||||
height: 10mm;
|
||||
font-size: 8mm;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.badge-template .warnung {
|
||||
font-size: 9mm;
|
||||
color: red;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
/* Employee Image Generator Styles */
|
||||
|
||||
.o_employee_image_generator {
|
||||
max-width: 900px;
|
||||
}
|
||||
|
||||
.o_employee_image_generator .img-container {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.o_employee_image_generator .img-container img {
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.o_employee_image_generator canvas {
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Cropper.js Container Anpassungen */
|
||||
.o_employee_image_generator .cropper-container {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Progress Bar */
|
||||
.o_employee_image_generator .progress {
|
||||
height: 3px;
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
|
||||
.o_employee_image_generator .progress-bar {
|
||||
background-color: #0d6efd;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
/* Step Indicators */
|
||||
.o_employee_image_generator .step-indicator {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* Upload Area */
|
||||
.o_employee_image_generator .upload-area {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.o_employee_image_generator .upload-area:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
/* Canvas Preview */
|
||||
.o_employee_image_generator canvas {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Modal Anpassungen */
|
||||
.o_employee_image_generator .modal-dialog {
|
||||
margin: 1.75rem auto;
|
||||
}
|
||||
|
||||
.o_employee_image_generator .modal-content {
|
||||
border: none;
|
||||
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.o_employee_image_generator .modal-header {
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
padding: 1rem 1.5rem;
|
||||
}
|
||||
|
||||
.o_employee_image_generator .modal-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.o_employee_image_generator .modal-footer {
|
||||
border-top: 1px solid #dee2e6;
|
||||
padding: 1rem 1.5rem;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.o_employee_image_generator .modal-dialog {
|
||||
max-width: 95%;
|
||||
margin: 0.5rem auto;
|
||||
}
|
||||
|
||||
.o_employee_image_generator .modal-body {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,228 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { Component, useState, useRef, onMounted } from "@odoo/owl";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
/**
|
||||
* Employee Image Generator Client Action
|
||||
*
|
||||
* Ermöglicht das Erstellen von professionellen Namensschildern für Mitarbeiter.
|
||||
* Verwendet Cropper.js für Bildbearbeitung und Canvas API für Text-Overlay.
|
||||
*/
|
||||
class EmployeeImageGenerator extends Component {
|
||||
static template = "open_workshop_employee_imagegenerator.ImageGeneratorDialog";
|
||||
static props = {
|
||||
"*": true,
|
||||
};
|
||||
|
||||
setup() {
|
||||
this.orm = useService("orm");
|
||||
this.action = useService("action");
|
||||
this.notification = useService("notification");
|
||||
|
||||
// Hole Context aus props oder action
|
||||
const context = this.props.action?.context || this.props.context || {};
|
||||
|
||||
this.state = useState({
|
||||
step: 1, // 1: Upload, 2: Crop, 3: Text, 4: Preview
|
||||
employeeId: context.employee_id,
|
||||
employeeName: context.employee_name || '',
|
||||
jobFocus: context.job_focus || '',
|
||||
companyLogo: null,
|
||||
imageDataUrl: null,
|
||||
croppedImageDataUrl: null,
|
||||
finalImageDataUrl: null,
|
||||
});
|
||||
|
||||
this.fileInput = useRef("fileInput");
|
||||
this.imagePreview = useRef("imagePreview");
|
||||
this.cropperImage = useRef("cropperImage");
|
||||
this.badgeTemplate = useRef("badgeTemplate");
|
||||
|
||||
this.cropper = null;
|
||||
// Zielgröße für das zugeschnittene Foto
|
||||
this.cropWidth = 369;
|
||||
this.cropHeight = 492;
|
||||
// Zielgröße für das finale Badge (A4-Format)
|
||||
this.badgeWidth = 794;
|
||||
this.badgeHeight = 1123;
|
||||
// Umrechnungsfaktor für mm (falls benötigt)
|
||||
this.pxPerMm = this.badgeWidth / 210;
|
||||
|
||||
// Lade Company-Logo beim Start
|
||||
this.loadCompanyLogo();
|
||||
}
|
||||
|
||||
async loadCompanyLogo() {
|
||||
try {
|
||||
// Hole das Logo der aktuellen Company
|
||||
const company = await this.orm.call('res.company', 'search_read', [[]], {
|
||||
fields: ['logo'],
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
if (company && company.length > 0 && company[0].logo) {
|
||||
// Company-Logo ist base64-codiert, füge data:image/png prefix hinzu
|
||||
this.state.companyLogo = `data:image/png;base64,${company[0].logo}`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Konnte Company-Logo nicht laden:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Schritt 1: Datei-Upload
|
||||
onFileSelect(ev) {
|
||||
const files = ev.target.files;
|
||||
if (files && files.length > 0) {
|
||||
const file = files[0];
|
||||
|
||||
// Prüfe ob es ein Bild ist
|
||||
if (!file.type.startsWith('image/')) {
|
||||
this.notification.add('Bitte wähle eine Bilddatei aus', {
|
||||
type: 'warning'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Lade Bild als Data URL
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
this.state.imageDataUrl = e.target.result;
|
||||
this.state.step = 2;
|
||||
|
||||
// Initialisiere Cropper im nächsten Tick
|
||||
setTimeout(() => this.initCropper(), 100);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
}
|
||||
|
||||
// Schritt 2: Initialisiere Cropper.js
|
||||
initCropper() {
|
||||
if (this.cropper) {
|
||||
this.cropper.destroy();
|
||||
}
|
||||
|
||||
const imageElement = this.cropperImage.el;
|
||||
if (!imageElement || !window.Cropper) {
|
||||
console.error('Cropper.js not loaded or image element not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const aspectRatio = this.cropWidth / this.cropHeight;
|
||||
|
||||
this.cropper = new window.Cropper(imageElement, {
|
||||
aspectRatio: aspectRatio,
|
||||
viewMode: 1, // Erlaubt unbegrenztes Zoomen
|
||||
dragMode: 'move', // Bild verschieben
|
||||
autoCropArea: 0.7, // Crop-Rahmen ist 70% des Containers - besser sichtbar
|
||||
restore: false,
|
||||
guides: true,
|
||||
center: true,
|
||||
highlight: false,
|
||||
cropBoxMovable: false, // Crop-Rahmen NICHT verschiebbar
|
||||
cropBoxResizable: false, // Crop-Rahmen NICHT in Größe veränderbar
|
||||
toggleDragModeOnDblclick: false,
|
||||
zoomable: true,
|
||||
zoomOnWheel: true, // Zoom mit Mausrad
|
||||
zoomOnTouch: true, // Zoom mit Touch-Gesten
|
||||
minContainerWidth: 200,
|
||||
minContainerHeight: 200,
|
||||
ready: function() {
|
||||
// Setze initiales Zoom-Level auf "fit in container"
|
||||
this.cropper.zoomTo(0);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Bild zuschneiden und weiter zu Schritt 3
|
||||
cropImage() {
|
||||
if (!this.cropper) {
|
||||
return;
|
||||
}
|
||||
|
||||
const canvas = this.cropper.getCroppedCanvas({
|
||||
width: this.cropWidth,
|
||||
height: this.cropHeight,
|
||||
});
|
||||
|
||||
if (canvas) {
|
||||
this.state.croppedImageDataUrl = canvas.toDataURL('image/jpeg', 0.9);
|
||||
this.state.step = 3;
|
||||
|
||||
// Destroy cropper um Ressourcen freizugeben
|
||||
this.cropper.destroy();
|
||||
this.cropper = null;
|
||||
|
||||
// Gehe zu Schritt 3 (das Template ist bereits sichtbar via t-if)
|
||||
// Die Vorschau wird automatisch angezeigt durch das HTML-Template
|
||||
}
|
||||
}
|
||||
|
||||
// Schritt 3: Text-Änderung → Vorschau ist Live durch HTML-Template
|
||||
onTextChange() {
|
||||
// Das Template aktualisiert sich automatisch durch t-model
|
||||
// Keine zusätzliche Logik nötig - OWL reactive state macht das automatisch!
|
||||
}
|
||||
|
||||
// Schritt 4: Finale Speicherung - Speichere nur das zugeschnittene Foto + job_focus
|
||||
async saveFinalImage() {
|
||||
try {
|
||||
// Verwende das bereits zugeschnittene Foto (369×492px)
|
||||
if (!this.state.croppedImageDataUrl) {
|
||||
this.notification.add('Kein zugeschnittenes Foto vorhanden', { type: 'danger' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Extrahiere Base64-Daten (entferne data:image/jpeg;base64, prefix)
|
||||
const imageData = this.state.croppedImageDataUrl.split(',')[1];
|
||||
|
||||
// Speichere Foto + job_focus in Odoo
|
||||
await this.orm.write('hr.employee', [this.state.employeeId], {
|
||||
image_1920: imageData,
|
||||
job_focus: this.state.jobFocus || '',
|
||||
});
|
||||
|
||||
this.notification.add('Foto erfolgreich gespeichert!', {
|
||||
type: 'success',
|
||||
});
|
||||
|
||||
// Schließe Dialog und kehre zurück zum Mitarbeiter-Formular
|
||||
await this.action.doAction({
|
||||
type: 'ir.actions.act_window',
|
||||
res_model: 'hr.employee',
|
||||
res_id: this.state.employeeId,
|
||||
views: [[false, 'form']],
|
||||
target: 'current',
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Speichern:', error);
|
||||
this.notification.add(`Fehler beim Speichern: ${error.message}`, {
|
||||
type: 'danger',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Abbrechen
|
||||
cancel() {
|
||||
if (this.props.close) {
|
||||
this.props.close();
|
||||
} else {
|
||||
this.action.doAction({
|
||||
type: 'ir.actions.act_window_close'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Zurück zum vorherigen Schritt
|
||||
previousStep() {
|
||||
if (this.state.step > 1) {
|
||||
this.state.step--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Registriere als Client Action
|
||||
registry.category("actions").add("employee_image_generator", EmployeeImageGenerator);
|
||||
|
|
@ -0,0 +1,178 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="open_workshop_employee_imagegenerator.ImageGeneratorDialog">
|
||||
<div class="o_employee_image_generator modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<!-- Header -->
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">
|
||||
<i class="fa fa-image me-2"/>
|
||||
Namensschild erstellen
|
||||
</h4>
|
||||
<button type="button" class="btn-close" t-on-click="cancel"/>
|
||||
</div>
|
||||
|
||||
<!-- Body -->
|
||||
<div class="modal-body">
|
||||
<!-- Schritt-Anzeige -->
|
||||
<div class="mb-3">
|
||||
<div class="progress" style="height: 3px;">
|
||||
<div class="progress-bar"
|
||||
role="progressbar"
|
||||
t-att-style="'width: ' + (state.step * 33) + '%'"
|
||||
aria-valuenow="state.step"
|
||||
aria-valuemin="1"
|
||||
aria-valuemax="3"/>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between mt-2 text-muted small">
|
||||
<span t-att-class="{'text-primary fw-bold': state.step === 1}">1. Bild hochladen</span>
|
||||
<span t-att-class="{'text-primary fw-bold': state.step === 2}">2. Bild zuschneiden</span>
|
||||
<span t-att-class="{'text-primary fw-bold': state.step === 3}">3. Text & Vorschau</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Schritt 1: Upload -->
|
||||
<div t-if="state.step === 1" class="text-center p-5">
|
||||
<i class="fa fa-cloud-upload fa-5x text-muted mb-3"/>
|
||||
<h5>Wähle ein Foto aus</h5>
|
||||
<p class="text-muted">Das Bild sollte mindestens 369x492 Pixel groß sein</p>
|
||||
<input type="file"
|
||||
t-ref="fileInput"
|
||||
class="d-none"
|
||||
accept="image/*"
|
||||
t-on-change="onFileSelect"/>
|
||||
<button class="btn btn-primary btn-lg"
|
||||
t-on-click="() => this.fileInput.el.click()">
|
||||
<i class="fa fa-folder-open me-2"/>
|
||||
Datei auswählen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Schritt 2: Crop -->
|
||||
<div t-if="state.step === 2">
|
||||
<div class="alert alert-info mb-3">
|
||||
<i class="fa fa-info-circle me-2"/>
|
||||
Bewege und zoome das Bild, um den gewünschten Ausschnitt zu wählen
|
||||
</div>
|
||||
<div class="img-container" style="max-height: 500px; overflow: hidden; display: flex; align-items: center; justify-content: center;">
|
||||
<img t-ref="cropperImage"
|
||||
t-att-src="state.imageDataUrl"
|
||||
style="max-width: 100%; max-height: 100%; display: block;"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Schritt 3: Text und Vorschau -->
|
||||
<div t-if="state.step === 3">
|
||||
<div class="row">
|
||||
<!-- Linke Spalte: Eingabefelder -->
|
||||
<div class="col-md-6">
|
||||
<h5 class="mb-3">Textinformationen</h5>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="employeeName" class="form-label">Name</label>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
id="employeeName"
|
||||
t-model="state.employeeName"
|
||||
t-on-input="onTextChange"
|
||||
placeholder="Max Mustermann"/>
|
||||
<small class="form-text text-muted">
|
||||
Dieser Name wird auf dem Namensschild angezeigt
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="jobFocus" class="form-label">Schwerpunkte</label>
|
||||
<textarea class="form-control"
|
||||
id="jobFocus"
|
||||
rows="3"
|
||||
t-model="state.jobFocus"
|
||||
t-on-input="onTextChange"
|
||||
placeholder="z.B. Fahrrad-Reparatur, E-Bikes, Werkstatt"/>
|
||||
<small class="form-text text-muted">
|
||||
Tätigkeitsbereiche oder Spezialisierung (optional)
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-light">
|
||||
<strong>Tipp:</strong> Kurze Texte sehen besser aus.
|
||||
Maximal 2 Zeilen für Schwerpunkte.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rechte Spalte: Vorschau -->
|
||||
<div class="col-md-6">
|
||||
<h5 class="mb-3">Vorschau</h5>
|
||||
<div class="border rounded p-3 d-flex justify-content-center align-items-center"
|
||||
style="background: rgb(204, 204, 204); min-height: 400px;">
|
||||
<!-- Wrapper nimmt nur den skalierten Platz ein (21cm × 29.7cm × 0.35 = 7.35cm × 10.4cm) -->
|
||||
<div style="width: 7.35cm; height: 10.4cm; position: relative;">
|
||||
<div style="width: 21cm; height: 29.7cm; transform: scale(0.35); transform-origin: top left; position: absolute; top: 0; left: 0;">
|
||||
<div t-ref="badgeTemplate" class="badge-template">
|
||||
<header>
|
||||
<div class="logo">
|
||||
<img t-if="state.companyLogo" t-att-src="state.companyLogo" alt="Logo" class="logo"/>
|
||||
</div>
|
||||
</header>
|
||||
<div class="content">
|
||||
<div class="headline1">An der Theke für Dich</div>
|
||||
<div class="headline2">Werkstattaufsicht und Werkzeugausgabe</div>
|
||||
<img t-if="state.croppedImageDataUrl"
|
||||
t-att-src="state.croppedImageDataUrl"
|
||||
alt="avatar"
|
||||
class="centered-image"/>
|
||||
<div class="your-name" t-esc="state.employeeName || 'Dein Name'"/>
|
||||
<hr/>
|
||||
<div class="headline3">Themenschwerpunkte:</div>
|
||||
<div class="topics" t-esc="state.jobFocus || 'Deine Schwerpunkte'"/>
|
||||
|
||||
</div>
|
||||
<footer><div class="warnung">! Zugang zur Theke nur für Berechtigte !</div></footer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<small class="text-muted d-block mt-2 text-center">
|
||||
A4-Format (21×29.7cm) - 35% Vorschau
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="modal-footer">
|
||||
<button type="button"
|
||||
class="btn btn-secondary"
|
||||
t-on-click="cancel">
|
||||
Abbrechen
|
||||
</button>
|
||||
|
||||
<button t-if="state.step > 1 and state.step < 3"
|
||||
type="button"
|
||||
class="btn btn-light"
|
||||
t-on-click="previousStep">
|
||||
<i class="fa fa-arrow-left me-2"/>
|
||||
Zurück
|
||||
</button>
|
||||
|
||||
<button t-if="state.step === 2"
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
t-on-click="cropImage">
|
||||
Weiter
|
||||
<i class="fa fa-arrow-right ms-2"/>
|
||||
</button>
|
||||
|
||||
<button t-if="state.step === 3"
|
||||
type="button"
|
||||
class="btn btn-success"
|
||||
t-on-click="saveFinalImage">
|
||||
<i class="fa fa-check me-2"/>
|
||||
Speichern
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Erweitere das Employee Form View -->
|
||||
<record id="view_employee_form_image_generator" model="ir.ui.view">
|
||||
<field name="name">hr.employee.form.image.generator</field>
|
||||
<field name="model">hr.employee</field>
|
||||
<field name="inherit_id" ref="hr.view_employee_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<!-- Füge Button nach dem image_1920 Feld ein -->
|
||||
<xpath expr="//field[@name='image_1920']" position="after">
|
||||
<button name="action_open_image_generator"
|
||||
type="object"
|
||||
string="✏️ Bild bearbeiten"
|
||||
class="btn-link"
|
||||
style="margin-left: 8px;"
|
||||
invisible="not id"/>
|
||||
</xpath>
|
||||
|
||||
<!-- Füge job_focus Feld nach job_title ein -->
|
||||
<xpath expr="//field[@name='job_title']" position="after">
|
||||
<field name="job_focus" placeholder="z.B. Fahrrad-Reparatur, E-Bikes, Werkstatt"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
3
open_workshop_pos/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from . import models
|
||||
from .hooks import post_init_hook
|
||||
69
open_workshop_pos/__manifest__.py
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
'name': 'Open Workshop POS',
|
||||
'version': '18.0.1.0.1',
|
||||
'category': 'Point of Sale',
|
||||
'summary': 'POS Integration für Open Workshop - Machine Access & Customer UI',
|
||||
'description': """
|
||||
Open Workshop POS Integration
|
||||
==============================
|
||||
|
||||
Dieses Modul erweitert den Odoo Point of Sale um Open Workshop Funktionalität:
|
||||
|
||||
* Machine Access List - Anzeige verfügbarer Maschinen
|
||||
* Customer Sidebar - Kundenspezifische Maschinenfreigaben
|
||||
* POS Sidebar - Maschinenauswahl und Status
|
||||
* Product Screen Patches - Integration in POS Workflow
|
||||
|
||||
Abhängigkeiten:
|
||||
* open_workshop_base - Kernfunktionalität
|
||||
* point_of_sale - Odoo POS System
|
||||
|
||||
Autor: HobbyHimmel
|
||||
""",
|
||||
'author': 'HobbyHimmel',
|
||||
'website': 'https://hobbyhimmel.de',
|
||||
'license': 'LGPL-3',
|
||||
'depends': [
|
||||
'open_workshop_base',
|
||||
'point_of_sale',
|
||||
'l10n_de', # Kontenplan für Deutschland (erstellt Bankjournal für POS Demo-Daten)
|
||||
],
|
||||
'data': [
|
||||
# Views
|
||||
],
|
||||
'assets': {
|
||||
'point_of_sale._assets_pos': [
|
||||
# JavaScript
|
||||
'open_workshop_pos/static/src/js/ows_pos_sidebar.js',
|
||||
'open_workshop_pos/static/src/js/ows_pos_customer_sidebar.js',
|
||||
'open_workshop_pos/static/src/js/ows_machine_access_list.js',
|
||||
'open_workshop_pos/static/src/js/ows_product_screen_template_patch.js',
|
||||
'open_workshop_pos/static/src/js/ows_product_screen_default_category.js',
|
||||
'open_workshop_pos/static/src/js/ows_order_patch.js',
|
||||
'open_workshop_pos/static/src/js/ows_receipt_header_patch.js',
|
||||
'open_workshop_pos/static/src/js/ows_control_buttons_patch.js',
|
||||
'open_workshop_pos/static/src/js/ows_product_card_patch.js',
|
||||
'open_workshop_pos/static/src/js/ows_partner_search_mode_patch.js',
|
||||
|
||||
# XML Templates
|
||||
'open_workshop_pos/static/src/xml/ows_pos_sidebar.xml',
|
||||
'open_workshop_pos/static/src/xml/ows_pos_customer_sidebar.xml',
|
||||
'open_workshop_pos/static/src/xml/ows_machine_access_list.xml',
|
||||
'open_workshop_pos/static/src/xml/ows_product_screen_template_patch.xml',
|
||||
'open_workshop_pos/static/src/xml/ows_product_card_patch.xml',
|
||||
'open_workshop_pos/static/src/xml/ows_receipt_header_patch.xml',
|
||||
'open_workshop_pos/static/src/xml/ows_voucher_codes_patch.xml',
|
||||
'open_workshop_pos/static/src/xml/ows_control_buttons_patch.xml',
|
||||
'open_workshop_pos/static/src/xml/ows_payment_screen_patch.xml',
|
||||
'open_workshop_pos/static/src/xml/ows_partner_list_patch.xml',
|
||||
|
||||
# CSS
|
||||
'open_workshop_pos/static/src/css/pos.css',
|
||||
],
|
||||
},
|
||||
'installable': True,
|
||||
'application': False,
|
||||
'auto_install': False,
|
||||
'post_init_hook': 'post_init_hook',
|
||||
}
|
||||