Compare commits
38 Commits
13.0_dev
...
18.0-Multi
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 05f9ef0990 | |||
| 33db478c72 | |||
| 7e8840f2a5 | |||
| d4a835f178 | |||
| 0fe8417602 | |||
| 1f59e16b26 | |||
| 59e4b19dee | |||
| 021d01efe6 | |||
| f4216d790c | |||
| eb17894a13 |
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
|
||||
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": []
|
||||
}
|
||||
]
|
||||
}
|
||||
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
|
||||
|
||||
320
FEATURE_REQUEST/open_workshop_feature_plan.md
Normal file
|
|
@ -0,0 +1,320 @@
|
|||
# 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_api/ # ⏳ GEPLANT – REST API für WordPress
|
||||
```
|
||||
|
||||
**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`
|
||||
- ✅ `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_api ⏳ GEPLANT
|
||||
|
||||
**Status: NOCH NICHT IMPLEMENTIERT**
|
||||
|
||||
Dieses Modul stellt die **REST API** bereit, über die WordPress öffentlich verfügbare Daten abholt.
|
||||
|
||||
## 5.1 Ziele
|
||||
|
||||
- Minimaler externer Zugriff (nur API-Endpunkte)
|
||||
- JSON-Ausgabe für WordPress
|
||||
- Keine Odoo-Website erforderlich
|
||||
- Odoo selbst bleibt **nicht öffentlich erreichbar**
|
||||
|
||||
## 5.2 API-Endpunkte
|
||||
|
||||
Pflicht:
|
||||
|
||||
```
|
||||
GET /api/v1/machines
|
||||
GET /api/v1/machine/<id>
|
||||
```
|
||||
|
||||
Empfohlen:
|
||||
|
||||
```
|
||||
GET /api/v1/areas
|
||||
GET /api/v1/files/<attachment_id>
|
||||
GET /api/v1/events (später für Kurse)
|
||||
```
|
||||
|
||||
## 5.3 JSON Beispiel
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 12,
|
||||
"name": "Formatkreissäge",
|
||||
"status": "available",
|
||||
"area": "Holzwerkstatt",
|
||||
"image_url": "https://odoo.example.org/api/v1/file/29",
|
||||
"manual_url": "https://odoo.example.org/api/v1/file/55",
|
||||
"training_required": true
|
||||
}
|
||||
```
|
||||
|
||||
## 5.4 Sicherheitsmechanismen
|
||||
|
||||
- CORS nur für WordPress-Domain erlauben
|
||||
- Optionaler Token im Header (`Authorization: Bearer <token>`)
|
||||
- Kein Zugriff auf `/web`, `/api/web`, `/pos`
|
||||
- Rate Limiting via Reverse Proxy
|
||||
|
||||
---
|
||||
|
||||
# 6. WordPress Integration ⏳ GEPLANT
|
||||
|
||||
**Status: VORBEREITET (API noch nicht implementiert)**
|
||||
|
||||
WordPress bleibt die öffentliche Website.
|
||||
|
||||
## 6.1 Warum?
|
||||
|
||||
- Sehr hohe Performance (CDN, Caching)
|
||||
- SEO-optimiert
|
||||
- Keine Belastung deiner Internetleitung
|
||||
- Keine Sicherheitsrisiken im internen Odoo
|
||||
- Keine Lizenzkosten
|
||||
|
||||
## 6.2 WordPress Plugin (bereitgestellt)
|
||||
|
||||
Das Plugin:
|
||||
|
||||
- Ruft Odoo-API ab
|
||||
- Rendert Maschinenliste via Shortcode
|
||||
- Konsumiert JSON-Daten
|
||||
- Unterstützt Token-Auth
|
||||
|
||||
Beispiel:
|
||||
|
||||
```
|
||||
[openworkshop_machines]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 7. Finaler Architektur-Plan
|
||||
|
||||
```
|
||||
Internet
|
||||
|
|
||||
WordPress (Frontend, CDN)
|
||||
|
|
||||
fetch JSON via REST API
|
||||
|
|
||||
----------------------------
|
||||
| API Gateway (open_workshop_api)
|
||||
| Exposed only: /api/v1/*
|
||||
----------------------------
|
||||
|
|
||||
Odoo Backend (LAN/VPN)
|
||||
Maschinen, POS, Benutzer, Einweisungen
|
||||
```
|
||||
|
||||
**Kein direkter Zugriff auf Odoo-Weboberfläche!**
|
||||
|
||||
---
|
||||
|
||||
# 8. Sicherheit
|
||||
|
||||
- Reverse Proxy (Traefik/Nginx) muss alle Odoo-Backoffice-URLS sperren
|
||||
- Nur `/api/v1/*` darf öffentlich sein
|
||||
- HTTPS erzwingen
|
||||
- Rate-Limit + Firewall-Regeln
|
||||
- Token-Authentifizierung optional zusätzlich einsetzbar
|
||||
|
||||
---
|
||||
|
||||
# 9. Vorteile dieser Architektur
|
||||
|
||||
✔ **WordPress bleibt ultraschnell** (1000 Besucher/Tag problemlos)
|
||||
✔ **Odoo bleibt sicher** hinter Firewall
|
||||
✔ **Keine Benutzerkosten** → alle Ehrenamtlichen können intern mitarbeiten
|
||||
✔ **Modular, wartbar, zukunftssicher**
|
||||
✔ **API erlaubt feine Steuerung**, welche Daten öffentlich sind
|
||||
✔ **Keine Last auf DSL-Leitung** (WordPress hostet extern)
|
||||
✔ **Maintenance-Integration** ohne Extra-Modul direkt in Base
|
||||
|
||||
---
|
||||
|
||||
# 10. Implementierungsstand (08.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. Migration Workflow | ✅ **FERTIG** | SQL + Python, CI/CD integriert |
|
||||
| 6. open_workshop_api | ⏳ **GEPLANT** | REST API für WordPress |
|
||||
| 7. WordPress Plugin | ⏳ **GEPLANT** | Frontend-Integration |
|
||||
|
||||
---
|
||||
|
||||
# 11. Nächste Schritte
|
||||
|
||||
1. **open_workshop_api** entwickeln
|
||||
- REST Controller implementieren
|
||||
- JSON-Serializer für machines/areas
|
||||
- CORS und Security konfigurieren
|
||||
- Token-Auth optional hinzufügen
|
||||
|
||||
2. **WordPress Plugin** anpassen
|
||||
- API-Endpunkte konfigurieren
|
||||
- Shortcode-Rendering
|
||||
- Caching implementieren
|
||||
|
||||
3. **Testing & Deployment**
|
||||
- API-Tests schreiben
|
||||
- Reverse Proxy konfigurieren
|
||||
- Performance-Tests
|
||||
- Go-Live vorbereiten
|
||||
|
||||
---
|
||||
|
||||
# 12. Endfazit
|
||||
|
||||
Diese Architektur hat sich bewährt:
|
||||
|
||||
- ✅ **Technisch korrekt** – _inherits Pattern statt separatem Modul
|
||||
- ✅ **Performant** – Maintenance.equipment als Single Source of Truth
|
||||
- ✅ **Sicher** – API-Layer trennt intern/extern
|
||||
- ✅ **Kostenoptimiert** – Keine Odoo.sh Lizenzen nötig
|
||||
- ✅ **Langfristig erweiterbar** – Modularer Aufbau
|
||||
- ✅ **Produktiv im Einsatz** – Migration erfolgreich abgeschlossen
|
||||
|
||||
Die API ist **zentrales Zukunftsmodul** für die WordPress-Integration.
|
||||
|
||||
**Letztes Update: 08.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
|
||||
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
from . import models
|
||||
from . import controllers
|
||||
from . import post_init_hook
|
||||
# damit run_migration sichtbar ist:
|
||||
run_migration = post_init_hook.run_migration
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
{
|
||||
'name': 'POS Open Workshop',
|
||||
'license': 'AGPL-3',
|
||||
'version': '13.0.1.0.0',
|
||||
'summary': 'Erstellt Maschinenfreigaben basierend auf POS-Einweisungsprodukten',
|
||||
'depends': ['base','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',
|
||||
'views/assets.xml',
|
||||
'data/data.xml',
|
||||
],
|
||||
'qweb': [
|
||||
'static/src/xml/ows_briefing_details.xml',
|
||||
'static/src/xml/ows_briefing_details_edit.xml',
|
||||
'static/src/xml/ows_pos_order_selector.xml',
|
||||
'static/src/xml/ows_machine_sidebar.xml',
|
||||
'static/src/xml/ows_pos_machine_access_view.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'assets': {
|
||||
'point_of_sale.assets': [
|
||||
'static/src/js/machine_access_sidebar.js',
|
||||
'static/src/css/pos.css',
|
||||
],
|
||||
},
|
||||
'post_init_hook': 'run_migration',
|
||||
'description': """
|
||||
Diese App erstellt Maschinenfreigaben basierend auf POS-Einweisungsprodukten.
|
||||
Die App ist für den Einsatz in der Odoo-Version 13.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")
|
||||
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
|
||||
191
aspl_equipment_qrcode_generator/report/custom_qrcode.xml
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
<?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">
|
||||
<div t-field="equipment.qr_code" t-options="{'widget': 'image'}" 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">
|
||||
<div t-field="equipment.qr_code" t-options="{'widget': 'image'}" 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;">
|
||||
<t t-esc="equipment.name"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="width:25%">Model</td>
|
||||
<td style="width:5%">:</td>
|
||||
<td style="width:35%;word-wrap: break-word;">
|
||||
<t t-esc="equipment.model"/>
|
||||
</td>
|
||||
<td rowspan="5" style="width:35%">
|
||||
<t t-if="equipment.name">
|
||||
<div t-field="equipment.qr_code" t-options="{'widget': 'image'}"
|
||||
style="width:30mm;height:10mm;"/>
|
||||
</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;">
|
||||
<t t-esc="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;">
|
||||
<t t-esc="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;">
|
||||
<t t-esc="equipment.warranty_date"/>
|
||||
</td>
|
||||
<!-- <td style="width:35%;"></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,84 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import base64
|
||||
import math
|
||||
from io import BytesIO
|
||||
|
||||
import qrcode
|
||||
from odoo import models
|
||||
|
||||
|
||||
def generate_qr_code(value):
|
||||
qr = qrcode.QRCode(
|
||||
version=1,
|
||||
error_correction=qrcode.constants.ERROR_CORRECT_L,
|
||||
box_size=20,
|
||||
border=4,
|
||||
)
|
||||
qr.add_data(value)
|
||||
qr.make(fit=True)
|
||||
img = qr.make_image()
|
||||
temp = BytesIO()
|
||||
img.save(temp, format="PNG")
|
||||
qr_img = base64.b64encode(temp.getvalue())
|
||||
return qr_img
|
||||
|
||||
|
||||
def _prepare_data(env, data):
|
||||
equipment_label_layout_id = env['equipment.label.layout'].browse(data['equipment_label_layout_id'])
|
||||
equipment_dict = {}
|
||||
equipment_ids = equipment_label_layout_id.equipment_ids
|
||||
for equipment in equipment_ids:
|
||||
if not equipment.name:
|
||||
continue
|
||||
equipment_dict[equipment] = 1
|
||||
combine_equipment_detail = ""
|
||||
|
||||
# Generate Equipment Redirect LInk
|
||||
url = env['ir.config_parameter'].sudo().get_param('web.base.url')
|
||||
menuId = env.ref('maintenance.menu_equipment_form').sudo().id
|
||||
actionId = env.ref('maintenance.hr_equipment_action').sudo().id
|
||||
|
||||
equipment_link = url + '/web#id=' + str(equipment.id) + '&menu_id=' + str(menuId) + '&action=' + str(
|
||||
actionId) + '&model=maintenance.equipment&view_type=form'
|
||||
|
||||
# Prepare main Equipment Detail
|
||||
main_equipment_detail = ""
|
||||
main_equipment_detail = main_equipment_detail.join(
|
||||
"Name: " + str(equipment.name) + "\n" +
|
||||
"Model: " + str(equipment.model) + "\n" +
|
||||
"Mfg serial no: " + str(equipment.serial_no) + "\n"
|
||||
"Warranty Exp. Date: " +str(equipment.warranty_date) + "\n"
|
||||
"Category: " +str(equipment.category_id.name)
|
||||
)
|
||||
# main_equipment_detail = equipment_link + '\n' + '\n' + main_equipment_detail
|
||||
|
||||
# Prepare Child Equipment Detail
|
||||
combine_equipment_detail = main_equipment_detail
|
||||
|
||||
combine_equipment_detail += '\n' + '\n' + equipment_link
|
||||
|
||||
# Generate Qr Code depends on Details
|
||||
qr_image = generate_qr_code(combine_equipment_detail)
|
||||
equipment.write({
|
||||
'qr_code': qr_image
|
||||
})
|
||||
env.cr.commit()
|
||||
page_numbers = (len(equipment_ids) - 1) // (equipment_label_layout_id.rows * equipment_label_layout_id.columns) + 1
|
||||
|
||||
dict_equipment = {
|
||||
'rows': equipment_label_layout_id.rows,
|
||||
'columns': equipment_label_layout_id.columns,
|
||||
'page_numbers': page_numbers,
|
||||
'equipment_data': equipment_dict
|
||||
}
|
||||
return dict_equipment
|
||||
|
||||
|
||||
class ReportProductTemplateLabel(models.AbstractModel):
|
||||
_name = 'report.aspl_equipment_qrcode_generator.maintenance_quip'
|
||||
_description = 'Equipment QR-code Report'
|
||||
|
||||
def _get_report_values(self, docids, data):
|
||||
return _prepare_data(self.env, data)
|
||||
|
|
@ -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,36 @@
|
|||
# -*- 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.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):
|
||||
xml_id = 'aspl_equipment_qrcode_generator.report_equipment_label'
|
||||
data = {
|
||||
'equipment_label_layout_id':self.id
|
||||
}
|
||||
|
||||
return self.env.ref(xml_id).report_action(None, data=data)
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
<?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>
|
||||
<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>
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
id,name,street,zip,city,phone,email,company_type,customer_rank,supplier_rank
|
||||
res_partner_demo_1, AAAA Max Mustermann,Musterstraße 1,,,,,person,15,0
|
||||
res_partner_demo_2, Benjamin Winter,,,,,,person,1,0
|
||||
res_partner_demo_3, Martin Berthelon,Fabrikstr. 3,73728,Esslingen,,martin.berthelon@hotmail.fr,person,15,0
|
||||
res_partner_demo_4,Aaron Christ,Hohewartstraße 46,70469,Stuttgart,,christ.aaron@web.de,person,14,0
|
||||
res_partner_demo_5,Aaron Dörr,Riegeläckerstr. 60,71229,Leonberg,,aaron_doerr@web.de,person,33,0
|
||||
res_partner_demo_6,Aaron Gale,Chopinstr. 20,70195,Stuttgart,015172165290,aarongale1@live.com,person,4,0
|
||||
res_partner_demo_7,Aaron Zimmermann,Heinrichstr. 15,38106 ,Braunschweig,016091647469,,person,1,0
|
||||
res_partner_demo_8,Abalrahman Alsadi,Bachstr. 29,70563,Stuttgart,,abdulrahman.m.saadi@gmail.com,person,1,0
|
||||
res_partner_demo_9,Abdullah Zengin,Engelbertstr. 124,70499,Stuttgart,,,person,3,0
|
||||
res_partner_demo_10,Abdussamed Korkmaz,Bertha-von-Suttner-Straße 1,74366,Kirchheim Am Neckar,,korkmaz.abdussamed@gmail.com,person,1,0
|
||||
res_partner_demo_11,Achim Brendle,Oberwiesenstraße 45,70619,Stuttgart,7114797505,achim.brendle@web.de,person,2,0
|
||||
res_partner_demo_12,Achim Jatkowski,Hummelstr. 38,70569,Stuttgart,017621512316,achim.jatkowski@gmail.com,person,1,0
|
||||
res_partner_demo_13,Achim Jung,Kurt Tucholsky Str. 6,71254,Ditzingen,07156174013,acjung@web.de,person,1,0
|
||||
res_partner_demo_14,Achim Kelbel,Vivaldiweg 6,70195,Stuttgart,,a.kelbel@t-online.de,person,2,0
|
||||
res_partner_demo_15,Achim Kramer,Reinsburger 172,70197,Stuttgart,,achim@zibra.de,person,1,0
|
||||
res_partner_demo_16,Adalbert Zeisl,Bachstr. 20,71364,Winnenden,07195-2092884,betz1000@gmx.de,person,2,0
|
||||
res_partner_demo_17,Adalina Schäfer,Sancenbacherstr. 26,74538,Rosengarten,015778855550,lina_max_schaefer@gmx.de,person,1,0
|
||||
res_partner_demo_18,Adam Riegel,Marabustr. 35 / 84,70378,Stuttgart,0711 532082,,person,1,0
|
||||
res_partner_demo_19,Adam Swais,Obertürkheimerstr. 54,73733,Esslingen,,adamswais@web.de,person,1,0
|
||||
res_partner_demo_20,Adela Spulber,Obere Bismarck Str. 97,70197,Stuttgart,,,person,1,0
|
||||
res_partner_demo_21,Adem Uzun,Liesel-Bach-Str. 54,71034,Böblingen,015251690873,adem.uzun2@gmail.com,person,1,0
|
||||
res_partner_demo_22,Adnan Djekic,Vesoulerstr. 33,70839,Gerlingen,01724227468,adnandjekic@alice-dsl.net,person,1,0
|
||||
res_partner_demo_23,Adrian Berres,Bärgstadter Str. 90,63928,Gehenbühl,,a.berres@gmx.de,person,1,0
|
||||
res_partner_demo_24,Adrian Lanksweirt,Heidestraße 6,70469,Stuttgart,,adrian.lanksweirt@gmail.com,person,1,0
|
||||
res_partner_demo_25,Adrian Popov,Hallerstr. 42,90419,Nürnberg,+4915114305751,adrinuernberg@gmail.com,person,2,0
|
||||
res_partner_demo_26,Agnes Krettek,Seyfferstr. 62,70187,Stuttgart,,agneskrettek@gmail.com,person,1,0
|
||||
res_partner_demo_27,Ahmad Taijan,Rümelinstr 69,70191,Stuttgart,,,person,2,0
|
||||
res_partner_demo_28,Aileen Becker,Eichendorffstr. 4,73630,Remshalden,015780645637,aileen.becker@gmx.de,person,87,0
|
||||
res_partner_demo_29,Ailey Simpson,Eierstraße 44 A,70199,Stuttgart,,aileywsimpson@gmail.com,person,1,0
|
||||
res_partner_demo_30,Akira Mitsu,Fritz-Ulrich-Weg 5,70567,Stuttgart,,mitsuakira0914@gmail.com,person,5,0
|
||||
res_partner_demo_31,Aksel Özdemir,Rotebühlstraße 53,70178,Stuttgart,,aksel.oezdemir@gmx.de,person,2,0
|
||||
res_partner_demo_32,Albert Ebenbichler,Am Backhaus 9,73666,Boltmannsweiler,01726101655,info@albert-ebenbichler.com,person,1,0
|
||||
res_partner_demo_33,Albert Kaupp,Waldäckerstr. 10,70435,Stuttgart,0711 8263232,albert.kaupp@online.de,person,2,0
|
||||
res_partner_demo_34,Albrecht Barth,Klopstockstr. 39,70193,Stuttgart,,albrecht.barth@web.de,person,3,0
|
||||
res_partner_demo_35,Albrecht Schlayer,Im Netzbrunnen 17,70825,K-Münchingen,,aws1308@gmail.com,person,1,0
|
||||
res_partner_demo_36,Alec Dobler,Kräherwald 251,70193,Stuttgart,,,person,1,0
|
||||
res_partner_demo_37,Alejandro Cano Perez,Burgstallstraße 66,70199,Stuttgart,,cano.perez@gmx.de,person,2,0
|
||||
res_partner_demo_38,Alejandro Rodriguez,Im Hirschwinkel 1,76297,Stutensee,015771409317,ralexei95@yahoo.de,person,1,0
|
||||
res_partner_demo_39,Alejandro Zarza Aguado,Reinsburgstr. 152,70197,Stuttgart,017628401435,11alex96@gmail.com,person,1,0
|
||||
res_partner_demo_40,Aleksandar Vasić,Lothringer Str. 5,70435,Stuttgart,,aleksvasic@web.de,person,3,0
|
||||
res_partner_demo_41,Alen Minasyan,Kastanienallee 41/1,71638,Ludwigsburg,,bidilik@gmx.de,person,1,0
|
||||
res_partner_demo_42,Alex Olenberg,Theodor-Rottschildstr. 25,73760,Stuttgart,,,person,26,0
|
||||
res_partner_demo_43,Alex Schaut,Braunenbergweg 9,70806,Kornwestheim,07154 16530,aschaut@gmx.de,person,3,0
|
||||
res_partner_demo_44,Alexander Adloff,Charlottenstraße 2,74074,Heilbronn,,alexadloff@gmx.de,person,3,0
|
||||
res_partner_demo_45,Alexander Bauer,Im Himmel 20,70569,Stuttgart,071172237601,ab.312@icloud.com,person,1,0
|
||||
res_partner_demo_46,Alexander Blendl,Neckarstr. 8,70736,Fellbach,,blendl.alex@gmail.com,person,4,0
|
||||
res_partner_demo_47,Alexander Borshov,Schellingstraße 24,71277,Rutesheim,,aborshov@gmail.com,person,1,0
|
||||
res_partner_demo_48,Alexander Bosch,Osterwiesenstr. 37,70794,Filderstadt,,bosch-alexander@web.de,person,1,0
|
||||
res_partner_demo_49,Alexander Braig,Holzgrund Str. 25,70806,Kornwestheim,,a.braig84@gmx.de,person,17,0
|
||||
res_partner_demo_50,Alexander Carolus,Kornbergstr. 23,70176,Stuttgart,,alexander.carolus,person,1,0
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
/opt/odoo/odoo/odoo-bin shell -d hobbyhimmel < /home/odoo/custom_addons/open_workshop/demo/export_partner.py
|
||||
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
import csv
|
||||
import random
|
||||
|
||||
# Beispielsweise 50 Kunden mit Namen und E-Mail
|
||||
partners = env['res.partner'].search(
|
||||
[('customer_rank', '>', 0), ('is_company', '=', False)],
|
||||
limit=50
|
||||
)
|
||||
|
||||
with open('/home/odoo/custom_addons/open_workshop/demo/demo_partners.csv', 'w', newline='') as f:
|
||||
writer = csv.writer(f)
|
||||
writer.writerow([
|
||||
'id',
|
||||
'name',
|
||||
'street',
|
||||
'zip',
|
||||
'city',
|
||||
'phone',
|
||||
'email',
|
||||
'company_type',
|
||||
'customer_rank',
|
||||
'supplier_rank'
|
||||
])
|
||||
|
||||
for idx, partner in enumerate(partners, start=1):
|
||||
partner_id = f'res_partner_demo_{idx}'
|
||||
writer.writerow([
|
||||
partner_id,
|
||||
partner.name or '',
|
||||
partner.street or '',
|
||||
partner.zip or '',
|
||||
partner.city or '',
|
||||
partner.phone or '',
|
||||
partner.email or '',
|
||||
partner.company_type or 'person',
|
||||
partner.customer_rank,
|
||||
partner.supplier_rank,
|
||||
])
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo import SUPERUSER_ID
|
||||
from odoo.api import Environment
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
MISSING_PARTNERS = [
|
||||
6534, 1594, 4700, 6557, 5392, 4960, 5226, 6535, 4666
|
||||
]
|
||||
|
||||
def insert_missing_partners(cr, registry):
|
||||
env = Environment(cr, SUPERUSER_ID, {})
|
||||
|
||||
for partner_id in MISSING_PARTNERS:
|
||||
cr.execute("""
|
||||
INSERT INTO res_partner (
|
||||
id, name, customer_rank, create_uid, create_date, write_uid, write_date
|
||||
)
|
||||
VALUES (%s, %s, 1, %s, now(), %s, now())
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
""", (partner_id, f"Fehlender Partner {partner_id}", SUPERUSER_ID, SUPERUSER_ID))
|
||||
|
||||
cr.execute("SELECT setval('res_partner_id_seq', (SELECT MAX(id) FROM res_partner));")
|
||||
|
||||
_logger.info(f"[OWS Repair] {len(MISSING_PARTNERS)} fehlende Partner hinzugefügt.")
|
||||
cr.commit()
|
||||
|
||||
# Automatischer Start in odoo-bin shell
|
||||
if 'env' in globals():
|
||||
insert_missing_partners(env.cr, env.registry)
|
||||
|
|
@ -1,177 +0,0 @@
|
|||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<odoo>
|
||||
<record id="cat_einweisungen" model="product.category">
|
||||
<field name="name">Einweisungen</field>
|
||||
</record>
|
||||
<record id="cat_maschinennutzung" model="product.category">
|
||||
<field name="name">Maschinennutzung</field>
|
||||
</record>
|
||||
<record id="prod_3d_druck_30_minuten" model="product.product">
|
||||
<field name="name">3D Druck (30 Minuten)</field>
|
||||
<field name="default_code" />
|
||||
<field name="list_price">0.25</field>
|
||||
<field name="available_in_pos">True</field>
|
||||
<field ref="cat_maschinennutzung" name="categ_id" />
|
||||
</record>
|
||||
<record id="prod_bandschleifer_1_minute" model="product.product">
|
||||
<field name="name">Bandschleifer (1 Minute)</field>
|
||||
<field name="default_code" />
|
||||
<field name="list_price">0.1</field>
|
||||
<field name="available_in_pos">True</field>
|
||||
<field ref="cat_maschinennutzung" name="categ_id" />
|
||||
</record>
|
||||
<record id="prod_bandsäge_1_minute" model="product.product">
|
||||
<field name="name">Bandsäge (1 Minute)</field>
|
||||
<field name="default_code" />
|
||||
<field name="list_price">0.1</field>
|
||||
<field name="available_in_pos">True</field>
|
||||
<field ref="cat_maschinennutzung" name="categ_id" />
|
||||
</record>
|
||||
<record id="prod_cnc_fräse_1_minute" model="product.product">
|
||||
<field name="name">CNC Fräse (1 Minute)</field>
|
||||
<field name="default_code" />
|
||||
<field name="list_price">0.1</field>
|
||||
<field name="available_in_pos">True</field>
|
||||
<field ref="cat_maschinennutzung" name="categ_id" />
|
||||
</record>
|
||||
<record id="prod_cnc_sicherheitseinweisung" model="product.product">
|
||||
<field name="name">CNC Sicherheitseinweisung</field>
|
||||
<field name="default_code" />
|
||||
<field name="list_price">25.0</field>
|
||||
<field name="available_in_pos">True</field>
|
||||
<field ref="cat_einweisungen" name="categ_id" />
|
||||
</record>
|
||||
<record id="prod_drehbank_1_minute" model="product.product">
|
||||
<field name="name">Drehbank (1 Minute)</field>
|
||||
<field name="default_code" />
|
||||
<field name="list_price">0.1</field>
|
||||
<field name="available_in_pos">True</field>
|
||||
<field ref="cat_maschinennutzung" name="categ_id" />
|
||||
</record>
|
||||
<record id="prod_einweisung_3d_drucker_delta" model="product.product">
|
||||
<field name="name">Einweisung 3D Drucker Delta</field>
|
||||
<field name="default_code" />
|
||||
<field name="list_price">15.0</field>
|
||||
<field name="available_in_pos">True</field>
|
||||
<field ref="cat_einweisungen" name="categ_id" />
|
||||
</record>
|
||||
<record id="prod_einweisung_3d_drucker_prusa" model="product.product">
|
||||
<field name="name">Einweisung 3D Drucker Prusa</field>
|
||||
<field name="default_code" />
|
||||
<field name="list_price">20.0</field>
|
||||
<field name="available_in_pos">True</field>
|
||||
<field ref="cat_einweisungen" name="categ_id" />
|
||||
</record>
|
||||
<record id="prod_einweisung_bandsäge" model="product.product">
|
||||
<field name="name">Einweisung Bandsäge</field>
|
||||
<field name="default_code" />
|
||||
<field name="list_price">15.0</field>
|
||||
<field name="available_in_pos">True</field>
|
||||
<field ref="cat_einweisungen" name="categ_id" />
|
||||
</record>
|
||||
<record id="prod_einweisung_drehbank" model="product.product">
|
||||
<field name="name">Einweisung Drehbank</field>
|
||||
<field name="default_code" />
|
||||
<field name="list_price">20.0</field>
|
||||
<field name="available_in_pos">True</field>
|
||||
<field ref="cat_einweisungen" name="categ_id" />
|
||||
</record>
|
||||
<record id="prod_einweisung_fks" model="product.product">
|
||||
<field name="name">Einweisung FKS</field>
|
||||
<field name="default_code" />
|
||||
<field name="list_price">20.0</field>
|
||||
<field name="available_in_pos">True</field>
|
||||
<field ref="cat_einweisungen" name="categ_id" />
|
||||
</record>
|
||||
<record id="prod_einweisung_hobel" model="product.product">
|
||||
<field name="name">Einweisung Hobel</field>
|
||||
<field name="default_code" />
|
||||
<field name="list_price">15.0</field>
|
||||
<field name="available_in_pos">True</field>
|
||||
<field ref="cat_einweisungen" name="categ_id" />
|
||||
</record>
|
||||
<record id="prod_einweisung_laser" model="product.product">
|
||||
<field name="name">Einweisung Laser</field>
|
||||
<field name="default_code" />
|
||||
<field name="list_price">15.0</field>
|
||||
<field name="available_in_pos">True</field>
|
||||
<field ref="cat_einweisungen" name="categ_id" />
|
||||
</record>
|
||||
<record id="prod_einweisung_metallfräse" model="product.product">
|
||||
<field name="name">Einweisung Metallfräse</field>
|
||||
<field name="default_code" />
|
||||
<field name="list_price">20.0</field>
|
||||
<field name="available_in_pos">True</field>
|
||||
<field ref="cat_einweisungen" name="categ_id" />
|
||||
</record>
|
||||
<record id="prod_einweisung_schweißgerät" model="product.product">
|
||||
<field name="name">Einweisung Schweißgerät</field>
|
||||
<field name="default_code" />
|
||||
<field name="list_price">10.0</field>
|
||||
<field name="available_in_pos">True</field>
|
||||
<field ref="cat_einweisungen" name="categ_id" />
|
||||
</record>
|
||||
<record id="prod_einweisung_in_maschinelle_holzverbindungen" model="product.product">
|
||||
<field name="name">Einweisung in maschinelle Holzverbindungen</field>
|
||||
<field name="default_code" />
|
||||
<field name="list_price">15.0</field>
|
||||
<field name="available_in_pos">True</field>
|
||||
<field ref="cat_einweisungen" name="categ_id" />
|
||||
</record>
|
||||
<record id="prod_formatkreissäge_1_minute" model="product.product">
|
||||
<field name="name">Formatkreissäge (1 Minute)</field>
|
||||
<field name="default_code" />
|
||||
<field name="list_price">0.1</field>
|
||||
<field name="available_in_pos">True</field>
|
||||
<field ref="cat_maschinennutzung" name="categ_id" />
|
||||
</record>
|
||||
<record id="prod_fräse___deckel_1_minute" model="product.product">
|
||||
<field name="name">Fräse - Deckel (1 Minute)</field>
|
||||
<field name="default_code" />
|
||||
<field name="list_price">0.1</field>
|
||||
<field name="available_in_pos">True</field>
|
||||
<field ref="cat_maschinennutzung" name="categ_id" />
|
||||
</record>
|
||||
<record id="prod_hobel_1_minute" model="product.product">
|
||||
<field name="name">Hobel (1 Minute)</field>
|
||||
<field name="default_code" />
|
||||
<field name="list_price">0.1</field>
|
||||
<field name="available_in_pos">True</field>
|
||||
<field ref="cat_maschinennutzung" name="categ_id" />
|
||||
</record>
|
||||
<record id="prod_laser_aktivminute" model="product.product">
|
||||
<field name="name">Laser (Aktivminute)</field>
|
||||
<field name="default_code" />
|
||||
<field name="list_price">0.7000000000000001</field>
|
||||
<field name="available_in_pos">True</field>
|
||||
<field ref="cat_maschinennutzung" name="categ_id" />
|
||||
</record>
|
||||
<record id="prod_sandstrahlbox_1_minute" model="product.product">
|
||||
<field name="name">Sandstrahlbox (1 Minute)</field>
|
||||
<field name="default_code" />
|
||||
<field name="list_price">0.2</field>
|
||||
<field name="available_in_pos">True</field>
|
||||
<field ref="cat_maschinennutzung" name="categ_id" />
|
||||
</record>
|
||||
<record id="prod_schweißgerät_1_minute" model="product.product">
|
||||
<field name="name">Schweißgerät (1 Minute)</field>
|
||||
<field name="default_code" />
|
||||
<field name="list_price">0.2</field>
|
||||
<field name="available_in_pos">True</field>
|
||||
<field ref="cat_maschinennutzung" name="categ_id" />
|
||||
</record>
|
||||
<record id="prod_schweißkabine_eigenes_schweißgerät___1_minute" model="product.product">
|
||||
<field name="name">Schweißkabine (eigenes Schweißgerät - 1 Minute)</field>
|
||||
<field name="default_code" />
|
||||
<field name="list_price">0.1</field>
|
||||
<field name="available_in_pos">True</field>
|
||||
<field ref="cat_maschinennutzung" name="categ_id" />
|
||||
</record>
|
||||
<record id="prod_sonstige_dienstleistungen_nutzung" model="product.product">
|
||||
<field name="name">Sonstige Dienstleistungen/Nutzung</field>
|
||||
<field name="default_code" />
|
||||
<field name="list_price">1.0</field>
|
||||
<field name="available_in_pos">True</field>
|
||||
<field ref="cat_maschinennutzung" name="categ_id" />
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
/opt/odoo/odoo/odoo-bin shell -d hobbyhimmel < /home/odoo/custom_addons/open_workshop/data/export_products_and_categories.py
|
||||
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
# /opt/odoo/odoo/odoo-bin shell -d <alte datebase> < export_categories.py
|
||||
import csv
|
||||
from odoo import api, SUPERUSER_ID
|
||||
import os
|
||||
|
||||
|
||||
categories = env['product.category'].search([('name', 'in', ['Einweisungen', 'Maschinennutzung'])])
|
||||
file_path = os.path.join(os.getcwd(), 'product_category.csv')
|
||||
|
||||
with open(file_path, 'w', newline='') as csvfile:
|
||||
writer = csv.writer(csvfile)
|
||||
writer.writerow(['id', 'name', 'parent_id/id'])
|
||||
for cat in categories:
|
||||
xml_id = f"open_workshop.cat_{cat.name.lower().replace(' ', '_')}"
|
||||
parent_id = cat.parent_id and f"base.{cat.parent_id.xml_id}" or ''
|
||||
writer.writerow([xml_id, cat.name, parent_id])
|
||||
|
||||
# Aufruf in odoo shell z. B.:
|
||||
# env = odoo.api.Environment(cr, SUPERUSER_ID, {})
|
||||
# export_categories(env)
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
# /opt/odoo/odoo/odoo-bin shell -d <alte datebase> < export_products.py
|
||||
import csv
|
||||
from odoo import api, SUPERUSER_ID
|
||||
import os
|
||||
|
||||
# Kategorien suchen
|
||||
category_names = ['Einweisungen', 'Maschinennutzung']
|
||||
categories = env['product.category'].search([('name', 'in', category_names)])
|
||||
products = env['product.product'].search([('categ_id', 'in', categories.ids)])
|
||||
|
||||
file_path = os.path.join(os.getcwd(), 'product_product.csv')
|
||||
|
||||
with open(file_path, 'w', newline='') as csvfile:
|
||||
writer = csv.writer(csvfile)
|
||||
writer.writerow(['id', 'name', 'default_code', 'list_price', 'categ_id/id', 'available_in_pos'])
|
||||
for prod in products:
|
||||
cat_xml_id = f"open_workshop.cat_{prod.categ_id.name.lower().replace(' ', '_')}"
|
||||
xml_id = f"open_workshop.prod_{prod.default_code or prod.name.lower().replace(' ', '_')}"
|
||||
writer.writerow([
|
||||
xml_id,
|
||||
prod.name,
|
||||
prod.default_code or '',
|
||||
prod.list_price,
|
||||
cat_xml_id,
|
||||
'1' if prod.available_in_pos else '0'
|
||||
])
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# /opt/odoo/odoo/odoo-bin shell -d <alte datebase> < export_products_and_categories.py
|
||||
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
def xml_safe_id(name):
|
||||
return name.lower().replace(' ', '_').replace('/', '_').replace('(', '').replace(')', '').replace('-', '_')
|
||||
|
||||
def export_categories_and_products(env):
|
||||
root = ET.Element("odoo")
|
||||
|
||||
# Export Kategorien
|
||||
categories = env['product.category'].search([
|
||||
('name', 'in', ['Einweisungen', 'Maschinennutzung'])
|
||||
])
|
||||
for cat in categories:
|
||||
record = ET.SubElement(root, "record", {
|
||||
"id": f"cat_{xml_safe_id(cat.name)}",
|
||||
"model": "product.category"
|
||||
})
|
||||
ET.SubElement(record, "field", name="name").text = cat.name
|
||||
if cat.parent_id:
|
||||
ET.SubElement(record, "field", name="parent_id", attrib={"ref": f"cat_{xml_safe_id(cat.parent_id.name)}"})
|
||||
|
||||
# Export Produkte
|
||||
products = env['product.product'].search([
|
||||
('categ_id.name', 'in', ['Einweisungen', 'Maschinennutzung'])
|
||||
])
|
||||
for product in products:
|
||||
record = ET.SubElement(root, "record", {
|
||||
"id": f"prod_{xml_safe_id(product.name)}",
|
||||
"model": "product.product"
|
||||
})
|
||||
ET.SubElement(record, "field", name="name").text = product.name or ''
|
||||
ET.SubElement(record, "field", name="default_code").text = product.default_code or ''
|
||||
ET.SubElement(record, "field", name="list_price").text = str(product.list_price or 0.0)
|
||||
ET.SubElement(record, "field", name="available_in_pos").text = "True"
|
||||
if product.categ_id:
|
||||
ET.SubElement(record, "field", name="categ_id", attrib={"ref": f"cat_{xml_safe_id(product.categ_id.name)}"})
|
||||
|
||||
tree = ET.ElementTree(root)
|
||||
tree.write("data_product_and_categories.xml", encoding="utf-8", xml_declaration=True)
|
||||
print("✅ XML export saved to data_product_and_categories.xml")
|
||||
|
||||
# Automatischer Start in odoo-bin shell
|
||||
if 'env' in globals():
|
||||
export_categories_and_products(env)
|
||||
|
|
@ -1 +0,0 @@
|
|||
/opt/odoo/odoo/odoo-bin -d hobbyhimmel13_dev -i open_workshop --stop-after-init
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
<odoo>
|
||||
<record id="machine_prusa_training_prod_einweisung_3d_drucker_prusa" model="ows.machine.training">
|
||||
<field name="machine_id" ref="machine_prusa"/>
|
||||
<field name="training_id" ref="prod_einweisung_3d_drucker_prusa"/>
|
||||
</record>
|
||||
<record id="machine_formatkreissaege_usage_prod_formatkreissäge_1_minute" model="ows.machine.product">
|
||||
<field name="machine_id" ref="machine_formatkreissaege"/>
|
||||
<field name="product_id" ref="prod_formatkreissäge_1_minute"/>
|
||||
</record>
|
||||
<record id="machine_bandsaege_holz_usage_prod_bandsäge_1_minute" model="ows.machine.product">
|
||||
<field name="machine_id" ref="machine_bandsaege_holz"/>
|
||||
<field name="product_id" ref="prod_bandsäge_1_minute"/>
|
||||
</record>
|
||||
<record id="machine_bandsaege_holz_training_prod_einweisung_bandsäge" model="ows.machine.training">
|
||||
<field name="machine_id" ref="machine_bandsaege_holz"/>
|
||||
<field name="training_id" ref="prod_einweisung_bandsäge"/>
|
||||
</record>
|
||||
<record id="machine_kreissaege_metall_usage_prod_formatkreissäge_1_minute" model="ows.machine.product">
|
||||
<field name="machine_id" ref="machine_kreissaege_metall"/>
|
||||
<field name="product_id" ref="prod_formatkreissäge_1_minute"/>
|
||||
</record>
|
||||
<record id="machine_bandsaege_metall_usage_prod_bandsäge_1_minute" model="ows.machine.product">
|
||||
<field name="machine_id" ref="machine_bandsaege_metall"/>
|
||||
<field name="product_id" ref="prod_bandsäge_1_minute"/>
|
||||
</record>
|
||||
<record id="machine_bandsaege_metall_training_prod_einweisung_bandsäge" model="ows.machine.training">
|
||||
<field name="machine_id" ref="machine_bandsaege_metall"/>
|
||||
<field name="training_id" ref="prod_einweisung_bandsäge"/>
|
||||
</record>
|
||||
<record id="machine_drehbank_usage_prod_drehbank_1_minute" model="ows.machine.product">
|
||||
<field name="machine_id" ref="machine_drehbank"/>
|
||||
<field name="product_id" ref="prod_drehbank_1_minute"/>
|
||||
</record>
|
||||
<record id="machine_drehbank_training_prod_einweisung_drehbank" model="ows.machine.training">
|
||||
<field name="machine_id" ref="machine_drehbank"/>
|
||||
<field name="training_id" ref="prod_einweisung_drehbank"/>
|
||||
</record>
|
||||
<record id="machine_fraese_usage_prod_cnc_fräse_1_minute" model="ows.machine.product">
|
||||
<field name="machine_id" ref="machine_fraese"/>
|
||||
<field name="product_id" ref="prod_cnc_fräse_1_minute"/>
|
||||
</record>
|
||||
<record id="machine_fraese_usage_prod_fräse___deckel_1_minute" model="ows.machine.product">
|
||||
<field name="machine_id" ref="machine_fraese"/>
|
||||
<field name="product_id" ref="prod_fräse___deckel_1_minute"/>
|
||||
</record>
|
||||
<record id="machine_fraese_training_prod_einweisung_metallfräse" model="ows.machine.training">
|
||||
<field name="machine_id" ref="machine_fraese"/>
|
||||
<field name="training_id" ref="prod_einweisung_metallfräse"/>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -1 +0,0 @@
|
|||
/opt/odoo/odoo/odoo-bin -d hobbyhimmel --update=open_workshop --dev=all --stop-after-init
|
||||
2
log/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
*
|
||||
!.gitignore
|
||||
300
open_workshop-api-for-wordpress/openworkshop-odoo-api.php
Normal file
|
|
@ -0,0 +1,300 @@
|
|||
<?php
|
||||
/**
|
||||
* Plugin Name: OpenWorkshop Odoo API
|
||||
* Plugin URI: https://hobbyhimmel.de/
|
||||
* Description: Bindet Daten aus Odoo (OpenWorkshop) per REST-API in WordPress ein. Stellt u.a. den Shortcode [openworkshop_machines] bereit.
|
||||
* Version: 0.1.0
|
||||
* Author: HobbyHimmel / Matthias Lotz
|
||||
* License: GPLv3
|
||||
* License URI: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
* Text Domain: openworkshop-odoo-api
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class OpenWorkshop_Odoo_API_Plugin {
|
||||
|
||||
const OPTION_GROUP = 'openworkshop_odoo_api_options';
|
||||
const OPTION_NAME = 'openworkshop_odoo_api_settings';
|
||||
|
||||
public function __construct() {
|
||||
add_action( 'admin_menu', array( $this, 'register_settings_page' ) );
|
||||
add_action( 'admin_init', array( $this, 'register_settings' ) );
|
||||
add_shortcode( 'openworkshop_machines', array( $this, 'shortcode_machines' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register settings page under Settings → OpenWorkshop Odoo API
|
||||
*/
|
||||
public function register_settings_page() {
|
||||
add_options_page(
|
||||
__( 'OpenWorkshop Odoo API', 'openworkshop-odoo-api' ),
|
||||
__( 'OpenWorkshop Odoo API', 'openworkshop-odoo-api' ),
|
||||
'manage_options',
|
||||
'openworkshop-odoo-api',
|
||||
array( $this, 'render_settings_page' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register settings (base URL + optional token)
|
||||
*/
|
||||
public function register_settings() {
|
||||
register_setting(
|
||||
self::OPTION_GROUP,
|
||||
self::OPTION_NAME,
|
||||
array(
|
||||
'type' => 'array',
|
||||
'sanitize_callback' => array( $this, 'sanitize_settings' ),
|
||||
'default' => array(
|
||||
'base_url' => '',
|
||||
'api_token' => '',
|
||||
'machines_endpoint' => '/api/v1/machines',
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
add_settings_section(
|
||||
'openworkshop_odoo_api_main',
|
||||
__( 'Odoo API Einstellungen', 'openworkshop-odoo-api' ),
|
||||
'__return_false',
|
||||
'openworkshop-odoo-api'
|
||||
);
|
||||
|
||||
add_settings_field(
|
||||
'base_url',
|
||||
__( 'Odoo Basis-URL', 'openworkshop-odoo-api' ),
|
||||
array( $this, 'render_field_base_url' ),
|
||||
'openworkshop-odoo-api',
|
||||
'openworkshop_odoo_api_main'
|
||||
);
|
||||
|
||||
add_settings_field(
|
||||
'machines_endpoint',
|
||||
__( 'Endpoint für Maschinenliste', 'openworkshop-odoo-api' ),
|
||||
array( $this, 'render_field_machines_endpoint' ),
|
||||
'openworkshop-odoo-api',
|
||||
'openworkshop_odoo_api_main'
|
||||
);
|
||||
|
||||
add_settings_field(
|
||||
'api_token',
|
||||
__( 'API Token (optional)', 'openworkshop-odoo-api' ),
|
||||
array( $this, 'render_field_api_token' ),
|
||||
'openworkshop-odoo-api',
|
||||
'openworkshop_odoo_api_main'
|
||||
);
|
||||
}
|
||||
|
||||
public function sanitize_settings( $input ) {
|
||||
$output = array();
|
||||
|
||||
$output['base_url'] = isset( $input['base_url'] )
|
||||
? esc_url_raw( rtrim( $input['base_url'], '/' ) )
|
||||
: '';
|
||||
|
||||
$output['machines_endpoint'] = isset( $input['machines_endpoint'] )
|
||||
? sanitize_text_field( $input['machines_endpoint'] )
|
||||
: '/api/v1/machines';
|
||||
|
||||
$output['api_token'] = isset( $input['api_token'] )
|
||||
? sanitize_text_field( $input['api_token'] )
|
||||
: '';
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
public function get_settings() {
|
||||
$settings = get_option( self::OPTION_NAME, array() );
|
||||
$defaults = array(
|
||||
'base_url' => '',
|
||||
'api_token' => '',
|
||||
'machines_endpoint' => '/api/v1/machines',
|
||||
);
|
||||
|
||||
return wp_parse_args( $settings, $defaults );
|
||||
}
|
||||
|
||||
public function render_settings_page() {
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$settings = $this->get_settings();
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1><?php esc_html_e( 'OpenWorkshop Odoo API Einstellungen', 'openworkshop-odoo-api' ); ?></h1>
|
||||
<form method="post" action="options.php">
|
||||
<?php
|
||||
settings_fields( self::OPTION_GROUP );
|
||||
do_settings_sections( 'openworkshop-odoo-api' );
|
||||
submit_button();
|
||||
?>
|
||||
</form>
|
||||
|
||||
<h2><?php esc_html_e( 'Shortcodes', 'openworkshop-odoo-api' ); ?></h2>
|
||||
<p><?php esc_html_e( 'Maschinenliste anzeigen:', 'openworkshop-odoo-api' ); ?></p>
|
||||
<code>[openworkshop_machines]</code>
|
||||
<p><?php esc_html_e( 'Optional können Attribute verwendet werden, um z. B. eine andere Anzahl von Maschinen anzuzeigen.', 'openworkshop-odoo-api' ); ?></p>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
public function render_field_base_url() {
|
||||
$settings = $this->get_settings();
|
||||
?>
|
||||
<input type="text"
|
||||
name="<?php echo esc_attr( self::OPTION_NAME ); ?>[base_url]"
|
||||
value="<?php echo esc_attr( $settings['base_url'] ); ?>"
|
||||
class="regular-text"
|
||||
placeholder="https://odoo.example.org" />
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'Basis-URL deiner Odoo-Instanz (ohne Slash am Ende). Die API-Route wird daran angehängt.', 'openworkshop-odoo-api' ); ?>
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
|
||||
public function render_field_machines_endpoint() {
|
||||
$settings = $this->get_settings();
|
||||
?>
|
||||
<input type="text"
|
||||
name="<?php echo esc_attr( self::OPTION_NAME ); ?>[machines_endpoint]"
|
||||
value="<?php echo esc_attr( $settings['machines_endpoint'] ); ?>"
|
||||
class="regular-text"
|
||||
placeholder="/api/v1/machines" />
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'Relativer Pfad zum Maschinen-Endpoint. Standard: /api/v1/machines', 'openworkshop-odoo-api' ); ?>
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
|
||||
public function render_field_api_token() {
|
||||
$settings = $this->get_settings();
|
||||
?>
|
||||
<input type="text"
|
||||
name="<?php echo esc_attr( self::OPTION_NAME ); ?>[api_token]"
|
||||
value="<?php echo esc_attr( $settings['api_token'] ); ?>"
|
||||
class="regular-text" />
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'Optionaler API-Token, der im Authorization-Header gesendet wird (Bearer <token>).', 'openworkshop-odoo-api' ); ?>
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Shortcode: [openworkshop_machines]
|
||||
* Attributes:
|
||||
* limit – Anzahl der Einträge (optional)
|
||||
*/
|
||||
public function shortcode_machines( $atts ) {
|
||||
$atts = shortcode_atts(
|
||||
array(
|
||||
'limit' => 0,
|
||||
),
|
||||
$atts,
|
||||
'openworkshop_machines'
|
||||
);
|
||||
|
||||
$data = $this->fetch_machines();
|
||||
|
||||
if ( is_wp_error( $data ) ) {
|
||||
return '<p>' . esc_html__( 'Fehler beim Laden der Maschinendaten aus Odoo.', 'openworkshop-odoo-api' ) . '</p>';
|
||||
}
|
||||
|
||||
if ( ! is_array( $data ) || empty( $data ) ) {
|
||||
return '<p>' . esc_html__( 'Keine Maschinen gefunden.', 'openworkshop-odoo-api' ) . '</p>';
|
||||
}
|
||||
|
||||
$limit = intval( $atts['limit'] );
|
||||
if ( $limit > 0 ) {
|
||||
$data = array_slice( $data, 0, $limit );
|
||||
}
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
<div class="openworkshop-machines">
|
||||
<table class="openworkshop-machines-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?php esc_html_e( 'Name', 'openworkshop-odoo-api' ); ?></th>
|
||||
<th><?php esc_html_e( 'Bereich', 'openworkshop-odoo-api' ); ?></th>
|
||||
<th><?php esc_html_e( 'Status', 'openworkshop-odoo-api' ); ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ( $data as $machine ) : ?>
|
||||
<tr>
|
||||
<td>
|
||||
<?php echo isset( $machine['name'] ) ? esc_html( $machine['name'] ) : ''; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php echo isset( $machine['area'] ) ? esc_html( $machine['area'] ) : ''; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php echo isset( $machine['status'] ) ? esc_html( $machine['status'] ) : ''; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the Odoo API and returns decoded JSON or WP_Error.
|
||||
*/
|
||||
protected function fetch_machines() {
|
||||
$settings = $this->get_settings();
|
||||
|
||||
if ( empty( $settings['base_url'] ) ) {
|
||||
return new WP_Error( 'openworkshop_no_base_url', __( 'Keine Odoo Basis-URL konfiguriert.', 'openworkshop-odoo-api' ) );
|
||||
}
|
||||
|
||||
$url = $settings['base_url'] . $settings['machines_endpoint'];
|
||||
|
||||
$args = array(
|
||||
'timeout' => 10,
|
||||
'headers' => array(
|
||||
'Accept' => 'application/json',
|
||||
),
|
||||
);
|
||||
|
||||
if ( ! empty( $settings['api_token'] ) ) {
|
||||
$args['headers']['Authorization'] = 'Bearer ' . $settings['api_token'];
|
||||
}
|
||||
|
||||
$response = wp_remote_get( $url, $args );
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$code = wp_remote_retrieve_response_code( $response );
|
||||
$body = wp_remote_retrieve_body( $response );
|
||||
|
||||
if ( $code < 200 || $code >= 300 ) {
|
||||
return new WP_Error( 'openworkshop_bad_status', sprintf( 'HTTP %d', $code ) );
|
||||
}
|
||||
|
||||
$data = json_decode( $body, true );
|
||||
if ( json_last_error() !== JSON_ERROR_NONE ) {
|
||||
return new WP_Error( 'openworkshop_json_error', json_last_error_msg() );
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
||||
function openworkshop_odoo_api_bootstrap() {
|
||||
static $instance = null;
|
||||
if ( $instance === null ) {
|
||||
$instance = new OpenWorkshop_Odoo_API_Plugin();
|
||||
}
|
||||
return $instance;
|
||||
}
|
||||
|
||||
openworkshop_odoo_api_bootstrap();
|
||||
30
open_workshop-api-for-wordpress/readme.txt
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
=== OpenWorkshop Odoo API ===
|
||||
Contributors: hobbyhimmel
|
||||
Tags: odoo, api, openworkshop, integration
|
||||
Requires at least: 5.8
|
||||
Tested up to: 6.6
|
||||
Stable tag: 0.1.0
|
||||
License: GPLv3
|
||||
License URI: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
Dieses Plugin bindet Maschinendaten aus einer Odoo/OpenWorkshop-Installation via REST-API in WordPress ein.
|
||||
|
||||
== Beschreibung ==
|
||||
|
||||
Das Plugin stellt u.a. den Shortcode [openworkshop_machines] bereit, der eine einfache Maschinenliste
|
||||
auf Basis eines JSON-Endpunkts in Odoo rendert.
|
||||
|
||||
In den Einstellungen kann die Basis-URL der Odoo-Instanz, der Endpoint (z.B. /api/v1/machines) sowie ein
|
||||
optionaler API-Token hinterlegt werden.
|
||||
|
||||
== Installation ==
|
||||
|
||||
1. ZIP in WordPress unter Plugins → Installieren → Plugin hochladen hochladen.
|
||||
2. Aktivieren.
|
||||
3. Unter Einstellungen → OpenWorkshop Odoo API die Basis-URL und den Endpoint konfigurieren.
|
||||
4. Den Shortcode [openworkshop_machines] in einer Seite oder einem Beitrag einfügen.
|
||||
|
||||
== Changelog ==
|
||||
|
||||
= 0.1.0 =
|
||||
* Erste Version.
|
||||
11
open_workshop.code-workspace
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
},
|
||||
{
|
||||
"path": "../../../usr/lib/python3/dist-packages/odoo"
|
||||
}
|
||||
],
|
||||
"settings": {}
|
||||
}
|
||||
3
open_workshop/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from . import models
|
||||
from . import controllers
|
||||
|
||||
39
open_workshop/__manifest__.py
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
'name': 'POS Open Workshop',
|
||||
'license': 'AGPL-3',
|
||||
'version': '18.0.1.0.1',
|
||||
'summary': 'Erstellt Maschinenfreigaben basierend auf POS-Einweisungsprodukten',
|
||||
'depends': ['base', 'account', 'hr','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': False, # Wird durch open_workshop_base ersetzt
|
||||
'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.
|
||||
""",
|
||||
}
|
||||
|
|
@ -1,11 +1,14 @@
|
|||
# Datei: controllers/pos_access.py
|
||||
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
|
||||
class OpenWorkshopPOSController(http.Controller):
|
||||
|
||||
@http.route('/open_workshop/partner_access', type='json', auth='user')
|
||||
def get_partner_machine_access(self, partner_id):
|
||||
def get_partner_machine_access(self, **kwargs):
|
||||
partner_id = kwargs.get('params', {}).get('partner_id')
|
||||
if not partner_id:
|
||||
return {"error": "Missing partner_id"}
|
||||
|
||||
Machine = request.env['ows.machine'].sudo()
|
||||
return Machine.get_access_list_grouped(partner_id)
|
||||
|
||||
|
|
@ -8,6 +8,42 @@ import logging
|
|||
_logger = logging.getLogger(__name__)
|
||||
_logger.info("✅ ows_models.py geladen")
|
||||
|
||||
class HREmployee(models.Model):
|
||||
_inherit = 'hr.employee'
|
||||
|
||||
@api.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})")
|
||||
|
||||
# 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': 'office@hobbyhimmel.de',
|
||||
'work_phone': False,
|
||||
'active': True, # Reaktivieren falls archiviert
|
||||
})
|
||||
_logger.info(f"[OWS] Admin-Angestellter reaktiviert und umbenannt: {admin_employee.name} (ID: {admin_employee.id})")
|
||||
else:
|
||||
_logger.warning("[OWS] Kein Angestellter für Administrator gefunden.")
|
||||
return
|
||||
|
||||
# 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))
|
||||
|
||||
|
||||
class ResPartner(models.Model):
|
||||
_inherit = 'res.partner'
|
||||
_logger.info("✅ ows ResPartner geladen")
|
||||
|
|
@ -141,32 +177,50 @@ class ResPartner(models.Model):
|
|||
def _compute_machine_access_html(self):
|
||||
areas = self.env['ows.machine.area'].search([], order="name")
|
||||
for partner in self:
|
||||
html = "<div style='display: grid; grid-template-columns: repeat(3, 1fr); gap: 2rem;'>"
|
||||
html = ""
|
||||
for area in areas:
|
||||
html += f"<div class='o_group' style='margin-bottom: 1em;'>"
|
||||
html += f"<table class='o_group o_inner_group'>"
|
||||
html += f"<thead><tr><th>{area.name}</th><th></th><th>Datum</th><th>Gültig bis</th></tr></thead><tbody>"
|
||||
html += f"""
|
||||
<div class="o_form_sheet">
|
||||
<h3 class="o_form_label">{area.name}</h3>
|
||||
<table class="table table-sm table-bordered o_form_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Maschine</th>
|
||||
<th>Status</th>
|
||||
<th>Datum</th>
|
||||
<th>Gültig bis</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
"""
|
||||
|
||||
machines = self.env['ows.machine'].search([('area_id', '=', area.id)], order="name")
|
||||
|
||||
for machine in machines:
|
||||
access = self.env['ows.machine.access'].search([
|
||||
('partner_id', '=', partner.id),
|
||||
('machine_id', '=', machine.id),
|
||||
], limit=1)
|
||||
icon = "<span class='fa fa-check text-success'></span>" if access else "<span class='fa fa-times text-danger'></span>"
|
||||
date_granted = access.date_granted.strftime('%Y-%m-%d') if access and access.date_granted else ""
|
||||
date_expiry = access.date_expiry.strftime('%Y-%m-%d') if access and access.date_expiry else ""
|
||||
icon = '<span class="text-success fa fa-check"/>' if access else '<span class="text-danger fa fa-times"/>'
|
||||
date_granted = access.date_granted.strftime('%Y-%m-%d') if access and access.date_granted else "-"
|
||||
date_expiry = access.date_expiry.strftime('%Y-%m-%d') if access and access.date_expiry else "-"
|
||||
|
||||
html += f"""
|
||||
<tr>
|
||||
<td class='o_td_label'><label>{machine.name}</label></td>
|
||||
<td class='o_td_field'>{icon}</td>
|
||||
<td class='o_td_field'>{date_granted}</td>
|
||||
<td class='o_td_field'>{date_expiry}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{machine.name}</td>
|
||||
<td>{icon}</td>
|
||||
<td>{date_granted}</td>
|
||||
<td>{date_expiry}</td>
|
||||
</tr>
|
||||
"""
|
||||
|
||||
html += "</tbody></table></div>"
|
||||
html += "</div>"
|
||||
|
||||
partner.machine_access_html = html
|
||||
|
||||
|
||||
|
||||
|
||||
@api.model
|
||||
def migrate_existing_partners(self):
|
||||
"""
|
||||
|
|
@ -283,6 +337,31 @@ class ResPartner(models.Model):
|
|||
|
||||
_logger.info(f"[OWS Migration] ✅ Maschinenfreigaben erstellt: {count_created}")
|
||||
self.env.cr.commit()
|
||||
|
||||
@api.model
|
||||
def archive_partners_without_users(self):
|
||||
"""
|
||||
Archiviert alle Partner (res.partner), die keine Benutzer (res.users) sind.
|
||||
"""
|
||||
Partner = self.env['res.partner']
|
||||
User = self.env['res.users']
|
||||
|
||||
# IDs aller Partner, die ein Benutzerkonto haben
|
||||
user_partner_ids = User.search([]).mapped('partner_id').ids
|
||||
|
||||
# Alle Partner ohne Benutzerkonto
|
||||
partners_to_archive = Partner.search([
|
||||
('id', 'not in', user_partner_ids),
|
||||
('active', '=', True),
|
||||
])
|
||||
|
||||
count = len(partners_to_archive)
|
||||
partners_to_archive.write({'active': False})
|
||||
for p in partners_to_archive:
|
||||
_logger.debug(f"[OWS] Archiviert Partner: {p.name} (ID {p.id})")
|
||||
_logger.info(f"[OWS] Archiviert {count} Partner ohne Benutzerkonto.")
|
||||
self.env.cr.commit()
|
||||
|
||||
|
||||
class OwsUser(models.Model):
|
||||
_name = 'ows.user'
|
||||
|
|
@ -307,15 +386,57 @@ class OwsUser(models.Model):
|
|||
]
|
||||
|
||||
|
||||
AVAILABLE_COLORS = [
|
||||
('#000000', 'schwarz'),
|
||||
('#ff0000', 'Rot'),
|
||||
('#E91E63', 'Pink'),
|
||||
('#9C27B0', 'Lila'),
|
||||
('#3F51B5', 'Indigo'),
|
||||
('#0000ff', 'Blau'),
|
||||
('#008000', 'Grün'),
|
||||
('#ffff00', 'Gelb'),
|
||||
('#FF9800', 'Orange'),
|
||||
('#795548', 'Braun'),
|
||||
('#ffffff', 'Weiss'),
|
||||
]
|
||||
|
||||
class OwsMachineArea(models.Model):
|
||||
_name = 'ows.machine.area'
|
||||
_table = "ows_machine_area"
|
||||
_table = 'ows_machine_area'
|
||||
_description = 'OWS: Maschinenbereich'
|
||||
_order = 'name'
|
||||
|
||||
name = fields.Char(required=True, translate=True)
|
||||
#color = fields.Integer(string="Farbe")
|
||||
color_hex = fields.Char(string="Farbe (Hex)", help="Hex-Farbcode wie #FF0000 für Rot")
|
||||
name = fields.Char(string="Name", required=True, translate=True)
|
||||
|
||||
color_hex = fields.Selection(
|
||||
selection=AVAILABLE_COLORS,
|
||||
string="Farbe (Hex)",
|
||||
required=True,
|
||||
)
|
||||
|
||||
color_hex_value = fields.Char(
|
||||
string="Farbcode",
|
||||
compute='_compute_color_hex_value',
|
||||
store=False
|
||||
)
|
||||
|
||||
color_name = fields.Char(
|
||||
string="Farbname",
|
||||
compute='_compute_color_name',
|
||||
store=False
|
||||
)
|
||||
|
||||
@api.depends('color_hex')
|
||||
def _compute_color_hex_value(self):
|
||||
for rec in self:
|
||||
rec.color_hex_value = rec.color_hex or ''
|
||||
|
||||
@api.depends('color_hex')
|
||||
def _compute_color_name(self):
|
||||
label_dict = dict(AVAILABLE_COLORS)
|
||||
for rec in self:
|
||||
rec.color_name = label_dict.get(rec.color_hex, 'Unbekannt')
|
||||
|
||||
|
||||
|
||||
class OwsMachine(models.Model):
|
||||
|
|
@ -325,13 +446,37 @@ class OwsMachine(models.Model):
|
|||
|
||||
name = fields.Char(required=True, translate=True)
|
||||
code = fields.Char(required=True, help="Eindeutiger Kurzcode, z.B. 'lasercutter'")
|
||||
category = fields.Selection([
|
||||
('green', 'Kategorie 1: grün'),
|
||||
('yellow', 'Kategorie 2: gelb'),
|
||||
('red', 'Kategorie 3: rot'),
|
||||
], string="Sicherheitskategorie", required=True, default='red', help="Sicherheitsrelevante Maschinenkategorie:\n"
|
||||
"- grün: keine Einweisungspflicht\n"
|
||||
"- gelb: empfohlene Einweisung\n"
|
||||
"- rot: Einweisung zwingend erforderlich")
|
||||
|
||||
category_icon = fields.Char(string="Kategorie-Symbol", compute="_compute_category_icon", store=False)
|
||||
|
||||
@api.depends('category')
|
||||
def _compute_category_icon(self):
|
||||
for rec in self:
|
||||
icon_map = {
|
||||
'green': '🟢',
|
||||
'yellow': '🟡',
|
||||
'red': '🔴',
|
||||
}
|
||||
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')
|
||||
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="Nutzungsprodukte Liste", compute="_compute_product_using_names", store=False,)
|
||||
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="Einweisungsprodukte Liste", compute="_compute_product_training_names", store=False,)
|
||||
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):
|
||||
|
|
@ -354,12 +499,34 @@ class OwsMachine(models.Model):
|
|||
|
||||
@api.model
|
||||
def get_access_list_grouped(self, partner_id):
|
||||
"""
|
||||
Gibt eine gruppierte Liste von Maschinenzugängen für einen bestimmten Partner zurück. Diese Funktion wird in
|
||||
Odoo POS Frontend verwendet um die Ansicht zu erzeugen auf Welche Maschinen der Partner Zugriff hat.
|
||||
|
||||
Für einen gegebenen Partner (über die partner_id) werden alle Maschinenbereiche (areas) abgefragt.
|
||||
Für jeden Bereich wird geprüft, auf welche Maschinen der Partner Zugriff hat. Das Ergebnis wird
|
||||
als Liste von Bereichen mit jeweils zugehörigen Maschinen und Zugriffsstatus zurückgegeben.
|
||||
|
||||
Zusätzlich werden sicherheitsrelevante Informationen des Partners (wie Sicherheitsunterweisung,
|
||||
Sicherheits-ID, RFID-Karte und Geburtstag) aus dem zugehörigen ows_user ermittelt und mitgeliefert.
|
||||
|
||||
Args:
|
||||
partner_id (int): Die ID des Partners, für den die Zugriffsübersicht erstellt werden soll.
|
||||
|
||||
Returns:
|
||||
dict: Ein Dictionary mit folgenden Schlüsseln:
|
||||
- 'access_by_area': Liste von Bereichen mit Maschinen und Zugriffsstatus.
|
||||
- 'security_briefing': Sicherheitsunterweisung des Nutzers (bool oder False).
|
||||
- 'security_id': Sicherheits-ID des Nutzers (str oder '').
|
||||
- 'rfid_card': RFID-Kartennummer des Nutzers (str oder '').
|
||||
- 'birthday': Geburtstag des Nutzers (str oder '').
|
||||
"""
|
||||
partner = self.env['res.partner'].browse(partner_id)
|
||||
areas = self.env['ows.machine.area'].search([], order="name")
|
||||
_logger.info("🔍 Maschinenbereiche: %s", areas.mapped('name'))
|
||||
_logger.info("🔍 Partner_id: %s", partner_id)
|
||||
res = []
|
||||
_logger.info("Access RPC called with partner_id=%s", partner_id)
|
||||
access_by_area = []
|
||||
for area in areas:
|
||||
machines = self.search([('area_id', '=', area.id)], order="name")
|
||||
machines = self.search([('area_id', '=', area.id), ('category', '=', 'red')], order="name")
|
||||
machine_list = []
|
||||
for machine in machines:
|
||||
has_access = bool(self.env['ows.machine.access'].search([
|
||||
|
|
@ -370,12 +537,22 @@ class OwsMachine(models.Model):
|
|||
'name': machine.name,
|
||||
'has_access': has_access,
|
||||
})
|
||||
res.append({
|
||||
'area': area.name,
|
||||
'color_hex': area.color_hex or '#000000',
|
||||
'machines': machine_list
|
||||
})
|
||||
return res
|
||||
if machine_list:
|
||||
access_by_area.append({
|
||||
'area': area.name,
|
||||
'color_hex': area.color_hex or '#000000',
|
||||
'machines': machine_list
|
||||
})
|
||||
|
||||
user = partner.ows_user_id[:1]
|
||||
return {
|
||||
'access_by_area': access_by_area,
|
||||
'security_briefing': user.security_briefing if user else False,
|
||||
'security_id': user.security_id if user else '',
|
||||
'rfid_card': user.rfid_card if user else '',
|
||||
'birthday': user.birthday if user else '',
|
||||
}
|
||||
|
||||
|
||||
|
||||
class OwsMachineAccess(models.Model):
|
||||
|
|
@ -398,7 +575,7 @@ class OwsMachineAccess(models.Model):
|
|||
class OwsMachineProduct(models.Model):
|
||||
_name = 'ows.machine.product'
|
||||
_table = 'ows_machine_product'
|
||||
_description = 'OWS: Zurordnung Produkt der Nutzung zur die Maschine'
|
||||
_description = 'OWS: Zuordnung Produkt der Nutzung zu der Maschine'
|
||||
|
||||
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')
|
||||
|
|
@ -406,7 +583,7 @@ class OwsMachineProduct(models.Model):
|
|||
class OwsMachineTraining(models.Model):
|
||||
_name = 'ows.machine.training'
|
||||
_table = 'ows_machine_training'
|
||||
_description = 'OWS: Zurordnung Produkt der Einweisung zur die Maschine'
|
||||
_description = 'OWS: Zuordnung Produkt der Einweisung zu der Maschine'
|
||||
|
||||
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')
|
||||
|
|
@ -1,12 +1,13 @@
|
|||
from odoo import models, fields, api
|
||||
from collections import defaultdict
|
||||
|
||||
#import debugpy
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
_logger.info("✅ pos_order.py geladen")
|
||||
_logger.info("✅ pos_order.py geladen Test 2")
|
||||
|
||||
# debugpy.listen(("0.0.0.0", 5678))
|
||||
#debugpy.listen(("0.0.0.0", 5678))
|
||||
print("✅ debugpy wartet auf Verbindung (Port 5678) ...")
|
||||
# Optional: Starte erst, wenn VS Code verbunden ist
|
||||
#debugpy.wait_for_client()
|
||||
|
|
@ -14,15 +15,15 @@ print("✅ debugpy wartet auf Verbindung (Port 5678) ...")
|
|||
class PosOrder(models.Model):
|
||||
_inherit = 'pos.order'
|
||||
|
||||
def _process_order(self, order, draft, existing_order):
|
||||
pos_order_id = super(PosOrder, self)._process_order(order, draft, existing_order)
|
||||
def _process_order(self, order, existing_order):
|
||||
_logger.info("🚨 DEBUG: _process_order wurde aufgerufen mit order: %s", order.get('name', 'unbekannt'))
|
||||
pos_order_id = super(PosOrder, self)._process_order(order, existing_order)
|
||||
pos_order = self.browse(pos_order_id)
|
||||
|
||||
training_products = self.env['ows.machine.training'].search([])
|
||||
product_map = {
|
||||
tp.training_id.product_tmpl_id.id: tp.machine_id.id
|
||||
for tp in training_products
|
||||
}
|
||||
product_map = defaultdict(list)
|
||||
for tp in training_products:
|
||||
product_map[tp.training_id.product_tmpl_id.id].append(tp.machine_id.id)
|
||||
|
||||
partner = pos_order.partner_id
|
||||
if not partner:
|
||||
|
|
@ -31,15 +32,13 @@ class PosOrder(models.Model):
|
|||
|
||||
for line in pos_order.lines:
|
||||
product_tmpl_id = line.product_id.product_tmpl_id.id
|
||||
machine_id = product_map.get(product_tmpl_id)
|
||||
|
||||
_logger.info("🔍 Prüfe Produkt %s → Maschine ID: %s", line.product_id.display_name, machine_id)
|
||||
|
||||
if machine_id:
|
||||
machine_ids = product_map.get(product_tmpl_id, [])
|
||||
_logger.info("🔍 Prüfe Produkt %s → Maschinen IDs: %s", line.product_id.display_name, machine_ids)
|
||||
for machine_id in machine_ids:
|
||||
already_exists = self.env['ows.machine.access'].search([
|
||||
('partner_id', '=', partner.id),
|
||||
('machine_id', '=', machine_id)
|
||||
])
|
||||
], limit=1)
|
||||
if not already_exists:
|
||||
self.env['ows.machine.access'].create({
|
||||
'partner_id': partner.id,
|
||||
9
open_workshop/static/src/css/category_color.css
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
.category-color-circle {
|
||||
display: inline-block;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
margin-left: 8px;
|
||||
vertical-align: middle;
|
||||
border: 1px solid #444;
|
||||
}
|
||||
57
open_workshop/static/src/css/pos.css
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
.custompane {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ows-sidebar {
|
||||
flex: 1 1 auto;
|
||||
width: 220px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
.order-entry:hover { cursor: pointer; }
|
||||
.order-entry.selected { background-color: #007bff; color: white; }
|
||||
|
||||
.ows-customer-list {
|
||||
flex: 1 1 auto;
|
||||
overflow-y: auto;
|
||||
min-height: 0; /* notwendig für Scrollbar */
|
||||
}
|
||||
|
||||
.client-details-grid {
|
||||
flex-shrink: 0;
|
||||
max-height: 60%;
|
||||
overflow-y: auto;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.pos *::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height:8px;
|
||||
}
|
||||
|
||||
.sidebar-line {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 0.5em;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding: 0.2em 0;
|
||||
}
|
||||
|
||||
.sidebar-date {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sidebar-name {
|
||||
flex-shrink: 1;
|
||||
text-align: right;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
72
open_workshop/static/src/js/ows_machine_access_list.js
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
// @odoo-module ows_machine_access_list.js
|
||||
|
||||
import { Component, useState } from "@odoo/owl";
|
||||
import { useBus } from "@web/core/utils/hooks";
|
||||
import { usePos } from "@point_of_sale/app/store/pos_hook";
|
||||
import { rpc } from "@web/core/network/rpc";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
export class OwsMachineAccessList extends Component {
|
||||
static template = 'open_workshop.OwsMachineAccessList';
|
||||
|
||||
setup() {
|
||||
this.pos = usePos();
|
||||
|
||||
this.state = useState({
|
||||
client: null,
|
||||
grouped_accesses: [],
|
||||
security_briefing: false,
|
||||
security_id: '',
|
||||
rfid_card: '',
|
||||
birthday: '',
|
||||
});
|
||||
|
||||
// 🔁 Reagiere auf Partnerwechsel über den Odoo-Bus
|
||||
useBus(this.env.bus, 'partner-changed', () => {
|
||||
this.updateAccessList();
|
||||
});
|
||||
|
||||
// 🔃 Beim Mounten initiale Daten laden
|
||||
this.updateAccessList();
|
||||
}
|
||||
|
||||
async updateAccessList() {
|
||||
const order = this.pos.get_order();
|
||||
const partner = order?.get_partner?.();
|
||||
this.state.client = partner || null;
|
||||
if (!partner) {
|
||||
this.state.grouped_accesses = [];
|
||||
this.state.security_briefing = false;
|
||||
this.state.security_id = '';
|
||||
this.state.rfid_card = '';
|
||||
this.state.birthday = '';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await rpc("/open_workshop/partner_access", {
|
||||
params: { partner_id: partner.id },
|
||||
});
|
||||
|
||||
this.state.grouped_accesses = data.access_by_area || [];
|
||||
this.state.security_briefing = data.security_briefing;
|
||||
this.state.security_id = data.security_id;
|
||||
this.state.rfid_card = data.rfid_card;
|
||||
this.state.birthday = data.birthday;
|
||||
} catch (error) {
|
||||
console.error("Fehler beim Laden der Einweisungen:", error);
|
||||
this.state.grouped_accesses = [];
|
||||
this.state.security_briefing = false;
|
||||
this.state.security_id = '';
|
||||
this.state.rfid_card = '';
|
||||
this.state.birthday = '';
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
registry.category("templates").add("open_workshop.OwsMachineAccessList", OwsMachineAccessList);
|
||||
|
||||
54
open_workshop/static/src/js/ows_pos_customer_sidebar.js
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
// @odoo-module ows_pos_customer_sidebar.js
|
||||
|
||||
import { Component } from "@odoo/owl";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { usePos } from "@point_of_sale/app/store/pos_hook";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { ask } from "@web/core/confirmation_dialog/confirmation_dialog";
|
||||
|
||||
export class OwsPosCustomerSidebar extends Component {
|
||||
static template = "open_workshop.OwsPosCustomerSidebar";
|
||||
|
||||
setup() {
|
||||
this.pos = usePos(); // ✅ Holt dir Zugriff auf den zentralen POS-Store
|
||||
this.dialog = useService("dialog");
|
||||
}
|
||||
|
||||
addOrder() {
|
||||
this.pos.add_new_order(); // ✅ Neue Order wird aktive Order
|
||||
this.pos.showScreen("ProductScreen");
|
||||
this.pos.selectPartner();
|
||||
this.env.bus.trigger('partner-changed'); // ✅ Event manuell auslösen
|
||||
}
|
||||
|
||||
async removeCurrentOrder() {
|
||||
this.pos.onDeleteOrder(this.pos.get_order())
|
||||
}
|
||||
|
||||
openTicketScreen() {
|
||||
this.pos.showScreen("TicketScreen");
|
||||
}
|
||||
|
||||
// 🔧 FIXED: Zugriff auf Order-Liste korrigiert
|
||||
getFilteredOrderList() {
|
||||
return this.pos.get_open_orders();
|
||||
}
|
||||
|
||||
getDate(order) {
|
||||
const date = new Date(order.date_order);
|
||||
const dd = String(date.getDate()).padStart(2, '0');
|
||||
const mm = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const hh = String(date.getHours()).padStart(2, '0');
|
||||
const mi = String(date.getMinutes()).padStart(2, '0');
|
||||
return `${dd}.${mm}. ${hh}:${mi}`;
|
||||
}
|
||||
|
||||
getPartner(order) {
|
||||
return order.get_partner()?.name || "Kein Kunde";
|
||||
}
|
||||
|
||||
selectOrder(order) {
|
||||
this.pos.set_order(order);
|
||||
this.env.bus.trigger('partner-changed');
|
||||
}
|
||||
}
|
||||
11
open_workshop/static/src/js/ows_pos_sidebar.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
// ows_pos_sidebar.js
|
||||
// @odoo-module
|
||||
|
||||
import { Component } from "@odoo/owl";
|
||||
import { OwsPosCustomerSidebar } from "./ows_pos_customer_sidebar";
|
||||
import { OwsMachineAccessList } from "./ows_machine_access_list";
|
||||
|
||||
export class OwsPosSidebar extends Component {
|
||||
static template = "open_workshop.OwsPosSidebar";
|
||||
static components = { OwsPosCustomerSidebar, OwsMachineAccessList };
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
// product_screen_template_patch.js
|
||||
// @odoo-module
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { ProductScreen } from "@point_of_sale/app/screens/product_screen/product_screen";
|
||||
import { OwsPosSidebar } from "./ows_pos_sidebar";
|
||||
|
||||
class OwsProductScreen extends ProductScreen {
|
||||
static components = Object.assign({}, ProductScreen.components, {
|
||||
OwsPosSidebar,
|
||||
});
|
||||
}
|
||||
|
||||
registry.category("pos_screens").remove("ProductScreen");
|
||||
registry.category("pos_screens").add("ProductScreen", OwsProductScreen);
|
||||
76
open_workshop/static/src/xml/ows_machine_access_list.xml
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
<t t-name="open_workshop.OwsMachineAccessList">
|
||||
<div class="client-details-grid p-2 small">
|
||||
|
||||
<!-- ✅ Sicherheitsbereich -->
|
||||
<t t-if="state.client">
|
||||
<div class="client-details-header">
|
||||
<ul>
|
||||
<li><span class="client-details-label">Einweisungen</span></li>
|
||||
</ul>
|
||||
|
||||
<div class="client-details-area border" t-att-style="'border: solid 3px #ffffff; margin: 5px;'">
|
||||
<ul>
|
||||
<li class="client-detail">
|
||||
<span class="detail client-details-vvow_briefing">✅</span>
|
||||
<span class="briefinglabel">Werkstatt</span>
|
||||
</li>
|
||||
|
||||
<li class="client-detail">
|
||||
<t t-if="!state.security_briefing">
|
||||
<span class="detail client-details-vvow_briefing_error">❌</span>
|
||||
</t>
|
||||
<t t-if="state.security_briefing">
|
||||
<span class="detail client-details-vvow_briefing">✅</span>
|
||||
</t>
|
||||
<span class="briefinglabel">Haftungsausschluss</span>
|
||||
</li>
|
||||
|
||||
<t t-if="!state.security_briefing">
|
||||
<li class="client-detail">
|
||||
<ul class="subpoints">
|
||||
<span class="detail client-details-vvow_sec_briefing_error">‼️Bitte Prüfen‼️</span>
|
||||
</ul>
|
||||
</li>
|
||||
</t>
|
||||
|
||||
<t t-if="state.security_briefing">
|
||||
<ul class="subpoints">
|
||||
<li class="client-detail">
|
||||
<span class="label">Id:</span>
|
||||
<span class="detail client-details-vvow_security_id">
|
||||
<t t-esc="state.security_id || 'N/A'" />
|
||||
</span>
|
||||
</li>
|
||||
<li class="client-detail">
|
||||
<span class="label">Geburtstag:</span>
|
||||
<span class="detail client-details-vvow_security_id">
|
||||
<t t-esc="state.birthday || 'N/A'" />
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</t>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!-- ✅ Maschinenliste: immer sichtbar, gefiltert -->
|
||||
<t t-foreach="state.grouped_accesses" t-as="area" t-key="area.area">
|
||||
<t t-if="area.machines.length > 0">
|
||||
<div class="client-details-area" t-att-style="'border: solid 3px ' + area.color_hex + '; margin: 5px;'">
|
||||
<ul>
|
||||
<t t-foreach="area.machines" t-as="machine" t-key="machine.name">
|
||||
<li class="client-detail">
|
||||
<span t-attf-class="detail {{ machine.has_access ? 'client-details-vvow_briefing' : 'client-details-vvow_briefing_error' }}">
|
||||
<t t-esc="machine.has_access ? '✅' : '❌'" />
|
||||
</span>
|
||||
<span class="briefinglabel"><t t-esc="machine.name"/></span>
|
||||
</li>
|
||||
</t>
|
||||
</ul>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
</div>
|
||||
</t>
|
||||
27
open_workshop/static/src/xml/ows_pos_customer_sidebar.xml
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
<t t-name="open_workshop.OwsPosCustomerSidebar" owl="1">
|
||||
<div class="ows-sidebar p-2 bg-light border-end h-100">
|
||||
<div class="ows-sidebar-header mb-2 d-flex justify-content-between align-items-center">
|
||||
<button class="btn btn-secondary" t-on-click="openTicketScreen">Orders</button>
|
||||
<div>
|
||||
<button class="btn btn-sm btn-success me-1" t-on-click="addOrder">+</button>
|
||||
<button class="btn btn-sm btn-danger" t-on-click="removeCurrentOrder">–</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ows-customer-list overflow-auto">
|
||||
<t t-foreach="getFilteredOrderList()" t-as="order" t-key="order.uid">
|
||||
<!--div class="order-entry p-1 rounded mb-1 border"
|
||||
t-att-class="order === pos.get_order() ? 'bg-primary text-white' : 'bg-white'"
|
||||
t-on-click="() => selectOrder(order)"-->
|
||||
<div t-att-class="'order-entry' + (order === pos.get_order() ? ' selected' : '')" t-on-click="() => this.selectOrder(order)">
|
||||
<div class="sidebar-line">
|
||||
<span class="sidebar-date"><t t-esc="getDate(order)"/></span>
|
||||
<span class="sidebar-name" t-att-title="getPartner(order)"><t t-esc="getPartner(order)"/></span>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
9
open_workshop/static/src/xml/ows_pos_sidebar.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
<t t-name="open_workshop.OwsPosSidebar" owl="1">
|
||||
<div class="custompane d-flex flex-column border-end bg-200">
|
||||
<OwsPosCustomerSidebar />
|
||||
<OwsMachineAccessList />
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
<t t-name="point_of_sale.ProductScreen" t-inherit="point_of_sale.ProductScreen" t-inherit-mode="extension">
|
||||
<xpath expr="//div[contains(@class, 'leftpane')]" position="before">
|
||||
<OwsPosSidebar />
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
8
open_workshop/views/assets.xml
Normal file
|
|
@ -0,0 +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"/>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
<record id="action_machine_area_list" model="ir.actions.act_window">
|
||||
<field name="name">Maschinenbereiche</field>
|
||||
<field name="res_model">ows.machine.area</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
</record>
|
||||
|
||||
<!-- Menüpunkt unter Maschinen > Konfiguration -->
|
||||
|
|
@ -15,10 +15,11 @@
|
|||
<field name="name">ows.machine.area.tree</field>
|
||||
<field name="model">ows.machine.area</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<list>
|
||||
<field name="name"/>
|
||||
<field name="color_hex" widget="color_picker"/>
|
||||
</tree>
|
||||
<field name="color_hex_value" string="Farbe (Hex)"/>
|
||||
<field name="color_name" string="Farbname"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
|
@ -30,7 +31,8 @@
|
|||
<form string="Maschinenbereich">
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="color_hex" widget="color_picker"/>
|
||||
<field name="color_hex"/>
|
||||
<field name="color_name" readonly="1"/>
|
||||
</group>
|
||||
</form>
|
||||
</field>
|
||||
|
|
@ -4,10 +4,10 @@
|
|||
<field name="name">ows.machine.product.tree</field>
|
||||
<field name="model">ows.machine.product</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree editable="bottom">
|
||||
<list editable="bottom">
|
||||
<field name="machine_id"/>
|
||||
<field name="product_id" domain="[('categ_id.name', '=', 'Maschinennutzung')]"/>
|
||||
</tree>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
|
@ -16,10 +16,10 @@
|
|||
<field name="name">ows.machine.training.tree</field>
|
||||
<field name="model">ows.machine.training</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree editable="bottom">
|
||||
<list editable="bottom">
|
||||
<field name="machine_id"/>
|
||||
<field name="training_id" domain="[('categ_id.name', '=', 'Einweisungen')]"/>
|
||||
</tree>
|
||||
<field name="training_id" domain="[('categ_id.name', 'in', ['Einweisungen', 'Kurse'])]"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
<record id="action_machine_product" model="ir.actions.act_window">
|
||||
<field name="name">Maschinen-Nutzungsprodukte</field>
|
||||
<field name="res_model">ows.machine.product</field>
|
||||
<field name="view_mode">tree</field>
|
||||
<field name="view_mode">list</field>
|
||||
<field name="view_id" ref="view_machine_product_tree"/>
|
||||
<field name="help" type="html">
|
||||
<p>Verwalte die Zuordnung von Maschinen zu Nutzungsprodukten.</p>
|
||||
|
|
@ -38,7 +38,7 @@
|
|||
<record id="action_machine_training" model="ir.actions.act_window">
|
||||
<field name="name">Maschinen-Einweisungsprodukte</field>
|
||||
<field name="res_model">ows.machine.training</field>
|
||||
<field name="view_mode">tree</field>
|
||||
<field name="view_mode">list</field>
|
||||
<field name="view_id" ref="view_machine_training_tree"/>
|
||||
<field name="help" type="html">
|
||||
<p>Verwalte die Zuordnung von Maschinen zu Einweisungsprodukten.</p>
|
||||
68
open_workshop/views/machine_views.xml
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
<!-- machine_views.xml -->
|
||||
<odoo>
|
||||
<!-- 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>
|
||||
<field name="category_icon" string="⚙" readonly="1"/>
|
||||
<field name="name"/>
|
||||
<field name="category"/>
|
||||
<field name="code"/>
|
||||
<field name="area_id" widget="many2one_color"/>
|
||||
<field name="product_names"/>
|
||||
<field name="training_names"/>
|
||||
<field name="storage_location"/>
|
||||
<field name="purchase_price"/>
|
||||
<field name="purchase_date"/>
|
||||
<field name="active"/>
|
||||
|
||||
</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>
|
||||
<group>
|
||||
<field name="category_icon" string="⚙" readonly="1"/>
|
||||
<field name="name"/>
|
||||
<field name="category"/>
|
||||
<field name="code"/>
|
||||
<field name="area_id"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="description"/>
|
||||
<field name="storage_location"/>
|
||||
<field name="purchase_price"/>
|
||||
<field name="purchase_date"/>
|
||||
<field name="active"/>
|
||||
</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>
|
||||
|
|
@ -4,14 +4,14 @@
|
|||
<record id="action_machine_list" model="ir.actions.act_window">
|
||||
<field name="name">Maschinen</field>
|
||||
<field name="res_model">ows.machine</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
</record>
|
||||
|
||||
<!-- Trainingsprodukt-Liste -->
|
||||
<record id="action_training_product_list" model="ir.actions.act_window">
|
||||
<field name="name">Einweisungs-Produkte</field>
|
||||
<field name="res_model">ows.machine.product</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
</record>
|
||||
|
||||
<!-- Menüstruktur -->
|
||||
|
|
@ -59,10 +59,10 @@
|
|||
<field name="name">ows.machine.product.tree</field>
|
||||
<field name="model">ows.machine.product</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<list>
|
||||
<field name="product_id"/>
|
||||
<field name="machine_id"/>
|
||||
</tree>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
127
open_workshop/views/res_partner_view.xml
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
<odoo>
|
||||
<!-- 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 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 -->
|
||||
|
||||
<record id="view_partner_form_inherit_open_workshop_tabs" model="ir.ui.view">
|
||||
<field name="name">res.partner.form.ows.tabs</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="inherit_id" ref="base.view_partner_form"/>
|
||||
<field name="priority" eval="20"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//notebook" position="inside">
|
||||
<page name="ows_machine_access" string="Offene Werkstatt (Hobbyhimmel)">
|
||||
<!-- EINWEISUNG: Zwei Felder nebeneinander -->
|
||||
<group name="container_row_2" string="Sicherheitseinweisung" col="2">
|
||||
<field name="security_briefing"/>
|
||||
<field name="security_id"/>
|
||||
</group>
|
||||
|
||||
<!-- MASCHINENFREIGABEN: Volle Breite -->
|
||||
<group string="Maschinenfreigaben" col="2">
|
||||
<field name="machine_access_ids" colspan="2" context="{'default_partner_id': id}" nolabel="1">
|
||||
<list>
|
||||
<field name="machine_id"/>
|
||||
<field name="date_granted"/>
|
||||
<field name="date_expiry"/>
|
||||
<field name="granted_by_pos"/>
|
||||
</list>
|
||||
</field>
|
||||
</group>
|
||||
|
||||
<!-- ÜBERSICHT: Volle Breite -->
|
||||
<group string="Maschinenfreigaben Übersicht" >
|
||||
<field name="machine_access_html" colspan="2" readonly="1" widget="html" nolabel="1"/>
|
||||
</group>
|
||||
</page>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Geburtstag direkt nach der USt-ID -->
|
||||
<record id="view_partner_form_inherit_ows_birthday" model="ir.ui.view">
|
||||
<field name="name">res.partner.form.ows.birthday</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="inherit_id" ref="base.view_partner_form"/>
|
||||
<field name="priority" eval="15"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='vat']" position="after">
|
||||
<field name="birthday"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- List View Anpassung -->
|
||||
<record id="ows_userList_inherit" model="ir.ui.view">
|
||||
<field name="name">res.partner.ows.tree</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="inherit_id" ref="base.view_partner_tree"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='vat']" position="after">
|
||||
<field name="create_date" optional="show"/>
|
||||
<field name="security_briefing" optional="show"/>
|
||||
<field name="security_id" optional="show"/>
|
||||
<field name="rfid_card" optional="show"/>
|
||||
<field name="category_id" widget="many2many_tags"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='vat']" position="replace">
|
||||
<field name="vat" invisible="1"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='email']" position="replace">
|
||||
<field name="email" invisible="1"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='phone']" position="replace">
|
||||
<field name="phone" invisible="1"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='state_id']" position="replace">
|
||||
<field name="state_id" invisible="1"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='country_id']" position="replace">
|
||||
<field name="country_id" invisible="1"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Standardwerte setzen (company_type = person) -->
|
||||
<record id="view_partner_form_inherit" model="ir.ui.view">
|
||||
<field name="name">res.partner.form.inherit.default_person</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="inherit_id" ref="base.view_partner_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="company_type" position="attributes">
|
||||
<attribute name="default">person</attribute>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Optional: Kontakte-Action, falls gebraucht -->
|
||||
<record id="contacts.action_contacts" model="ir.actions.act_window">
|
||||
<field name="view_mode">list,kanban,form,activity</field>
|
||||
</record>
|
||||
<record id="contacts.action_contacts_view_kanban" model="ir.actions.act_window.view">
|
||||
<field name="sequence" eval="1"/>
|
||||
</record>
|
||||
<record id="contacts.action_contacts_view_tree" model="ir.actions.act_window.view">
|
||||
<field name="sequence" eval="0"/>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
# Open Workshop (open_workshop ows)
|
||||
|
||||
Dieses Odoo v13.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.
|
||||
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
|
||||
|
||||
|
|
@ -28,40 +28,10 @@ Dieses Odoo v13.0 Modul erweitert das POS- und Kontakt-Modul um Funktionen für
|
|||
## Installation
|
||||
|
||||
1. Dieses Modul in den Custom-Addons-Ordner kopieren
|
||||
2. Vor der Installation von open_worshop muss vvow_pos deinstalliert werden. Die Funktionalität von vvow_pos wird durch open_workshop ersetzt und erweitert.
|
||||
3. ggf. muss die alte Datenbank manuell migiriert werden, es gibt ca 9 gelöscht res.partner auf die Verweise aus POS bestehen. Diese res.parnter müssen wieder hergestellt werden. Dazu gibt es ein Skript unter
|
||||
```folder
|
||||
scripts/fix_missing_pos_partner.py
|
||||
```
|
||||
```bash
|
||||
opt/odoo/odoo/odoo-bin shell -d hobbyhimmel < scrpts/fix_missing_pos_partner.py
|
||||
```
|
||||
4. Odoo starten mit:
|
||||
```bash
|
||||
odoo-bin -d deine_datenbank -u open_workshop
|
||||
```
|
||||
5. Alternativ im Backend unter Apps installieren
|
||||
|
||||
## Automatische Migrationen
|
||||
|
||||
Beim ersten Laden des Moduls werden folgende Migrationen durchgeführt:
|
||||
- Bestehende `res.partner` erhalten automatisch `ows.user`-Eintrag (inkl. Übernahme alter Felder wie vvow_birthday, vvow_security, vvow_security_id, vvow_rfid.
|
||||
- Alte Felder mit Maschinenfreigaben (`vvow_holz_*`, `vvow_metall_*`, `vvow_fablab_*`) werden in `ows.machine.access` übertragen
|
||||
- inkl. Übernahme des Änderungsdatum aus `mail.message` wann der Nutzer die Einweisung erhalten hat (ist noch fehlerhaft)
|
||||
2. Im Odoo Backend unter Apps installieren
|
||||
|
||||
## Entwicklerhinweise
|
||||
|
||||
### post_init_hook
|
||||
Die Datei `post_init_hook.py` ruft automatisch nach der Installation folgende Methoden auf:
|
||||
```python
|
||||
res.partner.migrate_existing_partners()
|
||||
res.partner.migrate_machine_access_from_old_fields()
|
||||
```
|
||||
|
||||
### Datenimport
|
||||
- Maschinenbereiche, Maschinen werden über `.xml`-Dateien in `data/` geladen
|
||||
- Die Zuordnung von Maschine zu Einweisungsprodukten und Nutzungsprodukten muss derzeit noch manuell erstellt werden. Ein skript dafür folgt.
|
||||
|
||||
## ToDos
|
||||
- Bearbeitung der Maschinenfreigaben im Backend
|
||||
- Automatische Erstellung von `mail.message` bei manueller Freigabe
|
||||
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)
|
||||
|
||||
43
open_workshop_base/__manifest__.py
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
{
|
||||
'name': 'Open Workshop Base',
|
||||
'license': 'AGPL-3',
|
||||
'version': '18.0.1.0.4', # 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'],
|
||||
'author': 'matthias.lotz',
|
||||
'category': 'Manufacturing',
|
||||
|
||||
'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/maintenance_equipment_views.xml',
|
||||
'views/res_partner_view.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',
|
||||
}
|
||||
3
open_workshop_base/controllers/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Datei: open_workshop/controllers/__init__.py
|
||||
|
||||
from . import pos_access
|
||||
14
open_workshop_base/controllers/pos_access.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
from odoo import http
|
||||
from odoo.http import request
|
||||
|
||||
class OpenWorkshopPOSController(http.Controller):
|
||||
|
||||
@http.route('/open_workshop/partner_access', type='json', auth='user')
|
||||
def get_partner_machine_access(self, **kwargs):
|
||||
partner_id = kwargs.get('params', {}).get('partner_id')
|
||||
if not partner_id:
|
||||
return {"error": "Missing partner_id"}
|
||||
|
||||
Machine = request.env['ows.machine'].sudo()
|
||||
return Machine.get_access_list_grouped(partner_id)
|
||||
|
||||
153
open_workshop_base/data/data.xml
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
<odoo>
|
||||
<!-- Bereiche -->
|
||||
<record id="area_fablab" model="ows.machine.area">
|
||||
<field name="name">Fablab</field>
|
||||
<field name="color_hex">#008000</field>
|
||||
</record>
|
||||
|
||||
<record id="area_holz" model="ows.machine.area">
|
||||
<field name="name">Holzbereich</field>
|
||||
<field name="color_hex">#ff0000</field>
|
||||
</record>
|
||||
|
||||
<record id="area_metall" model="ows.machine.area">
|
||||
<field name="name">Metallbereich</field>
|
||||
<field name="color_hex">#0000ff</field>
|
||||
</record>
|
||||
|
||||
<record id="area_elektronik" model="ows.machine.area">
|
||||
<field name="name">Elektronikbereich</field>
|
||||
<field name="color_hex">#ffff00</field>
|
||||
</record>
|
||||
|
||||
<!-- Maschinen im Fablab -->
|
||||
<record id="machine_sabako_laser" model="ows.machine">
|
||||
<field name="name">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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="serial_no">loetkolben</field>
|
||||
<field name="area_id" ref="area_elektronik"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
111
open_workshop_base/migrations/18.0.1.0.4/post-migration.py
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
# -*- 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. 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;
|
||||
5
open_workshop_base/models/__init__.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from . import ows_models
|
||||
from . import pos_order
|
||||
|
||||
|
||||
|
||||
675
open_workshop_base/models/ows_models.py
Normal file
|
|
@ -0,0 +1,675 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# ows_models.py
|
||||
# Part of Odoo Open Workshop
|
||||
|
||||
from odoo import models, fields, api
|
||||
from markupsafe import escape as html_escape
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
_logger.info("✅ ows_models.py geladen")
|
||||
|
||||
class HREmployee(models.Model):
|
||||
_inherit = 'hr.employee'
|
||||
|
||||
@api.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})")
|
||||
|
||||
# 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': 'office@hobbyhimmel.de',
|
||||
'work_phone': False,
|
||||
'active': True, # Reaktivieren falls archiviert
|
||||
})
|
||||
_logger.info(f"[OWS] Admin-Angestellter reaktiviert und umbenannt: {admin_employee.name} (ID: {admin_employee.id})")
|
||||
else:
|
||||
_logger.warning("[OWS] Kein Angestellter für Administrator gefunden.")
|
||||
return
|
||||
|
||||
# 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))
|
||||
|
||||
|
||||
class ResPartner(models.Model):
|
||||
_inherit = 'res.partner'
|
||||
_logger.info("✅ ows ResPartner geladen")
|
||||
|
||||
ows_user_id = fields.One2many('ows.user', 'partner_id', string="OWS Benutzerdaten")
|
||||
|
||||
# Alte Felder (weiterhin sichtbar für externe Programme)
|
||||
birthday = fields.Date(
|
||||
string="Geburtstag",
|
||||
compute='_compute_ows_user_fields',
|
||||
inverse='_inverse_birthday',
|
||||
store=False
|
||||
)
|
||||
rfid_card = fields.Text(
|
||||
string="RFID Card ID",
|
||||
compute='_compute_ows_user_fields',
|
||||
inverse='_inverse_rfid_card',
|
||||
store=False
|
||||
)
|
||||
security_briefing = fields.Boolean(
|
||||
string="Haftungsausschluss",
|
||||
compute='_compute_ows_user_fields',
|
||||
inverse='_inverse_security_briefing',
|
||||
store=False
|
||||
)
|
||||
security_id = fields.Text(
|
||||
string="Haftungsausschluss ID",
|
||||
compute='_compute_ows_user_fields',
|
||||
inverse='_inverse_security_id',
|
||||
store=False
|
||||
)
|
||||
# Neue direkte vvow_* Felder
|
||||
vvow_birthday = fields.Date(string="Geburtstag (vvow)")
|
||||
vvow_rfid_card = fields.Text(string="RFID Card ID (vvow)")
|
||||
vvow_security_briefing = fields.Boolean(string="Haftungsausschluss (vvow)")
|
||||
vvow_security_id = fields.Text(string="Haftungsausschluss ID (vvow)")
|
||||
|
||||
@api.depends('ows_user_id')
|
||||
def _compute_ows_user_fields(self):
|
||||
for partner in self:
|
||||
user = partner.ows_user_id[0] if partner.ows_user_id else False
|
||||
partner.birthday = user.birthday if user else False
|
||||
partner.rfid_card = user.rfid_card if user else False
|
||||
partner.security_briefing = user.security_briefing if user else False
|
||||
partner.security_id = user.security_id if user else False
|
||||
|
||||
def _inverse_birthday(self):
|
||||
for partner in self:
|
||||
user = partner.ows_user_id[0] if partner.ows_user_id else False
|
||||
if user:
|
||||
user.birthday = partner.birthday
|
||||
|
||||
def _inverse_rfid_card(self):
|
||||
for partner in self:
|
||||
user = partner.ows_user_id[0] if partner.ows_user_id else False
|
||||
if user:
|
||||
user.rfid_card = partner.rfid_card
|
||||
|
||||
def _inverse_security_briefing(self):
|
||||
for partner in self:
|
||||
user = partner.ows_user_id[0] if partner.ows_user_id else False
|
||||
if user:
|
||||
user.security_briefing = partner.security_briefing
|
||||
|
||||
def _inverse_security_id(self):
|
||||
for partner in self:
|
||||
user = partner.ows_user_id[0] if partner.ows_user_id else False
|
||||
if user:
|
||||
user.security_id = partner.security_id
|
||||
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
partners = super().create(vals_list)
|
||||
for vals, partner in zip(vals_list, partners):
|
||||
self.env['ows.user'].create({
|
||||
'partner_id': partner.id,
|
||||
'birthday': vals.get('birthday') or vals.get('vvow_birthday'),
|
||||
'rfid_card': vals.get('rfid_card') or vals.get('vvow_rfid_card'),
|
||||
'security_briefing': vals.get('security_briefing') or vals.get('vvow_security_briefing'),
|
||||
'security_id': vals.get('security_id') or vals.get('vvow_security_id'),
|
||||
})
|
||||
return partners
|
||||
|
||||
def write(self, vals):
|
||||
res = super().write(vals)
|
||||
for partner in self:
|
||||
user = partner.ows_user_id[0] if partner.ows_user_id else False
|
||||
if not user:
|
||||
continue
|
||||
|
||||
# Synchronisation alt -> user
|
||||
if 'birthday' in vals:
|
||||
user.birthday = vals['birthday']
|
||||
if 'rfid_card' in vals:
|
||||
user.rfid_card = vals['rfid_card']
|
||||
if 'security_briefing' in vals:
|
||||
user.security_briefing = vals['security_briefing']
|
||||
if 'security_id' in vals:
|
||||
user.security_id = vals['security_id']
|
||||
|
||||
# Synchronisation vvow_* -> user + alt
|
||||
if 'vvow_birthday' in vals:
|
||||
user.birthday = vals['vvow_birthday']
|
||||
partner.birthday = vals['vvow_birthday']
|
||||
if 'vvow_rfid_card' in vals:
|
||||
user.rfid_card = vals['vvow_rfid_card']
|
||||
partner.rfid_card = vals['vvow_rfid_card']
|
||||
if 'vvow_security_briefing' in vals:
|
||||
user.security_briefing = vals['vvow_security_briefing']
|
||||
partner.security_briefing = vals['vvow_security_briefing']
|
||||
if 'vvow_security_id' in vals:
|
||||
user.security_id = vals['vvow_security_id']
|
||||
partner.security_id = vals['vvow_security_id']
|
||||
return res
|
||||
|
||||
machine_access_ids = fields.One2many(
|
||||
'ows.machine.access',
|
||||
'partner_id',
|
||||
string='Maschinenfreigaben'
|
||||
)
|
||||
|
||||
machine_access_html = fields.Html(
|
||||
string="Maschinenfreigabe",
|
||||
compute="_compute_machine_access_html",
|
||||
sanitize=False
|
||||
)
|
||||
|
||||
|
||||
@api.depends('machine_access_ids')
|
||||
def _compute_machine_access_html(self):
|
||||
areas = self.env['ows.machine.area'].search([], order="name")
|
||||
for partner in self:
|
||||
html = ""
|
||||
for area in areas:
|
||||
html += f"""
|
||||
<div class="o_form_sheet">
|
||||
<h3 class="o_form_label">{area.name}</h3>
|
||||
<table class="table table-sm table-bordered o_form_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Maschine</th>
|
||||
<th>Status</th>
|
||||
<th>Datum</th>
|
||||
<th>Gültig bis</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
"""
|
||||
|
||||
machines = self.env['ows.machine'].search([('area_id', '=', area.id)], order="name")
|
||||
|
||||
for machine in machines:
|
||||
access = self.env['ows.machine.access'].search([
|
||||
('partner_id', '=', partner.id),
|
||||
('machine_id', '=', machine.id),
|
||||
], limit=1)
|
||||
icon = '<span class="text-success fa fa-check"/>' if access else '<span class="text-danger fa fa-times"/>'
|
||||
date_granted = access.date_granted.strftime('%Y-%m-%d') if access and access.date_granted else "-"
|
||||
date_expiry = access.date_expiry.strftime('%Y-%m-%d') if access and access.date_expiry else "-"
|
||||
|
||||
html += f"""
|
||||
<tr>
|
||||
<td>{machine.name}</td>
|
||||
<td>{icon}</td>
|
||||
<td>{date_granted}</td>
|
||||
<td>{date_expiry}</td>
|
||||
</tr>
|
||||
"""
|
||||
|
||||
html += "</tbody></table></div>"
|
||||
|
||||
partner.machine_access_html = html
|
||||
|
||||
|
||||
|
||||
|
||||
@api.model
|
||||
def migrate_existing_partners(self):
|
||||
"""
|
||||
Erstellt für alle vorhandenen res.partner einen ows.user,
|
||||
wenn noch keiner existiert, und übernimmt alte vvow_* Felder.
|
||||
Führt am Ende automatisch einen commit durch.
|
||||
|
||||
Verwendung in der odoo shell:
|
||||
env['res.partner'].migrate_existing_partners()
|
||||
env['ows.user'].search_count([])
|
||||
env['ows.user'].search([]).mapped('partner_id.name')
|
||||
"""
|
||||
migrated = 0
|
||||
skipped = 0
|
||||
|
||||
partners = self.env['res.partner'].search([])
|
||||
for partner in partners:
|
||||
if partner.ows_user_id:
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
# Werte lesen (werden evtl. durch _inherit hinzugefügt)
|
||||
vals = {
|
||||
'partner_id': partner.id,
|
||||
'birthday': getattr(partner, 'vvow_birthday', False),
|
||||
'rfid_card': getattr(partner, 'vvow_rfid_card', False),
|
||||
'security_briefing': getattr(partner, 'vvow_security_briefing', False),
|
||||
'security_id': getattr(partner, 'vvow_security_id', False),
|
||||
}
|
||||
self.env['ows.user'].create(vals)
|
||||
migrated += 1
|
||||
_logger.info(f"Erzeuge ows.user für Partner {partner.id} ({partner.name}) mit Werten: {vals}")
|
||||
|
||||
_logger.info(f"[OWS Migration] ✅ Migriert: {migrated}, ❌ Übersprungen: {skipped}")
|
||||
|
||||
# 🔐 Commit am Ende
|
||||
self.env.cr.commit()
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': "Migration abgeschlossen",
|
||||
'message': f"{migrated} Partner migriert, {skipped} übersprungen.",
|
||||
'sticky': False,
|
||||
}
|
||||
}
|
||||
|
||||
@api.model
|
||||
def migrate_machine_access_from_old_fields(self):
|
||||
"""
|
||||
Migriert alte vvow_* Boolean-Felder in strukturierte ows.machine.access Einträge.
|
||||
Das Freigabe-Datum wird aus dem Änderungsverlauf (mail.message.tracking_value_ids) extrahiert.
|
||||
"""
|
||||
mapping = [
|
||||
('vvow_holz_sander', 'bandschleifer'),
|
||||
('vvow_holz_felder_bandsaw', 'bandsaege_holz'),
|
||||
('vvow_holz_felder_jointer', 'dickenhobel'),
|
||||
('vvow_holz_felder_mill', 'felder_fraese'),
|
||||
('vvow_holz_felder_tablesaw', 'formatkreissaege'),
|
||||
('vvow_holz_mill', 'tischfraese'),
|
||||
('vvow_holz_lathe', 'drechselbank'),
|
||||
('vvow_holz_domino', 'festool_domino'),
|
||||
('vvow_holz_duoduebler', 'maffel_duo'),
|
||||
('vvow_holz_lamello', 'lamello_zeta_p2'),
|
||||
('vvow_fablab_laser_sabko', 'sabako_laser'),
|
||||
('vvow_fablab_3dprint_delta', '3d_delta'),
|
||||
('vvow_fablab_3dprint_prusa', 'prusa'),
|
||||
('vvow_fablab_3dprint_prusa_mmu', 'prusa_mmu'),
|
||||
('vvow_fablab_cnc_beamicon', 'cnc_beamicon'),
|
||||
('vvow_metall_welding_mig', 'mig_mag'),
|
||||
('vvow_metall_welding_wig', 'wig'),
|
||||
('vvow_metall_welding_misc', 'schweissen_allgemein'),
|
||||
('vvow_metall_lathe', 'drehbank'),
|
||||
('vvow_metall_bandsaw', 'bandsaege_metall'),
|
||||
('vvow_metall_mill_fp2', 'fraese'),
|
||||
('vvow_metall_bende', 'abkantbank'),
|
||||
('vvow_metall_chop_saw', 'kreissaege_metall'),
|
||||
]
|
||||
|
||||
MachineAccess = self.env['ows.machine.access']
|
||||
MailMessage = self.env['mail.message']
|
||||
TrackingValue = self.env['mail.tracking.value']
|
||||
|
||||
count_created = 0
|
||||
|
||||
for partner in self.search([]):
|
||||
for field_name, machine_code in mapping:
|
||||
if not getattr(partner, field_name, False):
|
||||
continue
|
||||
|
||||
machine = self.env['ows.machine'].search([('code', '=', machine_code)], limit=1)
|
||||
if not machine:
|
||||
continue
|
||||
|
||||
# Änderungsverlauf durchsuchen: Wann wurde das Feld auf True gesetzt?
|
||||
tracking = TrackingValue.search([
|
||||
('field', '=', field_name),
|
||||
('field_type', '=', 'boolean'),
|
||||
('new_value_integer', '=', 1),
|
||||
('mail_message_id.model', '=', 'res.partner'),
|
||||
('mail_message_id.res_id', '=', partner.id),
|
||||
], order='id ASC', limit=1)
|
||||
|
||||
date_granted = tracking.mail_message_id.date if tracking else fields.Datetime.now()
|
||||
|
||||
if not MachineAccess.search([('partner_id', '=', partner.id), ('machine_id', '=', machine.id)]):
|
||||
MachineAccess.create({
|
||||
'partner_id': partner.id,
|
||||
'machine_id': machine.id,
|
||||
'date_granted': date_granted,
|
||||
})
|
||||
count_created += 1
|
||||
|
||||
_logger.info(f"[OWS Migration] ✅ Maschinenfreigaben erstellt: {count_created}")
|
||||
self.env.cr.commit()
|
||||
|
||||
@api.model
|
||||
def archive_partners_without_users(self):
|
||||
"""
|
||||
Archiviert alle Partner (res.partner), die keine Benutzer (res.users) sind.
|
||||
"""
|
||||
Partner = self.env['res.partner']
|
||||
User = self.env['res.users']
|
||||
|
||||
# IDs aller Partner, die ein Benutzerkonto haben
|
||||
user_partner_ids = User.search([]).mapped('partner_id').ids
|
||||
|
||||
# Alle Partner ohne Benutzerkonto
|
||||
partners_to_archive = Partner.search([
|
||||
('id', 'not in', user_partner_ids),
|
||||
('active', '=', True),
|
||||
])
|
||||
|
||||
count = len(partners_to_archive)
|
||||
partners_to_archive.write({'active': False})
|
||||
for p in partners_to_archive:
|
||||
_logger.debug(f"[OWS] Archiviert Partner: {p.name} (ID {p.id})")
|
||||
_logger.info(f"[OWS] Archiviert {count} Partner ohne Benutzerkonto.")
|
||||
self.env.cr.commit()
|
||||
|
||||
|
||||
class OwsUser(models.Model):
|
||||
_name = 'ows.user'
|
||||
_description = 'OWS: Benutzerdaten'
|
||||
_table = 'ows_user'
|
||||
_logger.info("✅ ows_user geladen")
|
||||
|
||||
partner_id = fields.Many2one(
|
||||
'res.partner',
|
||||
required=True,
|
||||
ondelete='cascade',
|
||||
string='Kontakt'
|
||||
)
|
||||
|
||||
birthday = fields.Date('Geburtstag')
|
||||
rfid_card = fields.Text('RFID Card ID')
|
||||
security_briefing = fields.Boolean('Haftungsausschluss', default=False)
|
||||
security_id = fields.Text('Haftungsausschluss ID')
|
||||
|
||||
_sql_constraints = [
|
||||
('partner_unique', 'unique(partner_id)', 'Jeder Partner darf nur einen OWS-Datensatz haben.')
|
||||
]
|
||||
|
||||
|
||||
AVAILABLE_COLORS = [
|
||||
('#000000', 'schwarz'),
|
||||
('#ff0000', 'Rot'),
|
||||
('#E91E63', 'Pink'),
|
||||
('#9C27B0', 'Lila'),
|
||||
('#3F51B5', 'Indigo'),
|
||||
('#0000ff', 'Blau'),
|
||||
('#008000', 'Grün'),
|
||||
('#ffff00', 'Gelb'),
|
||||
('#FF9800', 'Orange'),
|
||||
('#795548', 'Braun'),
|
||||
('#ffffff', 'Weiss'),
|
||||
]
|
||||
|
||||
class OwsMachineArea(models.Model):
|
||||
_name = 'ows.machine.area'
|
||||
_table = 'ows_machine_area'
|
||||
_description = 'OWS: Maschinenbereich'
|
||||
_order = 'name'
|
||||
|
||||
name = fields.Char(string="Name", required=True, translate=True)
|
||||
|
||||
color_hex = fields.Selection(
|
||||
selection=AVAILABLE_COLORS,
|
||||
string="Farbe (Hex)",
|
||||
required=True,
|
||||
)
|
||||
|
||||
color_hex_value = fields.Char(
|
||||
string="Farbcode",
|
||||
compute='_compute_color_hex_value',
|
||||
store=False
|
||||
)
|
||||
|
||||
color_name = fields.Char(
|
||||
string="Farbname",
|
||||
compute='_compute_color_name',
|
||||
store=False
|
||||
)
|
||||
|
||||
@api.depends('color_hex')
|
||||
def _compute_color_hex_value(self):
|
||||
for rec in self:
|
||||
rec.color_hex_value = rec.color_hex or ''
|
||||
|
||||
@api.depends('color_hex')
|
||||
def _compute_color_name(self):
|
||||
label_dict = dict(AVAILABLE_COLORS)
|
||||
for rec in self:
|
||||
rec.color_name = label_dict.get(rec.color_hex, 'Unbekannt')
|
||||
|
||||
|
||||
|
||||
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.)'
|
||||
)
|
||||
|
||||
# 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'),
|
||||
('red', 'Kategorie 3: rot'),
|
||||
], string="Sicherheitskategorie", required=True, default='red', help="Sicherheitsrelevante Maschinenkategorie:\n"
|
||||
"- grün: keine Einweisungspflicht\n"
|
||||
"- gelb: empfohlene Einweisung\n"
|
||||
"- rot: Einweisung zwingend erforderlich")
|
||||
|
||||
category_icon = fields.Char(string="Kategorie-Symbol", compute="_compute_category_icon", store=False)
|
||||
|
||||
@api.depends('category')
|
||||
def _compute_category_icon(self):
|
||||
for rec in self:
|
||||
icon_map = {
|
||||
'green': '🟢',
|
||||
'yellow': '🟡',
|
||||
'red': '🔴',
|
||||
}
|
||||
rec.category_icon = icon_map.get(rec.category, '⚪')
|
||||
|
||||
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,)
|
||||
|
||||
@api.depends('product_ids.product_id.name')
|
||||
def _compute_product_using_names(self):
|
||||
for machine in self:
|
||||
names = machine.product_ids.mapped('product_id.name')
|
||||
machine.product_names = ", ".join(names)
|
||||
|
||||
@api.depends('training_ids.training_id.name')
|
||||
def _compute_product_training_names(self):
|
||||
for machine in self:
|
||||
names = machine.training_ids.mapped('training_id.name')
|
||||
machine.training_names = ", ".join(names)
|
||||
|
||||
# Keine eigenen SQL Constraints - Equipment hat bereits unique constraint für serial_no
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
"""
|
||||
Beim Erstellen einer ows.machine:
|
||||
1. Equipment IMMER automatisch erstellen
|
||||
2. Area → Location synchronisieren
|
||||
3. serial_no und name vom User übernehmen
|
||||
"""
|
||||
# Equipment IMMER automatisch erstellen
|
||||
equipment_vals = {
|
||||
'name': vals.get('name', 'Neue Maschine'),
|
||||
'serial_no': vals.get('serial_no', False),
|
||||
}
|
||||
|
||||
# Area → Location Mapping für Equipment
|
||||
if 'area_id' in vals and vals['area_id']:
|
||||
area = self.env['ows.machine.area'].browse(vals['area_id'])
|
||||
if area and area.name:
|
||||
equipment_vals['location'] = area.name
|
||||
|
||||
equipment = self.env['maintenance.equipment'].create(equipment_vals)
|
||||
vals['equipment_id'] = equipment.id
|
||||
|
||||
return super(OwsMachine, self).create(vals)
|
||||
|
||||
def write(self, vals):
|
||||
"""
|
||||
Bei Updates:
|
||||
1. Area → Location synchronisieren
|
||||
2. Name/Serial_no → Equipment synchronisieren
|
||||
"""
|
||||
# Area → Location Mapping
|
||||
if 'area_id' in vals and vals['area_id']:
|
||||
area = self.env['ows.machine.area'].browse(vals['area_id'])
|
||||
if area and area.name:
|
||||
vals['location'] = area.name
|
||||
|
||||
# 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):
|
||||
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):
|
||||
"""
|
||||
Gibt eine gruppierte Liste von Maschinenzugängen für einen bestimmten Partner zurück. Diese Funktion wird in
|
||||
Odoo POS Frontend verwendet um die Ansicht zu erzeugen auf Welche Maschinen der Partner Zugriff hat.
|
||||
|
||||
Für einen gegebenen Partner (über die partner_id) werden alle Maschinenbereiche (areas) abgefragt.
|
||||
Für jeden Bereich wird geprüft, auf welche Maschinen der Partner Zugriff hat. Das Ergebnis wird
|
||||
als Liste von Bereichen mit jeweils zugehörigen Maschinen und Zugriffsstatus zurückgegeben.
|
||||
|
||||
Zusätzlich werden sicherheitsrelevante Informationen des Partners (wie Sicherheitsunterweisung,
|
||||
Sicherheits-ID, RFID-Karte und Geburtstag) aus dem zugehörigen ows_user ermittelt und mitgeliefert.
|
||||
|
||||
Args:
|
||||
partner_id (int): Die ID des Partners, für den die Zugriffsübersicht erstellt werden soll.
|
||||
|
||||
Returns:
|
||||
dict: Ein Dictionary mit folgenden Schlüsseln:
|
||||
- 'access_by_area': Liste von Bereichen mit Maschinen und Zugriffsstatus.
|
||||
- 'security_briefing': Sicherheitsunterweisung des Nutzers (bool oder False).
|
||||
- 'security_id': Sicherheits-ID des Nutzers (str oder '').
|
||||
- 'rfid_card': RFID-Kartennummer des Nutzers (str oder '').
|
||||
- 'birthday': Geburtstag des Nutzers (str oder '').
|
||||
"""
|
||||
partner = self.env['res.partner'].browse(partner_id)
|
||||
areas = self.env['ows.machine.area'].search([], order="name")
|
||||
_logger.info("Access RPC called with partner_id=%s", partner_id)
|
||||
access_by_area = []
|
||||
for area in areas:
|
||||
machines = self.search([('area_id', '=', area.id), ('category', '=', 'red')], order="name")
|
||||
machine_list = []
|
||||
for machine in machines:
|
||||
has_access = bool(self.env['ows.machine.access'].search([
|
||||
('partner_id', '=', partner_id),
|
||||
('machine_id', '=', machine.id),
|
||||
], limit=1))
|
||||
machine_list.append({
|
||||
'name': machine.name,
|
||||
'has_access': has_access,
|
||||
})
|
||||
if machine_list:
|
||||
access_by_area.append({
|
||||
'area': area.name,
|
||||
'color_hex': area.color_hex or '#000000',
|
||||
'machines': machine_list
|
||||
})
|
||||
|
||||
user = partner.ows_user_id[:1]
|
||||
return {
|
||||
'access_by_area': access_by_area,
|
||||
'security_briefing': user.security_briefing if user else False,
|
||||
'security_id': user.security_id if user else '',
|
||||
'rfid_card': user.rfid_card if user else '',
|
||||
'birthday': user.birthday if user else '',
|
||||
}
|
||||
|
||||
|
||||
|
||||
class OwsMachineAccess(models.Model):
|
||||
_name = 'ows.machine.access'
|
||||
_table = 'ows_machine_access'
|
||||
_description = 'OWS: Maschinenfreigabe'
|
||||
_order = 'partner_id, machine_id'
|
||||
|
||||
partner_id = fields.Many2one('res.partner', required=True, ondelete='cascade')
|
||||
machine_id = fields.Many2one('ows.machine', required=True)
|
||||
date_granted = fields.Date(default=fields.Date.today)
|
||||
date_expiry = fields.Date(string="Ablaufdatum")
|
||||
granted_by_pos = fields.Boolean(default=True)
|
||||
|
||||
_sql_constraints = [
|
||||
('partner_machine_unique', 'unique(partner_id, machine_id)', 'Der Kunde hat diese Freigabe bereits.')
|
||||
]
|
||||
|
||||
|
||||
class OwsMachineProduct(models.Model):
|
||||
_name = 'ows.machine.product'
|
||||
_table = 'ows_machine_product'
|
||||
_description = 'OWS: Zuordnung Produkt der Nutzung zu der Maschine'
|
||||
|
||||
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')
|
||||
|
||||
class OwsMachineTraining(models.Model):
|
||||
_name = 'ows.machine.training'
|
||||
_table = 'ows_machine_training'
|
||||
_description = 'OWS: Zuordnung Produkt der Einweisung zu der Maschine'
|
||||
|
||||
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')
|
||||
50
open_workshop_base/models/pos_order.py
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
from odoo import models, fields, api
|
||||
from collections import defaultdict
|
||||
|
||||
#import debugpy
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
_logger.info("✅ pos_order.py geladen Test 2")
|
||||
|
||||
#debugpy.listen(("0.0.0.0", 5678))
|
||||
print("✅ debugpy wartet auf Verbindung (Port 5678) ...")
|
||||
# Optional: Starte erst, wenn VS Code verbunden ist
|
||||
#debugpy.wait_for_client()
|
||||
|
||||
class PosOrder(models.Model):
|
||||
_inherit = 'pos.order'
|
||||
|
||||
def _process_order(self, order, existing_order):
|
||||
_logger.info("🚨 DEBUG: _process_order wurde aufgerufen mit order: %s", order.get('name', 'unbekannt'))
|
||||
pos_order_id = super(PosOrder, self)._process_order(order, existing_order)
|
||||
pos_order = self.browse(pos_order_id)
|
||||
|
||||
training_products = self.env['ows.machine.training'].search([])
|
||||
product_map = defaultdict(list)
|
||||
for tp in training_products:
|
||||
product_map[tp.training_id.product_tmpl_id.id].append(tp.machine_id.id)
|
||||
|
||||
partner = pos_order.partner_id
|
||||
if not partner:
|
||||
_logger.info("🟡 POS-Bestellung ohne Partner – keine Freigabe möglich")
|
||||
return pos_order_id
|
||||
|
||||
for line in pos_order.lines:
|
||||
product_tmpl_id = line.product_id.product_tmpl_id.id
|
||||
machine_ids = product_map.get(product_tmpl_id, [])
|
||||
_logger.info("🔍 Prüfe Produkt %s → Maschinen IDs: %s", line.product_id.display_name, machine_ids)
|
||||
for machine_id in machine_ids:
|
||||
already_exists = self.env['ows.machine.access'].search([
|
||||
('partner_id', '=', partner.id),
|
||||
('machine_id', '=', machine_id)
|
||||
], limit=1)
|
||||
if not already_exists:
|
||||
self.env['ows.machine.access'].create({
|
||||
'partner_id': partner.id,
|
||||
'machine_id': machine_id,
|
||||
'granted_by_pos': True
|
||||
})
|
||||
_logger.info("✅ Maschinenfreigabe erstellt: %s für %s", machine_id, partner.name)
|
||||
|
||||
return pos_order_id
|
||||
8
open_workshop_base/security/ir.model.access.csv
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_ows_machine_access_user,ows.machine.access,model_ows_machine_access,base.group_user,1,1,1,1
|
||||
access_ows_machine_user,ows.machine,model_ows_machine,base.group_user,1,1,1,1
|
||||
access_ows_machine_product_user,ows.machine.product,model_ows_machine_product,base.group_user,1,1,1,1
|
||||
access_ows_machine_training_user,access_ows_machine_training_user,model_ows_machine_training,base.group_user,1,1,1,1
|
||||
access_ows_machine_area,ows.machine.area,model_ows_machine_area,base.group_user,1,1,1,1
|
||||
access_ows_user,ows.user,model_ows_user,base.group_user,1,1,1,1
|
||||
access_ows_machine_training,ows.machine.training,model_ows_machine_training,base.group_user,1,1,1,1
|
||||
|
9
open_workshop_base/static/src/css/category_color.css
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
.category-color-circle {
|
||||
display: inline-block;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
margin-left: 8px;
|
||||
vertical-align: middle;
|
||||
border: 1px solid #444;
|
||||
}
|
||||
8
open_workshop_base/views/assets.xml
Normal file
|
|
@ -0,0 +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_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>
|
||||
40
open_workshop_base/views/machine_area_views.xml
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<!-- machine_area_views.xml -->
|
||||
<odoo>
|
||||
<!-- Action zum Anzeigen der Bereiche -->
|
||||
<record id="action_machine_area_list" model="ir.actions.act_window">
|
||||
<field name="name">Maschinenbereiche</field>
|
||||
<field name="res_model">ows.machine.area</field>
|
||||
<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_base.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>
|
||||
<field name="model">ows.machine.area</field>
|
||||
<field name="arch" type="xml">
|
||||
<list>
|
||||
<field name="name"/>
|
||||
<field name="color_hex_value" string="Farbe (Hex)"/>
|
||||
<field name="color_name" string="Farbname"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Formularansicht -->
|
||||
<record id="view_machine_area_form" model="ir.ui.view">
|
||||
<field name="name">ows.machine.area.form</field>
|
||||
<field name="model">ows.machine.area</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Maschinenbereich">
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="color_hex"/>
|
||||
<field name="color_name" readonly="1"/>
|
||||
</group>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||