Compare commits
107 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7f827cf7eb | |||
| dfb77411d9 | |||
| 59539e0201 | |||
| b6a0f0462d | |||
| 5fcaef0336 | |||
| bb3e492d30 | |||
| aeb8e5660b | |||
| 4c03959437 | |||
| 75d91984d1 | |||
| 77cd3f4595 | |||
| 4684d33241 | |||
| 85a90f90fd | |||
| 0d27ca36b0 | |||
| b85ec97d9c | |||
| f9cbcffe34 | |||
| fe09d1439f | |||
| 23b9867eec | |||
| 00df958985 | |||
| 98c96018d6 | |||
| b2f7a5edbf | |||
| d6c2986ebf | |||
| 67184e7e01 | |||
| ee0e6c8b98 | |||
| 1c9829a7aa | |||
| 1abeb97afa | |||
| daddb51327 | |||
| 5d1faa7b5b | |||
| 21b0d6305f | |||
| 988421a0c5 | |||
| b26e70dbe9 | |||
| 38d0becfad | |||
| 943d48af58 | |||
| 09f28c8070 | |||
| 40a5fe8b0f | |||
| 31a40e95d7 | |||
| 91209dead4 | |||
| 9cb6dc8ac5 | |||
| 93385abb0f | |||
| e443f8709d | |||
| b151894c39 | |||
| 0c4b58e0a6 | |||
| e460acddf3 | |||
| a1c6903893 | |||
| 72a46e3657 | |||
| 641bfb3ade | |||
| a73c0cb299 | |||
| 4626379d6e | |||
| 9be8f320f7 | |||
| d6a98cfbd0 | |||
| 0135035a54 | |||
| f79e126c8c | |||
| 253d289633 | |||
| de317f46e6 | |||
| 8ae586cca6 | |||
| e18f2880a4 | |||
| 43cf5754fd | |||
| 4c048b5ca9 | |||
| 686b201792 | |||
| bd235e3ca0 | |||
| a5f8fd32c3 | |||
| 3fa050f153 | |||
| e53c4028c9 | |||
| 5057985024 | |||
| 00f33faebe | |||
| 2eff81ce54 | |||
| 92490aeb9c | |||
| 1b203eeb10 | |||
| ed339b0e2e | |||
| e1feeb6d75 | |||
| 550cdac1eb | |||
| b68ac293c8 | |||
| 42db76a9c7 | |||
| f87755e7d0 | |||
| 8fb58c744e | |||
| ceb8af7e48 | |||
| 71c6ba56ed | |||
| 744b7b3234 | |||
| 3619526af0 | |||
| bb3d1bf7c9 | |||
| bf605539fa | |||
| 12d5902e3c | |||
| 977aa2d1b3 | |||
| 58c7e8f258 | |||
| 7cd458b72f | |||
| dff2de1755 | |||
| d56ae65b56 | |||
| bc0459ab9b | |||
| 7230bcb6f8 | |||
| f07f9dd8b3 | |||
| 5c7fd4330d | |||
| ab696db035 | |||
| 1069630e86 | |||
| 558dff276a | |||
| e0a9205fea | |||
| 24baff2a86 | |||
| 62dbf92b36 | |||
| bbb5181a74 | |||
| 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
|
|
||||||
|
|
@ -1,69 +0,0 @@
|
||||||
name: POS Test Feedback
|
|
||||||
about: Rückmeldung zu einem Test des POS-Systems geben
|
|
||||||
title: "[Feedback] "
|
|
||||||
labels: [feedback, test]
|
|
||||||
assignees: []
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
## 🧪 POS-Test: Feedbackformular
|
|
||||||
|
|
||||||
Bitte gib uns Rückmeldung zu den einzelnen Funktionen. Beschreibe ggf. Probleme oder Auffälligkeiten.
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: sonstiges
|
|
||||||
attributes:
|
|
||||||
label: Sonstiges Feedback oder Fehler
|
|
||||||
description: Alles andere, was dir beim Test aufgefallen ist (z. B. Layout, Ladezeiten, Fehlermeldungen).
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: nutzerauswahl
|
|
||||||
attributes:
|
|
||||||
label: Nutzer auswählen
|
|
||||||
description: Funktioniert die Auswahl des Nutzers im POS wie erwartet?
|
|
||||||
placeholder: z.B. Nutzer nicht auffindbar, Anzeige langsam etc.
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: haftung
|
|
||||||
attributes:
|
|
||||||
label: Haftungsausschluss prüfen
|
|
||||||
description: Wird der Haftungsausschluss korrekt angezeigt bzw. berücksichtigt?
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: maschinenfreigabe
|
|
||||||
attributes:
|
|
||||||
label: Maschinenfreigabe prüfen
|
|
||||||
description: Wird korrekt angezeigt, ob der Nutzer eine Einweisung für eine Maschine hat?
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: abrechnung
|
|
||||||
attributes:
|
|
||||||
label: Abrechnung (Bargeld / SumUp)
|
|
||||||
description: Funktioniert die Abrechnung für den Nutzer?
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: coupon
|
|
||||||
attributes:
|
|
||||||
label: Coupons (erstellen / einlösen)
|
|
||||||
description: Funktionieren Erstellen und Einlösen von Coupons korrekt?
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: nutzerdaten
|
|
||||||
attributes:
|
|
||||||
label: Nutzerdaten aktualisieren / Haftungsausschluss abwählen
|
|
||||||
description: Lassen sich Nutzerdaten wie RFID oder der Haftungsausschluss korrekt ändern?
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: einweisung
|
|
||||||
attributes:
|
|
||||||
label: Einweisung verkaufen / prüfen
|
|
||||||
description: Lässt sich eine Einweisung verkaufen und wird sie korrekt zugewiesen?
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: backend
|
|
||||||
attributes:
|
|
||||||
label: Backend-Funktionen
|
|
||||||
description: Können Maschinen und Einweisungen im Backend wie erwartet verwaltet werden?
|
|
||||||
|
|
||||||
|
|
||||||
7
.gitignore
vendored
|
|
@ -15,7 +15,7 @@ dist/
|
||||||
downloads/
|
downloads/
|
||||||
eggs/
|
eggs/
|
||||||
.eggs/
|
.eggs/
|
||||||
lib/
|
#lib/
|
||||||
lib64/
|
lib64/
|
||||||
parts/
|
parts/
|
||||||
sdist/
|
sdist/
|
||||||
|
|
@ -159,3 +159,8 @@ cython_debug/
|
||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
#.idea/
|
#.idea/
|
||||||
|
|
||||||
|
# DokuWiki API Test Script (enthält Credentials)
|
||||||
|
test_dokuwiki.py
|
||||||
|
|
||||||
|
# Python Virtual Environment
|
||||||
|
venv/
|
||||||
|
|
|
||||||
289
.vscode/.jslintrc
vendored
Normal file
|
|
@ -0,0 +1,289 @@
|
||||||
|
{
|
||||||
|
"globals": {
|
||||||
|
"$": false,
|
||||||
|
"_": false,
|
||||||
|
"fuzzy": false,
|
||||||
|
"jQuery": false,
|
||||||
|
"moment": false,
|
||||||
|
"odoo": false,
|
||||||
|
"openerp": false,
|
||||||
|
"self": false
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"browser": true
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"no-alert": "error",
|
||||||
|
"no-array-constructor": "error",
|
||||||
|
"no-bitwise": "off",
|
||||||
|
"no-caller": "error",
|
||||||
|
"no-case-declarations": "error",
|
||||||
|
"no-catch-shadow": "error",
|
||||||
|
"no-class-assign": "error",
|
||||||
|
"no-cond-assign": "error",
|
||||||
|
"no-confusing-arrow": "error",
|
||||||
|
"no-console": "error",
|
||||||
|
"no-const-assign": "error",
|
||||||
|
"no-constant-condition": "error",
|
||||||
|
"no-continue": "off",
|
||||||
|
"no-control-regex": "error",
|
||||||
|
"no-debugger": "error",
|
||||||
|
"no-delete-var": "error",
|
||||||
|
"no-div-regex": "error",
|
||||||
|
"no-dupe-args": "error",
|
||||||
|
"no-dupe-class-members": "error",
|
||||||
|
"no-dupe-keys": "error",
|
||||||
|
"no-duplicate-case": "error",
|
||||||
|
"no-duplicate-imports": "error",
|
||||||
|
"no-else-return": "error",
|
||||||
|
"no-empty": "error",
|
||||||
|
"no-empty-character-class": "error",
|
||||||
|
"no-empty-function": "error",
|
||||||
|
"no-empty-pattern": "error",
|
||||||
|
"no-eq-null": "error",
|
||||||
|
"no-eval": "error",
|
||||||
|
"no-ex-assign": "error",
|
||||||
|
"no-extend-native": "error",
|
||||||
|
"no-extra-bind": "error",
|
||||||
|
"no-extra-boolean-cast": "error",
|
||||||
|
"no-extra-label": "error",
|
||||||
|
"no-extra-parens": "error",
|
||||||
|
"no-extra-semi": "error",
|
||||||
|
"no-fallthrough": "error",
|
||||||
|
"no-floating-decimal": "error",
|
||||||
|
"no-func-assign": "error",
|
||||||
|
"no-implicit-coercion": ["error", {
|
||||||
|
"allow": ["~"]
|
||||||
|
}],
|
||||||
|
"no-implicit-globals": "error",
|
||||||
|
"no-implied-eval": "error",
|
||||||
|
"no-inline-comments": "error",
|
||||||
|
"no-inner-declarations": "error",
|
||||||
|
"no-invalid-regexp": "error",
|
||||||
|
"no-invalid-this": "off",
|
||||||
|
"no-irregular-whitespace": "error",
|
||||||
|
"no-iterator": "error",
|
||||||
|
"no-label-var": "error",
|
||||||
|
"no-labels": "error",
|
||||||
|
"no-lone-blocks": "error",
|
||||||
|
"no-lonely-if": "error",
|
||||||
|
"no-loop-func": "off",
|
||||||
|
"no-magic-numbers": "off",
|
||||||
|
"no-mixed-operators": "error",
|
||||||
|
"no-mixed-requires": "error",
|
||||||
|
"no-mixed-spaces-and-tabs": "error",
|
||||||
|
"no-multi-spaces": "error",
|
||||||
|
"no-multi-str": "error",
|
||||||
|
"no-multiple-empty-lines": "error",
|
||||||
|
"no-native-reassign": "error",
|
||||||
|
"no-negated-condition": "error",
|
||||||
|
"no-negated-in-lhs": "error",
|
||||||
|
"no-nested-ternary": "off",
|
||||||
|
"no-new": "error",
|
||||||
|
"no-new-func": "error",
|
||||||
|
"no-new-object": "error",
|
||||||
|
"no-new-require": "error",
|
||||||
|
"no-new-symbol": "error",
|
||||||
|
"no-new-wrappers": "error",
|
||||||
|
"no-obj-calls": "error",
|
||||||
|
"no-octal": "error",
|
||||||
|
"no-octal-escape": "error",
|
||||||
|
"no-param-reassign": "error",
|
||||||
|
"no-path-concat": "error",
|
||||||
|
"no-plusplus": "off",
|
||||||
|
"no-process-env": "error",
|
||||||
|
"no-process-exit": "error",
|
||||||
|
"no-proto": "error",
|
||||||
|
"no-prototype-builtins": "error",
|
||||||
|
"no-redeclare": "error",
|
||||||
|
"no-regex-spaces": "error",
|
||||||
|
"no-restricted-globals": "error",
|
||||||
|
"no-restricted-imports": "error",
|
||||||
|
"no-restricted-modules": "error",
|
||||||
|
"no-restricted-syntax": "error",
|
||||||
|
"no-return-assign": "error",
|
||||||
|
"no-script-url": "error",
|
||||||
|
"no-self-assign": "error",
|
||||||
|
"no-self-compare": "error",
|
||||||
|
"no-sequences": "error",
|
||||||
|
"no-shadow": "error",
|
||||||
|
"no-shadow-restricted-names": "error",
|
||||||
|
"no-whitespace-before-property": "error",
|
||||||
|
"no-spaced-func": "error",
|
||||||
|
"no-sparse-arrays": "error",
|
||||||
|
"no-sync": "error",
|
||||||
|
"no-tabs": "error",
|
||||||
|
"no-ternary": "off",
|
||||||
|
"no-trailing-spaces": "error",
|
||||||
|
"no-this-before-super": "error",
|
||||||
|
"no-throw-literal": "error",
|
||||||
|
"no-undef": "error",
|
||||||
|
"no-undef-init": "error",
|
||||||
|
"no-undefined": "off",
|
||||||
|
"no-unexpected-multiline": "error",
|
||||||
|
"no-underscore-dangle": "off",
|
||||||
|
"no-unmodified-loop-condition": "error",
|
||||||
|
"no-unneeded-ternary": "error",
|
||||||
|
"no-unreachable": "error",
|
||||||
|
"no-unsafe-finally": "error",
|
||||||
|
"no-unused-expressions": "error",
|
||||||
|
"no-unused-labels": "error",
|
||||||
|
"no-unused-vars": "error",
|
||||||
|
"no-use-before-define": "error",
|
||||||
|
"no-useless-call": "error",
|
||||||
|
"no-useless-computed-key": "error",
|
||||||
|
"no-useless-concat": "error",
|
||||||
|
"no-useless-constructor": "error",
|
||||||
|
"no-useless-escape": "error",
|
||||||
|
"no-useless-rename": "error",
|
||||||
|
"no-void": "error",
|
||||||
|
"no-var": "off",
|
||||||
|
"no-warning-comments": "off",
|
||||||
|
"no-with": "error",
|
||||||
|
"array-bracket-spacing": "off",
|
||||||
|
"array-callback-return": "error",
|
||||||
|
"arrow-body-style": "error",
|
||||||
|
"arrow-parens": "error",
|
||||||
|
"arrow-spacing": "off",
|
||||||
|
"accessor-pairs": "error",
|
||||||
|
"block-scoped-var": "off",
|
||||||
|
"block-spacing": ["error", "always"],
|
||||||
|
"brace-style": "error",
|
||||||
|
"callback-return": "error",
|
||||||
|
"camelcase": "off",
|
||||||
|
"capitalized-comments": ["error", "always", {
|
||||||
|
"ignoreConsecutiveComments": true,
|
||||||
|
"ignoreInlineComments": true
|
||||||
|
}],
|
||||||
|
"comma-dangle": ["error", "always-multiline"],
|
||||||
|
"comma-spacing": ["error", {
|
||||||
|
"before": false,
|
||||||
|
"after": true
|
||||||
|
}],
|
||||||
|
"comma-style": "error",
|
||||||
|
"complexity": [
|
||||||
|
"error",
|
||||||
|
15
|
||||||
|
],
|
||||||
|
"computed-property-spacing": "off",
|
||||||
|
"consistent-return": "off",
|
||||||
|
"consistent-this": "off",
|
||||||
|
"constructor-super": "error",
|
||||||
|
"curly": "error",
|
||||||
|
"default-case": "off",
|
||||||
|
"dot-location": ["error", "property"],
|
||||||
|
"dot-notation": "error",
|
||||||
|
"eol-last": "error",
|
||||||
|
"eqeqeq": "error",
|
||||||
|
"func-names": "off",
|
||||||
|
"func-style": "off",
|
||||||
|
"generator-star-spacing": "off",
|
||||||
|
"global-require": "error",
|
||||||
|
"guard-for-in": "off",
|
||||||
|
"handle-callback-err": "error",
|
||||||
|
"id-blacklist": "error",
|
||||||
|
"id-length": "off",
|
||||||
|
"id-match": "error",
|
||||||
|
"indent": "error",
|
||||||
|
"init-declarations": "error",
|
||||||
|
"jsx-quotes": "error",
|
||||||
|
"key-spacing": "off",
|
||||||
|
"keyword-spacing": "error",
|
||||||
|
"linebreak-style": [
|
||||||
|
"error",
|
||||||
|
"unix"
|
||||||
|
],
|
||||||
|
"lines-around-comment": "error",
|
||||||
|
"max-depth": "error",
|
||||||
|
"max-len": ["error", {
|
||||||
|
"code": 80,
|
||||||
|
"ignorePattern": "odoo\\.define\\(",
|
||||||
|
"tabWidth": 4
|
||||||
|
}],
|
||||||
|
"max-lines": "off",
|
||||||
|
"max-nested-callbacks": "error",
|
||||||
|
"max-params": "off",
|
||||||
|
"max-statements": "off",
|
||||||
|
"max-statements-per-line": "error",
|
||||||
|
"multiline-ternary": "off",
|
||||||
|
"new-cap": "off",
|
||||||
|
"new-parens": "error",
|
||||||
|
"newline-after-var": "off",
|
||||||
|
"newline-before-return": "off",
|
||||||
|
"newline-per-chained-call": "off",
|
||||||
|
"object-curly-newline": ["error", { "consistent": true }],
|
||||||
|
"object-curly-spacing": ["error", "never"],
|
||||||
|
"object-property-newline": ["error", {
|
||||||
|
"allowAllPropertiesOnSameLine": true
|
||||||
|
}],
|
||||||
|
"object-shorthand": "off",
|
||||||
|
"one-var": "off",
|
||||||
|
"one-var-declaration-per-line": "off",
|
||||||
|
"operator-assignment": "error",
|
||||||
|
"operator-linebreak": "error",
|
||||||
|
"padded-blocks": "off",
|
||||||
|
"prefer-arrow-callback": "off",
|
||||||
|
"prefer-const": "error",
|
||||||
|
"prefer-reflect": "off",
|
||||||
|
"prefer-rest-params": "off",
|
||||||
|
"prefer-spread": "off",
|
||||||
|
"prefer-template": "off",
|
||||||
|
"quote-props": "off",
|
||||||
|
"quotes": "off",
|
||||||
|
"radix": "error",
|
||||||
|
"require-yield": "error",
|
||||||
|
"rest-spread-spacing": "off",
|
||||||
|
"semi": [
|
||||||
|
"error",
|
||||||
|
"always"
|
||||||
|
],
|
||||||
|
"semi-spacing": "error",
|
||||||
|
"sort-imports": "error",
|
||||||
|
"sort-vars": "off",
|
||||||
|
"space-before-blocks": "error",
|
||||||
|
"space-before-function-paren": "error",
|
||||||
|
"space-in-parens": "off",
|
||||||
|
"space-infix-ops": "off",
|
||||||
|
"space-unary-ops": "off",
|
||||||
|
"spaced-comment": ["error", "always"],
|
||||||
|
"strict": ["error", "function"],
|
||||||
|
"template-curly-spacing": "off",
|
||||||
|
"unicode-bom": "error",
|
||||||
|
"use-isnan": "error",
|
||||||
|
"valid-jsdoc": ["error", {
|
||||||
|
"prefer": {
|
||||||
|
"arg": "param",
|
||||||
|
"argument": "param",
|
||||||
|
"augments": "extends",
|
||||||
|
"constructor": "class",
|
||||||
|
"exception": "throws",
|
||||||
|
"func": "function",
|
||||||
|
"method": "function",
|
||||||
|
"prop": "property",
|
||||||
|
"return": "returns",
|
||||||
|
"virtual": "abstract",
|
||||||
|
"yield": "yields"
|
||||||
|
},
|
||||||
|
"preferType": {
|
||||||
|
"array": "Array",
|
||||||
|
"bool": "Boolean",
|
||||||
|
"boolean": "Boolean",
|
||||||
|
"number": "Number",
|
||||||
|
"object": "Object",
|
||||||
|
"str": "String",
|
||||||
|
"string": "String"
|
||||||
|
},
|
||||||
|
"requireParamDescription": false,
|
||||||
|
"requireReturn": false,
|
||||||
|
"requireReturnDescription": false,
|
||||||
|
"requireReturnType": false
|
||||||
|
}],
|
||||||
|
"valid-typeof": "error",
|
||||||
|
"vars-on-top": "off",
|
||||||
|
"wrap-iife": "error",
|
||||||
|
"wrap-regex": "error",
|
||||||
|
"yield-star-spacing": "off",
|
||||||
|
"yoda": "error"
|
||||||
|
},
|
||||||
|
"parserOptions": {}
|
||||||
|
}
|
||||||
64
.vscode/README.md
vendored
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
# Quickstart: Debugging für open_workshop
|
||||||
|
|
||||||
|
Diese Datei hilft dir, das `open_workshop` Addon schnell in VS Code zu öffnen und sowohl das Addon als auch (optional) den Odoo‑Core zu debuggen.
|
||||||
|
|
||||||
|
Kurzfassung
|
||||||
|
- Öffne in VS Code den Ordner `extra-addons/open_workshop`.
|
||||||
|
- Starte die Development-Container/Composer Umgebung mit `./dev.sh` (Option 1 oder 2).
|
||||||
|
- Verwende die DevContainer‑Funktion oder die Debug‑Konfigurationen unten.
|
||||||
|
|
||||||
|
1) Container starten
|
||||||
|
|
||||||
|
Empfohlen: im Projektstamm (`odoo`) ausführen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./dev.sh
|
||||||
|
# Option 1: Normal starten (ODOO_DEV=1)
|
||||||
|
# Option 2: Debug-Modus (ODOO_DEBUG=1) — der Container wartet auf eine Debug-Verbindung
|
||||||
|
```
|
||||||
|
|
||||||
|
2) VS Code als DevContainer verbinden (empfohlen)
|
||||||
|
|
||||||
|
- Öffne VS Code im lokalen Ordner `extra-addons/open_workshop`.
|
||||||
|
- Command Palette → `Dev Containers: Attach to Running Container...` → wähle `odoo-dev`.
|
||||||
|
- VS Code öffnet den Container als Arbeitsumgebung (remoteUser ist `odoo`).
|
||||||
|
- Öffne dann das Workspace‑Verzeichnis `/mnt/extra-addons/open_workshop`.
|
||||||
|
|
||||||
|
Vorteil: Du arbeitest direkt im Container (kein lokales Kopieren der Core‑Sourcen nötig) und Breakpoints funktionieren zuverlässig.
|
||||||
|
|
||||||
|
3) Debugging (Attach)
|
||||||
|
|
||||||
|
- Wenn du als DevContainer verbunden bist, verwende die Debug‑Konfiguration "Odoo Attach (container)" (port 5678).
|
||||||
|
- Wenn du lokal arbeitest und den Host‑Port benutzt, verwende "Odoo Attach (host)" (port 4338).
|
||||||
|
|
||||||
|
4) Pfad‑Mapping
|
||||||
|
|
||||||
|
- Die `launch.json` enthält Pfadabbildungen:
|
||||||
|
- `${workspaceFolder}` ↔ `/mnt/extra-addons/open_workshop` (dein Addon)
|
||||||
|
- `${workspaceFolder}/../../odoo-source/odoo` ↔ `/usr/lib/python3/dist-packages/odoo` (falls du lokal eine Kopie des Odoo‑Cores hast)
|
||||||
|
|
||||||
|
Hinweis: Lokale `odoo-source` ist nicht erforderlich, wenn du per DevContainer arbeitest, weil VS Code die Dateien direkt im Container liest.
|
||||||
|
|
||||||
|
5) Troubleshooting
|
||||||
|
|
||||||
|
- Debug-Verbindung schlägt fehl: prüfe, ob der Container im Debug‑Modus läuft und der Port gemappt ist:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.dev.yaml ps
|
||||||
|
docker compose -f docker-compose.dev.yaml logs -f odoo-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
- VS Code meldet, dass Breakpoints nicht aufgelöst werden: vergewissere dich, dass die `pathMappings` korrekt sind und die lokalen Dateien existieren (oder nutze DevContainer).
|
||||||
|
|
||||||
|
6) Image/Compose aktualisieren
|
||||||
|
|
||||||
|
- Wenn du `Dockerfile.Dev` geändert hast: neu bauen (Option 3 in `./dev.sh`), dann Container neu starten (Option 5 oder down/up).
|
||||||
|
|
||||||
|
7) Kurze Checklist für Mitentwickler
|
||||||
|
|
||||||
|
- `./dev.sh` → Option 3 (einmalig) wenn du das Dev‑Image bauen musst.
|
||||||
|
- `./dev.sh` → Option 1 oder 2 zum Starten.
|
||||||
|
- In VS Code: Öffne `extra-addons/open_workshop`, dann `Dev Containers: Attach to Running Container...`.
|
||||||
|
- Starte Debug mit "Odoo Attach (container)" oder "Odoo Attach (host)".
|
||||||
|
|
||||||
|
|
||||||
45
.vscode/launch.json
vendored
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Odoo Attach (host)",
|
||||||
|
"type": "debugpy",
|
||||||
|
"request": "attach",
|
||||||
|
"connect": {
|
||||||
|
"host": "localhost",
|
||||||
|
"port": 4338
|
||||||
|
},
|
||||||
|
"pathMappings": [
|
||||||
|
{
|
||||||
|
"localRoot": "${workspaceFolder}",
|
||||||
|
"remoteRoot": "/mnt/extra-addons/open_workshop"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"localRoot": "${workspaceFolder}/../../odoo-source/odoo",
|
||||||
|
"remoteRoot": "/usr/lib/python3/dist-packages/odoo"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"justMyCode": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Odoo Attach (container)",
|
||||||
|
"type": "debugpy",
|
||||||
|
"request": "attach",
|
||||||
|
"connect": {
|
||||||
|
"host": "localhost",
|
||||||
|
"port": 5678
|
||||||
|
},
|
||||||
|
"pathMappings": [
|
||||||
|
{
|
||||||
|
"localRoot": "${workspaceFolder}",
|
||||||
|
"remoteRoot": "/mnt/extra-addons/open_workshop"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"localRoot": "${workspaceFolder}/../../odoo-source/odoo",
|
||||||
|
"remoteRoot": "/usr/lib/python3/dist-packages/odoo"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"justMyCode": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
30
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
"python.pythonPath": "/usr/bin/python3",
|
||||||
|
"python.analysis.extraPaths": [
|
||||||
|
"${workspaceFolder}",
|
||||||
|
"${workspaceFolder}/../../odoo-source/odoo"
|
||||||
|
],
|
||||||
|
"python.analysis.typeCheckingMode": "off"
|
||||||
|
}
|
||||||
|
{
|
||||||
|
"editor.rulers": [80, 100, 120],
|
||||||
|
"files.eol": "\n",
|
||||||
|
"[python]": {
|
||||||
|
"editor.defaultFormatter": "ms-python.python",
|
||||||
|
"editor.insertSpaces": true,
|
||||||
|
"editor.tabSize": 4
|
||||||
|
},
|
||||||
|
"[javascript]": {
|
||||||
|
"editor.insertSpaces": true,
|
||||||
|
"editor.tabSize": 2
|
||||||
|
},
|
||||||
|
"python.analysis.extraPaths": [
|
||||||
|
"../../odoo-source/odoo",
|
||||||
|
],
|
||||||
|
"python.testing.pytestArgs": [
|
||||||
|
"--odoo-http",
|
||||||
|
"."
|
||||||
|
],
|
||||||
|
"python.testing.pytestEnabled": false,
|
||||||
|
"python.testing.unittestEnabled": false
|
||||||
|
}
|
||||||
68
.vscode/tasks.json
vendored
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "start-odoo-dev",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "docker",
|
||||||
|
"args": ["compose", "-f", "${workspaceFolder}/../../odoo/docker-compose.dev.yaml", "up", "-d"],
|
||||||
|
"options": {
|
||||||
|
"env": {
|
||||||
|
"ODOO_DEV": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"group": { "kind": "build", "isDefault": true },
|
||||||
|
"presentation": { "echo": true, "reveal": "always", "focus": false, "panel": "shared" },
|
||||||
|
"problemMatcher": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "start-odoo-debug",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "docker",
|
||||||
|
"args": ["compose", "-f", "${workspaceFolder}/../../odoo/docker-compose.dev.yaml", "up", "-d"],
|
||||||
|
"options": {
|
||||||
|
"env": {
|
||||||
|
"ODOO_DEBUG": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"group": "build",
|
||||||
|
"presentation": { "echo": true, "reveal": "always", "focus": false, "panel": "shared" },
|
||||||
|
"problemMatcher": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "rebuild-odoo-dev",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "docker",
|
||||||
|
"args": ["compose", "-f", "${workspaceFolder}/../../odoo/docker-compose.dev.yaml", "up", "--build", "-d"],
|
||||||
|
"group": "build",
|
||||||
|
"presentation": { "echo": true, "reveal": "always", "focus": false, "panel": "shared" },
|
||||||
|
"problemMatcher": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "stop-odoo-dev",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "docker",
|
||||||
|
"args": ["compose", "-f", "${workspaceFolder}/../..odoo/docker-compose.dev.yaml", "down"],
|
||||||
|
"group": "build",
|
||||||
|
"presentation": { "echo": true, "reveal": "always", "focus": false, "panel": "shared" },
|
||||||
|
"problemMatcher": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "odoo-logs",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "docker",
|
||||||
|
"args": ["compose", "-f", "${workspaceFolder}/../../odoo/docker-compose.dev.yaml", "logs", "-f", "odoo-dev"],
|
||||||
|
"group": "test",
|
||||||
|
"presentation": { "echo": true, "reveal": "always", "focus": false, "panel": "shared" },
|
||||||
|
"problemMatcher": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "shell-odoo",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "docker",
|
||||||
|
"args": ["compose", "-f", "${workspaceFolder}/../../odoo/docker-compose.dev.yaml", "exec", "odoo-dev", "bash"],
|
||||||
|
"presentation": { "echo": true, "reveal": "always", "focus": true, "panel": "shared" },
|
||||||
|
"problemMatcher": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
178
FEATURE_REQUEST/odoo_wp_architektur_empfehlung.md
Normal file
|
|
@ -0,0 +1,178 @@
|
||||||
|
# Odoo vs WordPress vs Odoo.sh – Architektur- & Strategie-Empfehlung
|
||||||
|
*Für HobbyHimmel / FabLab – Traffic: ~1000 Besucher, 2000 Views pro Tag*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🚀 Zusammenfassung (TL;DR)
|
||||||
|
|
||||||
|
| Bereich | Empfehlung |
|
||||||
|
|--------|------------|
|
||||||
|
| Öffentliche Website | **WordPress behalten** |
|
||||||
|
| Odoo als öffentliche Website | **Nicht geeignet** (Performance + Security) |
|
||||||
|
| Odoo.sh | **Nicht geeignet** (Benutzerkosten, Einschränkungen) |
|
||||||
|
| Odoo self-hosted | **Optimal für interne Prozesse** |
|
||||||
|
| Integration | **WordPress ↔ Odoo via REST API** |
|
||||||
|
| Zielarchitektur | **WP Frontend + Odoo Backend** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 1. Zielsetzung
|
||||||
|
|
||||||
|
HobbyHimmel benötigt:
|
||||||
|
|
||||||
|
- Eine öffentliche Website (ca. 1000 Besucher / 2000 Views täglich)
|
||||||
|
- Interne Verwaltungsprozesse (Maschinen, Einweisungen, Kurse, POS)
|
||||||
|
- Möglichkeit, ausgewählte Daten (Maschinenstatus, Kurstermine…) öffentlich darzustellen
|
||||||
|
- Möglichst geringe Kosten
|
||||||
|
- Hohe Sicherheit (keine offene interne Datenbank)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 2. Analyse der drei Optionen
|
||||||
|
|
||||||
|
## 2.1 WordPress (beste Lösung für öffentliche Webseite)
|
||||||
|
|
||||||
|
**Stärken:**
|
||||||
|
- Sehr schneller Seitenaufbau, Caching, CDN
|
||||||
|
- Perfekt für hohe Besucherzahlen
|
||||||
|
- Themes, SEO, Plug-ins
|
||||||
|
- Komplett getrennt vom internen FabLab-System
|
||||||
|
|
||||||
|
**Schwächen:**
|
||||||
|
- Kein direkter Zugriff auf Odoo-Daten (muss über API geschehen)
|
||||||
|
|
||||||
|
**Fazit:**
|
||||||
|
⭐⭐⭐⭐⭐ **Beste Lösung für alle öffentlichen Besucher.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2.2 Odoo (self-hosted, intern)
|
||||||
|
|
||||||
|
**Stärken:**
|
||||||
|
- Ideal für Maschinenverwaltung, POS, Maintenance, Events
|
||||||
|
- Volle Freiheit, beliebige Module zu installieren
|
||||||
|
- Keine Lizenzkosten für Ehrenamtliche, solange sie keinen Login benötigen
|
||||||
|
- Perfekt für interne Daten & Prozesse
|
||||||
|
|
||||||
|
**Schwächen:**
|
||||||
|
- Als öffentliche Website ungeeignet (Performance, kein CDN)
|
||||||
|
- DSL-Upload wäre Flaschenhals
|
||||||
|
|
||||||
|
**Fazit:**
|
||||||
|
⭐⭐⭐⭐⭐ **Beste Lösung für interne Prozesse.
|
||||||
|
Nicht geeignet als öffentliche Website.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2.3 Odoo.sh
|
||||||
|
|
||||||
|
**Vorteile:**
|
||||||
|
- Kein DSL-Problem
|
||||||
|
- Hosting, Deployment & Backups werden automatisiert
|
||||||
|
|
||||||
|
**ABER:**
|
||||||
|
- **sehr hohe Kosten** bei >10 Ehrenamtlichen (pro Benutzer Lizenzpflicht)
|
||||||
|
- **stark eingeschränkt** bei Custom-Code, Addons, APIs
|
||||||
|
- Performance besser, aber **immer noch weit unter WordPress**
|
||||||
|
- Website-Teil ist **nicht auf hohen Traffic optimiert**
|
||||||
|
|
||||||
|
**Fazit:**
|
||||||
|
⭐⭐ **Für HobbyHimmel ungeeignet.**
|
||||||
|
Zu teuer, zu eingeschränkt, keine Website-Performance wie WP.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 3. Performance-Bewertung
|
||||||
|
|
||||||
|
| System | Performance für 1000 Besucher/Tag |
|
||||||
|
|--------|-----------------------------------|
|
||||||
|
| WordPress (mit CDN) | ⭐⭐⭐⭐⭐ |
|
||||||
|
| Odoo.sh Website | ⭐⭐ |
|
||||||
|
| Odoo self-hosted Website über DSL | ⭐ (Upload-Limit!) |
|
||||||
|
|
||||||
|
WordPress ist **um Größenordnungen** besser für Public Traffic.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 4. Sicherheitsbewertung
|
||||||
|
|
||||||
|
| System | Risiko bei Public Exposure |
|
||||||
|
|--------|----------------------------|
|
||||||
|
| WordPress | gering (gefiltert, gehärtet, cached) |
|
||||||
|
| Odoo Website | hoch (direkter Zugriff auf Business-Daten) |
|
||||||
|
| Odoo API | gering (nur ausgewählte Daten, stark begrenzt) |
|
||||||
|
|
||||||
|
Odoo sollte **nicht** öffentlich zugänglich sein, außer über **stark eingeschränkte REST API**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 5. Kostenbewertung
|
||||||
|
|
||||||
|
| Option | Monatliche Kosten | Skalierbarkeit |
|
||||||
|
|--------|-------------------|----------------|
|
||||||
|
| WordPress gehostet | 5–20 € | sehr gut |
|
||||||
|
| Odoo self-hosted | ~0–20 € | sehr gut |
|
||||||
|
| **Odoo.sh** | **ab ~40 € pro Benutzer** | schlecht bei vielen Ehrenamtlichen |
|
||||||
|
|
||||||
|
**Odoo.sh wäre ruinös teuer für ein FabLab.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 6. Empfohlene Zielarchitektur
|
||||||
|
|
||||||
|
```
|
||||||
|
Internet
|
||||||
|
|
|
||||||
|
| 1000 Besucher/Tag
|
||||||
|
v
|
||||||
|
WordPress Website (öffentlich)
|
||||||
|
|
|
||||||
|
| REST-API (lesend!)
|
||||||
|
v
|
||||||
|
Odoo API Gateway (open_workshop_api)
|
||||||
|
|
|
||||||
|
| internal-only
|
||||||
|
v
|
||||||
|
Odoo Backend (POS, Maintenance, Trainings, Events)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Vorteile:**
|
||||||
|
- Odoo bleibt geschützt im LAN oder VPN
|
||||||
|
- Kein DSL-Upload-Limit problematisch → API ist sehr leichtgewichtig
|
||||||
|
- WordPress ist ultraschnell & SEO-stark
|
||||||
|
- Odoo liefert Daten dynamisch an WP (z. B. Maschinenliste, Kurse)
|
||||||
|
- Keine Benutzerkosten für Ehrenamtliche
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 7. Fazit
|
||||||
|
|
||||||
|
Die beste Lösung für HobbyHimmel besteht aus einer Kombination:
|
||||||
|
|
||||||
|
# 🟩 **WordPress für öffentliche Website**
|
||||||
|
# 🟩 **Odoo (self-hosted) für Verwaltung & Prozesse**
|
||||||
|
# 🟩 **API-Modul für WordPress-Anbindung**
|
||||||
|
# 🟥 **Keine Odoo Website für Öffentlichkeit**
|
||||||
|
# 🟥 **Kein Odoo.sh wegen hoher Lizenzkosten**
|
||||||
|
|
||||||
|
Diese Architektur ist:
|
||||||
|
|
||||||
|
- Sicher
|
||||||
|
- Schnell
|
||||||
|
- Zukunftssicher
|
||||||
|
- Kosteneffizient
|
||||||
|
- Flexibel
|
||||||
|
- Perfekt für ein FabLab mit vielen Ehrenamtlichen
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 8. Nächste Schritte
|
||||||
|
|
||||||
|
Wenn gewünscht, kann ich liefern:
|
||||||
|
|
||||||
|
### ✔ Architekturdiagramm (grafisch)
|
||||||
|
### ✔ open_workshop_api Modul als Odoo-Modul-Skeleton
|
||||||
|
### ✔ WordPress-Shortcodes oder Plugin
|
||||||
|
### ✔ Sicherheitsempfehlungen (CORS, Token, Firewalls)
|
||||||
|
### ✔ Optimierungskonzept für den späteren Betrieb
|
||||||
|
|
||||||
323
FEATURE_REQUEST/open_workshop_IoT.md
Normal file
|
|
@ -0,0 +1,323 @@
|
||||||
|
# Projektplan: MQTT-basierte IoT-Events in Odoo 18 Community (Simulation-first)
|
||||||
|
|
||||||
|
Stand: 2026-01-10
|
||||||
|
Ziel: **Odoo-18-Community (self-hosted)** soll **Geräte-Events** (Timer/Maschinenlaufzeit, Waage, Zustandswechsel) über **MQTT** aufnehmen und in Odoo als **saubere, nachvollziehbare Sessions/Events** verarbeiten.
|
||||||
|
Wichtig: **Hardware wird zunächst vollständig simuliert** (Software-Simulatoren), damit die Odoo-Schnittstelle stabil steht, bevor echte Geräte angebunden werden.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Ziele und Nicht-Ziele
|
||||||
|
|
||||||
|
### 1.1 Ziele (MVP)
|
||||||
|
- Einheitliche **Device-Event-Schnittstelle** in Odoo (REST/Webhook) inkl. Authentifizierung
|
||||||
|
- **Event-Log** in Odoo (persistente Rohereignisse + Normalisierung)
|
||||||
|
- **Session-Logik** für Timer/Maschinenlaufzeit (Start/Stop/Heartbeat)
|
||||||
|
- **Simulation** von:
|
||||||
|
- Maschinenzustand (running/idle/fault)
|
||||||
|
- Timer-Events (run_start/run_stop)
|
||||||
|
- Waage (stable_weight/tare)
|
||||||
|
- Reproduzierbare Tests (Unit/Integration) und eindeutige Fehlerdiagnostik (Logging)
|
||||||
|
|
||||||
|
### 1.2 Nicht-Ziele (für Phase 1)
|
||||||
|
- Keine Enterprise-IoT-Box, keine Enterprise-Module
|
||||||
|
- Keine echte Hardware-Treiberentwicklung in Phase 1
|
||||||
|
- Kein POS-Frontend-Live-Widget (optional erst in späterer Phase)
|
||||||
|
- Keine Abrechnungslogik/Preisregeln (kann vorbereitet, aber nicht umgesetzt werden)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Zielarchitektur (Simulation-first)
|
||||||
|
|
||||||
|
### 2.1 Komponenten
|
||||||
|
1. **MQTT Broker** (z. B. Mosquitto in Docker)
|
||||||
|
2. **Device Simulator(s)** (Python/Node)
|
||||||
|
- veröffentlicht MQTT Topics wie echte Geräte
|
||||||
|
3. **Gateway/Bridge (Software)**
|
||||||
|
- abonniert MQTT Topics
|
||||||
|
- validiert/normalisiert Payload
|
||||||
|
- sendet Events via HTTPS an Odoo (Webhook)
|
||||||
|
4. **Odoo Modul** `ows_iot_bridge`
|
||||||
|
- REST Controller `/ows/iot/event`
|
||||||
|
- Modelle für Devices, Events, Sessions
|
||||||
|
- Business-Regeln für Session-Start/Stop/Hysterese (softwareseitig)
|
||||||
|
5. Optional später: **Realtime-Feed** (WebSocket) für POS/Display
|
||||||
|
|
||||||
|
### 2.2 Datenfluss
|
||||||
|
1. Simulator publiziert MQTT → `hobbyhimmel/machines/<machine_id>/state`
|
||||||
|
2. Bridge konsumiert MQTT, mappt auf Event-Format
|
||||||
|
3. Bridge POSTet JSON an Odoo Endpoint
|
||||||
|
4. Odoo speichert Roh-Event + erzeugt ggf. Session-Updates
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Schnittstelle zu Odoo (Kern des Projekts)
|
||||||
|
|
||||||
|
### 3.1 Authentifizierung
|
||||||
|
- Pro Device oder pro Bridge ein **API-Token** (Bearer)
|
||||||
|
- Header: `Authorization: Bearer <token>`
|
||||||
|
- Token in Odoo in `ows.iot.device` oder `ir.config_parameter` hinterlegt
|
||||||
|
- Optional: IP-Allowlist / Rate-Limit (in Reverse Proxy)
|
||||||
|
|
||||||
|
### 3.2 Endpoint (REST/Webhook)
|
||||||
|
- `POST /ows/iot/event`
|
||||||
|
- Content-Type: `application/json`
|
||||||
|
- Antwort:
|
||||||
|
- `200 OK` mit `{ "status": "ok", "event_id": "...", "session_id": "..." }`
|
||||||
|
- Fehler: `400` (Schema), `401/403` (Auth), `409` (Duplikat), `500` (Server)
|
||||||
|
|
||||||
|
### 3.3 Idempotenz / Duplikaterkennung
|
||||||
|
- Jedes Event enthält eine eindeutige `event_uid`
|
||||||
|
- Odoo legt Unique-Constraint auf `event_uid` (pro device)
|
||||||
|
- Wiederholte Events liefern `409 Conflict` oder `200 OK (duplicate=true)` (Designentscheidung)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Ereignis- und Topic-Standard (Versioniert)
|
||||||
|
|
||||||
|
### 4.1 MQTT Topics (v1)
|
||||||
|
- Maschinenzustand:
|
||||||
|
`hobbyhimmel/machines/<machine_id>/state`
|
||||||
|
- Maschinenereignisse:
|
||||||
|
`hobbyhimmel/machines/<machine_id>/event`
|
||||||
|
- Waage:
|
||||||
|
`hobbyhimmel/scales/<scale_id>/event`
|
||||||
|
- Geräte-Status (optional):
|
||||||
|
`hobbyhimmel/devices/<device_id>/status`
|
||||||
|
|
||||||
|
### 4.2 Gemeinsames JSON Event Schema (v1)
|
||||||
|
Pflichtfelder:
|
||||||
|
- `schema_version`: `"v1"`
|
||||||
|
- `event_uid`: UUID/string
|
||||||
|
- `ts`: ISO-8601 UTC (z. B. `"2026-01-10T12:34:56Z"`)
|
||||||
|
- `source`: `"simulator" | "device" | "gateway"`
|
||||||
|
- `device_id`: string
|
||||||
|
- `entity_type`: `"machine" | "scale" | "sensor"`
|
||||||
|
- `entity_id`: string (z. B. machine_id)
|
||||||
|
- `event_type`: string (siehe unten)
|
||||||
|
- `payload`: object
|
||||||
|
- `confidence`: `"high" | "medium" | "low"` (für Sensorfusion)
|
||||||
|
|
||||||
|
### 4.3 Event-Typen (v1)
|
||||||
|
**Maschine/Timer**
|
||||||
|
- `run_start` (Start einer Laufphase)
|
||||||
|
- `run_stop` (Ende einer Laufphase)
|
||||||
|
- `heartbeat` (optional, laufend während running)
|
||||||
|
- `state_changed` (idle/running/fault)
|
||||||
|
- `fault` (Fehler mit Code/Severity)
|
||||||
|
|
||||||
|
**Waage**
|
||||||
|
- `stable_weight` (stabiler Messwert)
|
||||||
|
- `weight` (laufend)
|
||||||
|
- `tare`
|
||||||
|
- `zero`
|
||||||
|
- `error`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Odoo Datenmodell (Vorschlag)
|
||||||
|
|
||||||
|
### 5.1 `ows.iot.device`
|
||||||
|
- `name`
|
||||||
|
- `device_id` (unique)
|
||||||
|
- `token_hash` (oder Token in separater Tabelle)
|
||||||
|
- `device_type` (machine/scale/...)
|
||||||
|
- `active`
|
||||||
|
- `last_seen`
|
||||||
|
- `notes`
|
||||||
|
|
||||||
|
### 5.2 `ows.iot.event`
|
||||||
|
- `event_uid` (unique)
|
||||||
|
- `device_id` (m2o -> device)
|
||||||
|
- `entity_type`, `entity_id`
|
||||||
|
- `event_type`
|
||||||
|
- `timestamp`
|
||||||
|
- `payload_json` (Text/JSON)
|
||||||
|
- `confidence`
|
||||||
|
- `processing_state` (new/processed/error)
|
||||||
|
- `session_id` (m2o optional)
|
||||||
|
|
||||||
|
### 5.3 `ows.machine.session` (Timer-Sessions)
|
||||||
|
- `machine_id` (Char oder m2o auf bestehendes Maschinenmodell)
|
||||||
|
- `start_ts`, `stop_ts`
|
||||||
|
- `duration_s` (computed)
|
||||||
|
- `state` (running/stopped/aborted)
|
||||||
|
- `origin` (sensor/manual/sim)
|
||||||
|
- `confidence_summary`
|
||||||
|
- `event_ids` (o2m)
|
||||||
|
|
||||||
|
> Hinweis: Wenn du bereits `ows.machine` aus deinem open_workshop nutzt, referenziert `machine_id` direkt dieses Modell.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Verarbeitungslogik (Phase 1: minimal, robust)
|
||||||
|
|
||||||
|
### 6.1 Session-Automat (State Machine)
|
||||||
|
- Eingang: Events `run_start`, `run_stop`, optional `heartbeat`
|
||||||
|
- Regeln:
|
||||||
|
- `run_start` erstellt neue Session, wenn keine läuft
|
||||||
|
- `run_start` während laufender Session → nur Log, keine neue Session
|
||||||
|
- `run_stop` schließt laufende Session
|
||||||
|
- Timeout-Regel: wenn `heartbeat` ausbleibt (z. B. 10 min) → Session `aborted`
|
||||||
|
|
||||||
|
### 6.2 Hysterese (Simulation als Stellvertreter für Sensorik)
|
||||||
|
In Simulation definierst du:
|
||||||
|
- Start, wenn „running“ mindestens **2s stabil**
|
||||||
|
- Stop, wenn „idle“ mindestens **15s stabil**
|
||||||
|
Diese Parameter als Odoo Systemparameter, z. B.:
|
||||||
|
- `ows_iot.start_debounce_s`
|
||||||
|
- `ows_iot.stop_debounce_s`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Simulation (Software statt Hardware)
|
||||||
|
|
||||||
|
### 7.1 Device Simulator: Maschine
|
||||||
|
- Konfigurierbar:
|
||||||
|
- Muster: random, fixed schedule, manuell per CLI
|
||||||
|
- Zustände: idle/running/fault
|
||||||
|
- Optional: „power_w“ und „vibration“ als Felder im Payload
|
||||||
|
- Publiziert MQTT in realistischen Intervallen
|
||||||
|
|
||||||
|
### 7.2 Device Simulator: Waage
|
||||||
|
- Modi:
|
||||||
|
- stream weight (mehrfach pro Sekunde)
|
||||||
|
- stable_weight nur auf „stabil“
|
||||||
|
- tare/zero Events per CLI
|
||||||
|
|
||||||
|
### 7.3 Bridge Simulator (MQTT → Odoo)
|
||||||
|
- Abonniert alle relevanten Topics
|
||||||
|
- Validiert Schema v1
|
||||||
|
- POSTet Events an Odoo
|
||||||
|
- Retry-Queue (lokal) bei Odoo-Ausfall
|
||||||
|
- Metriken/Logs:
|
||||||
|
- gesendete Events, Fehlerquoten, Latenz
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Milestones & Deliverables
|
||||||
|
|
||||||
|
### M0 – Repo/Grundgerüst (0.5–1 Tag)
|
||||||
|
**Deliverables**
|
||||||
|
- Git-Repo Struktur
|
||||||
|
- Docker Compose: mosquitto + simulator + bridge
|
||||||
|
- Odoo Modul Skeleton `ows_iot_bridge`
|
||||||
|
|
||||||
|
### M1 – Odoo Endpoint & Modelle (1–2 Tage)
|
||||||
|
**Deliverables**
|
||||||
|
- `/ows/iot/event` Controller inkl. Token-Auth
|
||||||
|
- Modelle `ows.iot.device`, `ows.iot.event`
|
||||||
|
- Admin UI: Geräte anlegen, Token setzen, letzte Events anzeigen
|
||||||
|
|
||||||
|
### M2 – Session-Engine (1–2 Tage)
|
||||||
|
**Deliverables**
|
||||||
|
- Modell `ows.machine.session`
|
||||||
|
- Event → Session Zuordnung
|
||||||
|
- Timeout/Abbruch-Logik
|
||||||
|
- Parameter für Debounce/Timeout
|
||||||
|
|
||||||
|
### M3 – Simulatoren & End-to-End Test (1–2 Tage)
|
||||||
|
**Deliverables**
|
||||||
|
- Machine Simulator + Scale Simulator
|
||||||
|
- Bridge mit Retry-Queue
|
||||||
|
- End-to-End: Simulator → MQTT → Bridge → Odoo → Session entsteht
|
||||||
|
|
||||||
|
### M4 – Qualität & Betrieb (1 Tag)
|
||||||
|
**Deliverables**
|
||||||
|
- Logging-Konzept (Odoo + Bridge)
|
||||||
|
- Tests (Unit: Controller/Auth, Integration: Session Engine)
|
||||||
|
- Doku: Event-Schema v1, Topics, Beispielpayloads
|
||||||
|
|
||||||
|
### Optional M5 – POS/Anzeige (später)
|
||||||
|
- Realtime Anzeige im POS oder auf Display
|
||||||
|
- Live-Weight in POS
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Testplan (Simulation-first)
|
||||||
|
|
||||||
|
### 9.1 Unit Tests (Odoo)
|
||||||
|
- Auth: gültiger Token → 200
|
||||||
|
- ungültig/fehlend → 401/403
|
||||||
|
- Schema-Validation → 400
|
||||||
|
- Idempotenz: duplicate `event_uid` → 409 oder duplicate-flag
|
||||||
|
|
||||||
|
### 9.2 Integration Tests
|
||||||
|
- Sequenz: start → heartbeat → stop → Session duration plausibel
|
||||||
|
- stop ohne start → kein Crash, Event loggt Fehlerzustand
|
||||||
|
- Timeout: start → keine heartbeat → Session aborted
|
||||||
|
|
||||||
|
### 9.3 Last-/Stabilitätstest (Simulator)
|
||||||
|
- 20 Maschinen, je 1 Event/s, 1h Lauf
|
||||||
|
- Ziel: Odoo bleibt stabil, Event-Insert performant, Queue läuft nicht über
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Betriebs- und Sicherheitskonzept
|
||||||
|
|
||||||
|
- Token-Rotation möglich (neues Token, altes deaktivieren)
|
||||||
|
- Reverse Proxy:
|
||||||
|
- HTTPS
|
||||||
|
- Rate limiting auf `/ows/iot/event`
|
||||||
|
- optional Basic WAF Regeln
|
||||||
|
- Payload-Größenlimit (z. B. 16–64 KB)
|
||||||
|
- Bridge persistiert Retry-Queue auf Disk
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Offene Entscheidungen (später, nicht blocker für MVP)
|
||||||
|
|
||||||
|
- Event-Consumer in Odoo vs. Bridge-only Webhook (empfohlen: Webhook)
|
||||||
|
- Genaues Mapping zu bestehenden open_workshop Modellen (`ows.machine`, Nutzer/RFID)
|
||||||
|
- Abrechnung: Preisregeln, Rundungen, POS-Integration
|
||||||
|
- Realtime: Odoo Bus vs. eigener WebSocket Service
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Nächste Schritte (konkret)
|
||||||
|
|
||||||
|
1. `ows_iot_bridge` Modul: Endpoint + Device/Event Modelle implementieren
|
||||||
|
2. Docker Compose: mosquitto + bridge + simulator bereitstellen
|
||||||
|
3. Simulatoren produzieren v1-Events; Bridge sendet an Odoo
|
||||||
|
4. Session-Engine anschließen und End-to-End testen
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Anhang A: Beispiel-Event (Maschine run_start)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"schema_version": "v1",
|
||||||
|
"event_uid": "c2a7d6f1-7d8d-4a63-9a7f-4e6d7b0d9e2a",
|
||||||
|
"ts": "2026-01-10T12:34:56Z",
|
||||||
|
"source": "simulator",
|
||||||
|
"device_id": "esp32-fraser-01",
|
||||||
|
"entity_type": "machine",
|
||||||
|
"entity_id": "formatkreissaege",
|
||||||
|
"event_type": "run_start",
|
||||||
|
"confidence": "high",
|
||||||
|
"payload": {
|
||||||
|
"power_w": 820,
|
||||||
|
"vibration": 0.73,
|
||||||
|
"reason": "power_threshold"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Anhang B: Beispiel-Event (Waage stable_weight)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"schema_version": "v1",
|
||||||
|
"event_uid": "b6d0a2c5-9b1f-4a0b-8b19-7f2e1b8f3d11",
|
||||||
|
"ts": "2026-01-10T12:40:12Z",
|
||||||
|
"source": "simulator",
|
||||||
|
"device_id": "scale-sim-01",
|
||||||
|
"entity_type": "scale",
|
||||||
|
"entity_id": "waage-01",
|
||||||
|
"event_type": "stable_weight",
|
||||||
|
"confidence": "high",
|
||||||
|
"payload": {
|
||||||
|
"weight_g": 1532.4,
|
||||||
|
"unit": "g",
|
||||||
|
"stable_ms": 1200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
538
FEATURE_REQUEST/open_workshop_feature_plan.md
Normal file
|
|
@ -0,0 +1,538 @@
|
||||||
|
# 0. Migration: open_workshop → open_workshop_base ✅ ABGESCHLOSSEN
|
||||||
|
|
||||||
|
**Status: ERLEDIGT (08.12.2025)**
|
||||||
|
|
||||||
|
Die Migration wurde erfolgreich durchgeführt:
|
||||||
|
|
||||||
|
- ✅ SQL-Migration: Modul umbenannt in Datenbank
|
||||||
|
- ✅ Pre-Migration: maintenance.equipment Integration vorbereitet
|
||||||
|
- ✅ Post-Migration: 23 Maschinen zu maintenance.equipment migriert
|
||||||
|
- ✅ _inherits Pattern implementiert (ows.machine → maintenance.equipment)
|
||||||
|
- ✅ Automatische Migration in CI/CD Pipeline integriert
|
||||||
|
- ✅ UX verbessert: Equipment wird automatisch erstellt
|
||||||
|
- ✅ Location readonly und auto-sync von area
|
||||||
|
- ✅ Menü umbenannt zu "Ausrüstung"
|
||||||
|
|
||||||
|
**Technische Details:**
|
||||||
|
- Alte Felder entfernt: code, description, storage_location, purchase_price, purchase_date
|
||||||
|
- Delegation an maintenance.equipment: name, serial_no, location, cost, effective_date
|
||||||
|
- JSONB für mehrsprachige Namen (de_DE, en_US)
|
||||||
|
- Area → Location Synchronisation automatisch
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Open Workshop – FEATURE PLAN
|
||||||
|
## Vollständige Modularisierung + WordPress REST API Integration (API = Pflicht)
|
||||||
|
|
||||||
|
Dieses Dokument ist die **finale, vollständig korrigierte Version** des Feature-Plans.
|
||||||
|
Es kombiniert alle Inhalte der vorherigen Dokumente, **konsolidiert, bereinigt und vereinheitlicht**,
|
||||||
|
und stellt klar:
|
||||||
|
|
||||||
|
# ⭐ Die API ist ein **Pflichtbestandteil** der finalen Architektur.
|
||||||
|
|
||||||
|
Das Dokument ist **eigenständig** und enthält alles, was ein Entwickler oder KI-Agent benötigt,
|
||||||
|
um das gesamte Open-Workshop-System korrekt modularisiert aufzubauen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 1. Gesamtziel
|
||||||
|
|
||||||
|
Das Projekt *Open Workshop* soll in eine moderne, klare und skalierbare Architektur überführt werden:
|
||||||
|
|
||||||
|
- **Odoo (intern)** als Verwaltungs-Backend für Maschinen, Einweisungen, POS, Maintenance, Events.
|
||||||
|
- **WordPress (extern)** als öffentliche Website mit hoher Performance und SEO.
|
||||||
|
- **REST API (Pflicht!)** als Kommunikationsschicht zwischen beiden Systemen.
|
||||||
|
|
||||||
|
Damit entsteht:
|
||||||
|
|
||||||
|
```
|
||||||
|
WordPress (öffentlich)
|
||||||
|
↑
|
||||||
|
| REST API (nur lesend)
|
||||||
|
↓
|
||||||
|
Odoo (intern, geschützt)
|
||||||
|
```
|
||||||
|
|
||||||
|
Diese Architektur vermeidet:
|
||||||
|
|
||||||
|
- Performance-Probleme durch DSL-Uplink
|
||||||
|
- Sicherheitsrisiken von Odoo im Internet
|
||||||
|
- Hohe Kosten für Odoo.sh Benutzerlizenzen
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 2. Aktuelle Modulstruktur (Stand: 08.12.2025)
|
||||||
|
|
||||||
|
```
|
||||||
|
open_workshop/
|
||||||
|
├── open_workshop/ # ✅ Alt-Modul (installable=False, Kompatibilität)
|
||||||
|
├── open_workshop_base/ # ✅ FERTIG – Backend, _inherits maintenance.equipment
|
||||||
|
├── open_workshop_pos/ # ✅ FERTIG – POS-Integration (JS, XML, UI)
|
||||||
|
├── open_workshop_dokuwiki/ # ⏳ IN ENTWICKLUNG – DokuWiki-Integration
|
||||||
|
└── open_workshop_api/ # 💤 OPTIONAL – REST API (zurückgestellt)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Entfernt:** open_workshop_maintenance (verworfen - Funktionalität direkt in open_workshop_base integriert)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 3. Modul: open_workshop_base ✅ FERTIG
|
||||||
|
|
||||||
|
**Status: PRODUKTIV (18.0.1.0.4)**
|
||||||
|
|
||||||
|
Enthält:
|
||||||
|
|
||||||
|
- ✅ `ows.machine` mit _inherits zu `maintenance.equipment` (delegiert alle Felder von maintenance.equipment transparent an ows.machine)
|
||||||
|
- ✅ `ows.machine.area` (Bereiche mit JSONB-Namen)
|
||||||
|
- ✅ `ows.machine.product` (Nutzungsprodukte)
|
||||||
|
- ✅ `ows.machine.training` (Einweisungsprodukte)
|
||||||
|
- ✅ `ows.machine.access` (Zugriffsfreigaben)
|
||||||
|
- ✅ Automatische Equipment-Erstellung bei Maschinenanlage
|
||||||
|
- ✅ Area → Location Synchronisation
|
||||||
|
- ✅ Sicherheitsregeln (ir.model.access)
|
||||||
|
- ✅ Backend-UI: Form, Tree, Search/Filter
|
||||||
|
- ✅ Smart Button zu Equipment-Details
|
||||||
|
- ✅ Integration mit OCA maintenance Modulen
|
||||||
|
|
||||||
|
**Abhängigkeiten:**
|
||||||
|
- base, account, hr, product, sale, contacts, **maintenance**
|
||||||
|
|
||||||
|
**Maintenance Integration:**
|
||||||
|
- Statt separatem Modul: Direkte Integration via _inherits
|
||||||
|
- Equipment-Felder: name, serial_no, location, cost, effective_date
|
||||||
|
- OWS-Felder: category, area_id, product_ids, training_ids
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 4. Modul: open_workshop_pos ✅ FERTIG
|
||||||
|
|
||||||
|
**Status: PRODUKTIV**
|
||||||
|
|
||||||
|
Vollständig separiertes POS-Modul mit:
|
||||||
|
|
||||||
|
### JS-Module:
|
||||||
|
- ✅ ows_machine_access_list.js
|
||||||
|
- ✅ ows_pos_customer_sidebar.js
|
||||||
|
- ✅ ows_pos_sidebar.js
|
||||||
|
- ✅ ows_product_screen_template_patch.js
|
||||||
|
|
||||||
|
### XML-Templates:
|
||||||
|
- ✅ QWeb Templates für POS-UI
|
||||||
|
- ✅ Maschinenfreigaben-Widget
|
||||||
|
- ✅ Customer Sidebar mit Zugriffsstatus
|
||||||
|
|
||||||
|
### Assets:
|
||||||
|
```json
|
||||||
|
'point_of_sale.assets': [
|
||||||
|
'open_workshop_pos/static/src/js/**/*.js',
|
||||||
|
'open_workshop_pos/static/src/xml/**/*.xml',
|
||||||
|
'open_workshop_pos/static/src/css/**/*.css'
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Abhängigkeiten:**
|
||||||
|
- open_workshop_base
|
||||||
|
- point_of_sale
|
||||||
|
|
||||||
|
**Integration:**
|
||||||
|
- Zeigt Maschinenzugriffe im POS
|
||||||
|
- Filterung nach Bereichen
|
||||||
|
- Live-Status der Freigaben
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 5. Modul: open_workshop_dokuwiki ⏳ IN ENTWICKLUNG
|
||||||
|
|
||||||
|
**Status: IN ENTWICKLUNG (Branch: feature/dokuwiki-integration)**
|
||||||
|
|
||||||
|
Dieses Modul integriert **DokuWiki** als Dokumentationssystem für Equipment.
|
||||||
|
|
||||||
|
## 5.1 Ziele
|
||||||
|
|
||||||
|
- Automatische Wiki-Seiten für jedes Equipment
|
||||||
|
- **Invertierte Include-Architektur**:
|
||||||
|
- Odoo generiert geschützte Hauptseite mit Equipment-Daten
|
||||||
|
- Hauptseite inkludiert User-Dokumentation via Include Plugin
|
||||||
|
- User editieren separate `:doku` Unterseite
|
||||||
|
- **Klare Datentrennung**:
|
||||||
|
- Odoo = Master für strukturierte Daten (Hauptseite, read-only für User)
|
||||||
|
- User = Master für Freitext-Dokumentation (`:doku` Unterseite, editierbar)
|
||||||
|
- **Generisches ACL-System**: Eine Regel für alle Equipment
|
||||||
|
- Versionierung durch DokuWiki eingebaut
|
||||||
|
|
||||||
|
## 5.2 Architektur
|
||||||
|
|
||||||
|
```
|
||||||
|
Odoo (maintenance.equipment)
|
||||||
|
↓
|
||||||
|
XML-RPC Client
|
||||||
|
↓
|
||||||
|
DokuWiki API (wiki.putPage)
|
||||||
|
↓
|
||||||
|
werkstatt:ausruestung:bereich:maschinenname
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5.3 DokuWiki-Seitenstruktur
|
||||||
|
|
||||||
|
**Zwei-Seiten-System pro Equipment:**
|
||||||
|
|
||||||
|
```
|
||||||
|
werkstatt:ausruestung:holzwerkstatt:formatkreissaege ← Hauptseite (Odoo generiert)
|
||||||
|
werkstatt:ausruestung:holzwerkstatt:formatkreissaege:doku ← Doku-Seite (User editieren)
|
||||||
|
werkstatt:ausruestung:lasercutter:epilog_fusion_m2 ← Hauptseite
|
||||||
|
werkstatt:ausruestung:lasercutter:epilog_fusion_m2:doku ← Doku-Seite
|
||||||
|
```
|
||||||
|
|
||||||
|
**Namespace-Regeln:**
|
||||||
|
- **Hauptnamespace**: `werkstatt:ausruestung:`
|
||||||
|
- **Hauptseite**: `werkstatt:ausruestung:bereich:maschinenname`
|
||||||
|
- Bereich: ows_area_id (normalisiert, lowercase)
|
||||||
|
- Maschinenname: Equipment name (normalisiert, ohne Sonderzeichen)
|
||||||
|
- **Komplett Odoo-generiert**, User können nicht editieren
|
||||||
|
|
||||||
|
- **Doku-Seite**: `werkstatt:ausruestung:bereich:maschinenname:doku`
|
||||||
|
- **User editieren hier frei** (Anleitungen, Tipps, Bilder)
|
||||||
|
- Wird via Include in Hauptseite eingebunden
|
||||||
|
- User können weitere Unterseiten anlegen (`:doku:bilder`, `:doku:videos`)
|
||||||
|
|
||||||
|
## 5.4 Hauptseiten-Template (Odoo-generiert)
|
||||||
|
|
||||||
|
Odoo erstellt die komplette Hauptseite inklusive Include für User-Dokumentation:
|
||||||
|
|
||||||
|
```wiki
|
||||||
|
====== Formatkreissäge ======
|
||||||
|
|
||||||
|
===== Grunddaten =====
|
||||||
|
* **Seriennummer:** FKS-2024-001
|
||||||
|
* **Bereich:** [[werkstatt:ausruestung:holzwerkstatt|Holzwerkstatt]]
|
||||||
|
* **Kategorie:** 🔴 Kategorie 3 (Einweisung erforderlich)
|
||||||
|
* **Standort:** Schrank A, Regal 2
|
||||||
|
* **Hersteller:** Bosch
|
||||||
|
|
||||||
|
===== Sicherheit =====
|
||||||
|
* **Einweisungspflichtig:** Ja
|
||||||
|
* **Nutzungsprodukte:**
|
||||||
|
* Holzwerkstatt Nutzung (5€/Tag)
|
||||||
|
* **Einweisungsprodukte:**
|
||||||
|
* Einweisung Formatkreissäge (einmalig 25€)
|
||||||
|
|
||||||
|
===== Wartung =====
|
||||||
|
* **Letzte Wartung:** 01.12.2025
|
||||||
|
* **Nächste Wartung:** 01.06.2026
|
||||||
|
* **Wartungshistorie:** [[odoo:maintenance:request:42|Alle Wartungsaufträge]]
|
||||||
|
|
||||||
|
===== Dokumentation & Anleitungen =====
|
||||||
|
{{page>.:formatkreissaege:doku&noheader}}
|
||||||
|
|
||||||
|
[[.:formatkreissaege:doku|✏️ Dokumentation bearbeiten]]
|
||||||
|
```
|
||||||
|
|
||||||
|
**User-Doku-Seite** (`:doku`, User editieren hier):
|
||||||
|
```wiki
|
||||||
|
==== Bedienung ====
|
||||||
|
1. Hauptschalter einschalten
|
||||||
|
2. Sägeblatt-Drehzahl einstellen
|
||||||
|
3. Anschlag positionieren
|
||||||
|
...
|
||||||
|
|
||||||
|
==== Tipps & Tricks ====
|
||||||
|
- Bei Hartholz: Vorschub langsamer
|
||||||
|
- Sägeblatt nach 50h wechseln
|
||||||
|
|
||||||
|
==== Bilder ====
|
||||||
|
{{.:doku:bild1.jpg?300}}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5.5 Technische Umsetzung
|
||||||
|
|
||||||
|
**Python-Bibliothek:**
|
||||||
|
- `dokuwiki` (Python XML-RPC Client für DokuWiki)
|
||||||
|
- Installation: `pip install dokuwiki`
|
||||||
|
|
||||||
|
**API-Methoden:**
|
||||||
|
```python
|
||||||
|
# DokuWiki XML-RPC API
|
||||||
|
wiki.putPage(page_id, content, summary, minor) # Seite erstellen/aktualisieren
|
||||||
|
wiki.getPage(page_id) # Seite lesen
|
||||||
|
wiki.getAllPages() # Alle Seiten auflisten
|
||||||
|
```
|
||||||
|
|
||||||
|
**Odoo-Integration:**
|
||||||
|
- Neues Model: `ows.dokuwiki.config` (DokuWiki-URL, Credentials)
|
||||||
|
- Methode auf `maintenance.equipment`: `sync_to_dokuwiki()`
|
||||||
|
- Automated Action: Bei Equipment-Änderung → Wiki aktualisieren
|
||||||
|
- Smart Button: "Wiki öffnen" → Link zur DokuWiki-Seite
|
||||||
|
|
||||||
|
**Sync-Algorithmus (vereinfacht!):**
|
||||||
|
```python
|
||||||
|
def sync_to_dokuwiki(self):
|
||||||
|
"""Generiert Hauptseite mit Include für User-Doku"""
|
||||||
|
# 1. Generiere Hauptseite komplett (Odoo hat volle Kontrolle)
|
||||||
|
content = self._generate_equipment_page()
|
||||||
|
|
||||||
|
# 2. Füge Include für User-Doku hinzu
|
||||||
|
wiki_id = self._get_normalized_name()
|
||||||
|
content += "\n\n===== Dokumentation & Anleitungen =====\n"
|
||||||
|
content += f"{{{{page>.:{{wiki_id}}:doku&noheader}}}}\n\n"
|
||||||
|
content += f"[[.:{{wiki_id}}:doku|✏️ Dokumentation bearbeiten]]\n"
|
||||||
|
|
||||||
|
# 3. Schreibe Hauptseite (überschreibt komplett)
|
||||||
|
main_page = f"werkstatt:ausruestung:{self.ows_area_id.wiki_name}:{wiki_id}"
|
||||||
|
dokuwiki_client.putPage(main_page, content, "Odoo Auto-Sync")
|
||||||
|
|
||||||
|
# 4. Erstelle leere Doku-Seite falls nicht vorhanden
|
||||||
|
doku_page = f"{main_page}:doku"
|
||||||
|
if not dokuwiki_client.getPage(doku_page):
|
||||||
|
initial_doku = "==== Bedienung ====\n\n==== Tipps & Tricks ====\n"
|
||||||
|
dokuwiki_client.putPage(doku_page, initial_doku, "Initial")
|
||||||
|
|
||||||
|
def _generate_equipment_page(self):
|
||||||
|
"""Generiert komplette Hauptseite mit Equipment-Daten"""
|
||||||
|
return f"""====== {self.name} ======
|
||||||
|
|
||||||
|
===== Grunddaten =====
|
||||||
|
* **Seriennummer:** {self.serial_no or 'N/A'}
|
||||||
|
* **Bereich:** [[werkstatt:ausruestung:{self.ows_area_id.wiki_name}|{self.ows_area_id.name}]]
|
||||||
|
* **Kategorie:** {self._get_category_icon()} {self._get_category_label()}
|
||||||
|
* **Standort:** {self.location or 'N/A'}
|
||||||
|
* **Hersteller:** {self.partner_id.name or 'N/A'}
|
||||||
|
|
||||||
|
===== Sicherheit =====
|
||||||
|
* **Einweisungspflichtig:** {self._is_training_required()}
|
||||||
|
* **Nutzungsprodukte:**
|
||||||
|
{self._format_product_list(self.ows_product_ids)}
|
||||||
|
* **Einweisungsprodukte:**
|
||||||
|
{self._format_product_list(self.ows_training_ids)}
|
||||||
|
|
||||||
|
===== Wartung =====
|
||||||
|
* **Letzte Wartung:** {self.last_maintenance_date or 'N/A'}
|
||||||
|
* **Nächste Wartung:** {self.next_action_date or 'N/A'}
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
**Vorteile dieser Architektur:**
|
||||||
|
- ✅ Keine Sync-Marker nötig (Hauptseite wird komplett überschrieben)
|
||||||
|
- ✅ User können Odoo-Daten nicht versehentlich löschen
|
||||||
|
- ✅ User-Content ist komplett geschützt (separate Seite)
|
||||||
|
- ✅ Include funktioniert transparent (User sehen alles auf einer Seite)
|
||||||
|
- ✅ Wartungsfreies ACL-System
|
||||||
|
|
||||||
|
## 5.6 ACL-System (Generisch & Wartungsfrei)
|
||||||
|
|
||||||
|
**Phase 1: Nur für Odoo sichtbar (Initial-Setup)**
|
||||||
|
|
||||||
|
```
|
||||||
|
# Komplett gesperrt für alle außer Odoo
|
||||||
|
werkstatt:ausruestung:* @odoo 8 # Voller Zugriff
|
||||||
|
werkstatt:ausruestung:* @ALL 0 # Kein Zugriff
|
||||||
|
```
|
||||||
|
|
||||||
|
**Phase 2: Werkstatt-Betreiber erhalten Zugriff (später)**
|
||||||
|
|
||||||
|
```
|
||||||
|
# User-Doku-Seiten editierbar (spezifischere Regel zuerst!)
|
||||||
|
werkstatt:ausruestung:*:doku @werkstatt 2 # Editieren erlaubt
|
||||||
|
werkstatt:ausruestung:*:doku:* @werkstatt 2 # Auch Unterseiten (Bilder, Videos, etc.)
|
||||||
|
|
||||||
|
# Hauptseiten nur lesen
|
||||||
|
werkstatt:ausruestung:* @werkstatt 1 # Nur Lesezugriff
|
||||||
|
werkstatt:ausruestung:* @ALL 1 # Alle können lesen
|
||||||
|
|
||||||
|
# Odoo hat vollen Zugriff überall
|
||||||
|
werkstatt:ausruestung:* @odoo 8 # Voller Zugriff
|
||||||
|
werkstatt:ausruestung:*:doku @odoo 8 # Auch auf Doku-Seiten (für Initial-Erstellung)
|
||||||
|
**Wie es funktioniert:**
|
||||||
|
|
||||||
|
| Seite | User @werkstatt | User @odoo | Ergebnis |
|
||||||
|
|-------|-----------------|------------|----------|
|
||||||
|
| `werkstatt:ausruestung:holzwerkstatt:formatkreissaege` | Matched: `werkstatt:ausruestung:*` → 1 | Matched: `werkstatt:ausruestung:*` → 8 | User: Lesen, Odoo: Schreiben |
|
||||||
|
| `werkstatt:ausruestung:holzwerkstatt:formatkreissaege:doku` | Matched: `werkstatt:ausruestung:*:doku` → 2 | Matched: `werkstatt:ausruestung:*:doku` → 8 | User: Editieren, Odoo: Voll |
|
||||||
|
- `16` = Admin (alle Rechte)
|
||||||
|
**Vorteile:**
|
||||||
|
- ✅ **Komplett generisch**: Gilt automatisch für alle Equipment
|
||||||
|
- ✅ **Wartungsfrei**: Kein manuelles ACL-Setup pro Equipment
|
||||||
|
- ✅ **Phasenweise Freischaltung**: Erst Odoo-Only, dann schrittweise für Werkstatt
|
||||||
|
- ✅ **Selbsterklärend**: User sehen sofort wo sie editieren können
|
||||||
|
- ✅ **Skalierbar**: Funktioniert auch für 1000+ Equipment-Seiten
|
||||||
|
| `ausruestung:holzwerkstatt:formatkreissaege` | Matched: `ausruestung:*` → 1 | Matched: `ausruestung:*` → 8 | User: Lesen, Odoo: Schreiben |
|
||||||
|
| `ausruestung:holzwerkstatt:formatkreissaege:doku` | Matched: `ausruestung:*:doku` → 2 | Matched: `ausruestung:*:doku` → 8 | User: Editieren, Odoo: Voll |
|
||||||
|
|
||||||
|
**Vorteile:**
|
||||||
|
- ✅ **Komplett generisch**: Gilt automatisch für alle Equipment
|
||||||
|
- ✅ **Wartungsfrei**: Kein manuelles ACL-Setup pro Equipment
|
||||||
|
- ✅ **Selbsterklärend**: User sehen sofort wo sie editieren können
|
||||||
|
- ✅ **Skalierbar**: Funktioniert auch für 1000+ Equipment-Seiten
|
||||||
|
|
||||||
|
**Odoo-Seite:**
|
||||||
|
- Credentials in `ir.config_parameter`:
|
||||||
|
- `dokuwiki.url` (z.B. `https://wiki.hobbyhimmel.de`)
|
||||||
|
- `dokuwiki.user` (z.B. `odoo`)
|
||||||
|
- `dokuwiki.password` (verschlüsselt gespeichert)
|
||||||
|
- XML-RPC über HTTPS
|
||||||
|
- Logging aller Wiki-Operationen in `mail.message`
|
||||||
|
|
||||||
|
## 5.7 Vorteile DokuWiki
|
||||||
|
|
||||||
|
✅ **Leichtgewichtig** – File-based, keine separate Datenbank
|
||||||
|
✅ **Versionierung** – Eingebautes History-System
|
||||||
|
✅ **Wiki-Syntax** – Perfekt für strukturierte Dokumentation
|
||||||
|
✅ **ACL-System** – Granulare Berechtigungen
|
||||||
|
✅ **Suchfunktion** – Volltext-Suche eingebaut
|
||||||
|
✅ **Plugins** – Erweiterbar (Markdown, Galleries, etc.)
|
||||||
|
✅ **Open Source** – Keine Lizenzkosten
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 6. Modul: open_workshop_api ⏳ GEPLANT
|
||||||
|
|
||||||
|
**Status: ZURÜCKGESTELLT (DokuWiki hat Priorität)**
|
||||||
|
|
||||||
|
REST API für externe Systeme (falls später WordPress/App-Integration gewünscht).
|
||||||
|
|
||||||
|
## 6.1 Potenzielle Endpunkte
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/v1/machines
|
||||||
|
GET /api/v1/machine/<id>
|
||||||
|
GET /api/v1/areas
|
||||||
|
```
|
||||||
|
|
||||||
|
**Hinweis:** Mit DokuWiki kann die öffentliche Dokumentation direkt über das Wiki erfolgen.
|
||||||
|
Eine separate REST API ist optional und wird nur bei Bedarf implementiert.
|
||||||
|
|
||||||
|
## 6.1 Warum DokuWiki statt WordPress?
|
||||||
|
|
||||||
|
- **Einfacher**: Keine separate Datenbank, file-based
|
||||||
|
- **Wiki-Syntax**: Perfekt für technische Dokumentation
|
||||||
|
- **Versionierung**: Eingebautes History-System
|
||||||
|
- **ACL**: Feinere Berechtigungskontrolle
|
||||||
|
- **Leichtgewichtig**: Weniger Overhead als WordPress
|
||||||
|
- **Integration**: XML-RPC API für bidirektionale Sync
|
||||||
|
|
||||||
|
## 6.2 Zukunft: WordPress optional
|
||||||
|
|
||||||
|
Falls später gewünscht, kann WordPress **zusätzlich** die öffentliche Maschinenliste anzeigen:
|
||||||
|
- DokuWiki für **Dokumentation** (intern/extern lesbar)
|
||||||
|
- WordPress für **Präsentation** (extern mit Bildern, SEO)
|
||||||
|
- Beide Systeme greifen auf Odoo-Daten zu
|
||||||
|
|
||||||
|
Aktuell hat DokuWiki-Integration **Priorität**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 7. Finaler Architektur-Plan
|
||||||
|
|
||||||
|
```
|
||||||
|
Internet
|
||||||
|
|
|
||||||
|
DokuWiki (Dokumentation)
|
||||||
|
WordPress (optional: Präsentation)
|
||||||
|
|
|
||||||
|
XML-RPC / REST API
|
||||||
|
|
|
||||||
|
----------------------------
|
||||||
|
| Odoo Backend (LAN/VPN)
|
||||||
|
| open_workshop_dokuwiki
|
||||||
|
| maintenance.equipment
|
||||||
|
----------------------------
|
||||||
|
POS, Benutzer, Einweisungen
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fokus:** Equipment-Dokumentation über DokuWiki, WordPress später optional.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 8. Sicherheit
|
||||||
|
|
||||||
|
**DokuWiki ACL:**
|
||||||
|
- Namespace `ausruestung:*` lesbar für alle (@ALL = 1)
|
||||||
|
- Namespace `ausruestung:*` schreibbar nur für Odoo (@odoo = 8)
|
||||||
|
- Manuelle Edits durch Admins möglich
|
||||||
|
|
||||||
|
**Odoo:**
|
||||||
|
- DokuWiki-Credentials in ir.config_parameter
|
||||||
|
- XML-RPC über HTTPS
|
||||||
|
- Logging aller Wiki-Operationen
|
||||||
|
- Backup-System für Wiki-Dateien
|
||||||
|
|
||||||
|
**Optional (REST API):**
|
||||||
|
- Reverse Proxy sperrt alle Odoo-URLs außer `/api/v1/*`
|
||||||
|
- HTTPS erzwingen
|
||||||
|
- Rate-Limit + Firewall
|
||||||
|
- Token-Auth
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 9. Vorteile dieser Architektur
|
||||||
|
|
||||||
|
✔ **DokuWiki für Dokumentation** – Wiki-Syntax perfekt für Anleitungen
|
||||||
|
✔ **Versionierung eingebaut** – History für alle Änderungen
|
||||||
|
✔ **Odoo bleibt sicher** hinter Firewall
|
||||||
|
✔ **Keine Benutzerkosten** – alle Ehrenamtlichen können intern mitarbeiten
|
||||||
|
✔ **Modular, wartbar, zukunftssicher**
|
||||||
|
✔ **Intelligente Sync** – Nur strukturierte Daten werden überschrieben, Freitext bleibt erhalten
|
||||||
|
✔ **Öffentlich lesbar** – Anleitungen für alle verfügbar
|
||||||
|
✔ **WordPress optional** – Kann später für Marketing/SEO ergänzt werden
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 10. Implementierungsstand (13.12.2025)
|
||||||
|
|
||||||
|
| Schritt | Status | Details |
|
||||||
|
|---------|--------|---------|
|
||||||
|
| 1. open_workshop_base | ✅ **FERTIG** | Version 18.0.1.0.4, produktiv |
|
||||||
|
| 2. open_workshop_pos | ✅ **FERTIG** | POS-Integration komplett |
|
||||||
|
| 3. ~~open_workshop_maintenance~~ | ❌ **VERWORFEN** | Direkt in Base integriert |
|
||||||
|
| 4. Maintenance Integration | ✅ **FERTIG** | _inherits Pattern implementiert |
|
||||||
|
| 5. Equipment-View Integration | ✅ **FERTIG** | Related fields, Menu-Migration |
|
||||||
|
| 6. Migration Workflow | ✅ **FERTIG** | SQL + Python, CI/CD integriert |
|
||||||
|
| 7. open_workshop_dokuwiki | ⏳ **IN ENTWICKLUNG** | Branch: feature/dokuwiki-integration |
|
||||||
|
| 8. open_workshop_api | 💤 **OPTIONAL** | Zurückgestellt, bei Bedarf |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 11. Nächste Schritte
|
||||||
|
|
||||||
|
1. **open_workshop_dokuwiki** entwickeln
|
||||||
|
- DokuWiki XML-RPC Client implementieren
|
||||||
|
- Template-System für Equipment-Seiten
|
||||||
|
- Sync-Mechanismus (Odoo → Wiki)
|
||||||
|
- Smart Button "Wiki öffnen"
|
||||||
|
- ACL-Konfiguration dokumentieren
|
||||||
|
|
||||||
|
2. **DokuWiki aufsetzen**
|
||||||
|
- Installation auf Server
|
||||||
|
- Namespace `ausruestung:` anlegen
|
||||||
|
- ACL konfigurieren (@odoo Gruppe)
|
||||||
|
- XML-RPC API aktivieren
|
||||||
|
|
||||||
|
3. **Testing**
|
||||||
|
- Equipment-Seiten automatisch generieren
|
||||||
|
- Sync bei Änderungen testen
|
||||||
|
- ACL-Rechte validieren
|
||||||
|
- Performance testen
|
||||||
|
|
||||||
|
4. **Optional: WordPress Plugin**
|
||||||
|
- Falls öffentliche Präsentation gewünscht
|
||||||
|
- REST API dann implementieren
|
||||||
|
- Caching-Layer hinzufügen
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 12. Endfazit
|
||||||
|
|
||||||
|
Diese Architektur vereint das Beste aus beiden Welten:
|
||||||
|
|
||||||
|
- ✅ **Technisch korrekt** – _inherits Pattern statt separatem Modul
|
||||||
|
- ✅ **Performant** – Maintenance.equipment als Single Source of Truth
|
||||||
|
- ✅ **Dokumentiert** – DokuWiki für strukturierte Anleitungen
|
||||||
|
- ✅ **Versioniert** – History-System eingebaut
|
||||||
|
- ✅ **Öffentlich zugänglich** – Wiki lesbar für alle
|
||||||
|
- ✅ **Kostenoptimiert** – Keine zusätzlichen Lizenzen
|
||||||
|
- ✅ **Langfristig erweiterbar** – Modularer Aufbau
|
||||||
|
- ✅ **Produktiv im Einsatz** – Migration erfolgreich abgeschlossen
|
||||||
|
|
||||||
|
**DokuWiki-Integration ist der nächste logische Schritt** für bessere Equipment-Dokumentation.
|
||||||
|
|
||||||
|
**Letztes Update: 13.12.2025**
|
||||||
|
|
||||||
142
FEATURE_REQUEST/open_workshop_modulstruktur.md
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
# Empfohlene Modulstruktur für Open_Workshop + öffentliches Website-Modul
|
||||||
|
|
||||||
|
## 🎯 Ziel
|
||||||
|
Du möchtest:
|
||||||
|
1. Ein **öffentliches Website-Modul**, das Maschineninformationen aus dem Maintenance-Modul anzeigt.
|
||||||
|
2. Dein gesamtes `open_workshop`-Repository sinnvoll modularisieren.
|
||||||
|
|
||||||
|
Diese Antwort beschreibt die optimale Struktur und die Gründe dafür.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# ✅ Öffentliches Modul ist sinnvoll
|
||||||
|
|
||||||
|
Du möchtest:
|
||||||
|
- Maschinen anzeigen (Maintenance)
|
||||||
|
- Zustand sichtbar machen
|
||||||
|
- Bilder & PDFs anzeigen
|
||||||
|
- ohne Login (auth="public")
|
||||||
|
|
||||||
|
➡️ Dafür ist ein eigenes Modul **open_workshop_website** ideal.
|
||||||
|
|
||||||
|
Dieses Website-Modul sollte *nur Anzeige* machen, keine Logik verwalten.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# ⭐ Empfohlene Modulstruktur (Best Practice)
|
||||||
|
|
||||||
|
```
|
||||||
|
open_workshop/
|
||||||
|
│
|
||||||
|
├── open_workshop_pos/ # POS-Integration, Einweisungen, Freigaben
|
||||||
|
│
|
||||||
|
├── open_workshop_maintenance/ # Verbindung zu maintenance.equipment
|
||||||
|
│ # z. B. Modelle, Felder, Extensions
|
||||||
|
│
|
||||||
|
└── open_workshop_website/ # Öffentliche Darstellung, QR-Codes, Website
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🧩 Gründe für diese modulare Struktur
|
||||||
|
|
||||||
|
## 1️⃣ Trennung nach funktionalen Bereichen
|
||||||
|
Odoo trennt:
|
||||||
|
- POS
|
||||||
|
- Maintenance
|
||||||
|
- Website
|
||||||
|
|
||||||
|
Du solltest dasselbe tun.
|
||||||
|
So bleibt alles klar, logisch und update-sicher.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2️⃣ Bessere Wartbarkeit
|
||||||
|
Wenn Odoo 19/20 Änderungen bringt:
|
||||||
|
|
||||||
|
- POS ändert sich → nur `open_workshop_pos`
|
||||||
|
- Maintenance ändert sich → nur `open_workshop_maintenance`
|
||||||
|
- Website ändert sich → nur `open_workshop_website`
|
||||||
|
|
||||||
|
Ein großes Sammelmodul wäre schwer zu warten.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3️⃣ Nutzer können auswählen, was sie brauchen
|
||||||
|
Beispiel:
|
||||||
|
|
||||||
|
Ein Kunde will nur Maschinen + Website, aber kein POS.
|
||||||
|
|
||||||
|
Dann installiert er:
|
||||||
|
|
||||||
|
- `open_workshop_maintenance`
|
||||||
|
- `open_workshop_website`
|
||||||
|
|
||||||
|
🔧 Ohne unnötige POS-Abhängigkeit.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4️⃣ Klare Modulabhängigkeiten
|
||||||
|
### `open_workshop_pos`
|
||||||
|
- depends: `point_of_sale`
|
||||||
|
|
||||||
|
### `open_workshop_maintenance`
|
||||||
|
- depends: `maintenance`
|
||||||
|
|
||||||
|
### `open_workshop_website`
|
||||||
|
- depends: `website`
|
||||||
|
- optional depends: `open_workshop_maintenance`
|
||||||
|
|
||||||
|
Alles sauber getrennt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5️⃣ Erweiterbarkeit bleibt einfach
|
||||||
|
Dein Projekt wächst schnell:
|
||||||
|
|
||||||
|
- QR-Codes
|
||||||
|
- Maschinenstatus
|
||||||
|
- API
|
||||||
|
- Dashboards
|
||||||
|
- Rollen mit Einweisungssichtbarkeit
|
||||||
|
|
||||||
|
Wenn alles in EINEM Modul wäre → unübersichtlich
|
||||||
|
Wenn modular → perfekt strukturiert
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# ❌ Warum alles in *einem einzigen Modul* schlecht wäre
|
||||||
|
|
||||||
|
- Mischmasch aus POS, Website und Maintenance-Code
|
||||||
|
- schwerer zu debuggen
|
||||||
|
- schlechtere Wiederverwendbarkeit
|
||||||
|
- mehr Merge-Konflikte
|
||||||
|
- Odoo selbst macht das nie so
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🎉 Fazit
|
||||||
|
|
||||||
|
Die modulare Struktur ist optimal und entspricht OCA/Odoo-Best-Practices:
|
||||||
|
|
||||||
|
| Modul | Zweck |
|
||||||
|
|-------|--------|
|
||||||
|
| **open_workshop_pos** | Einweisung, Freigaben, POS GUI |
|
||||||
|
| **open_workshop_maintenance** | Maschinenmodellierung, Maintenance-Anbindung |
|
||||||
|
| **open_workshop_website** | öffentliche Darstellung, QR-Codes |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 📦 Optional: Nächster Schritt
|
||||||
|
|
||||||
|
Ich kann dir komplette Modul-Gerüste erstellen:
|
||||||
|
|
||||||
|
- Manifest-Dateien
|
||||||
|
- Controller
|
||||||
|
- Views
|
||||||
|
- Templates
|
||||||
|
- QR-Code-Integration
|
||||||
|
- Demo-Daten
|
||||||
|
|
||||||
|
Sag einfach Bescheid:
|
||||||
|
👉 *„Bitte erstelle mir das Grundgerüst.“*
|
||||||
161
FEATURE_REQUEST/workshop_maintenance_overview.md
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
# Maschinen- und Geräteverwaltung im FabLab mit Odoo Maintenance + Open_Workshop
|
||||||
|
|
||||||
|
## 🎯 Ausgangssituation
|
||||||
|
|
||||||
|
In der Werkstatt existieren viele verschiedene Maschinen und Geräte:
|
||||||
|
|
||||||
|
- stationär
|
||||||
|
- mobil
|
||||||
|
- elektrisch
|
||||||
|
- mechanisch
|
||||||
|
- verschiedene Sicherheits- und Einweisungsanforderungen
|
||||||
|
|
||||||
|
Der aktuelle Zustand, Bilder und Bedienungsanleitungen sind unstrukturiert im Wiki gespeichert.
|
||||||
|
Das Ziel ist:
|
||||||
|
|
||||||
|
- eine zentrale, strukturierte Verwaltung in **Odoo**
|
||||||
|
- Anzeige des Maschinenzustands
|
||||||
|
- Zugriff für Nutzer auf Bedienungsanleitungen
|
||||||
|
- ohne Backend-Zugang
|
||||||
|
- am besten über Website/Portal + QR-Codes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 Die perfekte Lösung: Maintenance-Modul + Open_Workshop + Website
|
||||||
|
|
||||||
|
Die optimale Architektur besteht aus drei Komponenten:
|
||||||
|
|
||||||
|
1. **Maintenance-Modul**
|
||||||
|
→ Zentrale Verwaltung aller Maschinen/Devices
|
||||||
|
2. **Open_Workshop-Modul**
|
||||||
|
→ Freigaben, Einweisungen, POS-Integration
|
||||||
|
3. **Website-/Portal-Modul (Custom)**
|
||||||
|
→ Anzeige für Nutzer: Maschine, Zustand, Anleitung, QR-Codes
|
||||||
|
|
||||||
|
Diese Kombination ist robust, klar strukturiert und update-sicher.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# ✅ Warum das Maintenance-Modul ideal ist
|
||||||
|
|
||||||
|
Das Modell `maintenance.equipment` bietet bereits alles, was du brauchst:
|
||||||
|
|
||||||
|
| Feld | Nutzen |
|
||||||
|
|------|--------|
|
||||||
|
| `name` | Maschinenname |
|
||||||
|
| `category_id` | Maschinenbereich |
|
||||||
|
| `maintenance_state` | Betriebszustand |
|
||||||
|
| `note` | Beschreibung |
|
||||||
|
| `image_1920` | Maschinenbild |
|
||||||
|
| `message_attachment_count` | PDFs & Dateien |
|
||||||
|
| Anhänge | Bedienungsanleitungen |
|
||||||
|
|
||||||
|
Diese Felder machen das Maintenance-Modul zur perfekten **Source of Truth**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🧩 Wie du die Daten Nutzern sichtbar machst
|
||||||
|
|
||||||
|
Nicht das Backend öffnen.
|
||||||
|
Nicht `website=True` setzen.
|
||||||
|
Sondern:
|
||||||
|
|
||||||
|
### ✔ Eigener Website-/Portal-Controller
|
||||||
|
|
||||||
|
Beispiele:
|
||||||
|
|
||||||
|
### Maschinenliste
|
||||||
|
```
|
||||||
|
/workshop/machines
|
||||||
|
```
|
||||||
|
|
||||||
|
### Maschinendetailansicht
|
||||||
|
```
|
||||||
|
/workshop/machine/<id>
|
||||||
|
```
|
||||||
|
|
||||||
|
Dort sieht der Nutzer:
|
||||||
|
|
||||||
|
- Bild
|
||||||
|
- Betriebszustand
|
||||||
|
- Kategorie
|
||||||
|
- Beschreibung
|
||||||
|
- Bedienungsanleitung (PDF-Download)
|
||||||
|
- optional: Sicherheitsinfos
|
||||||
|
- optional: Einweisungsstatus (aus Open_Workshop)
|
||||||
|
|
||||||
|
Alles **read-only**, ohne Backend.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🔒 Zugriffsvarianten
|
||||||
|
|
||||||
|
Du entscheidest selbst:
|
||||||
|
|
||||||
|
## **Option A: Öffentlich**
|
||||||
|
- Keine Anmeldung nötig
|
||||||
|
- Zustand und Anleitung für alle sichtbar
|
||||||
|
- Ideal für Tablets im FabLab oder QR-Codes an Maschinen
|
||||||
|
|
||||||
|
## **Option B: Portal-Zugang**
|
||||||
|
- Nutzer müssen sich einloggen
|
||||||
|
- Anleitungen nur für registrierte Mitglieder sichtbar
|
||||||
|
|
||||||
|
## **Option C: Hybrid**
|
||||||
|
- Zustand öffentlich
|
||||||
|
- Anleitungen nur für eingeloggte Benutzer mit Einweisung
|
||||||
|
|
||||||
|
Diese Variante passt besonders gut zu deinem Open_Workshop-Modell.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🧰 Integration mit Open_Workshop
|
||||||
|
|
||||||
|
Die ideale Struktur ist:
|
||||||
|
|
||||||
|
```
|
||||||
|
maintenance.equipment ←→ ows.machine ←→ ows.machine.access
|
||||||
|
```
|
||||||
|
|
||||||
|
Das bedeutet:
|
||||||
|
|
||||||
|
- Maintenance = technische Daten
|
||||||
|
- Open_Workshop = Einweisungsprozesse
|
||||||
|
- Website = Darstellung
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 📲 QR-Codes an den Maschinen
|
||||||
|
|
||||||
|
Odoo kann automatisch QR-Codes generieren.
|
||||||
|
|
||||||
|
Du platzierst auf jeder Maschine einen QR-Code, z. B.:
|
||||||
|
|
||||||
|
```
|
||||||
|
/workshop/machine/42
|
||||||
|
```
|
||||||
|
|
||||||
|
Der Nutzer scannt ihn und sieht:
|
||||||
|
|
||||||
|
- Bild
|
||||||
|
- Zustand
|
||||||
|
- Anleitung
|
||||||
|
- ggf. "Einweisung vorhanden" / "Keine Einweisung"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🚀 Modularer Vorschlag für deine Architektur
|
||||||
|
|
||||||
|
### **1. Maintenance-Modul**
|
||||||
|
Zentrale Maschinenverwaltung
|
||||||
|
|
||||||
|
### **2. Open_Workshop-Modul**
|
||||||
|
Einweisung, Freigaben, POS
|
||||||
|
|
||||||
|
### **3. Custom Website-Modul**
|
||||||
|
- Maschinenliste
|
||||||
|
- Detailseiten
|
||||||
|
- PDF-Downloads
|
||||||
|
- Einweisungsstatus
|
||||||
|
- optional QR-Code-Automatik
|
||||||
|
|
||||||
79
README.md
|
|
@ -1,78 +1 @@
|
||||||
# Open Workshop (open_workshop ows)
|
Abhängigkeiten von maintenance erklären.
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
## Funktionen
|
|
||||||
|
|
||||||
### Erweiterungen an Kontakten (res.partner)
|
|
||||||
- Geburtstagsfeld, RFID-Karte, Haftungsausschluss usw. ausgelagert nach `ows.user`
|
|
||||||
- Automatische Erstellung des `ows.user`-Eintrags beim Anlegen eines Kontakts
|
|
||||||
- Übersichtliche Darstellung aller Maschinenfreigaben im Odoo Kontaktformular
|
|
||||||
|
|
||||||
### Maschinen und Bereiche
|
|
||||||
- Modell `ows.machine` mit Gruppierung nach Bereichen (`ows.machine.area`)
|
|
||||||
- Farblich kodierte Bereiche (Hex-Wert aus Datenbank) welche zur Darstellung im POS verwendet werden
|
|
||||||
|
|
||||||
### Einweisungen und Nutzungen
|
|
||||||
- Modelle `ows.machine.training` und `ows.machine.product`
|
|
||||||
- Konfigurierbare Produkte für Einweisung/Nutzung direkt im Backend
|
|
||||||
- Zuweisung von Nutzungsprodukten zu Maschinen
|
|
||||||
- Zuweisung von Einweisungsprodukten zu Maschinen
|
|
||||||
|
|
||||||
### Maschinenfreigaben
|
|
||||||
- Modell `ows.machine.access` verknüpft Partner und Maschine
|
|
||||||
- Darstellung im POS als tabellarische Übersicht mit Anzeige für eine bestehende Einweisung / Nutzungsberechtigung
|
|
||||||
- Anzeige im POS-Kundendetailsansicht innerhalb der Kundendetailsansicht
|
|
||||||
- Anzeige im Odoo Kontak Modul der Maschineneinweisungen
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
1. Dieses Modul in den Custom-Addons-Ordner kopieren
|
|
||||||
2. 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)
|
|
||||||
|
|
||||||
## 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
|
|
||||||
- Integration von Fristen (z.B. Ablaufdatum Einweisung)
|
|
||||||
|
|
||||||
## Autoren
|
|
||||||
- Matthias Lotz
|
|
||||||
|
|
||||||
## Lizenz
|
|
||||||
AGPL-3.0 oder später
|
|
||||||
|
|
||||||
---
|
|
||||||
Letzte Aktualisierung: 06.04.2025
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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", attachment=True)
|
||||||
|
comp_serial_no = fields.Char("Inventory Serial No", tracking=True)
|
||||||
|
serial_no = fields.Char('Mfg. Serial Number', copy=False)
|
||||||
|
|
||||||
|
def action_print_qrcode_layout(self):
|
||||||
|
action = self.env['ir.actions.act_window']._for_xml_id('aspl_equipment_qrcode_generator.action_open_label_layout_equipment')
|
||||||
|
action['context'] = {'default_equipment_ids': self.ids}
|
||||||
|
return action
|
||||||
|
|
||||||
|
def generate_serial_no(self):
|
||||||
|
for equipment_id in self:
|
||||||
|
if not equipment_id.comp_serial_no:
|
||||||
|
company_id = equipment_id.company_id.id
|
||||||
|
sequence_id = self.env['ir.sequence'].search(
|
||||||
|
[('name', '=', 'Equipment Company Sequence'), ('company_id', '=', company_id)])
|
||||||
|
if sequence_id:
|
||||||
|
data = sequence_id._next()
|
||||||
|
equipment_id.write({
|
||||||
|
'comp_serial_no': data
|
||||||
|
})
|
||||||
22
aspl_equipment_qrcode_generator/models/res_company.py
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from odoo import models, api
|
||||||
|
|
||||||
|
|
||||||
|
class ResCompany(models.Model):
|
||||||
|
_inherit = 'res.company'
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def create(self, vals):
|
||||||
|
result = super(ResCompany, self).create(vals)
|
||||||
|
sequence_id = self.env['ir.sequence'].search(
|
||||||
|
[('name', '=', 'Equipment Company Sequence'), ('company_id', '=', result.id)])
|
||||||
|
if not sequence_id:
|
||||||
|
self.env['ir.sequence'].create({
|
||||||
|
'name': 'Equipment Company Sequence',
|
||||||
|
'prefix': result.id,
|
||||||
|
'padding': 5,
|
||||||
|
'number_increment': 1,
|
||||||
|
'company_id': result.id
|
||||||
|
})
|
||||||
|
return result
|
||||||
3
aspl_equipment_qrcode_generator/report/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from . import custom_qrcode_generator
|
||||||
190
aspl_equipment_qrcode_generator/report/custom_qrcode.xml
Normal file
|
|
@ -0,0 +1,190 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
<template id="report_equipment_simple_label2x7">
|
||||||
|
<t t-set="qrcode_size" t-value="'height:14mm'"/>
|
||||||
|
<t t-set="table_style" t-value="'width:97mm;height:37.1mm;margin:inherit;' + table_style"/>
|
||||||
|
<td t-att-style="make_invisible and 'visibility:hidden;'" >
|
||||||
|
<div class="o_label_full" t-att-style="table_style">
|
||||||
|
<div class="o_label_name">
|
||||||
|
<strong t-field="equipment.name"/>
|
||||||
|
</div>
|
||||||
|
<div class="o_label_data">
|
||||||
|
<div class="text-center o_label_right_column">
|
||||||
|
<t t-if="equipment.qr_code">
|
||||||
|
<img t-att-src="'data:image/png;base64,' + equipment.qr_code.decode('utf-8')" style="width: 130px;margin-top: -40px;"/>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
<div class="text-left" style="line-height:normal;word-wrap: break-word;">
|
||||||
|
<span class="text-nowrap" t-field="equipment.serial_no"/>
|
||||||
|
<div class="o_label_extra_data">
|
||||||
|
<span t-field="equipment.comp_serial_no"/>
|
||||||
|
</div>
|
||||||
|
<t t-if="equipment.warranty_date">
|
||||||
|
<strong t-field="equipment.warranty_date"/>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
<div class="o_label_clear"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="report_equipment_simple_label4x7">
|
||||||
|
<t t-set="barcode_size" t-value="'width:80px;'"/>
|
||||||
|
<t t-set="table_style" t-value="'width:47mm;height:37.1mm;margin:inherit;' + table_style"/>
|
||||||
|
<td t-att-style="make_invisible and 'visibility:hidden;'" >
|
||||||
|
<div class="o_label_full" t-att-style="table_style">
|
||||||
|
<div class="o_label_name">
|
||||||
|
<strong t-field="equipment.name"/>
|
||||||
|
</div>
|
||||||
|
<div class= "text-center o_label_right_column">
|
||||||
|
<t t-if="equipment.qr_code">
|
||||||
|
<img t-att-src="'data:image/png;base64,' + equipment.qr_code.decode('utf-8')" style="width:95px;padding:0px;margin-top:-10px"/>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
<div class="text-left o_label_left_column" style="line-height:normal;word-wrap: break-word;">
|
||||||
|
<div class="o_label_data">
|
||||||
|
<strong t-field="equipment.serial_no"/>
|
||||||
|
<span t-field="equipment.comp_serial_no"/>
|
||||||
|
<t t-if="equipment.warranty_date">
|
||||||
|
<strong t-field="equipment.warranty_date"/>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="report_equipment_simple_label2x5">
|
||||||
|
<t t-set="barcode_size" t-value="'width:38mm;height:14mm;margin-top:50px;'"/>
|
||||||
|
<t t-set="table_style"
|
||||||
|
t-value="'width:97mm;height:50mm;font-size: 12px;border: 2px solid black;table-layout: fixed;'"/>
|
||||||
|
<td>
|
||||||
|
<table t-att-style="table_style">
|
||||||
|
<tr>
|
||||||
|
<td style="width:25%;">Name</td>
|
||||||
|
<td style="width:5%">:</td>
|
||||||
|
<td colspan="2" style="width:70%;word-wrap: break-word;">
|
||||||
|
<span t-field="equipment.name"/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="width:25%">Model</td>
|
||||||
|
<td style="width:5%">:</td>
|
||||||
|
<td style="width:35%;word-wrap: break-word;">
|
||||||
|
<span t-field="equipment.model"/>
|
||||||
|
</td>
|
||||||
|
<td rowspan="5" style="width:35%">
|
||||||
|
<t t-if="equipment.qr_code">
|
||||||
|
<img t-att-src="'data:image/png;base64,' + equipment.qr_code.decode('utf-8')"
|
||||||
|
style="width:30mm;height:30mm;"/>
|
||||||
|
</t>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td style="width:25%">Mfg Serial</td>
|
||||||
|
<td style="width:5%">:</td>
|
||||||
|
<td style="width:35%;font-size:10px;word-wrap: break-word;">
|
||||||
|
<span t-field="equipment.serial_no"/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="width:25%">Serial</td>
|
||||||
|
<td style="width:5%">:</td>
|
||||||
|
<td style="width:35%;word-wrap: break-word;">
|
||||||
|
<span t-field="equipment.comp_serial_no"/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="width:25%">Warranty Date</td>
|
||||||
|
<td style="width:5%">:</td>
|
||||||
|
<td style="width:35%;word-wrap: break-word;">
|
||||||
|
<span t-field="equipment.warranty_date"/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="width:25%"></td>
|
||||||
|
<td style="width:5%"></td>
|
||||||
|
<td style="width:35%;word-wrap: break-word;"></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="report_quipmentlabel">
|
||||||
|
<t t-call="web.html_container">
|
||||||
|
<t t-if="columns and rows">
|
||||||
|
<t t-if="columns == 2 and rows == 7">
|
||||||
|
<t t-set="padding_page" t-value="'padding: 14mm 3mm'"/>
|
||||||
|
<t t-set="report_to_call" t-value="'aspl_equipment_qrcode_generator.report_equipment_simple_label2x7'"/>
|
||||||
|
</t>
|
||||||
|
<t t-if="columns == 4 and rows == 7">
|
||||||
|
<t t-set="padding_page" t-value="'padding: 14mm 3mm'"/>
|
||||||
|
<t t-set="report_to_call" t-value="'aspl_equipment_qrcode_generator.report_equipment_simple_label4x7'"/>
|
||||||
|
</t>
|
||||||
|
<t t-if="columns == 2 and rows == 5">
|
||||||
|
<t t-set="padding_page" t-value="'padding: 14mm 3mm'"/>
|
||||||
|
<t t-set="report_to_call" t-value="'aspl_equipment_qrcode_generator.report_equipment_simple_label2x5'"/>
|
||||||
|
</t>
|
||||||
|
<t t-foreach="range(page_numbers)" t-as="page">
|
||||||
|
<div class="o_label_sheet" t-att-style="padding_page">
|
||||||
|
<table class="my-0 table table-sm table-borderless">
|
||||||
|
<t t-foreach="range(rows)" t-as="row">
|
||||||
|
<tr>
|
||||||
|
<t t-foreach="range(columns)" t-as="column">
|
||||||
|
<t t-if="equipment_data">
|
||||||
|
<t t-set="current_data" t-value="equipment_data.popitem()"/>
|
||||||
|
<t t-set="equipment" t-value="current_data[0]"/>
|
||||||
|
<t t-set="table_style" t-value="'border: 1px solid black'"/>
|
||||||
|
<t t-call="{{report_to_call}}"/>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
</tr>
|
||||||
|
</t>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="maintenance_quip">
|
||||||
|
<t t-call="web.basic_layout">
|
||||||
|
<div class="page">
|
||||||
|
<t t-call="aspl_equipment_qrcode_generator.report_quipmentlabel">
|
||||||
|
<t t-set="products" t-value="products"/>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<record id="paperformat_label_sheet_qrcode" model="report.paperformat">
|
||||||
|
<field name="name">A4 Label Sheet</field>
|
||||||
|
<field name="default" eval="True"/>
|
||||||
|
<field name="format">A4</field>
|
||||||
|
<field name="page_height">0</field>
|
||||||
|
<field name="page_width">0</field>
|
||||||
|
<field name="orientation">Portrait</field>
|
||||||
|
<field name="margin_top">0</field>
|
||||||
|
<field name="margin_bottom">0</field>
|
||||||
|
<field name="margin_left">0</field>
|
||||||
|
<field name="margin_right">0</field>
|
||||||
|
<field name="disable_shrinking" eval="True"/>
|
||||||
|
<field name="dpi">96</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="report_equipment_label" model="ir.actions.report">
|
||||||
|
<field name="name">Equipment QR-code (PDF)</field>
|
||||||
|
<field name="model">maintenance.equipment</field>
|
||||||
|
<field name="report_type">qweb-pdf</field>
|
||||||
|
<field name="report_name">aspl_equipment_qrcode_generator.maintenance_quip</field>
|
||||||
|
<field name="report_file">aspl_equipment_qrcode_generator.maintenance_quip</field>
|
||||||
|
<field name="paperformat_id" ref="aspl_equipment_qrcode_generator.paperformat_label_sheet_qrcode"/>
|
||||||
|
<field name="print_report_name">'Products Labels - %s' % (object.name)</field>
|
||||||
|
<field name="binding_model_id" eval="False"/>
|
||||||
|
<field name="binding_type">report</field>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from odoo import models
|
||||||
|
|
||||||
|
|
||||||
|
class ReportProductTemplateLabel(models.AbstractModel):
|
||||||
|
_name = 'report.aspl_equipment_qrcode_generator.maintenance_quip'
|
||||||
|
_description = 'Equipment QR-code Report'
|
||||||
|
|
||||||
|
def _get_report_values(self, docids, data=None):
|
||||||
|
"""
|
||||||
|
QR-Code-Generierung erfolgt im Wizard (equipment_label_layout.py).
|
||||||
|
Dieser Report rendert nur die bereits generierten QR-Codes.
|
||||||
|
"""
|
||||||
|
if not data:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
equipment_label_layout_id = self.env['equipment.label.layout'].browse(data.get('equipment_label_layout_id'))
|
||||||
|
|
||||||
|
equipment_dict = {}
|
||||||
|
for equipment in equipment_label_layout_id.equipment_ids:
|
||||||
|
equipment_dict[equipment] = 1
|
||||||
|
|
||||||
|
page_numbers = (len(equipment_label_layout_id.equipment_ids) - 1) // (
|
||||||
|
equipment_label_layout_id.rows * equipment_label_layout_id.columns
|
||||||
|
) + 1
|
||||||
|
|
||||||
|
return {
|
||||||
|
'rows': equipment_label_layout_id.rows,
|
||||||
|
'columns': equipment_label_layout_id.columns,
|
||||||
|
'page_numbers': page_numbers,
|
||||||
|
'equipment_data': equipment_dict
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
|
access_equipment_label_layout,access.equipment_label_layout,model_equipment_label_layout,,1,1,1,1
|
||||||
|
BIN
aspl_equipment_qrcode_generator/static/description/banner.gif
Normal file
|
After Width: | Height: | Size: 78 KiB |
BIN
aspl_equipment_qrcode_generator/static/description/icon.png
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 563 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 362 KiB |
165
aspl_equipment_qrcode_generator/static/description/index.html
Normal file
|
|
@ -0,0 +1,165 @@
|
||||||
|
<section class="oe_container" style="border-left:1px solid grey;border-right :1px solid grey;font-family: Urbanist ,'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;" >
|
||||||
|
<div class="oe_row oe_spaced">
|
||||||
|
<div style="display:flex;margin-top:5rem;padding : 0rem 5rem;">
|
||||||
|
<div><img src="./images/logo.png" style="width:200px;"/></div>
|
||||||
|
<div style="width: 100%;text-align: end;"><a href="https://hrms.aspiresoftserv.com/contactus" class="btn" target="_blank" style="background-color:#011D45;color:white;border-radius:5px;"><i class="fa fa-envelope" style="margin-right:0.5rem"></i>Request A Demo</a></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="oe_row oe_spaced" style="margin-top:5rem;text-align:center;width:100%;">
|
||||||
|
<h3 style="color:#011D45;">Euipment Qr-code</h3>
|
||||||
|
<h6 style="color:#011D45;width: 50%;margin: auto;text-align: left;">
|
||||||
|
The Equipment Management Module streamlines the intricate process of equipment management by generating unique QR codes for each asset. Scanning these QR codes instantly provides comprehensive details about the equipment. Additionally, the QR code features a direct link to the equipment's profile in Odoo, facilitating seamless management and updates.</h6>
|
||||||
|
<br/>
|
||||||
|
<h4 style="color:#011D45;width: 50%;margin: auto;text-align: left;margin-bottom:2px;">Prerequisite:-</h4>
|
||||||
|
|
||||||
|
<h6 style="color:#011D45;width: 50%;margin: auto;text-align: left;">
|
||||||
|
pip install qrcode==6.1
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="oe_row oe_spaced" style="display:flex;margin-top:3rem;">
|
||||||
|
<div style="width:750px;margin: auto;">
|
||||||
|
<img src="./banner.gif" style="width:100%">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="oe_row oe_spaced" style="display:flex;">
|
||||||
|
<h3 style="color:#011D45;margin: auto;">Screenshots</h3>
|
||||||
|
</div>
|
||||||
|
<div class="oe_row oe_spaced" style="display:flex;">
|
||||||
|
<div style="margin: auto;">
|
||||||
|
<div style="width:800px;padding: 2rem;">
|
||||||
|
<div style="width:100%;text-align:left;">
|
||||||
|
<h4 style="color:#011D45;">Sequence Configuration</h4>
|
||||||
|
<h6 style="color:#011D45;">Once you install the module, It will automatically create maintenance sequence for available company.</h6>
|
||||||
|
</div>
|
||||||
|
<div style="width:80%;margin:auto;margin-top: 1rem;">
|
||||||
|
<img src="./screenshots/1_sequence.png" style="width:100%;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="width:800px;background-color:#F4F6F6;padding: 2rem;">
|
||||||
|
<div style="width:100%;text-align:left;">
|
||||||
|
<h4 style="color:#011D45;">Serial number Generation</h4>
|
||||||
|
<h6 style=" color:#011D45;">In the equipment, you'll find a "Generate Serial Number" button. When you click this button, it automatically creates a serial number based on the company's predefined sequence, which is set up when the module is installed (check above screenshot related to sequence configuration).
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div style="width:80%;margin:auto;margin-top: 1rem;">
|
||||||
|
<img src="./screenshots/2_equipment_screen.png" style="width:100%">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="width:800px;padding: 2rem;">
|
||||||
|
<div style="width:100%;text-align:left;">
|
||||||
|
<h4 style="color:#011D45;">Print QR-Code View</h4>
|
||||||
|
<h6 style="color:#011D45;">In the equipment tree view, When you select an item, It will display a "QR Code" button.</h6>
|
||||||
|
</div>
|
||||||
|
<div style="width:80%;margin:auto;margin-top: 1rem;">
|
||||||
|
<img src="./screenshots/3_equipment_tree.png" style="width:100%;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="width:800px;background-color:#F4F6F6;padding: 2rem;">
|
||||||
|
<div style="width:100%;text-align:left;">
|
||||||
|
<h4 style="color:#011D45;">QR Code Size Options</h4>
|
||||||
|
<h6 style="color:#011D45;">The module will display all available options for QR code sizes, allowing you to choose the appropriate dimensions for generating your QR codes.</h6>
|
||||||
|
</div>
|
||||||
|
<div style="width:80%;margin:auto;margin-top: 1rem;">
|
||||||
|
<img src="./screenshots/4_qrcode_size_selection.png" style="width:100%;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="oe_row oe_spaced" style="display:flex;margin-top:3rem;">
|
||||||
|
<h3 style="color:#011D45;margin: auto;">Suggested Apps</h3>
|
||||||
|
</div>
|
||||||
|
<div id="slides" class="row carousel slide mt64 mb32" style="width:800px;margin:auto;" data-ride="carousel">
|
||||||
|
<div class="carousel-inner">
|
||||||
|
<div class="carousel-item active" style="min-height: 0px;">
|
||||||
|
<div class="row no-lg-gutters">
|
||||||
|
<div class="col-6 col-sm-3 text-center">
|
||||||
|
<a class="d-block"
|
||||||
|
href="https://apps.odoo.com/apps/modules/15.0/aspl_project_timesheet_activity/"
|
||||||
|
style="text-decoration: none;" target="_blank">
|
||||||
|
<img alt="Project Timesheet Activity" src="./images/aspl_project_timesheet_activity.png" style="width:60%;">
|
||||||
|
<div class="card-body text-center p-0 mt-3 mb-3 mb-lg-0">
|
||||||
|
<h6 class="card-text m-0" style="font-size: 16px; color:#011D45;font-weight: 600">
|
||||||
|
Project Timesheet<br>Activity</h6>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 col-sm-3 text-center">
|
||||||
|
<a class="d-block"
|
||||||
|
href="https://apps.odoo.com/apps/modules/15.0/aspl_save_view_to_favourite/"
|
||||||
|
style="text-decoration: none;" target="_blank">
|
||||||
|
<img alt="Project Timesheet Activity" src="./images/aspl_save_view_to_favourite.png" style="width:60%;">
|
||||||
|
<div class="card-body text-center p-0 mt-3 mb-3 mb-lg-0">
|
||||||
|
<h6 class="card-text m-0" style="font-size: 16px; color:#011D45;font-weight: 600">
|
||||||
|
Project Timesheet<br>Activity</h6>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 col-sm-3 text-center">
|
||||||
|
<a class="d-block"
|
||||||
|
href="https://apps.odoo.com/apps/modules/15.0/aspl_select_multi_company/"
|
||||||
|
style="text-decoration: none;" target="_blank">
|
||||||
|
<img alt="Select Multi Company" src="./images/aspl_select_multi_company.png" style="width:60%;">
|
||||||
|
<div class="card-body text-center p-0 mt-3 mb-3 mb-lg-0">
|
||||||
|
<h6 class="card-text m-0" style="font-size: 16px; color:#011D45;font-weight: 600">
|
||||||
|
Select Multi<br>Company</h6>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 col-sm-3 text-center">
|
||||||
|
<a class="d-block"
|
||||||
|
href="https://apps.odoo.com/apps/modules/15.0/aspl_project_task_milestone/"
|
||||||
|
style="text-decoration: none;" target="_blank">
|
||||||
|
<img alt="Project Task Milestone" src="./images/project_task_milestone.png" style="width:60%;">
|
||||||
|
<div class="card-body text-center p-0 mt-3 mb-3 mb-lg-0">
|
||||||
|
<h6 class="card-text m-0" style="font-size: 16px;color:#011D45; font-weight: 600">
|
||||||
|
Project Task <br>Milestone</h6>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 col-sm-3 text-center">
|
||||||
|
<a class="d-block"
|
||||||
|
href="https://apps.odoo.com/apps/modules/15.0/aspl_equipment_qrcode_generator/"
|
||||||
|
style="text-decoration: none;" target="_blank">
|
||||||
|
<img alt="Qr Code" src="./icon.png" style="width:60%;">
|
||||||
|
<div class="card-body text-center p-0 mt-3 mb-3 mb-lg-0">
|
||||||
|
<h6 class="card-text m-0" style="font-size: 16px;color:#011D45; font-weight: 600">
|
||||||
|
Qr Code</h6>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="oe_row oe_spaced" style="display:flex;margin-top:1rem;">
|
||||||
|
<div style="width:750px;margin: auto;">
|
||||||
|
<img src="./images/service_banner.png" style="width:100%">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="oe_row oe_spaced" style="display:flex;margin-top:4rem;">
|
||||||
|
<div style="width:800px;height:140px;margin: auto;">
|
||||||
|
<img src="./images/bg_image.png" width="100%;">
|
||||||
|
<div style="margin-left: 2rem;margin-top:-110px;display: flex;">
|
||||||
|
<div>
|
||||||
|
<h5 style="color:white;">Free Support</h5>
|
||||||
|
<p style="color:white;">We will provide free support for any issues,queries and bug fixing<br/>upto
|
||||||
|
90 days from the date of purchase of this application.</p>
|
||||||
|
</div>
|
||||||
|
<div style="margin-left: auto;margin-right: 2rem;margin-top: 2rem;">
|
||||||
|
<div style="background-color:white;color:#011D45;border-radius:5px;padding: 4px;width:205px;text-align:center;"><a
|
||||||
|
href="https://hrms.aspiresoftserv.com/contactus" style="text-decoration:none;color:#011D45;"
|
||||||
|
target="_blank">Request Free Support</a></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="oe_row oe_spaced" style="display:flex;margin-top:-35px;">
|
||||||
|
<div style="width:600px;display:flex;margin: auto;">
|
||||||
|
<div style="display: flex;"><i class="fa fa-envelope" style="margin-right:0.4rem;margin-top:0.2rem;color:#D81A58;"></i><a href="mailto:sales@aspiresoftserv.com" style="text-decoration:none;color:#D81A58;">sales@aspiresoftserv.com</a></div>
|
||||||
|
<div style="display: flex;margin: 0 3rem;"><i class="fa fa-skype" style="margin-right:0.4rem;color:#D81A58;margin-top:0.2rem;"></i><a href="skype:aspire.software?chat" style="text-decoration:none;color:#D81A58;">aspire.software</a></div>
|
||||||
|
<div style="display: flex;"><i class="fa fa-phone" style="margin-right:0.4rem;color:#D81A58;margin-top:0.2rem;"></i><span style="text-decoration:none;color:#D81A58;">+916351895006</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 196 KiB |
|
|
@ -0,0 +1,56 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<record id="view_extend_equipment_tree" model="ir.ui.view">
|
||||||
|
<field name="name">maintenance.equipment.tree.inherit</field>
|
||||||
|
<field name="model">maintenance.equipment</field>
|
||||||
|
<field name="priority" eval="16"/>
|
||||||
|
<field name="inherit_id" ref="maintenance.hr_equipment_view_tree"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//list" position="inside">
|
||||||
|
<header>
|
||||||
|
<button string="Print QR-code" type="object" name="action_print_qrcode_layout"/>
|
||||||
|
</header>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_extend_equipment_form" model="ir.ui.view">
|
||||||
|
<field name="name">maintenance.equipment.form.inherit</field>
|
||||||
|
<field name="model">maintenance.equipment</field>
|
||||||
|
<field name="priority" eval="16"/>
|
||||||
|
<field name="inherit_id" ref="maintenance.hr_equipment_view_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//page[@name='product_information']" position="inside">
|
||||||
|
<group>
|
||||||
|
<field name="comp_serial_no"/>
|
||||||
|
</group>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//sheet" position="before">
|
||||||
|
<header>
|
||||||
|
<button string="Generate Serial Number" type="object" name="generate_serial_no" class="oe_highlight"/>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="generate_serial_no_action" model="ir.actions.server">
|
||||||
|
<field name="name">Generate Serial Number</field>
|
||||||
|
<field name="model_id" ref="model_maintenance_equipment"/>
|
||||||
|
<field name="binding_model_id" ref="model_maintenance_equipment"/>
|
||||||
|
<field name="binding_view_types">list</field>
|
||||||
|
<field name="state">code</field>
|
||||||
|
<field name="code">action = records.generate_serial_no()</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="generate_qrcode_no_action" model="ir.actions.server">
|
||||||
|
<field name="name">Print QR-Code</field>
|
||||||
|
<field name="model_id" ref="model_maintenance_equipment"/>
|
||||||
|
<field name="binding_model_id" ref="model_maintenance_equipment"/>
|
||||||
|
<field name="binding_view_types">form</field>
|
||||||
|
<field name="state">code</field>
|
||||||
|
<field name="code">action = records.action_print_qrcode_layout()</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
3
aspl_equipment_qrcode_generator/wizard/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from . import equipment_label_layout
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from odoo import _, api, fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class EquipmentLabelLayout(models.TransientModel):
|
||||||
|
_name = 'equipment.label.layout'
|
||||||
|
_description = 'Choose the sheet layout to print the labels'
|
||||||
|
|
||||||
|
print_format = fields.Selection([
|
||||||
|
('2x5', '2 x 5'),
|
||||||
|
('2x7', '2 x 7'),
|
||||||
|
('4x7', '4 x 7')], string="Format", default='2x5', required=True)
|
||||||
|
equipment_ids = fields.Many2many('maintenance.equipment')
|
||||||
|
rows = fields.Integer(compute='_compute_dimensions')
|
||||||
|
columns = fields.Integer(compute='_compute_dimensions')
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def default_get(self, fields_list):
|
||||||
|
"""Override to properly set equipment_ids from context"""
|
||||||
|
res = super().default_get(fields_list)
|
||||||
|
if self.env.context.get('active_ids'):
|
||||||
|
res['equipment_ids'] = [(6, 0, self.env.context.get('active_ids', []))]
|
||||||
|
elif self.env.context.get('default_equipment_ids'):
|
||||||
|
res['equipment_ids'] = [(6, 0, self.env.context.get('default_equipment_ids', []))]
|
||||||
|
return res
|
||||||
|
|
||||||
|
@api.depends('print_format')
|
||||||
|
def _compute_dimensions(self):
|
||||||
|
for wizard in self:
|
||||||
|
if 'x' in wizard.print_format:
|
||||||
|
columns, rows = wizard.print_format.split('x')[:2]
|
||||||
|
wizard.columns = int(columns)
|
||||||
|
wizard.rows = int(rows)
|
||||||
|
else:
|
||||||
|
wizard.columns, wizard.rows = 1, 1
|
||||||
|
|
||||||
|
|
||||||
|
def process_label(self):
|
||||||
|
# Generiere QR-Codes für alle Equipment VOR dem Report
|
||||||
|
self._generate_qr_codes()
|
||||||
|
|
||||||
|
xml_id = 'aspl_equipment_qrcode_generator.report_equipment_label'
|
||||||
|
data = {
|
||||||
|
'equipment_label_layout_id': self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
# report_action benötigt die Equipment IDs als docids
|
||||||
|
return self.env.ref(xml_id).report_action(self.equipment_ids.ids, data=data)
|
||||||
|
|
||||||
|
def _generate_qr_codes(self):
|
||||||
|
"""Generiert QR-Codes für alle ausgewählten Equipment"""
|
||||||
|
import base64
|
||||||
|
from io import BytesIO
|
||||||
|
import qrcode
|
||||||
|
|
||||||
|
# Hole die base_url für die Equipment-Links
|
||||||
|
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
||||||
|
menu_id = self.env.ref('maintenance.menu_equipment_form').sudo().id
|
||||||
|
action_id = self.env.ref('maintenance.hr_equipment_action').sudo().id
|
||||||
|
|
||||||
|
for equipment in self.equipment_ids:
|
||||||
|
# Extrahiere Namen aus JSONB falls nötig
|
||||||
|
equipment_name = equipment.name
|
||||||
|
if isinstance(equipment_name, dict):
|
||||||
|
equipment_name = equipment_name.get('de_DE') or equipment_name.get('en_US') or str(equipment_name)
|
||||||
|
|
||||||
|
category_name = equipment.category_id.name if equipment.category_id else ""
|
||||||
|
if isinstance(category_name, dict):
|
||||||
|
category_name = category_name.get('de_DE') or category_name.get('en_US') or str(category_name)
|
||||||
|
|
||||||
|
# Erstelle Equipment-Link
|
||||||
|
equipment_link = f"{base_url}/web#id={equipment.id}&menu_id={menu_id}&action={action_id}&model=maintenance.equipment&view_type=form"
|
||||||
|
|
||||||
|
# Erstelle Equipment-Details für QR-Code
|
||||||
|
qr_data = f"Name: {equipment_name}\n"
|
||||||
|
qr_data += f"Model: {equipment.model or ''}\n"
|
||||||
|
qr_data += f"Mfg serial no: {equipment.serial_no or ''}\n"
|
||||||
|
qr_data += f"Warranty Exp. Date: {equipment.warranty_date or ''}\n"
|
||||||
|
qr_data += f"Category: {category_name}\n\n"
|
||||||
|
qr_data += equipment_link
|
||||||
|
|
||||||
|
# Generiere QR-Code
|
||||||
|
qr = qrcode.QRCode(
|
||||||
|
version=1,
|
||||||
|
error_correction=qrcode.constants.ERROR_CORRECT_L,
|
||||||
|
box_size=20,
|
||||||
|
border=4,
|
||||||
|
)
|
||||||
|
qr.add_data(qr_data)
|
||||||
|
qr.make(fit=True)
|
||||||
|
img = qr.make_image()
|
||||||
|
temp = BytesIO()
|
||||||
|
img.save(temp, format="PNG")
|
||||||
|
qr_image = base64.b64encode(temp.getvalue())
|
||||||
|
|
||||||
|
# Speichere QR-Code im Equipment
|
||||||
|
equipment.write({'qr_code': qr_image})
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="equipment_label_layout_form" model="ir.ui.view">
|
||||||
|
<field name="name">equipment.label.layout.form</field>
|
||||||
|
<field name="model">equipment.label.layout</field>
|
||||||
|
<field name="mode">primary</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form>
|
||||||
|
<group>
|
||||||
|
<field name="equipment_ids" invisible="1"/>
|
||||||
|
<group>
|
||||||
|
<field name="print_format" widget="radio"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<footer>
|
||||||
|
<button name="process_label" string="Confirm" type="object" class="btn-primary"/>
|
||||||
|
<button string="Discard" class="btn-secondary" special="cancel"/>
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_open_label_layout_equipment" model="ir.actions.act_window">
|
||||||
|
<field name="name">Choose Labels Layout</field>
|
||||||
|
<field name="res_model">equipment.label.layout</field>
|
||||||
|
<field name="view_ids"
|
||||||
|
eval="[(5, 0, 0),
|
||||||
|
(0, 0, {'view_mode': 'form', 'view_id': ref('equipment_label_layout_form')})]" />
|
||||||
|
<field name="target">new</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
|
|
@ -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
|
||||||
11
open_workshop.code-workspace
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": "."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../../usr/lib/python3/dist-packages/odoo"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {}
|
||||||
|
}
|
||||||
48
open_workshop_base/README.md
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
# Open Workshop (open_workshop ows)
|
||||||
|
|
||||||
|
Dieses Odoo v18.0 Modul erweitert das POS- und Kontakt-Modul um Funktionen für offene Werkstätten (FabLabs, Makerspaces etc.) und dient der Verwaltung von Maschinen, Naschinen Einweisungen Produkten, Maschinen Nutzungsprodukten und Zugangsberechtigungen zu den Maschinen.
|
||||||
|
|
||||||
|
## Funktionen
|
||||||
|
|
||||||
|
### Erweiterungen an Kontakten (res.partner)
|
||||||
|
- Geburtstagsfeld, RFID-Karte, Haftungsausschluss usw. ausgelagert nach `ows.user`
|
||||||
|
- Automatische Erstellung des `ows.user`-Eintrags beim Anlegen eines Kontakts
|
||||||
|
- Übersichtliche Darstellung aller Maschinenfreigaben im Odoo Kontaktformular
|
||||||
|
|
||||||
|
### Maschinen und Bereiche
|
||||||
|
- Modell `ows.machine` mit Gruppierung nach Bereichen (`ows.machine.area`)
|
||||||
|
- Farblich kodierte Bereiche (Hex-Wert aus Datenbank) welche zur Darstellung im POS verwendet werden
|
||||||
|
|
||||||
|
### Einweisungen und Nutzungen
|
||||||
|
- Modelle `ows.machine.training` und `ows.machine.product`
|
||||||
|
- Konfigurierbare Produkte für Einweisung/Nutzung direkt im Backend
|
||||||
|
- Zuweisung von Nutzungsprodukten zu Maschinen
|
||||||
|
- Zuweisung von Einweisungsprodukten zu Maschinen
|
||||||
|
|
||||||
|
### Maschinenfreigaben
|
||||||
|
- Modell `ows.machine.access` verknüpft Partner und Maschine
|
||||||
|
- Darstellung im POS als tabellarische Übersicht mit Anzeige für eine bestehende Einweisung / Nutzungsberechtigung
|
||||||
|
- Anzeige im POS-Kundendetailsansicht innerhalb der Kundendetailsansicht
|
||||||
|
- Anzeige im Odoo Kontak Modul der Maschineneinweisungen
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Dieses Modul in den Custom-Addons-Ordner kopieren
|
||||||
|
2. Im Odoo Backend unter Apps installieren
|
||||||
|
|
||||||
|
## Entwicklerhinweise
|
||||||
|
|
||||||
|
## ToDos
|
||||||
|
- Bearbeitung der Maschinenfreigaben im Backend
|
||||||
|
- Automatische Erstellung von `mail.message` bei manueller Freigabe
|
||||||
|
- Integration von Fristen (z.B. Ablaufdatum Einweisung)
|
||||||
|
|
||||||
|
## Autoren
|
||||||
|
- Matthias Lotz
|
||||||
|
|
||||||
|
## Lizenz
|
||||||
|
AGPL-3.0 oder später
|
||||||
|
|
||||||
|
---
|
||||||
|
Letzte Aktualisierung: 06.04.2025
|
||||||
|
|
||||||
56
open_workshop_base/__init__.py
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
from . import models
|
||||||
|
from . import controllers
|
||||||
|
|
||||||
|
|
||||||
|
def post_init_hook(env):
|
||||||
|
"""
|
||||||
|
Migration Hook: open_workshop → open_workshop_base
|
||||||
|
Wird automatisch nach der Installation/Update des Moduls ausgeführt
|
||||||
|
"""
|
||||||
|
cr = env.cr
|
||||||
|
|
||||||
|
# Prüfen ob altes Modul 'open_workshop' in DB existiert
|
||||||
|
cr.execute("""
|
||||||
|
SELECT id FROM ir_module_module
|
||||||
|
WHERE name = 'open_workshop' AND id != (
|
||||||
|
SELECT id FROM ir_module_module WHERE name = 'open_workshop_base'
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
if cr.fetchone():
|
||||||
|
import logging
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
_logger.info("=" * 70)
|
||||||
|
_logger.info("MIGRATION: Renaming module 'open_workshop' to 'open_workshop_base'")
|
||||||
|
_logger.info("=" * 70)
|
||||||
|
|
||||||
|
# Modulnamen aktualisieren
|
||||||
|
cr.execute("""
|
||||||
|
UPDATE ir_module_module
|
||||||
|
SET name = 'open_workshop_base'
|
||||||
|
WHERE name = 'open_workshop'
|
||||||
|
""")
|
||||||
|
_logger.info(f"✓ Updated ir_module_module: {cr.rowcount} row(s)")
|
||||||
|
|
||||||
|
# ir_model_data Referenzen aktualisieren
|
||||||
|
cr.execute("""
|
||||||
|
UPDATE ir_model_data
|
||||||
|
SET module = 'open_workshop_base'
|
||||||
|
WHERE module = 'open_workshop'
|
||||||
|
""")
|
||||||
|
_logger.info(f"✓ Updated ir_model_data: {cr.rowcount} row(s)")
|
||||||
|
|
||||||
|
# Abhängigkeiten aktualisieren
|
||||||
|
cr.execute("""
|
||||||
|
UPDATE ir_module_module_dependency
|
||||||
|
SET name = 'open_workshop_base'
|
||||||
|
WHERE name = 'open_workshop'
|
||||||
|
""")
|
||||||
|
if cr.rowcount > 0:
|
||||||
|
_logger.info(f"✓ Updated dependencies: {cr.rowcount} row(s)")
|
||||||
|
|
||||||
|
_logger.info("=" * 70)
|
||||||
|
_logger.info("MIGRATION COMPLETED")
|
||||||
|
_logger.info("=" * 70)
|
||||||
|
|
||||||
44
open_workshop_base/__manifest__.py
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
{
|
||||||
|
'name': 'Open Workshop Base',
|
||||||
|
'license': 'AGPL-3',
|
||||||
|
'version': '18.0.1.0.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', 'maintenance_equipment_status'],
|
||||||
|
'author': 'matthias.lotz',
|
||||||
|
'category': 'Manufacturing',
|
||||||
|
|
||||||
|
'data': [
|
||||||
|
'security/ir.model.access.csv',
|
||||||
|
'data/maintenance_equipment_status_data.xml',
|
||||||
|
'views/machine_product_training_views.xml',
|
||||||
|
'views/machine_area_views.xml',
|
||||||
|
'views/machine_views.xml',
|
||||||
|
'views/maintenance_equipment_views.xml',
|
||||||
|
'views/res_partner_view.xml',
|
||||||
|
'views/menu_views.xml',
|
||||||
|
'data/data.xml',
|
||||||
|
],
|
||||||
|
'installable': True,
|
||||||
|
'assets': {
|
||||||
|
'web.assets_backend': [
|
||||||
|
'open_workshop_base/static/src/css/category_color.css',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'description': """
|
||||||
|
Open Workshop Base - Kernmodul
|
||||||
|
================================
|
||||||
|
|
||||||
|
Dieses Modul stellt die Basis-Funktionalität für Open Workshop bereit:
|
||||||
|
|
||||||
|
* Maschinenmodelle (ows.machine) → nutzt maintenance.equipment als Single Source of Truth
|
||||||
|
* Bereiche (ows.machine.area)
|
||||||
|
* Einweisungslogik und Freigaben
|
||||||
|
* Produktverknüpfungen für Einweisungen
|
||||||
|
* Backend-UI (Form, Tree, Kanban)
|
||||||
|
* Automatische Migration bestehender Maschinen zu maintenance.equipment
|
||||||
|
|
||||||
|
Für POS-Integration installiere: open_workshop_pos
|
||||||
|
Für API-Zugriff installiere: open_workshop_api (erforderlich für WordPress Frontend)
|
||||||
|
""",
|
||||||
|
'post_init_hook': 'post_init_hook',
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
# Datei: controllers/pos_access.py
|
|
||||||
|
|
||||||
from odoo import http
|
from odoo import http
|
||||||
from odoo.http import request
|
from odoo.http import request
|
||||||
|
|
||||||
class OpenWorkshopPOSController(http.Controller):
|
class OpenWorkshopPOSController(http.Controller):
|
||||||
|
|
||||||
@http.route('/open_workshop/partner_access', type='json', auth='user')
|
@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()
|
Machine = request.env['ows.machine'].sudo()
|
||||||
return Machine.get_access_list_grouped(partner_id)
|
return Machine.get_access_list_grouped(partner_id)
|
||||||
|
|
||||||
|
|
@ -23,130 +23,130 @@
|
||||||
<!-- Maschinen im Fablab -->
|
<!-- Maschinen im Fablab -->
|
||||||
<record id="machine_sabako_laser" model="ows.machine">
|
<record id="machine_sabako_laser" model="ows.machine">
|
||||||
<field name="name">Sabako Laser</field>
|
<field name="name">Sabako Laser</field>
|
||||||
<field name="code">sabako_laser</field>
|
<field name="serial_no">sabako_laser</field>
|
||||||
<field name="area_id" ref="area_fablab"/>
|
<field name="area_id" ref="area_fablab"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="machine_prusa" model="ows.machine">
|
<record id="machine_prusa" model="ows.machine">
|
||||||
<field name="name">Prusa</field>
|
<field name="name">Prusa</field>
|
||||||
<field name="code">prusa</field>
|
<field name="serial_no">prusa</field>
|
||||||
<field name="area_id" ref="area_fablab"/>
|
<field name="area_id" ref="area_fablab"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="machine_prusa_mmu" model="ows.machine">
|
<record id="machine_prusa_mmu" model="ows.machine">
|
||||||
<field name="name">Prusa MMU</field>
|
<field name="name">Prusa MMU</field>
|
||||||
<field name="code">prusa_mmu</field>
|
<field name="serial_no">prusa_mmu</field>
|
||||||
<field name="area_id" ref="area_fablab"/>
|
<field name="area_id" ref="area_fablab"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="machine_3d_delta" model="ows.machine">
|
<record id="machine_3d_delta" model="ows.machine">
|
||||||
<field name="name">3D Delta</field>
|
<field name="name">3D Delta</field>
|
||||||
<field name="code">3d_delta</field>
|
<field name="serial_no">3d_delta</field>
|
||||||
<field name="area_id" ref="area_fablab"/>
|
<field name="area_id" ref="area_fablab"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="machine_cnc_beamicon" model="ows.machine">
|
<record id="machine_cnc_beamicon" model="ows.machine">
|
||||||
<field name="name">CNC Beamicon</field>
|
<field name="name">CNC Beamicon</field>
|
||||||
<field name="code">cnc_beamicon</field>
|
<field name="serial_no">cnc_beamicon</field>
|
||||||
<field name="area_id" ref="area_fablab"/>
|
<field name="area_id" ref="area_fablab"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<!-- Maschinen im Holzbereich -->
|
<!-- Maschinen im Holzbereich -->
|
||||||
<record id="machine_formatkreissaege" model="ows.machine">
|
<record id="machine_formatkreissaege" model="ows.machine">
|
||||||
<field name="name">Formatkreissäge</field>
|
<field name="name">Formatkreissäge</field>
|
||||||
<field name="code">formatkreissaege</field>
|
<field name="serial_no">formatkreissaege</field>
|
||||||
<field name="area_id" ref="area_holz"/>
|
<field name="area_id" ref="area_holz"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="machine_bandsaege_holz" model="ows.machine">
|
<record id="machine_bandsaege_holz" model="ows.machine">
|
||||||
<field name="name">Bandsäge</field>
|
<field name="name">Bandsäge</field>
|
||||||
<field name="code">bandsaege_holz</field>
|
<field name="serial_no">bandsaege_holz</field>
|
||||||
<field name="area_id" ref="area_holz"/>
|
<field name="area_id" ref="area_holz"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="machine_abrichte" model="ows.machine">
|
<record id="machine_abrichte" model="ows.machine">
|
||||||
<field name="name">Abricht Dickenhobel</field>
|
<field name="name">Abricht Dickenhobel</field>
|
||||||
<field name="code">dickenhobel</field>
|
<field name="serial_no">dickenhobel</field>
|
||||||
<field name="area_id" ref="area_holz"/>
|
<field name="area_id" ref="area_holz"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="machine_drechselbank" model="ows.machine">
|
<record id="machine_drechselbank" model="ows.machine">
|
||||||
<field name="name">Drechselbank</field>
|
<field name="name">Drechselbank</field>
|
||||||
<field name="code">drechselbank</field>
|
<field name="serial_no">drechselbank</field>
|
||||||
<field name="area_id" ref="area_holz"/>
|
<field name="area_id" ref="area_holz"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="machine_festool_domino" model="ows.machine">
|
<record id="machine_festool_domino" model="ows.machine">
|
||||||
<field name="name">Festool Domino Fräse</field>
|
<field name="name">Festool Domino Fräse</field>
|
||||||
<field name="code">festool_domino</field>
|
<field name="serial_no">festool_domino</field>
|
||||||
<field name="area_id" ref="area_holz"/>
|
<field name="area_id" ref="area_holz"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="machine_maffel_duo" model="ows.machine">
|
<record id="machine_maffel_duo" model="ows.machine">
|
||||||
<field name="name">Maffel Duo Dübler</field>
|
<field name="name">Maffel Duo Dübler</field>
|
||||||
<field name="code">maffel_duo</field>
|
<field name="serial_no">maffel_duo</field>
|
||||||
<field name="area_id" ref="area_holz"/>
|
<field name="area_id" ref="area_holz"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="machine_lamello" model="ows.machine">
|
<record id="machine_lamello" model="ows.machine">
|
||||||
<field name="name">Lamello Zeta P2</field>
|
<field name="name">Lamello Zeta P2</field>
|
||||||
<field name="code">lamello_zeta_p2</field>
|
<field name="serial_no">lamello_zeta_p2</field>
|
||||||
<field name="area_id" ref="area_holz"/>
|
<field name="area_id" ref="area_holz"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<!-- Maschinen im Metallbereich -->
|
<!-- Maschinen im Metallbereich -->
|
||||||
<record id="machine_kreissaege_metall" model="ows.machine">
|
<record id="machine_kreissaege_metall" model="ows.machine">
|
||||||
<field name="name">Kreissäge</field>
|
<field name="name">Kreissäge</field>
|
||||||
<field name="code">kreissaege_metall</field>
|
<field name="serial_no">kreissaege_metall</field>
|
||||||
<field name="area_id" ref="area_metall"/>
|
<field name="area_id" ref="area_metall"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="machine_bandsaege_metall" model="ows.machine">
|
<record id="machine_bandsaege_metall" model="ows.machine">
|
||||||
<field name="name">Bandsäge</field>
|
<field name="name">Bandsäge</field>
|
||||||
<field name="code">bandsaege_metall</field>
|
<field name="serial_no">bandsaege_metall</field>
|
||||||
<field name="area_id" ref="area_metall"/>
|
<field name="area_id" ref="area_metall"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="machine_mig_mag" model="ows.machine">
|
<record id="machine_mig_mag" model="ows.machine">
|
||||||
<field name="name">MIG/MAG Schweißgeräte</field>
|
<field name="name">MIG/MAG Schweißgeräte</field>
|
||||||
<field name="code">mig_mag</field>
|
<field name="serial_no">mig_mag</field>
|
||||||
<field name="area_id" ref="area_metall"/>
|
<field name="area_id" ref="area_metall"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="machine_wig" model="ows.machine">
|
<record id="machine_wig" model="ows.machine">
|
||||||
<field name="name">WIG Schweißgerät</field>
|
<field name="name">WIG Schweißgerät</field>
|
||||||
<field name="code">wig</field>
|
<field name="serial_no">wig</field>
|
||||||
<field name="area_id" ref="area_metall"/>
|
<field name="area_id" ref="area_metall"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="machine_schweissen" model="ows.machine">
|
<record id="machine_schweissen" model="ows.machine">
|
||||||
<field name="name">Schweißen allgemein</field>
|
<field name="name">Schweißen allgemein</field>
|
||||||
<field name="code">schweissen_allgemein</field>
|
<field name="serial_no">schweissen_allgemein</field>
|
||||||
<field name="area_id" ref="area_metall"/>
|
<field name="area_id" ref="area_metall"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="machine_drehbank" model="ows.machine">
|
<record id="machine_drehbank" model="ows.machine">
|
||||||
<field name="name">Drehbank</field>
|
<field name="name">Drehbank</field>
|
||||||
<field name="code">drehbank</field>
|
<field name="serial_no">drehbank</field>
|
||||||
<field name="area_id" ref="area_metall"/>
|
<field name="area_id" ref="area_metall"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="machine_fraese" model="ows.machine">
|
<record id="machine_fraese" model="ows.machine">
|
||||||
<field name="name">Fräse</field>
|
<field name="name">Fräse</field>
|
||||||
<field name="code">fraese</field>
|
<field name="serial_no">fraese</field>
|
||||||
<field name="area_id" ref="area_metall"/>
|
<field name="area_id" ref="area_metall"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="machine_abkantbank" model="ows.machine">
|
<record id="machine_abkantbank" model="ows.machine">
|
||||||
<field name="name">Abkantbank</field>
|
<field name="name">Abkantbank</field>
|
||||||
<field name="code">abkantbank</field>
|
<field name="serial_no">abkantbank</field>
|
||||||
<field name="area_id" ref="area_metall"/>
|
<field name="area_id" ref="area_metall"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<!-- Maschine im Elektronikbereich -->
|
<!-- Maschine im Elektronikbereich -->
|
||||||
<record id="machine_loetkolben" model="ows.machine">
|
<record id="machine_loetkolben" model="ows.machine">
|
||||||
<field name="name">Lötkolben</field>
|
<field name="name">Lötkolben</field>
|
||||||
<field name="code">loetkolben</field>
|
<field name="serial_no">loetkolben</field>
|
||||||
<field name="area_id" ref="area_elektronik"/>
|
<field name="area_id" ref="area_elektronik"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo noupdate="1">
|
||||||
|
<!-- Equipment Status für Open Workshop -->
|
||||||
|
|
||||||
|
<record id="equipment_status_inbetrieb" model="maintenance.equipment.status">
|
||||||
|
<field name="name">In Betrieb</field>
|
||||||
|
<field name="sequence" eval="10"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="equipment_status_defekt" model="maintenance.equipment.status">
|
||||||
|
<field name="name">Defekt</field>
|
||||||
|
<field name="sequence" eval="20"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="equipment_status_wartung" model="maintenance.equipment.status">
|
||||||
|
<field name="name">Wartung</field>
|
||||||
|
<field name="sequence" eval="30"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="equipment_status_ausgemustert" model="maintenance.equipment.status">
|
||||||
|
<field name="name">Ausgemustert</field>
|
||||||
|
<field name="sequence" eval="40"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
160
open_workshop_base/migrations/18.0.1.0.4/post-migration.py
Normal file
|
|
@ -0,0 +1,160 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import logging
|
||||||
|
from datetime import date
|
||||||
|
from odoo import SUPERUSER_ID, api
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def migrate(cr, version):
|
||||||
|
"""
|
||||||
|
Post-Migration: Migriere bestehende ows.machine Daten zu maintenance.equipment
|
||||||
|
Area wird in equipment.location gespeichert (nicht mehr in category!)
|
||||||
|
"""
|
||||||
|
_logger.info("=== Post-Migration: Daten-Migration zu equipment ===")
|
||||||
|
|
||||||
|
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||||
|
|
||||||
|
# 1. Zähle zu migrierende Maschinen
|
||||||
|
cr.execute("SELECT COUNT(*) FROM ows_machine WHERE equipment_id IS NULL")
|
||||||
|
machines_to_migrate = cr.fetchone()[0]
|
||||||
|
_logger.info(f"Found {machines_to_migrate} machines to migrate")
|
||||||
|
|
||||||
|
if machines_to_migrate == 0:
|
||||||
|
_logger.info("No machines to migrate - skipping")
|
||||||
|
return
|
||||||
|
|
||||||
|
_logger.info("Creating maintenance.equipment entries...")
|
||||||
|
|
||||||
|
# 2. Migriere alle Maschinen die noch kein equipment haben
|
||||||
|
cr.execute("""
|
||||||
|
SELECT
|
||||||
|
id, name, area_id,
|
||||||
|
active, create_uid, write_uid, create_date, write_date
|
||||||
|
FROM ows_machine
|
||||||
|
WHERE equipment_id IS NULL
|
||||||
|
""")
|
||||||
|
|
||||||
|
machines = cr.fetchall()
|
||||||
|
migrated_count = 0
|
||||||
|
|
||||||
|
# Default Company ermitteln
|
||||||
|
cr.execute("SELECT id FROM res_company ORDER BY id LIMIT 1")
|
||||||
|
default_company_id = cr.fetchone()[0] if cr.rowcount > 0 else None
|
||||||
|
|
||||||
|
for machine in machines:
|
||||||
|
(m_id, m_name_jsonb, m_area_id,
|
||||||
|
m_active, m_create_uid, m_write_uid, m_create_date, m_write_date) = machine
|
||||||
|
|
||||||
|
# Name aus JSONB extrahieren (de_DE oder en_US)
|
||||||
|
m_name = None
|
||||||
|
if m_name_jsonb:
|
||||||
|
m_name = m_name_jsonb.get('de_DE') or m_name_jsonb.get('en_US')
|
||||||
|
|
||||||
|
# Serial number generieren
|
||||||
|
m_serial_no = f"OWS-{m_id}"
|
||||||
|
|
||||||
|
# Location aus Area-Name ermitteln
|
||||||
|
location = None
|
||||||
|
if m_area_id:
|
||||||
|
# Hole Area-Name (JSONB!) und nutze als location
|
||||||
|
cr.execute("SELECT name FROM ows_machine_area WHERE id = %s", (m_area_id,))
|
||||||
|
area_result = cr.fetchone()
|
||||||
|
if area_result and area_result[0]:
|
||||||
|
area_name_jsonb = area_result[0]
|
||||||
|
if area_name_jsonb:
|
||||||
|
location = area_name_jsonb.get('de_DE') or area_name_jsonb.get('en_US')
|
||||||
|
|
||||||
|
# Equipment erstellen (name als JSONB!)
|
||||||
|
# Fallback: Wenn m_name leer, nutze serial_no
|
||||||
|
display_name = m_name if m_name and m_name.strip() else m_serial_no
|
||||||
|
|
||||||
|
# Verwende SQL direkt für korrektes JSONB handling
|
||||||
|
try:
|
||||||
|
cr.execute("""
|
||||||
|
INSERT INTO maintenance_equipment
|
||||||
|
(name, serial_no, location, cost, effective_date, equipment_assign_to,
|
||||||
|
company_id, active, create_uid, write_uid, create_date, write_date)
|
||||||
|
VALUES
|
||||||
|
(jsonb_build_object('de_DE', %s, 'en_US', %s), %s, %s, %s, %s, 'other',
|
||||||
|
%s, %s, %s, %s, %s, %s)
|
||||||
|
RETURNING id
|
||||||
|
""", (display_name, display_name, m_serial_no,
|
||||||
|
location, 0.0, date.today(),
|
||||||
|
default_company_id,
|
||||||
|
m_active if m_active is not None else True,
|
||||||
|
m_create_uid, m_write_uid, m_create_date, m_write_date))
|
||||||
|
|
||||||
|
equipment_id = cr.fetchone()[0]
|
||||||
|
|
||||||
|
# equipment_id in ows_machine setzen
|
||||||
|
cr.execute("""
|
||||||
|
UPDATE ows_machine
|
||||||
|
SET equipment_id = %s
|
||||||
|
WHERE id = %s
|
||||||
|
""", (equipment_id, m_id))
|
||||||
|
|
||||||
|
migrated_count += 1
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
_logger.error(f"Failed to migrate machine {m_id}: {e}")
|
||||||
|
|
||||||
|
# 3. Synchronisiere related fields von ows.machine → maintenance.equipment
|
||||||
|
# Nötig weil related fields mit store=True nicht automatisch bei SQL-INSERT synchronisiert werden
|
||||||
|
_logger.info("Synchronizing related fields (area_id, category) to maintenance.equipment...")
|
||||||
|
|
||||||
|
cr.execute("""
|
||||||
|
UPDATE maintenance_equipment me
|
||||||
|
SET ows_area_id = om.area_id
|
||||||
|
FROM ows_machine om
|
||||||
|
WHERE om.equipment_id = me.id
|
||||||
|
AND om.area_id IS NOT NULL
|
||||||
|
""")
|
||||||
|
synced_areas = cr.rowcount
|
||||||
|
_logger.info(f"✅ Synchronized {synced_areas} area_id values")
|
||||||
|
|
||||||
|
cr.execute("""
|
||||||
|
UPDATE maintenance_equipment me
|
||||||
|
SET ows_category = om.category
|
||||||
|
FROM ows_machine om
|
||||||
|
WHERE om.equipment_id = me.id
|
||||||
|
AND om.category IS NOT NULL
|
||||||
|
""")
|
||||||
|
synced_categories = cr.rowcount
|
||||||
|
_logger.info(f"✅ Synchronized {synced_categories} category values")
|
||||||
|
|
||||||
|
# 4. Setze Default-Status "In Betrieb" für migrierte Equipment ohne Status
|
||||||
|
# (Status-Records werden automatisch über XML-Datei erstellt)
|
||||||
|
_logger.info("Setting default status for equipment without status...")
|
||||||
|
|
||||||
|
# Prüfe ob Status "In Betrieb" existiert
|
||||||
|
cr.execute("SELECT id FROM maintenance_equipment_status WHERE name = 'In Betrieb' LIMIT 1")
|
||||||
|
status_result = cr.fetchone()
|
||||||
|
|
||||||
|
if status_result:
|
||||||
|
status_id = status_result[0]
|
||||||
|
cr.execute("""
|
||||||
|
UPDATE maintenance_equipment me
|
||||||
|
SET status_id = %s
|
||||||
|
WHERE me.status_id IS NULL
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1 FROM ows_machine om
|
||||||
|
WHERE om.equipment_id = me.id
|
||||||
|
)
|
||||||
|
""", (status_id,))
|
||||||
|
status_set = cr.rowcount
|
||||||
|
_logger.info(f"✅ Set default status for {status_set} equipment records")
|
||||||
|
else:
|
||||||
|
_logger.warning("⚠️ Status 'In Betrieb' not found - skipping status assignment")
|
||||||
|
|
||||||
|
# 5. Validierung
|
||||||
|
cr.execute("SELECT COUNT(*) FROM ows_machine WHERE equipment_id IS NULL")
|
||||||
|
remaining = cr.fetchone()[0]
|
||||||
|
|
||||||
|
if remaining > 0:
|
||||||
|
_logger.warning(f"⚠️ {remaining} machines could not be migrated!")
|
||||||
|
else:
|
||||||
|
_logger.info(f"✅ Successfully migrated {migrated_count} machines to equipment")
|
||||||
|
|
||||||
|
_logger.info("Post-migration completed")
|
||||||
|
|
||||||
59
open_workshop_base/migrations/18.0.1.0.4/pre-migration.py
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import logging
|
||||||
|
from odoo import SUPERUSER_ID, api
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def migrate(cr, version):
|
||||||
|
"""
|
||||||
|
Pre-Migration: Bereite equipment_id Spalte vor
|
||||||
|
Installiert maintenance-Modul falls nötig
|
||||||
|
"""
|
||||||
|
_logger.info("=== Pre-Migration: ows.machine → maintenance.equipment ===")
|
||||||
|
|
||||||
|
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||||
|
|
||||||
|
# 1. Prüfe ob maintenance installiert ist, falls nicht: installieren
|
||||||
|
cr.execute("""
|
||||||
|
SELECT state FROM ir_module_module
|
||||||
|
WHERE name = 'maintenance'
|
||||||
|
""")
|
||||||
|
result = cr.fetchone()
|
||||||
|
|
||||||
|
if not result or result[0] != 'installed':
|
||||||
|
_logger.info("Installing maintenance module...")
|
||||||
|
maintenance_module = env['ir.module.module'].search([('name', '=', 'maintenance')])
|
||||||
|
if maintenance_module:
|
||||||
|
maintenance_module.button_immediate_install()
|
||||||
|
_logger.info("✅ Maintenance module installed")
|
||||||
|
else:
|
||||||
|
raise Exception("Maintenance module not found in addons_path!")
|
||||||
|
else:
|
||||||
|
_logger.info("✓ Maintenance module already installed")
|
||||||
|
|
||||||
|
# 2. Spalte equipment_id hinzufügen (falls nicht vorhanden)
|
||||||
|
cr.execute("""
|
||||||
|
SELECT column_name
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'ows_machine' AND column_name = 'equipment_id'
|
||||||
|
""")
|
||||||
|
|
||||||
|
if not cr.fetchone():
|
||||||
|
_logger.info("Adding equipment_id column to ows_machine")
|
||||||
|
cr.execute("""
|
||||||
|
ALTER TABLE ows_machine
|
||||||
|
ADD COLUMN equipment_id INTEGER;
|
||||||
|
""")
|
||||||
|
cr.execute("""
|
||||||
|
ALTER TABLE ows_machine
|
||||||
|
ADD CONSTRAINT ows_machine_equipment_id_fkey
|
||||||
|
FOREIGN KEY (equipment_id)
|
||||||
|
REFERENCES maintenance_equipment(id)
|
||||||
|
ON DELETE CASCADE;
|
||||||
|
""")
|
||||||
|
_logger.info("✅ equipment_id column added")
|
||||||
|
else:
|
||||||
|
_logger.info("✓ equipment_id column already exists")
|
||||||
|
|
||||||
|
_logger.info("Pre-migration completed")
|
||||||
105
open_workshop_base/migrations/migrate_open_workshop_to_base.sql
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
-- ============================================================================
|
||||||
|
-- Pre-Deployment Migration Script: open_workshop → open_workshop_base
|
||||||
|
-- ============================================================================
|
||||||
|
-- Dieses SQL-Skript MUSS vor dem Odoo-Update in der Gitea Action ausgeführt werden
|
||||||
|
--
|
||||||
|
-- Verwendung in Gitea Action:
|
||||||
|
-- docker exec <db-container> psql -U odoo -d <dbname> < migrate_open_workshop_to_base.sql
|
||||||
|
--
|
||||||
|
-- Oder als Deployment-Hook vor "odoo -u open_workshop_base"
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
\echo '=========================================='
|
||||||
|
\echo 'Migration: open_workshop → open_workshop_base'
|
||||||
|
\echo '=========================================='
|
||||||
|
|
||||||
|
-- Prüfen ob Migration nötig ist
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
old_module_exists BOOLEAN;
|
||||||
|
new_module_exists BOOLEAN;
|
||||||
|
BEGIN
|
||||||
|
-- Prüfe ob altes Modul existiert
|
||||||
|
SELECT EXISTS(
|
||||||
|
SELECT 1 FROM ir_module_module WHERE name = 'open_workshop'
|
||||||
|
) INTO old_module_exists;
|
||||||
|
|
||||||
|
-- Prüfe ob neues Modul bereits existiert
|
||||||
|
SELECT EXISTS(
|
||||||
|
SELECT 1 FROM ir_module_module WHERE name = 'open_workshop_base'
|
||||||
|
) INTO new_module_exists;
|
||||||
|
|
||||||
|
IF old_module_exists THEN
|
||||||
|
RAISE NOTICE '✓ Found module "open_workshop" - starting migration';
|
||||||
|
|
||||||
|
IF new_module_exists THEN
|
||||||
|
RAISE NOTICE '⚠ Module "open_workshop_base" already exists - cleaning up';
|
||||||
|
-- Aufräumen falls Testinstallation vorhanden
|
||||||
|
DELETE FROM ir_model_data WHERE module = 'base' AND name = 'module_open_workshop_base';
|
||||||
|
DELETE FROM ir_module_module WHERE name = 'open_workshop_base';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- ===== MIGRATION DURCHFÜHREN =====
|
||||||
|
|
||||||
|
-- 1. Modulnamen und Version aktualisieren
|
||||||
|
UPDATE ir_module_module
|
||||||
|
SET name = 'open_workshop_base',
|
||||||
|
latest_version = '18.0.1.0.3' -- Auf 18.0.1.0.3 setzen, damit 18.0.1.0.4 Migration läuft
|
||||||
|
WHERE name = 'open_workshop';
|
||||||
|
|
||||||
|
RAISE NOTICE '✓ Updated ir_module_module: % row(s)', (SELECT 1);
|
||||||
|
|
||||||
|
-- 2. Alle ir_model_data Referenzen aktualisieren
|
||||||
|
UPDATE ir_model_data
|
||||||
|
SET module = 'open_workshop_base'
|
||||||
|
WHERE module = 'open_workshop';
|
||||||
|
|
||||||
|
RAISE NOTICE '✓ Updated ir_model_data: % row(s)', (
|
||||||
|
SELECT COUNT(*) FROM ir_model_data WHERE module = 'open_workshop_base'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 3. Alten ir_model_data Eintrag für module_open_workshop löschen
|
||||||
|
DELETE FROM ir_model_data
|
||||||
|
WHERE module = 'base' AND name = 'module_open_workshop';
|
||||||
|
|
||||||
|
RAISE NOTICE '✓ Cleaned up old module_open_workshop entry';
|
||||||
|
|
||||||
|
-- 4. Abhängigkeiten in anderen Modulen aktualisieren (falls vorhanden)
|
||||||
|
UPDATE ir_module_module_dependency
|
||||||
|
SET name = 'open_workshop_base'
|
||||||
|
WHERE name = 'open_workshop';
|
||||||
|
|
||||||
|
IF FOUND THEN
|
||||||
|
RAISE NOTICE '✓ Updated dependencies in other modules';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RAISE NOTICE '==========================================';
|
||||||
|
RAISE NOTICE '✓ MIGRATION COMPLETED SUCCESSFULLY';
|
||||||
|
RAISE NOTICE '==========================================';
|
||||||
|
RAISE NOTICE 'Next step: Run "odoo -u open_workshop_base -d <dbname>"';
|
||||||
|
|
||||||
|
ELSE
|
||||||
|
RAISE NOTICE 'ℹ Module "open_workshop" not found - migration not needed';
|
||||||
|
RAISE NOTICE 'This is normal for new installations';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
EXCEPTION
|
||||||
|
WHEN OTHERS THEN
|
||||||
|
RAISE EXCEPTION 'Migration failed: %', SQLERRM;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- Prüfung: Zeige aktuellen Status
|
||||||
|
\echo ''
|
||||||
|
\echo 'Current module status:'
|
||||||
|
SELECT name, state, latest_version
|
||||||
|
FROM ir_module_module
|
||||||
|
WHERE name LIKE 'open_workshop%'
|
||||||
|
ORDER BY name;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
\echo 'Model data count:'
|
||||||
|
SELECT module, COUNT(*) as entries
|
||||||
|
FROM ir_model_data
|
||||||
|
WHERE module LIKE 'open_workshop%'
|
||||||
|
GROUP BY module;
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
from . import ows_models
|
from . import ows_models
|
||||||
from . import pos_order
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -15,22 +15,31 @@ class HREmployee(models.Model):
|
||||||
def anonymize_for_testsystem(self):
|
def anonymize_for_testsystem(self):
|
||||||
"""Benennt Admin-Angestellten um und archiviert alle anderen für das Testsystem."""
|
"""Benennt Admin-Angestellten um und archiviert alle anderen für das Testsystem."""
|
||||||
admin_user = self.env['res.users'].search([('name', '=', 'Administrator')], limit=1)
|
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})")
|
_logger.info(f"[OWS] Administrator-Benutzer gefunden: {admin_user.name} (ID: {admin_user.id})")
|
||||||
admin_employee = self.search([('user_id', '=', admin_user.id)], limit=1)
|
|
||||||
|
# Suche auch archivierte Employees
|
||||||
|
admin_employee = self.with_context(active_test=False).search([('user_id', '=', admin_user.id)], limit=1)
|
||||||
|
|
||||||
if admin_employee:
|
if admin_employee:
|
||||||
|
# Administrator-Employee reaktivieren und umbenennen
|
||||||
admin_employee.write({
|
admin_employee.write({
|
||||||
'name': 'TESTSYSTEM',
|
'name': 'TESTSYSTEM',
|
||||||
'job_title': 'Testumgebung',
|
'job_title': 'Testumgebung',
|
||||||
'work_email': False,
|
'work_email': 'office@hobbyhimmel.de',
|
||||||
'work_phone': False,
|
'work_phone': False,
|
||||||
|
'active': True, # Reaktivieren falls archiviert
|
||||||
})
|
})
|
||||||
_logger.info("[OWS] Admin-Angestellter wurde umbenannt.")
|
_logger.info(f"[OWS] Admin-Angestellter reaktiviert und umbenannt: {admin_employee.name} (ID: {admin_employee.id})")
|
||||||
else:
|
else:
|
||||||
_logger.warning("[OWS] Kein Angestellter für user_admin gefunden.")
|
_logger.warning("[OWS] Kein Angestellter für Administrator gefunden.")
|
||||||
|
return
|
||||||
|
|
||||||
# Alle anderen Angestellten archivieren
|
# Alle anderen Angestellten archivieren (auch bereits archivierte berücksichtigen)
|
||||||
other_employees = self.search([('id', '!=', admin_employee.id)])
|
other_employees = self.with_context(active_test=False).search([('id', '!=', admin_employee.id)])
|
||||||
other_employees.write({'active': False})
|
other_employees.write({'active': False})
|
||||||
_logger.info("[OWS] %d Angestellte archiviert.", len(other_employees))
|
_logger.info("[OWS] %d Angestellte archiviert.", len(other_employees))
|
||||||
|
|
||||||
|
|
@ -168,24 +177,21 @@ class ResPartner(models.Model):
|
||||||
def _compute_machine_access_html(self):
|
def _compute_machine_access_html(self):
|
||||||
areas = self.env['ows.machine.area'].search([], order="name")
|
areas = self.env['ows.machine.area'].search([], order="name")
|
||||||
for partner in self:
|
for partner in self:
|
||||||
html = "<div class='tab-content'><div class='tab-pane active' id='machine_access_tab'>"
|
html = ""
|
||||||
html += "<div class='o_group'>"
|
|
||||||
|
|
||||||
for area in areas:
|
for area in areas:
|
||||||
html += "<table class='o_group o_inner_group o_group_col_6'><tbody>"
|
|
||||||
|
|
||||||
# Bereichsüberschrift
|
|
||||||
html += f"""
|
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>
|
<tr>
|
||||||
<td colspan="3" style="width: 100%;">
|
<th>Maschine</th>
|
||||||
<div class="o_horizontal_separator">{area.name}</div>
|
<th>Status</th>
|
||||||
</td>
|
<th>Datum</th>
|
||||||
</tr>
|
<th>Gültig bis</th>
|
||||||
<tr>
|
|
||||||
<td class="o_td_label"><label class="o_form_label"></label></td>
|
|
||||||
<td class="o_td_label"><label class="o_form_label">Datum</label></td>
|
|
||||||
<td class="o_td_label"><label class="o_form_label">Gültig bis</label></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
machines = self.env['ows.machine'].search([('area_id', '=', area.id)], order="name")
|
machines = self.env['ows.machine'].search([('area_id', '=', area.id)], order="name")
|
||||||
|
|
@ -195,25 +201,26 @@ class ResPartner(models.Model):
|
||||||
('partner_id', '=', partner.id),
|
('partner_id', '=', partner.id),
|
||||||
('machine_id', '=', machine.id),
|
('machine_id', '=', machine.id),
|
||||||
], limit=1)
|
], limit=1)
|
||||||
icon = "<span class='fa fa-check text-success'></span>" if access else "<span class='fa fa-times text-danger'></span>"
|
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_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 "-"
|
date_expiry = access.date_expiry.strftime('%Y-%m-%d') if access and access.date_expiry else "-"
|
||||||
|
|
||||||
html += f"""
|
html += f"""
|
||||||
<tr>
|
<tr>
|
||||||
<td class="o_td_label"><label class="o_form_label">{icon} {machine.name}</label></td>
|
<td>{machine.name}</td>
|
||||||
<td class="o_td_field">{date_granted}</td>
|
<td>{icon}</td>
|
||||||
<td class="o_td_field">{date_expiry}</td>
|
<td>{date_granted}</td>
|
||||||
|
<td>{date_expiry}</td>
|
||||||
</tr>
|
</tr>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
html += "</tbody></table>"
|
html += "</tbody></table></div>"
|
||||||
|
|
||||||
html += "</div></div></div>"
|
|
||||||
partner.machine_access_html = html
|
partner.machine_access_html = html
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def migrate_existing_partners(self):
|
def migrate_existing_partners(self):
|
||||||
"""
|
"""
|
||||||
|
|
@ -378,6 +385,7 @@ class OwsUser(models.Model):
|
||||||
('partner_unique', 'unique(partner_id)', 'Jeder Partner darf nur einen OWS-Datensatz haben.')
|
('partner_unique', 'unique(partner_id)', 'Jeder Partner darf nur einen OWS-Datensatz haben.')
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
AVAILABLE_COLORS = [
|
AVAILABLE_COLORS = [
|
||||||
('#000000', 'schwarz'),
|
('#000000', 'schwarz'),
|
||||||
('#ff0000', 'Rot'),
|
('#ff0000', 'Rot'),
|
||||||
|
|
@ -389,9 +397,83 @@ AVAILABLE_COLORS = [
|
||||||
('#ffff00', 'Gelb'),
|
('#ffff00', 'Gelb'),
|
||||||
('#FF9800', 'Orange'),
|
('#FF9800', 'Orange'),
|
||||||
('#795548', 'Braun'),
|
('#795548', 'Braun'),
|
||||||
('#1f1f1f', 'Grau'),
|
('#ffffff', 'Weiss'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
class MaintenanceEquipment(models.Model):
|
||||||
|
"""Erweitere maintenance.equipment mit OWS-Feldern für direkte Bearbeitung"""
|
||||||
|
_inherit = 'maintenance.equipment'
|
||||||
|
|
||||||
|
# Inverse Relation zu ows.machine
|
||||||
|
ows_machine_id = fields.One2many('ows.machine', 'equipment_id', string='OWS Maschine', readonly=True)
|
||||||
|
|
||||||
|
# OWS-Felder direkt auf Equipment (via related für einfache Bearbeitung)
|
||||||
|
# store=True für Performance beim Filtern/Suchen
|
||||||
|
ows_category = fields.Selection(
|
||||||
|
related='ows_machine_id.category',
|
||||||
|
string='Sicherheitskategorie',
|
||||||
|
readonly=False,
|
||||||
|
store=True
|
||||||
|
)
|
||||||
|
ows_category_icon = fields.Char(
|
||||||
|
related='ows_machine_id.category_icon',
|
||||||
|
string='⚙'
|
||||||
|
)
|
||||||
|
ows_area_id = fields.Many2one(
|
||||||
|
'ows.machine.area',
|
||||||
|
related='ows_machine_id.area_id',
|
||||||
|
string='Bereich',
|
||||||
|
readonly=False,
|
||||||
|
store=True
|
||||||
|
)
|
||||||
|
ows_product_ids = fields.One2many(
|
||||||
|
'ows.machine.product',
|
||||||
|
related='ows_machine_id.product_ids',
|
||||||
|
string='Nutzungsprodukte',
|
||||||
|
readonly=False
|
||||||
|
)
|
||||||
|
ows_training_ids = fields.One2many(
|
||||||
|
'ows.machine.training',
|
||||||
|
related='ows_machine_id.training_ids',
|
||||||
|
string='Einweisungsprodukte',
|
||||||
|
readonly=False
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.model_create_multi
|
||||||
|
def create(self, vals_list):
|
||||||
|
"""Erstelle automatisch ows.machine für jedes neue Equipment (außer wenn vom ows.machine.create aufgerufen)"""
|
||||||
|
records = super(MaintenanceEquipment, self).create(vals_list)
|
||||||
|
records._create_missing_ows_machines()
|
||||||
|
return records
|
||||||
|
|
||||||
|
def _create_missing_ows_machines(self):
|
||||||
|
"""Erstelle fehlende ows.machine Datensätze für Equipment ohne ows.machine"""
|
||||||
|
if self.env.context.get('skip_ows_machine_creation'):
|
||||||
|
return
|
||||||
|
|
||||||
|
for record in self:
|
||||||
|
if not record.ows_machine_id:
|
||||||
|
self.env['ows.machine'].with_context(skip_ows_machine_creation=True).create({
|
||||||
|
'equipment_id': record.id,
|
||||||
|
'category': record.ows_category or 'red',
|
||||||
|
'area_id': record.ows_area_id.id if record.ows_area_id else False,
|
||||||
|
})
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def load(self, fields, data):
|
||||||
|
"""Override load() um sicherzustellen, dass ows.machine nach dem Import erstellt wird"""
|
||||||
|
result = super(MaintenanceEquipment, self).load(fields, data)
|
||||||
|
|
||||||
|
# Nach erfolgreichem Import: Erstelle fehlende ows.machine Einträge
|
||||||
|
if result.get('ids'):
|
||||||
|
equipment_records = self.browse(result['ids'])
|
||||||
|
equipment_records._create_missing_ows_machines()
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class OwsMachineArea(models.Model):
|
class OwsMachineArea(models.Model):
|
||||||
_name = 'ows.machine.area'
|
_name = 'ows.machine.area'
|
||||||
_table = 'ows_machine_area'
|
_table = 'ows_machine_area'
|
||||||
|
|
@ -429,13 +511,41 @@ class OwsMachineArea(models.Model):
|
||||||
for rec in self:
|
for rec in self:
|
||||||
rec.color_name = label_dict.get(rec.color_hex, 'Unbekannt')
|
rec.color_name = label_dict.get(rec.color_hex, 'Unbekannt')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class OwsMachine(models.Model):
|
class OwsMachine(models.Model):
|
||||||
|
"""
|
||||||
|
Open Workshop Maschine - nutzt maintenance.equipment als Single Source of Truth.
|
||||||
|
|
||||||
|
Delegation Pattern (_inherits):
|
||||||
|
- Beim Erstellen wird automatisch ein maintenance.equipment erstellt
|
||||||
|
- Felder wie name, serial_no, cost etc. werden direkt von equipment übernommen
|
||||||
|
- Keine Datenduplizierung!
|
||||||
|
"""
|
||||||
_name = 'ows.machine'
|
_name = 'ows.machine'
|
||||||
_table = 'ows_machine'
|
_table = 'ows_machine'
|
||||||
_description = 'OWS: Maschine'
|
_description = 'OWS: Maschine'
|
||||||
|
_inherits = {'maintenance.equipment': 'equipment_id'}
|
||||||
|
|
||||||
name = fields.Char(required=True, translate=True)
|
# PFLICHT: Verknüpfung zu maintenance.equipment (Single Source of Truth)
|
||||||
code = fields.Char(required=True, help="Eindeutiger Kurzcode, z.B. 'lasercutter'")
|
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([
|
category = fields.Selection([
|
||||||
('green', 'Kategorie 1: grün'),
|
('green', 'Kategorie 1: grün'),
|
||||||
('yellow', 'Kategorie 2: gelb'),
|
('yellow', 'Kategorie 2: gelb'),
|
||||||
|
|
@ -457,16 +567,11 @@ class OwsMachine(models.Model):
|
||||||
}
|
}
|
||||||
rec.category_icon = icon_map.get(rec.category, '⚪')
|
rec.category_icon = icon_map.get(rec.category, '⚪')
|
||||||
|
|
||||||
description = fields.Text(string="Gerätebeschreibung", translate=True, help="Beschreibung der Maschine oder des Geräts.")
|
area_id = fields.Many2one('ows.machine.area', string='Bereich', help="Bereich, in dem die Maschine oder das Gerät steht.")
|
||||||
active = fields.Boolean(string="Aktive", default=True, help="Ist die Maschine oder das Gerät aktiv? Inaktive Maschinen werden nicht mehr in der POS-Ansicht angezeigt.")
|
product_ids = fields.One2many('ows.machine.product', 'machine_id', string="Nutzungsprodukte")
|
||||||
area_id = fields.Many2one('ows.machine.area', string='Bereich', help="Bereich in der Werkstatt, in dem die Maschine oder das Gerät steht.")
|
|
||||||
product_ids = fields.One2many('ows.machine.product', 'machine_id', string="Nutzungsprodukte", help="Dies ist das zugehörige Produkt, falls die Maschine oder das Geräte eine zeitliche Nutzungsgebühr hat.")
|
|
||||||
product_names = fields.Char(string="Liste der Nutzungsprodukte", 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_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,)
|
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')
|
@api.depends('product_ids.product_id.name')
|
||||||
def _compute_product_using_names(self):
|
def _compute_product_using_names(self):
|
||||||
|
|
@ -480,12 +585,67 @@ class OwsMachine(models.Model):
|
||||||
names = machine.training_ids.mapped('training_id.name')
|
names = machine.training_ids.mapped('training_id.name')
|
||||||
machine.training_names = ", ".join(names)
|
machine.training_names = ", ".join(names)
|
||||||
|
|
||||||
_sql_constraints = [
|
# Keine eigenen SQL Constraints - Equipment hat bereits unique constraint für serial_no
|
||||||
('code_unique', 'unique(code)', 'Maschinencode muss eindeutig sein.')
|
|
||||||
]
|
@api.model_create_multi
|
||||||
|
def create(self, vals_list):
|
||||||
|
"""
|
||||||
|
Beim Erstellen einer ows.machine:
|
||||||
|
1. Equipment nur erstellen wenn equipment_id NICHT angegeben wurde
|
||||||
|
2. Area → Location synchronisieren
|
||||||
|
3. serial_no und name vom User übernehmen
|
||||||
|
"""
|
||||||
|
# Equipment nur erstellen wenn noch nicht vorhanden
|
||||||
|
for vals in vals_list:
|
||||||
|
# Wenn equipment_id bereits gesetzt ist (z.B. bei Import), KEIN neues Equipment erstellen!
|
||||||
|
if 'equipment_id' not in vals:
|
||||||
|
equipment_vals = {
|
||||||
|
'name': vals.get('name', 'Neue Maschine'),
|
||||||
|
'serial_no': vals.get('serial_no', False),
|
||||||
|
}
|
||||||
|
|
||||||
|
equipment = self.env['maintenance.equipment'].with_context(skip_ows_machine_creation=True).create(equipment_vals)
|
||||||
|
vals['equipment_id'] = equipment.id
|
||||||
|
|
||||||
|
return super(OwsMachine, self).create(vals_list)
|
||||||
|
|
||||||
|
def write(self, vals):
|
||||||
|
"""
|
||||||
|
Bei Updates:
|
||||||
|
1. Name/Serial_no → Equipment synchronisieren
|
||||||
|
"""
|
||||||
|
# Name/Serial_no an Equipment weiterleiten
|
||||||
|
equipment_vals = {}
|
||||||
|
if 'name' in vals:
|
||||||
|
equipment_vals['name'] = vals['name']
|
||||||
|
if 'serial_no' in vals:
|
||||||
|
equipment_vals['serial_no'] = vals['serial_no']
|
||||||
|
|
||||||
|
if equipment_vals and self.equipment_id:
|
||||||
|
self.equipment_id.write(equipment_vals)
|
||||||
|
|
||||||
|
return super(OwsMachine, self).write(vals)
|
||||||
|
|
||||||
def name_get(self):
|
def name_get(self):
|
||||||
return [(rec.id, f"{rec.name} ({rec.code})") for rec in self]
|
result = []
|
||||||
|
for rec in self:
|
||||||
|
name = rec.name or 'Unbenannt'
|
||||||
|
if rec.serial_no: # Kommt direkt von equipment via _inherits
|
||||||
|
name = f"[{rec.serial_no}] {name}"
|
||||||
|
result.append((rec.id, name))
|
||||||
|
return result
|
||||||
|
|
||||||
|
def action_open_equipment(self):
|
||||||
|
"""Smart Button: Öffnet die Equipment-Detailansicht"""
|
||||||
|
self.ensure_one()
|
||||||
|
return {
|
||||||
|
'type': 'ir.actions.act_window',
|
||||||
|
'name': 'Equipment Details',
|
||||||
|
'res_model': 'maintenance.equipment',
|
||||||
|
'res_id': self.equipment_id.id,
|
||||||
|
'view_mode': 'form',
|
||||||
|
'target': 'current',
|
||||||
|
}
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def get_access_list_grouped(self, partner_id):
|
def get_access_list_grouped(self, partner_id):
|
||||||
|
|
@ -513,7 +673,7 @@ class OwsMachine(models.Model):
|
||||||
"""
|
"""
|
||||||
partner = self.env['res.partner'].browse(partner_id)
|
partner = self.env['res.partner'].browse(partner_id)
|
||||||
areas = self.env['ows.machine.area'].search([], order="name")
|
areas = self.env['ows.machine.area'].search([], order="name")
|
||||||
|
_logger.info("Access RPC called with partner_id=%s", partner_id)
|
||||||
access_by_area = []
|
access_by_area = []
|
||||||
for area in areas:
|
for area in areas:
|
||||||
machines = self.search([('area_id', '=', area.id), ('category', '=', 'red')], order="name")
|
machines = self.search([('area_id', '=', area.id), ('category', '=', 'red')], order="name")
|
||||||
|
|
@ -544,6 +704,7 @@ class OwsMachine(models.Model):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class OwsMachineAccess(models.Model):
|
class OwsMachineAccess(models.Model):
|
||||||
_name = 'ows.machine.access'
|
_name = 'ows.machine.access'
|
||||||
_table = 'ows_machine_access'
|
_table = 'ows_machine_access'
|
||||||
|
|
@ -564,7 +725,7 @@ class OwsMachineAccess(models.Model):
|
||||||
class OwsMachineProduct(models.Model):
|
class OwsMachineProduct(models.Model):
|
||||||
_name = 'ows.machine.product'
|
_name = 'ows.machine.product'
|
||||||
_table = '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')
|
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')
|
machine_id = fields.Many2one('ows.machine', required=True, ondelete='cascade')
|
||||||
|
|
@ -572,7 +733,7 @@ class OwsMachineProduct(models.Model):
|
||||||
class OwsMachineTraining(models.Model):
|
class OwsMachineTraining(models.Model):
|
||||||
_name = 'ows.machine.training'
|
_name = 'ows.machine.training'
|
||||||
_table = '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')
|
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')
|
machine_id = fields.Many2one('ows.machine', required=True, ondelete='cascade')
|
||||||
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>
|
||||||
|
|
@ -4,21 +4,19 @@
|
||||||
<record id="action_machine_area_list" model="ir.actions.act_window">
|
<record id="action_machine_area_list" model="ir.actions.act_window">
|
||||||
<field name="name">Maschinenbereiche</field>
|
<field name="name">Maschinenbereiche</field>
|
||||||
<field name="res_model">ows.machine.area</field>
|
<field name="res_model">ows.machine.area</field>
|
||||||
<field name="view_mode">tree,form</field>
|
<field name="view_mode">list,form</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<!-- Menüpunkt unter Maschinen > Konfiguration -->
|
|
||||||
<menuitem id="menu_machine_area" name="Bereiche" parent="menu_machine_config" action="open_workshop.action_machine_area_list" sequence="30"/>
|
|
||||||
|
|
||||||
<!-- Listenansicht -->
|
<!-- Listenansicht -->
|
||||||
<record id="view_machine_area_tree" model="ir.ui.view">
|
<record id="view_machine_area_tree" model="ir.ui.view">
|
||||||
<field name="name">ows.machine.area.tree</field>
|
<field name="name">ows.machine.area.tree</field>
|
||||||
<field name="model">ows.machine.area</field>
|
<field name="model">ows.machine.area</field>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<tree>
|
<list>
|
||||||
<field name="name"/>
|
<field name="name"/>
|
||||||
<field name="color_hex" widget="color_picker"/>
|
<field name="color_hex_value" string="Farbe (Hex)"/>
|
||||||
</tree>
|
<field name="color_name" string="Farbname"/>
|
||||||
|
</list>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
|
@ -30,7 +28,8 @@
|
||||||
<form string="Maschinenbereich">
|
<form string="Maschinenbereich">
|
||||||
<group>
|
<group>
|
||||||
<field name="name"/>
|
<field name="name"/>
|
||||||
<field name="color_hex" widget="color_picker"/>
|
<field name="color_hex"/>
|
||||||
|
<field name="color_name" readonly="1"/>
|
||||||
</group>
|
</group>
|
||||||
</form>
|
</form>
|
||||||
</field>
|
</field>
|
||||||
|
|
@ -1,25 +1,47 @@
|
||||||
<odoo>
|
<odoo>
|
||||||
<!-- Tree View: Nutzungsprodukte -->
|
<!-- Tree View: Nutzungsprodukte (normale Ansicht) -->
|
||||||
<record id="view_machine_product_tree" model="ir.ui.view">
|
<record id="view_machine_product_tree" model="ir.ui.view">
|
||||||
<field name="name">ows.machine.product.tree</field>
|
<field name="name">ows.machine.product.tree</field>
|
||||||
<field name="model">ows.machine.product</field>
|
<field name="model">ows.machine.product</field>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<tree editable="bottom">
|
<list editable="bottom">
|
||||||
<field name="machine_id"/>
|
<field name="machine_id"/>
|
||||||
<field name="product_id" domain="[('categ_id.name', '=', 'Maschinennutzung')]"/>
|
<field name="product_id" domain="[('categ_id.name', '=', 'Maschinennutzung')]"/>
|
||||||
</tree>
|
</list>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<!-- Tree View: Einweisungsprodukte -->
|
<!-- Tree View: Nutzungsprodukte (vereinfacht für Equipment) -->
|
||||||
|
<record id="view_ows_machine_product_tree_simple" model="ir.ui.view">
|
||||||
|
<field name="name">ows.machine.product.tree.simple</field>
|
||||||
|
<field name="model">ows.machine.product</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list editable="bottom">
|
||||||
|
<field name="product_id" domain="[('categ_id.name', '=', 'Maschinennutzung')]"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Tree View: Einweisungsprodukte (normale Ansicht) -->
|
||||||
<record id="view_machine_training_tree" model="ir.ui.view">
|
<record id="view_machine_training_tree" model="ir.ui.view">
|
||||||
<field name="name">ows.machine.training.tree</field>
|
<field name="name">ows.machine.training.tree</field>
|
||||||
<field name="model">ows.machine.training</field>
|
<field name="model">ows.machine.training</field>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<tree editable="bottom">
|
<list editable="bottom">
|
||||||
<field name="machine_id"/>
|
<field name="machine_id"/>
|
||||||
<field name="training_id" domain="[('categ_id.name', '=', 'Einweisungen')]"/>
|
<field name="training_id" domain="[('categ_id.name', 'in', ['Einweisungen', 'Kurse'])]"/>
|
||||||
</tree>
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Tree View: Einweisungsprodukte (vereinfacht für Equipment) -->
|
||||||
|
<record id="view_ows_machine_training_tree_simple" model="ir.ui.view">
|
||||||
|
<field name="name">ows.machine.training.tree.simple</field>
|
||||||
|
<field name="model">ows.machine.training</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list editable="bottom">
|
||||||
|
<field name="training_id" domain="[('categ_id.name', 'in', ['Einweisungen', 'Kurse'])]"/>
|
||||||
|
</list>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
|
@ -27,7 +49,7 @@
|
||||||
<record id="action_machine_product" model="ir.actions.act_window">
|
<record id="action_machine_product" model="ir.actions.act_window">
|
||||||
<field name="name">Maschinen-Nutzungsprodukte</field>
|
<field name="name">Maschinen-Nutzungsprodukte</field>
|
||||||
<field name="res_model">ows.machine.product</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="view_id" ref="view_machine_product_tree"/>
|
||||||
<field name="help" type="html">
|
<field name="help" type="html">
|
||||||
<p>Verwalte die Zuordnung von Maschinen zu Nutzungsprodukten.</p>
|
<p>Verwalte die Zuordnung von Maschinen zu Nutzungsprodukten.</p>
|
||||||
|
|
@ -38,7 +60,7 @@
|
||||||
<record id="action_machine_training" model="ir.actions.act_window">
|
<record id="action_machine_training" model="ir.actions.act_window">
|
||||||
<field name="name">Maschinen-Einweisungsprodukte</field>
|
<field name="name">Maschinen-Einweisungsprodukte</field>
|
||||||
<field name="res_model">ows.machine.training</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="view_id" ref="view_machine_training_tree"/>
|
||||||
<field name="help" type="html">
|
<field name="help" type="html">
|
||||||
<p>Verwalte die Zuordnung von Maschinen zu Einweisungsprodukten.</p>
|
<p>Verwalte die Zuordnung von Maschinen zu Einweisungsprodukten.</p>
|
||||||
105
open_workshop_base/views/machine_views.xml
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
<!-- machine_views.xml -->
|
||||||
|
<odoo>
|
||||||
|
<!-- Maschinen Such- und Filteransicht -->
|
||||||
|
<record id="view_machine_search" model="ir.ui.view">
|
||||||
|
<field name="name">ows.machine.search</field>
|
||||||
|
<field name="model">ows.machine</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<search string="Ausrüstung suchen">
|
||||||
|
<!-- OWS Felder -->
|
||||||
|
<field name="name" string="Name"/>
|
||||||
|
<field name="serial_no" string="Code"/>
|
||||||
|
<field name="area_id" string="Bereich"/>
|
||||||
|
<!-- maintenance.equipment Felder (via _inherits) -->
|
||||||
|
<field name="model" string="Modell"/>
|
||||||
|
<field name="partner_id" string="Hersteller"/>
|
||||||
|
<field name="location" string="Standort"/>
|
||||||
|
<field name="company_id" string="Unternehmen"/>
|
||||||
|
<separator/>
|
||||||
|
<filter string="Kategorie grün" name="filter_green" domain="[('category', '=', 'green')]"/>
|
||||||
|
<filter string="Kategorie gelb" name="filter_yellow" domain="[('category', '=', 'yellow')]"/>
|
||||||
|
<filter string="Kategorie rot" name="filter_red" domain="[('category', '=', 'red')]"/>
|
||||||
|
<separator/>
|
||||||
|
<filter string="Aktiv" name="filter_active" domain="[('active', '=', True)]"/>
|
||||||
|
<filter string="Archiviert" name="filter_inactive" domain="[('active', '=', False)]"/>
|
||||||
|
<separator/>
|
||||||
|
<group expand="1" string="Gruppieren nach">
|
||||||
|
<filter string="Bereich" name="group_area" context="{'group_by': 'area_id'}"/>
|
||||||
|
<filter string="Kategorie" name="group_category" context="{'group_by': 'category'}"/>
|
||||||
|
|
||||||
|
</group>
|
||||||
|
</search>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Maschinen Listenansicht -->
|
||||||
|
<record id="view_machine_tree" model="ir.ui.view">
|
||||||
|
<field name="name">ows.machine.tree</field>
|
||||||
|
<field name="model">ows.machine</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list sample="1" multi_edit="1">
|
||||||
|
<field name="category_icon" string="⚙" readonly="1"/>
|
||||||
|
<field name="name" optional="show"/>
|
||||||
|
<!--field name="serial_no" string="Code" optional="show"/-->
|
||||||
|
<field name="category" optional="show"/>
|
||||||
|
<field name="area_id" widget="many2one" optional="show"/>
|
||||||
|
<field name="location" optional="hide"/>
|
||||||
|
<field name="product_names" optional="show"/>
|
||||||
|
<field name="training_names" optional="show"/>
|
||||||
|
<field name="active" optional="show"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Maschinen Formularansicht -->
|
||||||
|
<record id="view_machine_form" model="ir.ui.view">
|
||||||
|
<field name="name">ows.machine.form</field>
|
||||||
|
<field name="model">ows.machine</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Maschine">
|
||||||
|
<sheet>
|
||||||
|
<div class="oe_button_box" name="button_box">
|
||||||
|
<button name="action_open_equipment" type="object" class="oe_stat_button" icon="fa-wrench">
|
||||||
|
<div class="o_field_widget o_stat_info">
|
||||||
|
<span class="o_stat_text">Equipment</span>
|
||||||
|
<span class="o_stat_text">Details</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<group>
|
||||||
|
<group string="Maschine" name="ows_group">
|
||||||
|
<field name="equipment_id" invisible="1"/>
|
||||||
|
<field name="name" placeholder="z.B. Sabako Laser"/>
|
||||||
|
<field name="serial_no" string="Code/Seriennummer" placeholder="z.B. sabako_laser"/>
|
||||||
|
<field name="category_icon" string="⚙" readonly="1"/>
|
||||||
|
<field name="category"/>
|
||||||
|
<field name="area_id" required="1"/>
|
||||||
|
</group>
|
||||||
|
<group name="info_group">
|
||||||
|
<field name="location" readonly="1" string="Standort (von Bereich)"/>
|
||||||
|
<field name="active"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<!-- Notebook für Produkte und Einweisungen -->
|
||||||
|
<notebook>
|
||||||
|
<page string="Nutzungsprodukte">
|
||||||
|
<field name="product_ids" context="{'default_machine_id': id}">
|
||||||
|
<list editable="bottom">
|
||||||
|
<field name="product_id" domain="[('categ_id.name', '=', 'Maschinennutzung')]" />
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
<page string="Einweisungsprodukte">
|
||||||
|
<field name="training_ids" context="{'default_machine_id': id}">
|
||||||
|
<list editable="bottom">
|
||||||
|
<field name="training_id" domain="[('categ_id.name', 'in', ['Einweisungen', 'Kurse'])]" />
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
80
open_workshop_base/views/maintenance_equipment_views.xml
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<!-- Erweitere Equipment Form mit OWS-Feldern -->
|
||||||
|
<record id="view_equipment_form_ows_extension" model="ir.ui.view">
|
||||||
|
<field name="name">maintenance.equipment.form.ows</field>
|
||||||
|
<field name="model">maintenance.equipment</field>
|
||||||
|
<field name="inherit_id" ref="maintenance.hr_equipment_view_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<!-- Füge OWS-Felder in die Details-Gruppe ein -->
|
||||||
|
<xpath expr="//field[@name='location']" position="after">
|
||||||
|
<field name="ows_area_id" string="Bereich"/>
|
||||||
|
<field name="ows_category" string="Sicherheitskategorie"/>
|
||||||
|
</xpath>
|
||||||
|
|
||||||
|
<!-- Füge Open Workshop Notebook-Page hinzu -->
|
||||||
|
<xpath expr="//notebook" position="inside">
|
||||||
|
<page string="Offene Werkstatt (Hobbyhimmel)" name="open_workshop" invisible="not ows_machine_id">
|
||||||
|
<group>
|
||||||
|
<group string="Sicherheit">
|
||||||
|
<field name="ows_machine_id" invisible="1"/>
|
||||||
|
<field name="ows_category_icon" readonly="1"/>
|
||||||
|
<field name="ows_category"/>
|
||||||
|
<field name="ows_area_id"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<group string="Nutzungsprodukte">
|
||||||
|
<field name="ows_product_ids" nolabel="1" context="{'tree_view_ref': 'open_workshop_base.view_ows_machine_product_tree_simple'}"/>
|
||||||
|
</group>
|
||||||
|
<group string="Einweisungsprodukte">
|
||||||
|
<field name="ows_training_ids" nolabel="1" context="{'tree_view_ref': 'open_workshop_base.view_ows_machine_training_tree_simple'}"/>
|
||||||
|
</group>
|
||||||
|
</page>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Erweitere Equipment List-View mit OWS-Spalten -->
|
||||||
|
<record id="view_equipment_tree_ows_extension" model="ir.ui.view">
|
||||||
|
<field name="name">maintenance.equipment.tree.ows</field>
|
||||||
|
<field name="model">maintenance.equipment</field>
|
||||||
|
<field name="inherit_id" ref="maintenance.hr_equipment_view_tree"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//field[@name='name']" position="after">
|
||||||
|
<field name="ows_category_icon" string="⚙" optional="show"/>
|
||||||
|
<field name="ows_area_id" optional="show"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Erweitere Equipment Search-View mit OWS-Filtern -->
|
||||||
|
<record id="view_equipment_search_ows_extension" model="ir.ui.view">
|
||||||
|
<field name="name">maintenance.equipment.search.ows</field>
|
||||||
|
<field name="model">maintenance.equipment</field>
|
||||||
|
<field name="inherit_id" ref="maintenance.hr_equipment_view_search"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<!-- Füge OWS-Felder zur Suche hinzu -->
|
||||||
|
<xpath expr="//field[@name='name']" position="after">
|
||||||
|
<field name="ows_area_id" string="Bereich"/>
|
||||||
|
<field name="ows_category" string="Kategorie"/>
|
||||||
|
</xpath>
|
||||||
|
|
||||||
|
<!-- Füge OWS-Filter hinzu -->
|
||||||
|
<xpath expr="//filter[@name='inactive']" position="after">
|
||||||
|
<separator/>
|
||||||
|
<filter string="Kategorie 1 (grün)" name="filter_green" domain="[('ows_category', '=', 'green')]"/>
|
||||||
|
<filter string="Kategorie 2 (gelb)" name="filter_yellow" domain="[('ows_category', '=', 'yellow')]"/>
|
||||||
|
<filter string="Kategorie 3 (rot)" name="filter_red" domain="[('ows_category', '=', 'red')]"/>
|
||||||
|
</xpath>
|
||||||
|
|
||||||
|
<!-- Füge OWS-Gruppierungen hinzu (erstelle group wenn nicht vorhanden) -->
|
||||||
|
<xpath expr="//search" position="inside">
|
||||||
|
<group expand="0" string="Gruppieren nach">
|
||||||
|
<filter string="Bereich" name="group_by_area" context="{'group_by': 'ows_area_id'}"/>
|
||||||
|
<filter string="Kategorie" name="group_by_category" context="{'group_by': 'ows_category'}"/>
|
||||||
|
</group>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
|
|
||||||
76
open_workshop_base/views/menu_views.xml
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
<!-- menu_views.xml -->
|
||||||
|
<odoo>
|
||||||
|
<!-- Equipment-Liste mit OWS-Filter (ersetzt Standard Equipment Action) -->
|
||||||
|
<record id="action_equipment_ows" model="ir.actions.act_window">
|
||||||
|
<field name="name">Ausrüstung</field>
|
||||||
|
<field name="res_model">maintenance.equipment</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
<field name="domain">[('ows_machine_id', '!=', False)]</field>
|
||||||
|
<field name="context">{
|
||||||
|
'default_ows_category': 'red',
|
||||||
|
'search_default_group_by_area': 1
|
||||||
|
}</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Trainingsprodukt-Liste -->
|
||||||
|
<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">list,form</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Integration in Maintenance Menü -->
|
||||||
|
|
||||||
|
<!-- Bereiche unter Maintenance → Configuration -->
|
||||||
|
<menuitem id="menu_maintenance_ows_areas"
|
||||||
|
name="Bereiche"
|
||||||
|
parent="maintenance.menu_maintenance_config"
|
||||||
|
action="action_machine_area_list"
|
||||||
|
sequence="50"/>
|
||||||
|
|
||||||
|
<!-- Zuordnungen Container unter Maintenance → Configuration -->
|
||||||
|
<menuitem id="menu_maintenance_ows_assignments"
|
||||||
|
name="Zuordnungen"
|
||||||
|
parent="maintenance.menu_maintenance_config"
|
||||||
|
sequence="51"/>
|
||||||
|
|
||||||
|
<!-- Nutzungsprodukte unter Zuordnungen -->
|
||||||
|
<menuitem id="menu_maintenance_machine_product"
|
||||||
|
name="Nutzungsprodukte"
|
||||||
|
parent="menu_maintenance_ows_assignments"
|
||||||
|
action="action_machine_product"
|
||||||
|
sequence="10"/>
|
||||||
|
|
||||||
|
<!-- Einweisungsprodukte unter Zuordnungen -->
|
||||||
|
<menuitem id="menu_maintenance_machine_training"
|
||||||
|
name="Einweisungsprodukte"
|
||||||
|
parent="menu_maintenance_ows_assignments"
|
||||||
|
action="action_machine_training"
|
||||||
|
sequence="20"/>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- List & Form Views für training.product -->
|
||||||
|
<record id="view_training_product_tree" model="ir.ui.view">
|
||||||
|
<field name="name">ows.machine.product.tree</field>
|
||||||
|
<field name="model">ows.machine.product</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list>
|
||||||
|
<field name="product_id"/>
|
||||||
|
<field name="machine_id"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_training_product_form" model="ir.ui.view">
|
||||||
|
<field name="name">ows.machine.product.form</field>
|
||||||
|
<field name="model">ows.machine.product</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Einweisungs-Produkt">
|
||||||
|
<group>
|
||||||
|
<field name="product_id"/>
|
||||||
|
<field name="machine_id"/>
|
||||||
|
</group>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
|
|
@ -1,43 +1,59 @@
|
||||||
<odoo>
|
<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 -->
|
<!-- Zentrale View für alle drei Tabs in garantierter Reihenfolge -->
|
||||||
|
|
||||||
<record id="view_partner_form_inherit_open_workshop_tabs" model="ir.ui.view">
|
<record id="view_partner_form_inherit_open_workshop_tabs" model="ir.ui.view">
|
||||||
<field name="name">res.partner.form.ows.tabs</field>
|
<field name="name">res.partner.form.ows.tabs</field>
|
||||||
<field name="model">res.partner</field>
|
<field name="model">res.partner</field>
|
||||||
<field name="inherit_id" ref="base.view_partner_form"/>
|
<field name="inherit_id" ref="base.view_partner_form"/>
|
||||||
<field name="priority" eval="10"/>
|
<field name="priority" eval="20"/>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<xpath expr="//page[@name='sales_purchases']" position="before">
|
<xpath expr="//notebook" position="inside">
|
||||||
|
<page name="ows_machine_access" string="Offene Werkstatt (Hobbyhimmel)">
|
||||||
<!-- Tab 1: HOBBYHIMMEL Basis -->
|
<!-- EINWEISUNG: Zwei Felder nebeneinander -->
|
||||||
<page name="ows_basic" string="HOBBYHIMMEL Basis">
|
<group name="container_row_2" string="Sicherheitseinweisung" col="2">
|
||||||
<group name="container_row_2">
|
|
||||||
<group string="Sicherheit">
|
|
||||||
<field name="security_briefing"/>
|
<field name="security_briefing"/>
|
||||||
<field name="security_id"/>
|
<field name="security_id"/>
|
||||||
</group>
|
</group>
|
||||||
<group string="Zugang">
|
|
||||||
<field name="rfid_card"/>
|
|
||||||
</group>
|
|
||||||
</group>
|
|
||||||
</page>
|
|
||||||
|
|
||||||
<!-- Tab 2: HOBBYHIMMEL Einweisungen (HTML) -->
|
<!-- MASCHINENFREIGABEN: Volle Breite -->
|
||||||
<page name="ows_machine_access_html" string="HOBBYHIMMEL Einweisungen">
|
<group string="Maschinenfreigaben" col="2">
|
||||||
<field name="machine_access_html" readonly="1" widget="html"/>
|
<field name="machine_access_ids" colspan="2" context="{'default_partner_id': id}" nolabel="1">
|
||||||
</page>
|
<list>
|
||||||
|
|
||||||
<!-- Tab 3: Einweisungen (Liste) -->
|
|
||||||
<page name="ows_machine_access_list" string="Einweisungen (Liste)">
|
|
||||||
<field name="machine_access_ids">
|
|
||||||
<tree>
|
|
||||||
<field name="machine_id"/>
|
<field name="machine_id"/>
|
||||||
<field name="date_granted"/>
|
<field name="date_granted"/>
|
||||||
<field name="date_expiry"/>
|
<field name="date_expiry"/>
|
||||||
<field name="granted_by_pos"/>
|
<field name="granted_by_pos"/>
|
||||||
</tree>
|
</list>
|
||||||
</field>
|
</field>
|
||||||
</page>
|
</group>
|
||||||
|
|
||||||
|
<!-- ÜBERSICHT: Volle Breite -->
|
||||||
|
<group string="Maschinenfreigaben Übersicht" >
|
||||||
|
<field name="machine_access_html" colspan="2" readonly="1" widget="html" nolabel="1"/>
|
||||||
|
</group>
|
||||||
|
</page>
|
||||||
</xpath>
|
</xpath>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
@ -100,7 +116,7 @@
|
||||||
|
|
||||||
<!-- Optional: Kontakte-Action, falls gebraucht -->
|
<!-- Optional: Kontakte-Action, falls gebraucht -->
|
||||||
<record id="contacts.action_contacts" model="ir.actions.act_window">
|
<record id="contacts.action_contacts" model="ir.actions.act_window">
|
||||||
<field name="view_mode">tree,kanban,form,activity</field>
|
<field name="view_mode">list,kanban,form,activity</field>
|
||||||
</record>
|
</record>
|
||||||
<record id="contacts.action_contacts_view_kanban" model="ir.actions.act_window.view">
|
<record id="contacts.action_contacts_view_kanban" model="ir.actions.act_window.view">
|
||||||
<field name="sequence" eval="1"/>
|
<field name="sequence" eval="1"/>
|
||||||
456
open_workshop_dokuwiki/README.md
Normal file
|
|
@ -0,0 +1,456 @@
|
||||||
|
# Equipment Übersichtstabelle - Setup
|
||||||
|
|
||||||
|
## Voraussetzungen
|
||||||
|
|
||||||
|
Das **datatables Plugin** muss in DokuWiki installiert sein:
|
||||||
|
```bash
|
||||||
|
# Als DokuWiki Admin: Administration → Erweiterungsverwaltung
|
||||||
|
# Suche: "datatables"
|
||||||
|
# Installiere: "DataTables Plugin" von Matthias Schulte
|
||||||
|
```
|
||||||
|
|
||||||
|
Das **include Plugin** muss in DokuWiki installiert sein (für Einbindung von Odoo-Status-Seiten):
|
||||||
|
```bash
|
||||||
|
# Als DokuWiki Admin: Administration → Erweiterungsverwaltung
|
||||||
|
# Suche: "include"
|
||||||
|
# Installiere: "Include Plugin" - sollte standardmäßig vorhanden sein
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setup-Schritte
|
||||||
|
|
||||||
|
### 1. Odoo Konfiguration
|
||||||
|
|
||||||
|
Die Übersichtstabelle wird vollständig aus Odoo heraus konfiguriert - **kein DokuWiki Template nötig!**
|
||||||
|
|
||||||
|
Gehe zu: **Einstellungen → Technisch → Parameter → Systemparameter**
|
||||||
|
|
||||||
|
Standardwerte (automatisch gesetzt beim Modul-Install):
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Basis-Namespace für Equipment-Dokumentation (konfigurierbar)
|
||||||
|
dokuwiki.equipment_namespace = werkstatt:ausstattung
|
||||||
|
|
||||||
|
# Seiten-ID wo die Übersicht erstellt wird (konfigurierbar)
|
||||||
|
dokuwiki.overview_page_id = werkstatt:ausstattung:uebersicht
|
||||||
|
|
||||||
|
# Titel der Übersichtsseite
|
||||||
|
dokuwiki.overview_title = Geräte & Maschinen - Übersicht
|
||||||
|
|
||||||
|
# Spaltenüberschriften (Pipe-separiert)
|
||||||
|
dokuwiki.overview_columns = Status|Sicherheits-Kategorie|Hersteller|Bemerkung|Typ|Bild|Standort|Dokumentation
|
||||||
|
|
||||||
|
# Spaltendaten mit Platzhaltern (Pipe-separiert)
|
||||||
|
dokuwiki.overview_column_data = {status_smiley}|{ows_machine_id.category_icon}|{partner_id}|{note}|{model}|{image}|{ows_machine_id.location}|{wiki_doku_link}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Erste Synchronisation
|
||||||
|
|
||||||
|
1. In Odoo: **Wartung → Konfiguration → Wiki-Synchronisation**
|
||||||
|
2. Wähle: **"Übersichtstabelle aktualisieren"**
|
||||||
|
3. Klicke: **"Synchronisieren"**
|
||||||
|
|
||||||
|
Die Übersichtsseite wird erstellt unter: `werkstatt:ausstattung:uebersicht` (konfigurierbar über Systemparameter)
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### DataTables Funktionen
|
||||||
|
- ✅ Sortieren nach allen Spalten (Klick auf Header)
|
||||||
|
- ✅ Filter/Suche über alle Felder
|
||||||
|
- ✅ Pagination bei vielen Einträgen
|
||||||
|
- ✅ Responsive Design
|
||||||
|
|
||||||
|
### Flexible Spalten
|
||||||
|
- ✅ Spalten in Odoo konfigurierbar (kein DokuWiki Template)
|
||||||
|
- ✅ Alle Platzhalter verfügbar (siehe unten)
|
||||||
|
- ✅ Spalten hinzufügen/entfernen ohne Code-Änderung
|
||||||
|
- ✅ Änderungen sofort beim nächsten Sync wirksam
|
||||||
|
|
||||||
|
### Standard-Spalten
|
||||||
|
| Spalte | Platzhalter | Ausgabe |
|
||||||
|
|--------|-------------|---------|
|
||||||
|
| Status | `{status_smiley}` | 😊 / 😐 / ☹️ |
|
||||||
|
| Sicherheits-Kategorie | `{ows_machine_id.category_icon}` | 🟢 / 🟡 / 🔴 |
|
||||||
|
| Hersteller | `{partner_id}` | BOSCH, Festool, ... |
|
||||||
|
| Bemerkung | `{note}` | Notizen |
|
||||||
|
| Typ | `{model}` | Modellbezeichnung |
|
||||||
|
| Bild | `{image}` | Thumbnail 100px |
|
||||||
|
| Standort | `{ows_machine_id.location}` | Regal A3, ... |
|
||||||
|
| Dokumentation | `{wiki_doku_link}` | Link zur Detail-Seite |
|
||||||
|
|
||||||
|
## Spalten anpassen
|
||||||
|
|
||||||
|
### Beispiel 1: Seriennummer hinzufügen
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Spaltenüberschriften erweitern
|
||||||
|
dokuwiki.overview_columns = Status|Typ|Seriennummer|Hersteller|Standort|Dokumentation
|
||||||
|
|
||||||
|
# Spaltendaten erweitern
|
||||||
|
dokuwiki.overview_column_data = {status_smiley}|{model}|{serial_no}|{partner_id}|{ows_machine_id.location}|{wiki_doku_link}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Beispiel 2: Minimale Ansicht
|
||||||
|
|
||||||
|
```python
|
||||||
|
dokuwiki.overview_columns = Name|Status|Kategorie|Dokumentation
|
||||||
|
dokuwiki.overview_column_data = {name}|{status_smiley}|{ows_machine_id.category_icon}|{wiki_doku_link}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Beispiel 3: Detaillierte Ansicht mit Kosten
|
||||||
|
|
||||||
|
```python
|
||||||
|
dokuwiki.overview_columns = Name|Hersteller|Modell|Seriennummer|Kosten|Garantie|Standort|Kategorie
|
||||||
|
dokuwiki.overview_column_data = {name}|{partner_id}|{model}|{serial_no}|{cost}|{warranty_date}|{location}|{ows_machine_id.category_icon}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Beispiel 4: Mit Tags
|
||||||
|
|
||||||
|
```python
|
||||||
|
dokuwiki.overview_columns = Status|Name|Typ|Tags|Standort|Dokumentation
|
||||||
|
dokuwiki.overview_column_data = {status_smiley}|{name}|{model}|{tags_list}|{location}|{wiki_doku_link}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Wichtig:** Anzahl der Pipes muss übereinstimmen!
|
||||||
|
- 3 Pipes = 4 Spalten
|
||||||
|
- Spalten und Daten müssen gleiche Anzahl haben
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
- ⚡ Schneller Sync: Keine DB-Iteration über alle Einträge
|
||||||
|
- ⚡ Generiert nur eine Seite (statt 156 einzelne)
|
||||||
|
- ⚡ DokuWiki cached die Seite automatisch
|
||||||
|
- ⚡ Spalten-Änderung ohne Code-Deploy
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### DataTables funktioniert nicht
|
||||||
|
→ Installiere DataTables Plugin in DokuWiki
|
||||||
|
|
||||||
|
### Bilder werden nicht angezeigt
|
||||||
|
→ Prüfe ob Equipment `image_1920` Feld gesetzt hat
|
||||||
|
→ Bilder müssen zuerst via "Alle Equipment" Sync hochgeladen werden
|
||||||
|
|
||||||
|
### Equipment fehlen in Tabelle
|
||||||
|
→ Nur Equipment mit gesetztem `ows_area_id` werden angezeigt
|
||||||
|
→ Prüfe Filter in `action_sync_overview_table()`
|
||||||
|
|
||||||
|
### Spalten werden nicht korrekt dargestellt
|
||||||
|
→ Prüfe ob Anzahl Pipes in `overview_columns` und `overview_column_data` übereinstimmt
|
||||||
|
→ Beispiel: 3 Pipes = 4 Spalten
|
||||||
|
|
||||||
|
### Platzhalter wird nicht ersetzt (z.B. `{status}` bleibt stehen)
|
||||||
|
→ Prüfe Schreibweise (case-sensitive!)
|
||||||
|
→ Siehe Liste der verfügbaren Platzhalter unten
|
||||||
|
→ Bei `ows.machine` Feldern: `{ows_machine_id.feldname}` verwenden
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────┐
|
||||||
|
│ Odoo Systempar. │
|
||||||
|
│ - Spalten │
|
||||||
|
│ - Platzhalter │
|
||||||
|
└────────┬────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────┐
|
||||||
|
│ Odoo Equipment │
|
||||||
|
│ (156 Einträge) │
|
||||||
|
└────────┬────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────┐
|
||||||
|
│ _generate_overview_ │
|
||||||
|
│ table_row() │ ← Pro Equipment eine Zeile
|
||||||
|
│ (Platzhalter ersetzen) │ mit Platzhalter-Engine
|
||||||
|
└────────┬────────────────┘
|
||||||
|
│ 156 Zeilen
|
||||||
|
▼
|
||||||
|
┌─────────────────────────┐
|
||||||
|
│ Seite zusammenbauen │
|
||||||
|
│ - Titel │
|
||||||
|
│ - DataTables Header │
|
||||||
|
│ - Zeilen │
|
||||||
|
│ - Footer │
|
||||||
|
└────────┬────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────┐
|
||||||
|
│ Seite speichern │
|
||||||
|
│ werkstatt:ausstattung: │
|
||||||
|
│ uebersicht │
|
||||||
|
└─────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Automatische Updates
|
||||||
|
|
||||||
|
Die Übersichtstabelle kann optional automatisch bei Equipment-Änderungen aktualisiert werden.
|
||||||
|
|
||||||
|
### Manueller Sync (empfohlen)
|
||||||
|
|
||||||
|
**Manueller Sync empfohlen für:**
|
||||||
|
- Nach Massen-Importen
|
||||||
|
- Einmal täglich/wöchentlich
|
||||||
|
- Bei strukturellen Änderungen (neue Bereiche, etc.)
|
||||||
|
|
||||||
|
**Vorteil:** Performanter, keine zusätzliche Last bei jeder Equipment-Änderung
|
||||||
|
|
||||||
|
### Automatischer Sync bei Equipment-Änderungen
|
||||||
|
|
||||||
|
**Was passiert beim Speichern eines Equipment?**
|
||||||
|
|
||||||
|
1. **Equipment-Detailseite wird immer aktualisiert** wenn:
|
||||||
|
- Eines der überwachten Felder geändert wurde (siehe unten)
|
||||||
|
- `wiki_auto_sync` für das Equipment aktiviert ist (Standard: `True`)
|
||||||
|
- Equipment bereits synchronisiert wurde (`wiki_synced = True`)
|
||||||
|
|
||||||
|
2. **Übersichtstabelle wird zusätzlich aktualisiert** wenn:
|
||||||
|
- Alle Bedingungen von (1) erfüllt sind UND
|
||||||
|
- Systemparameter `dokuwiki.auto_update_overview_table = True` gesetzt ist
|
||||||
|
|
||||||
|
**Überwachte Felder (lösen Sync aus):**
|
||||||
|
- `name`, `serial_no`, `ows_area_id`, `category_id`, `status_id`
|
||||||
|
- `model`, `partner_id`, `location`, `note`, `image_1920`, `tag_ids`
|
||||||
|
|
||||||
|
**Aktivierung Übersichtstabellen-Sync:**
|
||||||
|
```python
|
||||||
|
# In Odoo: Einstellungen → Technisch → Parameter → Systemparameter
|
||||||
|
dokuwiki.auto_update_overview_table = True
|
||||||
|
```
|
||||||
|
|
||||||
|
**Achtung:**
|
||||||
|
- Bei vielen gleichzeitigen Equipment-Änderungen kann dies Performance-Probleme verursachen!
|
||||||
|
- Die komplette Übersichtstabelle wird bei jeder Änderung neu generiert (ca. 156 Zeilen)
|
||||||
|
|
||||||
|
### Cronjob (Alternative)
|
||||||
|
```python
|
||||||
|
# In Odoo: Einstellungen → Technisch → Automatisierung → Geplante Aktionen
|
||||||
|
|
||||||
|
Name: Wiki Übersichtstabelle Update
|
||||||
|
Modell: maintenance.equipment
|
||||||
|
Aktion: action_sync_overview_table()
|
||||||
|
Intervall: Täglich um 02:00 Uhr
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Verfügbare Platzhalter
|
||||||
|
|
||||||
|
Alle Platzhalter aus dem Detail-Template sind verfügbar!
|
||||||
|
|
||||||
|
## Basis-Felder (maintenance.equipment)
|
||||||
|
|
||||||
|
- `{name}` - Equipment-Name
|
||||||
|
- `{serial_no}` - Seriennummer
|
||||||
|
- `{model}` - Modell
|
||||||
|
- `{category}` - Kategoriename
|
||||||
|
- `{status}` - Status (aus status_id)
|
||||||
|
- `{status_smiley}` - Status als Smiley (aus status_id.smiley Feld, z.B. 😊 oder ☹️)
|
||||||
|
- `{location}` - Standort
|
||||||
|
- `{ows_area}` - Bereichsname
|
||||||
|
- `{assign_date}` - Zuweisungsdatum (formatiert)
|
||||||
|
- `{cost}` - Kosten
|
||||||
|
- `{warranty_date}` - Garantiedatum (formatiert)
|
||||||
|
- `{note}` - Notizen
|
||||||
|
- `{partner_id}` - Lieferant (Name)
|
||||||
|
- `{partner_ref}` - Lieferanten-Referenz
|
||||||
|
|
||||||
|
## Tags (falls maintenance_equipment_tags installiert)
|
||||||
|
|
||||||
|
- `{tags}` - Komma-separierte Liste aller Tags (z.B. "Holz, CNC, Einweisung erforderlich")
|
||||||
|
- `{tags_list}` - DokuWiki Bullet-Liste mit Zeilenumbrüchen (ideal für Tabellenzellen)
|
||||||
|
- `{tags_count}` - Anzahl der zugewiesenen Tags
|
||||||
|
|
||||||
|
## Spezial-Felder
|
||||||
|
|
||||||
|
- `{view_type}` - "Bereich" oder "Einsatzzweck"
|
||||||
|
- `{view_name}` - Name des Bereichs/Einsatzzwecks
|
||||||
|
- `{wiki_doku_page}` - ID der zentralen Doku-Seite
|
||||||
|
- `{wiki_doku_link}` - Fertiger Link zur zentralen Doku-Seite
|
||||||
|
- `{odoo_link}` - Fertiger Link zur Odoo Equipment-Seite
|
||||||
|
- `{odoo_url}` - URL zur Odoo Equipment-Seite (ohne Link-Markup)
|
||||||
|
- `{sync_datetime}` - Aktuelles Datum/Zeit
|
||||||
|
- `{image}` - Equipment-Bild (100px Breite) - Format: `{{:media_id?100}}`
|
||||||
|
- `{image_large}` - Equipment-Bild (Originalgröße) - Format: `{{:media_id}}`
|
||||||
|
- `{image_id}` - Media-ID des Bildes (z.B. werkstatt:ausruestung:media:sabako-laser.jpg)
|
||||||
|
|
||||||
|
## ows.machine Felder (falls verknüpft)
|
||||||
|
|
||||||
|
- `{ows_machine_id.name}` - Name
|
||||||
|
- `{ows_machine_id.model}` - Modell
|
||||||
|
- `{ows_machine_id.serial_no}` - Seriennummer
|
||||||
|
- `{ows_machine_id.location}` - Standort
|
||||||
|
- `{ows_machine_id.note}` - Notizen
|
||||||
|
- `{ows_machine_id.category}` - Sicherheitskategorie (red/yellow/green)
|
||||||
|
- `{ows_machine_id.category_icon}` - Kategorie-Icon als Emoji (🔴/🟡/🟢)
|
||||||
|
|
||||||
|
## Tipps für Platzhalter
|
||||||
|
|
||||||
|
- **Case-sensitive**: `{Name}` funktioniert nicht, nur `{name}`
|
||||||
|
- **Pipe als Trenner**: Zwischen Spalten `|` verwenden
|
||||||
|
- **Leere Werte**: Werden automatisch durch `-` ersetzt
|
||||||
|
- **Links**: `{wiki_doku_link}` und `{odoo_link}` enthalten bereits DokuWiki Link-Syntax
|
||||||
|
- **Bilder**: `{image}` ist bereits formatiert mit `{{:...?100}}`
|
||||||
|
- **ows.machine**: Immer mit Präfix `ows_machine_id.` (z.B. `{ows_machine_id.location}`)
|
||||||
|
|
||||||
|
## Beispiel-Konfigurationen
|
||||||
|
|
||||||
|
### Minimalistische Ansicht
|
||||||
|
```
|
||||||
|
Spalten: Name|Status|Kategorie
|
||||||
|
Daten: {name}|{status_smiley}|{ows_machine_id.category_icon}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Standard-Ansicht (Default)
|
||||||
|
```
|
||||||
|
Spalten: Status|Sicherheits-Kategorie|Hersteller|Bemerkung|Typ|Bild|Standort|Dokumentation
|
||||||
|
Daten: {status_smiley}|{ows_machine_id.category_icon}|{partner_id}|{note}|{model}|{image}|{ows_machine_id.location}|{wiki_doku_link}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vollständige Ansicht
|
||||||
|
```
|
||||||
|
Spalten: Name|Status|Kategorie|Hersteller|Modell|S/N|Kosten|Garantie|Standort|Bereich|Bild|Doku
|
||||||
|
Daten: {name}|{status_smiley}|{ows_machine_id.category_icon}|{partner_id}|{model}|{serial_no}|{cost}|{warranty_date}|{location}|{ows_area}|{image}|{wiki_doku_link}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mit Tags
|
||||||
|
```
|
||||||
|
Spalten: Name|Status|Tags|Hersteller|Standort|Doku
|
||||||
|
Daten: {name}|{status_smiley}|{tags_list}|{partner_id}|{location}|{wiki_doku_link}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Best Practices: DokuWiki Seitenstruktur
|
||||||
|
|
||||||
|
## Erweiterbare Dokumentation ohne Umbenennung
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
Benutzer möchten zu Equipment-Seiten eigene Unterseiten hinzufügen, ohne die von Odoo generierten Seiten umbenennen zu müssen.
|
||||||
|
|
||||||
|
### Lösung: Parallele Seite + Namespace
|
||||||
|
|
||||||
|
DokuWiki erlaubt **gleichzeitig** eine Seite und einen gleichnamigen Namespace:
|
||||||
|
- Datei: `equipment.txt` (von Odoo generiert)
|
||||||
|
- Verzeichnis: `equipment/` (für Benutzer-Unterseiten)
|
||||||
|
|
||||||
|
**Die Seite `equipment.txt` hat Vorrang und bleibt die Haupt-Equipment-Seite!**
|
||||||
|
|
||||||
|
### Empfohlene Struktur
|
||||||
|
|
||||||
|
```
|
||||||
|
werkstatt/
|
||||||
|
└── ausstattung/
|
||||||
|
├── uebersicht # VON ODOO GENERIERT: Übersichtstabelle
|
||||||
|
├── odoo-status/ # VON ODOO GENERIERT: Nur-Lese Seiten (nur odoo.odoo schreibt)
|
||||||
|
│ ├── c_template.txt # Template für Equipment-Status-Seiten
|
||||||
|
│ ├── analog-oscilloscope-hm303-6.txt # Status-Seite für include-Plugin
|
||||||
|
│ ├── cnc-fraese-xyz.txt # Status-Seite für include-Plugin
|
||||||
|
│ └── ...
|
||||||
|
└── {Bereich Name}/ # VON BENUTZER ERSTELLT: Namespace wird von Odoo festgelegt
|
||||||
|
├── analog-oscilloscope-hm303-6.txt # Benutzer erstellt diese Seite manuell (Klick auf Link in Übersicht)
|
||||||
|
├── analog-oscilloscope-hm303-6/ # Benutzer-Unterseiten (optional)
|
||||||
|
│ ├── kalibrierung.txt
|
||||||
|
│ ├── messungen.txt
|
||||||
|
│ └── bilder/
|
||||||
|
│ └── oszillogramme.txt
|
||||||
|
├── cnc-fraese-xyz.txt # Benutzer erstellt diese Seite manuell (Klick auf Link in Übersicht)
|
||||||
|
└── cnc-fraese-xyz/ # Benutzer-Unterseiten (optional)
|
||||||
|
├── programme.txt
|
||||||
|
└── werkzeuge.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
**Wichtig:**
|
||||||
|
- **Odoo erstellt NUR:** `{overview_page_id}` (Übersichtstabelle) und `odoo-status/*.txt` (Status-Seiten)
|
||||||
|
- **Odoo erstellt NIEMALS:** Seiten in `{Bereich Name}/` - diese werden ausschließlich von Benutzern erstellt
|
||||||
|
- **Workflow:** Übersichtstabelle verlinkt auf `{Bereich Name}/equipment-name.txt` → Link ist rot (Seite existiert nicht) → Benutzer klickt darauf → DokuWiki bietet "Seite erstellen" an → Benutzer fügt `{{page>odoo-status:equipment-name}}` ein
|
||||||
|
- **Berechtigungen:** `odoo-status/` Namespace hat spezielle Berechtigungen (nur odoo.odoo kann schreiben)
|
||||||
|
|
||||||
|
### Wichtige Regeln
|
||||||
|
|
||||||
|
#### ✅ DO: Unterverzeichnis OHNE start.txt
|
||||||
|
|
||||||
|
```
|
||||||
|
equipment/
|
||||||
|
├── subpage1.txt
|
||||||
|
├── subpage2.txt
|
||||||
|
└── weitere/
|
||||||
|
└── details.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
**Vorteile:**
|
||||||
|
- Breadcrumb zeigt: `Home » Werkstatt » Ausrüstung » Equipment » Subpage1`
|
||||||
|
- Keine Duplikation oder Verwirrung
|
||||||
|
- Equipment-Hauptseite bleibt `equipment.txt`
|
||||||
|
|
||||||
|
#### ❌ DON'T: Unterverzeichnis MIT start.txt
|
||||||
|
|
||||||
|
```
|
||||||
|
equipment/
|
||||||
|
├── start.txt # ❌ NICHT erstellen!
|
||||||
|
├── subpage1.txt
|
||||||
|
└── subpage2.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
**Probleme:**
|
||||||
|
- Breadcrumb wird verwirrend: `Equipment` könnte auf zwei Seiten verweisen
|
||||||
|
- `equipment` und `equipment:start` zeigen unterschiedliche Inhalte
|
||||||
|
- Benutzer wissen nicht, welche Seite die "richtige" ist
|
||||||
|
|
||||||
|
### Include-Plugin für Odoo-Status einbinden
|
||||||
|
|
||||||
|
**Wichtig:** Diese Seite wird NICHT von Odoo erstellt! Der Benutzer erstellt sie manuell nach Klick auf den Link in der Übersichtstabelle.
|
||||||
|
|
||||||
|
In der Benutzer-Dokumentationsseite wird der Odoo-generierte Status eingebunden:
|
||||||
|
|
||||||
|
```dokuwiki
|
||||||
|
===== Analog Oscilloscope HM303-6 =====
|
||||||
|
|
||||||
|
==== Odoo Status ====
|
||||||
|
|
||||||
|
{{page>werkstatt:ausstattung:odoo-status:analog-oscilloscope-hm303-6}}
|
||||||
|
|
||||||
|
==== Eigene Dokumentation ====
|
||||||
|
|
||||||
|
Hier kann der Benutzer seine eigenen Inhalte hinzufügen...
|
||||||
|
|
||||||
|
==== Weitere Dokumentation ====
|
||||||
|
|
||||||
|
<catlist .:analog-oscilloscope-hm303-6 -noNSInBold -sortByTitle>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Syntax-Erklärung:**
|
||||||
|
- `{{page>...}}` → Bindet Odoo-Status-Seite ein (nur lesbar für Benutzer)
|
||||||
|
- `.:namespace` → Relativer Namespace für catlist (Punkt + Doppelpunkt!)
|
||||||
|
- `-noNSInBold` → Namespace-Präfix nicht fett darstellen
|
||||||
|
- `-sortByTitle` → Alphabetisch sortieren
|
||||||
|
- `-exclude:{ns1 ns2}` → Optional: Namespaces ausschließen
|
||||||
|
|
||||||
|
**Ergebnis:**
|
||||||
|
- Odoo-Status wird direkt in Benutzer-Seite eingebettet
|
||||||
|
- Benutzer sehen aktuelle Odoo-Daten ohne direkt auf odoo-status zuzugreifen
|
||||||
|
- Catlist zeigt alle Benutzer-Unterseiten im `equipment/` Verzeichnis
|
||||||
|
- Automatisch aktualisiert wenn Benutzer neue Seiten erstellen
|
||||||
|
- Kein manuelles Pflegen von Links nötig
|
||||||
|
|
||||||
|
### Workflow
|
||||||
|
|
||||||
|
1. **Odoo synchronisiert** → Erstellt/aktualisiert `{overview_page_id}` (Übersichtstabelle) und `odoo-status/equipment.txt` (Status-Seiten)
|
||||||
|
2. **Benutzer sieht Übersichtstabelle** → Equipment sind verlinkt, aber Links sind rot (Seiten existieren nicht)
|
||||||
|
3. **Benutzer klickt roten Link** → DokuWiki zeigt "Diese Seite existiert noch nicht - erstellen?"
|
||||||
|
4. **Benutzer erstellt Seite** → Fügt `{{page>werkstatt:ausstattung:odoo-status:equipment-name}}` ein und eigene Inhalte
|
||||||
|
5. **Benutzer erstellt optional Unterseiten** → Erstellt Verzeichnis `equipment/` für weitere Dokumentation
|
||||||
|
6. **Catlist zeigt automatisch Unterseiten** → Wenn in Hauptseite eingebunden
|
||||||
|
7. **Keine Konflikte** → Odoo schreibt NIEMALS in `{Bereich Name}/`, nur in `start` und `odoo-status/`
|
||||||
|
|
||||||
|
### Vorteile dieser Struktur
|
||||||
|
|
||||||
|
- ✅ **Klare Trennung:** `odoo-status/` (nur Odoo schreibt) vs. `{Bereich Name}/` (nur Benutzer schreiben)
|
||||||
|
- ✅ **Berechtigungsschutz:** Benutzer können Odoo-Daten nicht versehentlich überschreiben
|
||||||
|
- ✅ **Keine Überschreibgefahr:** Odoo erstellt niemals Seiten in Benutzer-Namespaces
|
||||||
|
- ✅ **Einfaches Einbinden:** Ein `{{page>...}}` in Benutzer-Seite, fertig
|
||||||
|
- ✅ **Keine Namenskollisionen:** Odoo und Benutzer haben komplett getrennte Namespaces
|
||||||
|
- ✅ **Benutzer entscheiden:** Nur wer dokumentieren will, erstellt eine Seite (über Link in Übersichtstabelle)
|
||||||
|
- ✅ **Flexibel erweiterbar:** Benutzer können beliebig viele Unterseiten erstellen
|
||||||
|
- ✅ **Automatische Updates:** Odoo-Status wird in alle einbindenden Seiten propagiert
|
||||||
|
- ✅ **Konfigurierbarer Namespace:** Basis-Pfad über Systemparameter `dokuwiki.equipment_namespace` anpassbar
|
||||||
4
open_workshop_dokuwiki/__init__.py
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from . import models
|
||||||
|
from . import wizard
|
||||||
|
from .hooks import post_init_hook, pre_init_hook
|
||||||
68
open_workshop_dokuwiki/__manifest__.py
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
{
|
||||||
|
'name': 'Open Workshop - DokuWiki Integration',
|
||||||
|
'version': '18.0.2.1.0',
|
||||||
|
'category': 'Maintenance',
|
||||||
|
'summary': 'Synchronisiert Equipment-Daten mit DokuWiki für Dokumentation',
|
||||||
|
'description': """
|
||||||
|
Open Workshop DokuWiki Integration
|
||||||
|
===================================
|
||||||
|
|
||||||
|
Dieses Modul synchronisiert Equipment-Daten aus Odoo mit einem DokuWiki System.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
---------
|
||||||
|
* Automatische Erstellung von Wiki-Seiten für Equipment
|
||||||
|
* Übersichtstabelle aller Equipment (DataTables mit Sortierung/Filterung)
|
||||||
|
* Status-Seiten für Include-Plugin (nur von Odoo generiert, nur lesbar)
|
||||||
|
* Benutzer erstellen eigene Dokumentationsseiten und binden Odoo-Status ein
|
||||||
|
* Smart Button "Wiki öffnen" im Equipment-Formular
|
||||||
|
* Automatische Synchronisation bei Equipment-Änderungen (optional)
|
||||||
|
|
||||||
|
Architektur:
|
||||||
|
------------
|
||||||
|
* Odoo generiert NUR: {overview_page_id} (Übersichtstabelle) und odoo-status/ Seiten
|
||||||
|
* Odoo generiert NIEMALS: Benutzer-Dokumentationsseiten in {bereich}/ Namespaces
|
||||||
|
* Namespace-Struktur (konfigurierbar über dokuwiki.equipment_namespace):
|
||||||
|
- {equipment_namespace}:{overview_page_id} - Übersichtstabelle (von Odoo)
|
||||||
|
- {equipment_namespace}:odoo-status:{equipment_id} - Status-Seiten (von Odoo, nur lesbar)
|
||||||
|
- {equipment_namespace}:odoo-status:c_template - Template für Status-Seiten
|
||||||
|
- {equipment_namespace}:{bereich}:{equipment_id} - Benutzer-Dokumentation (von Benutzern erstellt!)
|
||||||
|
|
||||||
|
Workflow:
|
||||||
|
---------
|
||||||
|
1. Odoo synchronisiert Equipment → erstellt odoo-status/ Seiten
|
||||||
|
2. Übersichtstabelle verlinkt auf {bereich}/{equipment_id} → Link ist rot (existiert nicht)
|
||||||
|
3. Benutzer klickt roten Link → DokuWiki: "Seite erstellen?"
|
||||||
|
4. Benutzer erstellt Seite und fügt {{page>odoo-status:equipment_id}} ein
|
||||||
|
|
||||||
|
ACL-Empfehlung:
|
||||||
|
---------------
|
||||||
|
* odoo-status/ Namespace: Nur odoo.odoo kann schreiben, alle anderen nur lesen
|
||||||
|
* {bereich}/ Namespaces: Benutzer haben volle Schreibrechte
|
||||||
|
""",
|
||||||
|
'author': 'Hobbyhimmel',
|
||||||
|
'website': 'https://hobbyhimmel.de',
|
||||||
|
'license': 'LGPL-3',
|
||||||
|
'depends': [
|
||||||
|
'open_workshop_base',
|
||||||
|
'maintenance',
|
||||||
|
'maintenance_equipment_status',
|
||||||
|
],
|
||||||
|
'external_dependencies': {
|
||||||
|
'python': ['dokuwiki'],
|
||||||
|
},
|
||||||
|
'data': [
|
||||||
|
'security/ir.model.access.csv',
|
||||||
|
'data/ir_config_parameter.xml',
|
||||||
|
'data/maintenance_equipment_status_data.xml',
|
||||||
|
'views/maintenance_equipment_views.xml',
|
||||||
|
'views/maintenance_equipment_status_views.xml',
|
||||||
|
'wizard/equipment_wiki_sync_wizard_views.xml',
|
||||||
|
],
|
||||||
|
'post_init_hook': 'post_init_hook',
|
||||||
|
'pre_init_hook': 'pre_init_hook',
|
||||||
|
'installable': True,
|
||||||
|
'application': False,
|
||||||
|
'auto_install': False,
|
||||||
|
}
|
||||||
8
open_workshop_dokuwiki/data/ir_config_parameter.xml
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<!--
|
||||||
|
DokuWiki System Parameters werden über post_init_hook initialisiert
|
||||||
|
Dadurch werden sie bei Install und Update korrekt gehandhabt
|
||||||
|
und User-Änderungen bleiben erhalten
|
||||||
|
-->
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo noupdate="1">
|
||||||
|
<!-- DokuWiki Smiley-Werte für Status aus open_workshop_base -->
|
||||||
|
|
||||||
|
<record id="open_workshop_base.equipment_status_inbetrieb" model="maintenance.equipment.status">
|
||||||
|
<field name="smiley">😊</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="open_workshop_base.equipment_status_defekt" model="maintenance.equipment.status">
|
||||||
|
<field name="smiley">☹️</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="open_workshop_base.equipment_status_wartung" model="maintenance.equipment.status">
|
||||||
|
<field name="smiley">🛠️</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="open_workshop_base.equipment_status_ausgemustert" model="maintenance.equipment.status">
|
||||||
|
<field name="smiley">❌</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
15
open_workshop_dokuwiki/hooks.py
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
def pre_init_hook(env):
|
||||||
|
"""
|
||||||
|
Hook der VOR der Modul-Installation/Update ausgeführt wird.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def post_init_hook(env):
|
||||||
|
"""
|
||||||
|
Hook der nach der Modul-Installation/Update ausgeführt wird.
|
||||||
|
Initialisiert die DokuWiki-Parameter falls sie nicht existieren.
|
||||||
|
"""
|
||||||
|
env['res.config.settings']._init_dokuwiki_parameters()
|
||||||
101
open_workshop_dokuwiki/migrations/18.0.2.1.0/pre-migration.py
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import logging
|
||||||
|
from psycopg2 import sql
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def migrate(cr, version):
|
||||||
|
"""
|
||||||
|
Fügt die neuen Wiki-ID-Felder zur maintenance_equipment Tabelle hinzu.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cr: Database cursor
|
||||||
|
version: Aktuelle Version vor dem Upgrade (sollte 18.0.2.0.0 sein)
|
||||||
|
"""
|
||||||
|
_logger.info("=" * 80)
|
||||||
|
_logger.info("PRE-MIGRATION SCRIPT: open_workshop_dokuwiki 18.0.2.1.0")
|
||||||
|
_logger.info("Aktuelle Version: %s", version)
|
||||||
|
_logger.info("=" * 80)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Prüfe ob die Tabelle maintenance_equipment existiert
|
||||||
|
cr.execute("""
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT FROM information_schema.tables
|
||||||
|
WHERE table_schema = 'public'
|
||||||
|
AND table_name = 'maintenance_equipment'
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
table_exists = cr.fetchone()[0]
|
||||||
|
|
||||||
|
if not table_exists:
|
||||||
|
_logger.warning("Tabelle maintenance_equipment existiert nicht - überspringe Migration")
|
||||||
|
return
|
||||||
|
|
||||||
|
_logger.info("Tabelle maintenance_equipment gefunden")
|
||||||
|
|
||||||
|
# Prüfe ob die Felder bereits existieren
|
||||||
|
cr.execute("""
|
||||||
|
SELECT column_name
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'public'
|
||||||
|
AND table_name = 'maintenance_equipment'
|
||||||
|
AND column_name IN ('wiki_status_id', 'wiki_doku_id')
|
||||||
|
""")
|
||||||
|
existing_fields = [row[0] for row in cr.fetchall()]
|
||||||
|
_logger.info("Bereits existierende Felder: %s", existing_fields)
|
||||||
|
|
||||||
|
# Füge wiki_status_id hinzu falls nicht vorhanden
|
||||||
|
if 'wiki_status_id' not in existing_fields:
|
||||||
|
_logger.info("Füge Spalte wiki_status_id hinzu...")
|
||||||
|
cr.execute("""
|
||||||
|
ALTER TABLE maintenance_equipment
|
||||||
|
ADD COLUMN wiki_status_id VARCHAR
|
||||||
|
""")
|
||||||
|
cr.commit() # Wichtig: Commit nach jeder Änderung
|
||||||
|
_logger.info("✓ Spalte wiki_status_id erfolgreich hinzugefügt")
|
||||||
|
else:
|
||||||
|
_logger.info("→ Spalte wiki_status_id existiert bereits, überspringe")
|
||||||
|
|
||||||
|
# Füge wiki_doku_id hinzu falls nicht vorhanden
|
||||||
|
if 'wiki_doku_id' not in existing_fields:
|
||||||
|
_logger.info("Füge Spalte wiki_doku_id hinzu...")
|
||||||
|
cr.execute("""
|
||||||
|
ALTER TABLE maintenance_equipment
|
||||||
|
ADD COLUMN wiki_doku_id VARCHAR
|
||||||
|
""")
|
||||||
|
cr.commit() # Wichtig: Commit nach jeder Änderung
|
||||||
|
_logger.info("✓ Spalte wiki_doku_id erfolgreich hinzugefügt")
|
||||||
|
else:
|
||||||
|
_logger.info("→ Spalte wiki_doku_id existiert bereits, überspringe")
|
||||||
|
|
||||||
|
# Verifiziere dass beide Felder jetzt existieren
|
||||||
|
cr.execute("""
|
||||||
|
SELECT column_name
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'public'
|
||||||
|
AND table_name = 'maintenance_equipment'
|
||||||
|
AND column_name IN ('wiki_status_id', 'wiki_doku_id')
|
||||||
|
ORDER BY column_name
|
||||||
|
""")
|
||||||
|
final_fields = [row[0] for row in cr.fetchall()]
|
||||||
|
_logger.info("Felder nach Migration: %s", final_fields)
|
||||||
|
|
||||||
|
if len(final_fields) == 2:
|
||||||
|
_logger.info("=" * 80)
|
||||||
|
_logger.info("✓ PRE-MIGRATION ERFOLGREICH ABGESCHLOSSEN")
|
||||||
|
_logger.info("=" * 80)
|
||||||
|
else:
|
||||||
|
_logger.error("!" * 80)
|
||||||
|
_logger.error("FEHLER: Nicht alle Felder wurden hinzugefügt!")
|
||||||
|
_logger.error("Erwartet: ['wiki_doku_id', 'wiki_status_id']")
|
||||||
|
_logger.error("Gefunden: %s", final_fields)
|
||||||
|
_logger.error("!" * 80)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
_logger.error("!" * 80)
|
||||||
|
_logger.error("FEHLER IM PRE-MIGRATION SCRIPT!")
|
||||||
|
_logger.error("Exception: %s", str(e))
|
||||||
|
_logger.error("!" * 80)
|
||||||
|
raise
|
||||||
5
open_workshop_dokuwiki/models/__init__.py
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from . import dokuwiki_client
|
||||||
|
from . import maintenance_equipment
|
||||||
|
from . import maintenance_equipment_status
|
||||||
|
from . import res_config_settings
|
||||||
392
open_workshop_dokuwiki/models/dokuwiki_client.py
Normal file
|
|
@ -0,0 +1,392 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import logging
|
||||||
|
from dokuwiki import DokuWiki, DokuWikiError
|
||||||
|
from odoo import api, models
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Connection Cache (pro Request wiederverwendet)
|
||||||
|
_connection_cache = {}
|
||||||
|
|
||||||
|
|
||||||
|
class DokuWikiClient(models.AbstractModel):
|
||||||
|
"""
|
||||||
|
Wrapper für DokuWiki XML-RPC API Client.
|
||||||
|
Managed die Verbindung zum DokuWiki und stellt Methoden für Seitenoperationen bereit.
|
||||||
|
|
||||||
|
Performance-Optimierung: Verbindungen werden gecached und wiederverwendet
|
||||||
|
um den Overhead von XML-RPC Verbindungsaufbauten zu minimieren.
|
||||||
|
"""
|
||||||
|
_name = 'dokuwiki.client'
|
||||||
|
_description = 'DokuWiki API Client'
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _get_cache_key(self):
|
||||||
|
"""
|
||||||
|
Generiert einen Cache-Key basierend auf den Verbindungsparametern.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Cache-Key (URL + User)
|
||||||
|
"""
|
||||||
|
IrConfigParameter = self.env['ir.config_parameter'].sudo()
|
||||||
|
wiki_url = IrConfigParameter.get_param('dokuwiki.url', '')
|
||||||
|
wiki_user = IrConfigParameter.get_param('dokuwiki.user', '')
|
||||||
|
return f"{wiki_url}:{wiki_user}"
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _get_wiki_connection(self, use_cache=True):
|
||||||
|
"""
|
||||||
|
Erstellt eine DokuWiki-Verbindung mit Credentials aus System-Parametern.
|
||||||
|
Verbindungen werden gecached und wiederverwendet für bessere Performance.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
use_cache (bool): Wenn True, wird eine gecachte Verbindung wiederverwendet
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
DokuWiki: Verbundenes DokuWiki-Client-Objekt
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
UserError: Wenn Konfiguration fehlt oder Verbindung fehlschlägt
|
||||||
|
"""
|
||||||
|
IrConfigParameter = self.env['ir.config_parameter'].sudo()
|
||||||
|
|
||||||
|
# Credentials aus System-Parametern holen
|
||||||
|
wiki_url = IrConfigParameter.get_param('dokuwiki.url')
|
||||||
|
wiki_user = IrConfigParameter.get_param('dokuwiki.user')
|
||||||
|
wiki_password = IrConfigParameter.get_param('dokuwiki.password')
|
||||||
|
|
||||||
|
if not all([wiki_url, wiki_user, wiki_password]):
|
||||||
|
raise UserError(
|
||||||
|
"DokuWiki-Konfiguration unvollständig. "
|
||||||
|
"Bitte unter Einstellungen → Technisch → Parameter → Systemparameter konfigurieren:\n"
|
||||||
|
"- dokuwiki.url\n"
|
||||||
|
"- dokuwiki.user\n"
|
||||||
|
"- dokuwiki.password"
|
||||||
|
)
|
||||||
|
|
||||||
|
# URL validieren/normalisieren
|
||||||
|
if not wiki_url.startswith(('http://', 'https://')):
|
||||||
|
raise UserError(f"DokuWiki URL muss mit http:// oder https:// beginnen: {wiki_url}")
|
||||||
|
|
||||||
|
# Cache-Key generieren
|
||||||
|
cache_key = self._get_cache_key()
|
||||||
|
|
||||||
|
# Gecachte Verbindung wiederverwenden wenn möglich
|
||||||
|
if use_cache and cache_key in _connection_cache:
|
||||||
|
try:
|
||||||
|
# Teste ob Verbindung noch lebt
|
||||||
|
cached_wiki = _connection_cache[cache_key]
|
||||||
|
_ = cached_wiki.version # Test-Call
|
||||||
|
_logger.debug(f"Wiederverwendung gecachter DokuWiki-Verbindung: {wiki_url}")
|
||||||
|
return cached_wiki
|
||||||
|
except Exception as e:
|
||||||
|
_logger.warning(f"Gecachte Verbindung ungültig, erstelle neue: {e}")
|
||||||
|
del _connection_cache[cache_key]
|
||||||
|
|
||||||
|
# Neue Verbindung erstellen
|
||||||
|
try:
|
||||||
|
_logger.info(f"Neue DokuWiki-Verbindung: {wiki_url} (User: {wiki_user})")
|
||||||
|
wiki = DokuWiki(wiki_url, wiki_user, wiki_password)
|
||||||
|
# Test-Verbindung
|
||||||
|
version = wiki.version
|
||||||
|
_logger.info(f"DokuWiki-Verbindung erfolgreich: Version {version}")
|
||||||
|
|
||||||
|
# In Cache speichern
|
||||||
|
if use_cache:
|
||||||
|
_connection_cache[cache_key] = wiki
|
||||||
|
_logger.debug(f"Verbindung im Cache gespeichert: {cache_key}")
|
||||||
|
|
||||||
|
return wiki
|
||||||
|
except DokuWikiError as e:
|
||||||
|
error_details = f"URL: {wiki_url}, User: {wiki_user}, Fehler: {e}"
|
||||||
|
_logger.error(f"DokuWiki-Verbindung fehlgeschlagen: {error_details}")
|
||||||
|
raise UserError(
|
||||||
|
f"Verbindung zu DokuWiki fehlgeschlagen.\n\n"
|
||||||
|
f"Details:\n"
|
||||||
|
f"- URL: {wiki_url}\n"
|
||||||
|
f"- User: {wiki_user}\n"
|
||||||
|
f"- Fehler: {e}\n\n"
|
||||||
|
f"Hinweise:\n"
|
||||||
|
f"- Prüfen Sie ob die URL korrekt ist (sollte auf /lib/exe/xmlrpc.php zeigen)\n"
|
||||||
|
f"- Stellen Sie sicher, dass XML-RPC in DokuWiki aktiviert ist\n"
|
||||||
|
f"- Prüfen Sie die Benutzer-Zugangsdaten"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
_logger.error(f"Unerwarteter Fehler bei DokuWiki-Verbindung: {e}", exc_info=True)
|
||||||
|
raise UserError(f"Unerwarteter Fehler: {e}")
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def clear_connection_cache(self):
|
||||||
|
"""
|
||||||
|
Löscht den Verbindungs-Cache (z.B. nach Konfigurationsänderungen).
|
||||||
|
"""
|
||||||
|
global _connection_cache
|
||||||
|
_connection_cache.clear()
|
||||||
|
_logger.info("DokuWiki Verbindungs-Cache geleert")
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def create_page(self, page_id, content, summary="Erstellt von Odoo"):
|
||||||
|
"""
|
||||||
|
Erstellt oder aktualisiert eine DokuWiki-Seite.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
page_id (str): DokuWiki Page-ID (z.B. "werkstatt:ausruestung:holzwerkstatt:maschine1")
|
||||||
|
content (str): Wiki-Markup-Inhalt der Seite
|
||||||
|
summary (str): Änderungskommentar für die Wiki-History
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True bei Erfolg
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
UserError: Bei API-Fehlern
|
||||||
|
"""
|
||||||
|
wiki = self._get_wiki_connection()
|
||||||
|
|
||||||
|
try:
|
||||||
|
_logger.info(f"Erstelle/Aktualisiere Wiki-Seite: {page_id} ({len(content)} Zeichen)")
|
||||||
|
wiki.pages.set(page_id, content, summary=summary)
|
||||||
|
_logger.info(f"Wiki-Seite erfolgreich erstellt/aktualisiert: {page_id}")
|
||||||
|
return True
|
||||||
|
except DokuWikiError as e:
|
||||||
|
_logger.error(f"DokuWiki API Fehler bei Seite {page_id}: {e}", exc_info=True)
|
||||||
|
raise UserError(
|
||||||
|
f"Fehler beim Erstellen der Wiki-Seite '{page_id}':\n\n"
|
||||||
|
f"{e}\n\n"
|
||||||
|
f"Mögliche Ursachen:\n"
|
||||||
|
f"- XML-RPC nicht aktiviert in DokuWiki\n"
|
||||||
|
f"- Keine Schreibrechte für den Namespace\n"
|
||||||
|
f"- Ungültige Zeichen in der Page-ID\n"
|
||||||
|
f"- DokuWiki gibt Fehler/Warnings aus (prüfen Sie die DokuWiki-Logs)"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
_logger.error(f"Unerwarteter Fehler beim Erstellen der Seite {page_id}: {e}", exc_info=True)
|
||||||
|
raise UserError(f"Unerwarteter Fehler: {e}")
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def get_page(self, page_id):
|
||||||
|
"""
|
||||||
|
Liest den Inhalt einer DokuWiki-Seite.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
page_id (str): DokuWiki Page-ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Wiki-Markup-Inhalt der Seite (leer wenn Seite nicht existiert)
|
||||||
|
"""
|
||||||
|
wiki = self._get_wiki_connection()
|
||||||
|
|
||||||
|
try:
|
||||||
|
content = wiki.pages.get(page_id)
|
||||||
|
return content or ""
|
||||||
|
except DokuWikiError as e:
|
||||||
|
_logger.warning(f"Fehler beim Lesen der Wiki-Seite {page_id}: {e}")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def page_exists(self, page_id):
|
||||||
|
"""
|
||||||
|
Prüft ob eine DokuWiki-Seite existiert.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
page_id (str): DokuWiki Page-ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True wenn Seite existiert
|
||||||
|
"""
|
||||||
|
content = self.get_page(page_id)
|
||||||
|
return bool(content.strip())
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def delete_page(self, page_id, summary="Gelöscht von Odoo"):
|
||||||
|
"""
|
||||||
|
Löscht eine DokuWiki-Seite (setzt Inhalt auf leer).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
page_id (str): DokuWiki Page-ID
|
||||||
|
summary (str): Änderungskommentar für die Wiki-History
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True bei Erfolg
|
||||||
|
"""
|
||||||
|
wiki = self._get_wiki_connection()
|
||||||
|
|
||||||
|
try:
|
||||||
|
wiki.pages.set(page_id, "", summary=summary)
|
||||||
|
_logger.info(f"Wiki-Seite gelöscht: {page_id}")
|
||||||
|
return True
|
||||||
|
except DokuWikiError as e:
|
||||||
|
_logger.error(f"Fehler beim Löschen der Wiki-Seite {page_id}: {e}")
|
||||||
|
raise UserError(f"Fehler beim Löschen der Wiki-Seite: {e}")
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def list_pages(self, namespace):
|
||||||
|
"""
|
||||||
|
Listet alle Seiten in einem DokuWiki-Namespace auf.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
namespace (str): DokuWiki-Namespace (z.B. "werkstatt:ausruestung")
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: Liste von Dictionaries mit Page-Informationen
|
||||||
|
"""
|
||||||
|
wiki = self._get_wiki_connection()
|
||||||
|
|
||||||
|
try:
|
||||||
|
pages = wiki.pages.list(namespace)
|
||||||
|
return pages
|
||||||
|
except DokuWikiError as e:
|
||||||
|
_logger.error(f"Fehler beim Auflisten des Namespace {namespace}: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def get_wiki_url(self, page_id):
|
||||||
|
"""
|
||||||
|
Generiert die vollständige URL zu einer DokuWiki-Seite.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
page_id (str): DokuWiki Page-ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Vollständige URL zur Wiki-Seite
|
||||||
|
"""
|
||||||
|
IrConfigParameter = self.env['ir.config_parameter'].sudo()
|
||||||
|
wiki_url = IrConfigParameter.get_param('dokuwiki.url', '')
|
||||||
|
|
||||||
|
if not wiki_url:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# Entferne trailing slash falls vorhanden
|
||||||
|
wiki_url = wiki_url.rstrip('/')
|
||||||
|
|
||||||
|
return f"{wiki_url}/doku.php?id={page_id}"
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def upload_media(self, media_id, file_content, overwrite=True):
|
||||||
|
"""
|
||||||
|
Lädt eine Mediendatei (Bild) ins DokuWiki hoch.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
media_id (str): DokuWiki Media-ID (z.B. "werkstatt:ausruestung:equipment_3.jpg")
|
||||||
|
file_content (bytes): Binärer Dateiinhalt
|
||||||
|
overwrite (bool): Existierende Datei überschreiben
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True bei Erfolg
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
UserError: Bei Fehler beim Upload
|
||||||
|
"""
|
||||||
|
wiki = self._get_wiki_connection()
|
||||||
|
|
||||||
|
import tempfile
|
||||||
|
import os
|
||||||
|
import base64
|
||||||
|
import imghdr
|
||||||
|
|
||||||
|
tmp_path = None
|
||||||
|
|
||||||
|
# Unterstütze mehrere Eingabetypen:
|
||||||
|
# - ir.attachment record (Recordset)
|
||||||
|
# - attachment id (int)
|
||||||
|
# - rohe Bytes
|
||||||
|
# - base64-codierter String
|
||||||
|
try:
|
||||||
|
content_bytes = None
|
||||||
|
ext = ''
|
||||||
|
|
||||||
|
# ir.attachment Record
|
||||||
|
if hasattr(file_content, '_name') and getattr(file_content, '_name') == 'ir.attachment':
|
||||||
|
att = file_content.sudo()
|
||||||
|
if not att.datas:
|
||||||
|
raise UserError(f"Attachment {att.id} enthält keine Daten")
|
||||||
|
content_bytes = base64.b64decode(att.datas)
|
||||||
|
fname = att.datas_fname or ''
|
||||||
|
ext = os.path.splitext(fname)[1]
|
||||||
|
|
||||||
|
# attachment id
|
||||||
|
elif isinstance(file_content, int):
|
||||||
|
att = self.env['ir.attachment'].sudo().browse(file_content)
|
||||||
|
if not att.exists() or not att.datas:
|
||||||
|
raise UserError(f"Attachment {file_content} nicht gefunden oder leer")
|
||||||
|
content_bytes = base64.b64decode(att.datas)
|
||||||
|
fname = att.datas_fname or ''
|
||||||
|
ext = os.path.splitext(fname)[1]
|
||||||
|
|
||||||
|
# base64 string
|
||||||
|
elif isinstance(file_content, str):
|
||||||
|
try:
|
||||||
|
content_bytes = base64.b64decode(file_content)
|
||||||
|
except Exception:
|
||||||
|
# Fallback: treat as empty
|
||||||
|
raise UserError("Übergebener String konnte nicht als base64 decodiert werden")
|
||||||
|
ext = os.path.splitext(media_id)[1]
|
||||||
|
|
||||||
|
# rohe bytes
|
||||||
|
else:
|
||||||
|
# assume bytes-like
|
||||||
|
content_bytes = file_content
|
||||||
|
# try extension from media_id
|
||||||
|
ext = os.path.splitext(media_id)[1]
|
||||||
|
|
||||||
|
# Wenn noch keine Extension, versuche Bildtyp zu erkennen
|
||||||
|
if not ext:
|
||||||
|
try:
|
||||||
|
img_type = imghdr.what(None, content_bytes)
|
||||||
|
if img_type:
|
||||||
|
ext = f'.{img_type}'
|
||||||
|
except Exception:
|
||||||
|
ext = ''
|
||||||
|
|
||||||
|
if not ext:
|
||||||
|
ext = '.bin'
|
||||||
|
|
||||||
|
# Sicherstellen, dass ext mit Punkt beginnt
|
||||||
|
if not ext.startswith('.'):
|
||||||
|
ext = f'.{ext}'
|
||||||
|
|
||||||
|
# Temporäre Datei erstellen (dokuwiki.py erwartet Datei-Pfad)
|
||||||
|
with tempfile.NamedTemporaryFile(delete=False, suffix=ext) as tmp_file:
|
||||||
|
tmp_file.write(content_bytes)
|
||||||
|
tmp_path = tmp_file.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
wiki.medias.add(media_id, tmp_path, overwrite=overwrite)
|
||||||
|
_logger.info(f"Mediendatei hochgeladen: {media_id} (tmp={tmp_path})")
|
||||||
|
return True
|
||||||
|
except DokuWikiError as e:
|
||||||
|
error_msg = f"Fehler beim Hochladen der Mediendatei {media_id}: {e}"
|
||||||
|
_logger.error(error_msg)
|
||||||
|
raise UserError(error_msg)
|
||||||
|
finally:
|
||||||
|
# Temporäre Datei löschen
|
||||||
|
if tmp_path and os.path.exists(tmp_path):
|
||||||
|
try:
|
||||||
|
os.unlink(tmp_path)
|
||||||
|
except Exception:
|
||||||
|
_logger.warning(f"Konnte temporäre Datei nicht löschen: {tmp_path}")
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def get_media_url(self, media_id):
|
||||||
|
"""
|
||||||
|
Generiert die vollständige URL zu einer DokuWiki-Mediendatei.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
media_id (str): DokuWiki Media-ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Vollständige URL zur Mediendatei
|
||||||
|
"""
|
||||||
|
IrConfigParameter = self.env['ir.config_parameter'].sudo()
|
||||||
|
wiki_url = IrConfigParameter.get_param('dokuwiki.url', '')
|
||||||
|
|
||||||
|
if not wiki_url:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# Entferne trailing slash falls vorhanden
|
||||||
|
wiki_url = wiki_url.rstrip('/')
|
||||||
|
|
||||||
|
# Ersetze : durch / für Media-Pfad
|
||||||
|
media_path = media_id.replace(':', '/')
|
||||||
|
|
||||||
|
return f"{wiki_url}/lib/exe/fetch.php?media={media_id}"
|
||||||
974
open_workshop_dokuwiki/models/maintenance_equipment.py
Normal file
|
|
@ -0,0 +1,974 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
from datetime import datetime
|
||||||
|
from odoo import api, fields, models, _
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Cache für Wiki-Templates (wird beim Server-Neustart geleert)
|
||||||
|
_template_cache = {}
|
||||||
|
|
||||||
|
class MaintenanceEquipment(models.Model):
|
||||||
|
"""
|
||||||
|
Erweitert das Maintenance Equipment Modell um DokuWiki-Integration.
|
||||||
|
Diese Klasse ermöglicht die Synchronisation von Werkstatt-Equipment-Daten mit einem
|
||||||
|
DokuWiki-System. Jedes Equipment erhält eine zentrale Dokumentationsseite sowie
|
||||||
|
verschiedene Ansichtsseiten (nach Bereich, später auch nach Einsatzzweck).
|
||||||
|
Hauptfunktionen:
|
||||||
|
- Automatische Generierung von Wiki-Seiten aus Equipment-Daten
|
||||||
|
- Template-basierte Wiki-Seiten-Erstellung (verwendet c_template aus DokuWiki)
|
||||||
|
- Bild-Upload und Einbindung in Wiki-Seiten
|
||||||
|
- Zentrale Dokumentationsseite pro Equipment (manuell erweiterbar)
|
||||||
|
- Bereichs-spezifische Übersichtsseiten (automatisch aktualisiert)
|
||||||
|
- Automatische Übersichtstabelle aller Equipment (DataTable mit Sortierung/Filterung)
|
||||||
|
- Automatische Synchronisation bei Feldänderungen (optional)
|
||||||
|
Wiki-Seitenstruktur:
|
||||||
|
- {equipment_namespace}:{overview_page_id} - Übersichtstabelle (von Odoo generiert)
|
||||||
|
- {equipment_namespace}:odoo-status:{wiki_doku_id} - Status-Seite (von Odoo generiert, nur lesbar)
|
||||||
|
- {equipment_namespace}:odoo-status:c_template - Template für Status-Seiten
|
||||||
|
- {equipment_namespace}:{bereich}:{wiki_doku_id} - Benutzer-Dokumentation (NICHT von Odoo erstellt!)
|
||||||
|
Neue Felder:
|
||||||
|
- image_1920: Bild des Equipment (wird automatisch ins Wiki hochgeladen)
|
||||||
|
- wiki_doku_id: Eindeutige ID für Wiki-Dokumentation (aus Equipment-Name generiert)
|
||||||
|
- wiki_page_url: Berechnete URL zur Wiki-Seite
|
||||||
|
- wiki_synced: Status der Synchronisation
|
||||||
|
- wiki_last_sync: Zeitstempel der letzten Synchronisation
|
||||||
|
- wiki_auto_sync: Schalter für automatische Synchronisation bei Änderungen
|
||||||
|
Template-System:
|
||||||
|
Die Wiki-Seiten werden aus einem Template ({equipment_namespace}:odoo-status:c_template)
|
||||||
|
generiert, das folgende Platzhalter unterstützt:
|
||||||
|
- {feldname} - Direkte Equipment-Felder (z.B. {name}, {serial_no})
|
||||||
|
- {ows_machine_id.feldname} - Maschinenfelder (z.B. {ows_machine_id.power})
|
||||||
|
- {wiki_status_page} - ID der Odoo-Status-Seite
|
||||||
|
- {wiki_status_link} - DokuWiki-Link zur Odoo-Status-Seite
|
||||||
|
- {image} - Bild-Einbindung (wird automatisch hochgeladen)
|
||||||
|
- {tags} - Komma-separierte Tag-Liste
|
||||||
|
- {sync_datetime} - Zeitstempel der Synchronisation
|
||||||
|
Übersichtstabelle:
|
||||||
|
Generiert eine zentrale Übersichtstabelle aller Equipment mit konfigurierbaren
|
||||||
|
Spalten. Die Tabelle verwendet das DokuWiki DataTable-Plugin für interaktive
|
||||||
|
Sortierung und Filterung. Konfiguration über Systemparameter:
|
||||||
|
- dokuwiki.overview_page_id - Seiten-ID der Übersicht
|
||||||
|
- dokuwiki.overview_title - Titel der Seite
|
||||||
|
- dokuwiki.overview_columns - Spaltenüberschriften (pipe-separiert)
|
||||||
|
- dokuwiki.overview_column_data - Spaltendaten mit Platzhaltern
|
||||||
|
Verwendung:
|
||||||
|
1. Equipment anlegen/bearbeiten in Odoo
|
||||||
|
2. Bereich (ows_area_id) setzen (erforderlich für Wiki-Sync)
|
||||||
|
3. Optional: Bild hochladen (wird automatisch ins Wiki übertragen)
|
||||||
|
4. "Zum Wiki synchronisieren" klicken (oder automatisch bei Änderungen)
|
||||||
|
5. Wiki-Seiten werden erstellt/aktualisiert
|
||||||
|
6. "Wiki-Seite öffnen" für direkten Browser-Zugriff
|
||||||
|
Notes:
|
||||||
|
- Zentrale Dokumentationsseite wird nur beim ersten Sync erstellt
|
||||||
|
- Bereichsansichten werden bei jedem Sync aktualisiert
|
||||||
|
- Bilder werden als {equipment_namespace}:{bereich}:{wiki_doku_id}.jpg gespeichert (parallele Struktur zu Pages)
|
||||||
|
- Wiki-Namen werden normalisiert (Umlaute, Sonderzeichen, Kleinschreibung)
|
||||||
|
- Bei fehlendem Template wird Fallback-Inhalt verwendet
|
||||||
|
- Übersichtstabelle kann manuell oder automatisch aktualisiert werden
|
||||||
|
"""
|
||||||
|
_inherit = 'maintenance.equipment'
|
||||||
|
|
||||||
|
# Bild-Feld
|
||||||
|
image_1920 = fields.Image(
|
||||||
|
string='Bild',
|
||||||
|
max_width=1920,
|
||||||
|
max_height=1920,
|
||||||
|
help='Equipment-Bild (wird beim Wiki-Sync hochgeladen)'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Wiki-Felder
|
||||||
|
wiki_status_id = fields.Char(
|
||||||
|
string='Wiki Status-ID',
|
||||||
|
readonly=True,
|
||||||
|
help='Eindeutige ID für die Odoo-Status-Seite (automatisch generiert aus Name + Seriennummer, z.B. "formatkreissaege-eq001")'
|
||||||
|
)
|
||||||
|
wiki_doku_id = fields.Char(
|
||||||
|
string='Wiki Dokumentations-ID',
|
||||||
|
readonly=False,
|
||||||
|
help='ID für die Benutzer-Dokumentationsseite (editierbar, initial aus Equipment-Namen generiert, z.B. "formatkreissaege"). Mehrere Equipment können die gleiche ID nutzen.'
|
||||||
|
)
|
||||||
|
wiki_page_url = fields.Char(
|
||||||
|
string='Wiki-Seiten URL',
|
||||||
|
compute='_compute_wiki_page_url',
|
||||||
|
help='URL zur Wiki-Seite im Browser'
|
||||||
|
)
|
||||||
|
wiki_synced = fields.Boolean(
|
||||||
|
string='Wiki synchronisiert',
|
||||||
|
default=False,
|
||||||
|
help='Gibt an, ob die Wiki-Seite aktuell ist'
|
||||||
|
)
|
||||||
|
wiki_last_sync = fields.Datetime(
|
||||||
|
string='Letzte Wiki-Synchronisation',
|
||||||
|
readonly=True,
|
||||||
|
help='Zeitpunkt der letzten erfolgreichen Wiki-Synchronisation'
|
||||||
|
)
|
||||||
|
wiki_auto_sync = fields.Boolean(
|
||||||
|
string='Automatische Wiki-Synchronisation',
|
||||||
|
default=True,
|
||||||
|
help='Bei Änderungen automatisch zum Wiki synchronisieren'
|
||||||
|
)
|
||||||
|
wiki_doku_link = fields.Html(
|
||||||
|
string='Wiki Doku',
|
||||||
|
compute='_compute_wiki_doku_link',
|
||||||
|
sanitize=False,
|
||||||
|
help='Klickbarer Link zur Wiki-Dokumentationsseite'
|
||||||
|
)
|
||||||
|
wiki_status_link = fields.Html(
|
||||||
|
string='Wiki Status',
|
||||||
|
compute='_compute_wiki_status_link',
|
||||||
|
sanitize=False,
|
||||||
|
help='Klickbarer Link zur Wiki-Status-Seite (Odoo-Daten)'
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.depends('wiki_doku_id', 'wiki_page_url')
|
||||||
|
def _compute_wiki_doku_link(self):
|
||||||
|
"""
|
||||||
|
Generiert einen HTML-Link mit wiki_doku_id als Text und wiki_page_url als Ziel.
|
||||||
|
"""
|
||||||
|
for record in self:
|
||||||
|
if record.wiki_doku_id and record.wiki_page_url:
|
||||||
|
record.wiki_doku_link = f'<a href="{record.wiki_page_url}" target="_blank" style="color: #007bff;">{record.wiki_doku_id}</a>'
|
||||||
|
else:
|
||||||
|
record.wiki_doku_link = ''
|
||||||
|
|
||||||
|
@api.depends('wiki_status_id', 'name', 'comp_serial_no', 'ows_area_id')
|
||||||
|
def _compute_wiki_status_link(self):
|
||||||
|
"""
|
||||||
|
Generiert einen HTML-Link mit wiki_status_id als Text zur Odoo-Status-Seite.
|
||||||
|
"""
|
||||||
|
dokuwiki_client = self.env['dokuwiki.client']
|
||||||
|
for record in self:
|
||||||
|
# wiki_status_id dynamisch berechnen falls noch nicht gesetzt
|
||||||
|
wiki_status_id = record.wiki_status_id or record._get_wiki_status_id()
|
||||||
|
if wiki_status_id and record.ows_area_id:
|
||||||
|
status_page_id = record._get_wiki_status_page_id()
|
||||||
|
if status_page_id:
|
||||||
|
status_url = dokuwiki_client.get_wiki_url(status_page_id)
|
||||||
|
record.wiki_status_link = f'<a href="{status_url}" target="_blank" style="color: #007bff;">{wiki_status_id}</a>'
|
||||||
|
else:
|
||||||
|
record.wiki_status_link = ''
|
||||||
|
else:
|
||||||
|
record.wiki_status_link = ''
|
||||||
|
|
||||||
|
@api.depends('ows_area_id')
|
||||||
|
def _compute_wiki_page_url(self):
|
||||||
|
"""
|
||||||
|
Berechnet die URL zur Haupt-Wiki-Seite (nach Bereich).
|
||||||
|
"""
|
||||||
|
dokuwiki_client = self.env['dokuwiki.client']
|
||||||
|
|
||||||
|
for record in self:
|
||||||
|
wiki_doku_id = record._get_wiki_doku_id()
|
||||||
|
if wiki_doku_id and record.ows_area_id:
|
||||||
|
page_id = record._get_wiki_page_id_by_area()
|
||||||
|
record.wiki_page_url = dokuwiki_client.get_wiki_url(page_id)
|
||||||
|
else:
|
||||||
|
record.wiki_page_url = False
|
||||||
|
|
||||||
|
def _get_wiki_status_id(self):
|
||||||
|
"""
|
||||||
|
Generiert die Wiki-Status-ID aus Equipment-Namen und comp_serial_no.
|
||||||
|
Format: normalisierter_name-comp_serial_no (z.B. "formatkreissaege-eq001")
|
||||||
|
|
||||||
|
Diese ID ist eindeutig und wird für die Odoo-Status-Seite und Media-Dateien verwendet.
|
||||||
|
Die comp_serial_no wird automatisch generiert falls noch nicht vorhanden.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Wiki-Status-ID oder False wenn kein Name vorhanden
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
|
||||||
|
# Verwende wiki_status_id falls bereits gesetzt
|
||||||
|
if self.wiki_status_id:
|
||||||
|
return self.wiki_status_id
|
||||||
|
|
||||||
|
# Name ist erforderlich
|
||||||
|
if not self.name:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# comp_serial_no automatisch generieren falls nicht vorhanden
|
||||||
|
if not self.comp_serial_no:
|
||||||
|
self.generate_serial_no()
|
||||||
|
# Refresh nach generate_serial_no
|
||||||
|
self.refresh()
|
||||||
|
|
||||||
|
# wiki_status_id aus Name + comp_serial_no generieren
|
||||||
|
normalized_name = self._normalize_wiki_name(self.name)
|
||||||
|
normalized_serial = self._normalize_wiki_name(self.comp_serial_no)
|
||||||
|
return f"{normalized_name}-{normalized_serial}"
|
||||||
|
|
||||||
|
def _get_wiki_doku_id(self):
|
||||||
|
"""
|
||||||
|
Generiert die Wiki-Dokumentations-ID aus Equipment-Namen (ohne Seriennummer).
|
||||||
|
Format: normalisierter_name (z.B. "formatkreissaege")
|
||||||
|
|
||||||
|
Diese ID ist editierbar und wird für die Benutzer-Dokumentationsseite verwendet.
|
||||||
|
Mehrere Equipment können die gleiche wiki_doku_id haben (z.B. alle Akkuschrauber).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Wiki-Doku-ID oder False wenn kein Name vorhanden
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
|
||||||
|
# Verwende wiki_doku_id falls bereits gesetzt
|
||||||
|
if self.wiki_doku_id:
|
||||||
|
return self.wiki_doku_id
|
||||||
|
|
||||||
|
# Name ist erforderlich
|
||||||
|
if not self.name:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# wiki_doku_id nur aus Name generieren (Duplikate sind erwünscht)
|
||||||
|
normalized_name = self._normalize_wiki_name(self.name)
|
||||||
|
return normalized_name
|
||||||
|
|
||||||
|
def _get_wiki_page_id_by_area(self):
|
||||||
|
"""
|
||||||
|
Generiert die Wiki-Page-ID für die Benutzer-Dokumentationsseite (nach Bereich).
|
||||||
|
Format: {equipment_namespace}:{area_name}:{wiki_doku_id}
|
||||||
|
|
||||||
|
WICHTIG: Diese Seite wird NICHT von Odoo erstellt! Sie wird nur in der Übersichtstabelle
|
||||||
|
verlinkt. Benutzer erstellen sie manuell durch Klick auf den Link.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Page-ID für Benutzer-Dokumentation
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
|
||||||
|
if not self.ows_area_id:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Equipment-Namespace aus Systemparameter laden
|
||||||
|
IrConfigParameter = self.env['ir.config_parameter'].sudo()
|
||||||
|
equipment_namespace = IrConfigParameter.get_param(
|
||||||
|
'dokuwiki.equipment_namespace',
|
||||||
|
default='werkstatt:ausstattung'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Area-Name normalisieren (Umlaute, Leerzeichen, Sonderzeichen)
|
||||||
|
area_name = self._normalize_wiki_name(self.ows_area_id.name)
|
||||||
|
|
||||||
|
wiki_doku_id = self._get_wiki_doku_id()
|
||||||
|
return f"{equipment_namespace}:{area_name}:{wiki_doku_id}"
|
||||||
|
|
||||||
|
def _get_wiki_status_page_id(self):
|
||||||
|
"""
|
||||||
|
Generiert die Wiki-Page-ID für die Odoo-Status-Seite (nur lesbar für Benutzer).
|
||||||
|
Format: {equipment_namespace}:odoo-status:{wiki_status_id}
|
||||||
|
Diese Seite wird von Odoo generiert und kann mit dem include-Plugin eingebunden werden.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Page-ID der Odoo-Status-Seite
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
|
||||||
|
# Namespace aus Systemparameter laden (verwendet jetzt central_documentation_namespace)
|
||||||
|
IrConfigParameter = self.env['ir.config_parameter'].sudo()
|
||||||
|
status_namespace = IrConfigParameter.get_param(
|
||||||
|
'dokuwiki.central_documentation_namespace',
|
||||||
|
default='werkstatt:ausstattung:odoo-status'
|
||||||
|
)
|
||||||
|
|
||||||
|
wiki_status_id = self._get_wiki_status_id()
|
||||||
|
return f"{status_namespace}:{wiki_status_id}"
|
||||||
|
|
||||||
|
def _normalize_wiki_name(self, name):
|
||||||
|
"""
|
||||||
|
Normalisiert einen Namen für DokuWiki-Page-IDs.
|
||||||
|
- Umlaute ersetzen
|
||||||
|
- Leerzeichen und Sonderzeichen durch Bindestriche ersetzen
|
||||||
|
- Kleinbuchstaben
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): Original-Name
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Normalisierter Name
|
||||||
|
"""
|
||||||
|
if not name:
|
||||||
|
return "unnamed"
|
||||||
|
|
||||||
|
# Umlaute ersetzen
|
||||||
|
replacements = {
|
||||||
|
'ä': 'ae', 'ö': 'oe', 'ü': 'ue',
|
||||||
|
'Ä': 'ae', 'Ö': 'oe', 'Ü': 'ue',
|
||||||
|
'ß': 'ss'
|
||||||
|
}
|
||||||
|
for old, new in replacements.items():
|
||||||
|
name = name.replace(old, new)
|
||||||
|
|
||||||
|
# Nur alphanumerische Zeichen und Bindestriche
|
||||||
|
name = re.sub(r'[^a-zA-Z0-9\-_]', '-', name)
|
||||||
|
|
||||||
|
# Mehrfache Bindestriche durch einen ersetzen
|
||||||
|
name = re.sub(r'-+', '-', name)
|
||||||
|
|
||||||
|
# Führende/Trailing Bindestriche entfernen
|
||||||
|
name = name.strip('-')
|
||||||
|
|
||||||
|
# Kleinbuchstaben
|
||||||
|
name = name.lower()
|
||||||
|
|
||||||
|
return name or "unnamed"
|
||||||
|
|
||||||
|
def _render_template_from_wiki(self, view_type='area'):
|
||||||
|
"""
|
||||||
|
Lädt c_template aus DokuWiki und ersetzt Platzhalter mit Odoo-Feldwerten.
|
||||||
|
Template-Pfad wird dynamisch aus dem Systemparameter 'dokuwiki.central_documentation_namespace' generiert.
|
||||||
|
Standard: werkstatt:ausstattung:odoo-status:c_template
|
||||||
|
|
||||||
|
Platzhalter-Format:
|
||||||
|
- {feldname} für maintenance.equipment Felder, z.B. {name}, {serial_no}
|
||||||
|
- {ows_machine_id.feldname} für ows.machine Felder, z.B. {ows_machine_id.power}
|
||||||
|
- {wiki_status_page} für die Odoo-Status-Seite ID
|
||||||
|
- {wiki_status_link} für Link zur Odoo-Status-Seite
|
||||||
|
- {ows_area} für Bereichsname
|
||||||
|
- {category} für Kategoriename
|
||||||
|
- {sync_datetime} für aktuelles Datum/Zeit
|
||||||
|
|
||||||
|
Args:
|
||||||
|
view_type (str): 'area' oder 'purpose'
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Gerenderter Wiki-Markup-Inhalt
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
global _template_cache
|
||||||
|
dokuwiki_client = self.env['dokuwiki.client']
|
||||||
|
|
||||||
|
# Template-Pfad aus Systemparameter generieren
|
||||||
|
IrConfigParameter = self.env['ir.config_parameter'].sudo()
|
||||||
|
status_namespace = IrConfigParameter.get_param(
|
||||||
|
'dokuwiki.central_documentation_namespace',
|
||||||
|
default='werkstatt:ausstattung:odoo-status'
|
||||||
|
)
|
||||||
|
template_page_id = f"{status_namespace}:c_template"
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Template aus Cache oder Wiki laden
|
||||||
|
if template_page_id not in _template_cache:
|
||||||
|
template_content = dokuwiki_client.get_page(template_page_id)
|
||||||
|
if not template_content:
|
||||||
|
_logger.warning(f"Template {template_page_id} nicht gefunden, verwende Fallback")
|
||||||
|
return self._generate_wiki_main_page_content_fallback(view_type)
|
||||||
|
|
||||||
|
_template_cache[template_page_id] = template_content
|
||||||
|
_logger.info(f"Template geladen und gecacht: {template_page_id} ({len(template_content)} Zeichen)")
|
||||||
|
else:
|
||||||
|
_logger.debug(f"Template aus Cache verwendet: {template_page_id}")
|
||||||
|
|
||||||
|
template_content = _template_cache[template_page_id]
|
||||||
|
|
||||||
|
# Werte-Dictionary vorbereiten
|
||||||
|
values = self._prepare_template_values(view_type)
|
||||||
|
|
||||||
|
# Debug-Logging für partner_ref
|
||||||
|
if 'partner_ref' in values:
|
||||||
|
_logger.info(f"partner_ref Wert für {self.name}: '{values['partner_ref']}'")
|
||||||
|
|
||||||
|
# Platzhalter ersetzen
|
||||||
|
rendered_content = template_content
|
||||||
|
for key, value in values.items():
|
||||||
|
placeholder = '{' + key + '}'
|
||||||
|
rendered_content = rendered_content.replace(placeholder, str(value or ''))
|
||||||
|
|
||||||
|
return rendered_content
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
_logger.warning(f"Fehler beim Laden des Templates {template_page_id}: {e}")
|
||||||
|
return self._generate_wiki_main_page_content_fallback(view_type)
|
||||||
|
|
||||||
|
def _prepare_template_values(self, view_type='area'):
|
||||||
|
"""
|
||||||
|
Bereitet Dictionary mit allen verfügbaren Feldwerten für Template-Rendering vor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
view_type (str): 'area' oder 'purpose'
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Dictionary mit Feldnamen als Keys und Werten
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
|
||||||
|
# Odoo-Status-Seite
|
||||||
|
status_page_id = self._get_wiki_status_page_id()
|
||||||
|
|
||||||
|
# Odoo Base URL
|
||||||
|
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url', 'http://localhost:8069')
|
||||||
|
odoo_equipment_url = f"{base_url}/web#id={self.id}&model=maintenance.equipment&view_type=form"
|
||||||
|
|
||||||
|
# Wiki Page IDs
|
||||||
|
wiki_page_id = self._get_wiki_page_id_by_area() if self.ows_area_id else ''
|
||||||
|
|
||||||
|
# Basis-Werte für maintenance.equipment
|
||||||
|
values = {
|
||||||
|
# Spezielle Werte
|
||||||
|
'wiki_status_page': status_page_id,
|
||||||
|
'wiki_status_link': f"[[{status_page_id}|📊 Odoo Status]]",
|
||||||
|
'wiki_page_id': wiki_page_id, # Page-ID der Hauptseite (nach Bereich)
|
||||||
|
'wiki_page_url': self.wiki_page_url or '', # Externe URL zur Hauptseite
|
||||||
|
'odoo_link': f"[[{odoo_equipment_url}|🔗 In Odoo öffnen]]",
|
||||||
|
'odoo_url': odoo_equipment_url,
|
||||||
|
'sync_datetime': datetime.now().strftime('%d.%m.%Y %H:%M'),
|
||||||
|
|
||||||
|
# Standard Equipment-Felder
|
||||||
|
'name': self.name or '',
|
||||||
|
'serial_no': self.serial_no or '',
|
||||||
|
'model': self.model or '',
|
||||||
|
'ows_area': self.ows_area_id.name if self.ows_area_id else '',
|
||||||
|
'category': self.category_id.name if self.category_id else '',
|
||||||
|
'status': self.status_id.name if self.status_id else '',
|
||||||
|
'status_smiley': self.status_id.smiley if self.status_id and self.status_id.smiley else '',
|
||||||
|
'location': self.location or '',
|
||||||
|
'assign_date': self.assign_date.strftime('%d.%m.%Y') if self.assign_date else '',
|
||||||
|
'cost': str(self.cost) if self.cost else '',
|
||||||
|
'warranty_date': self.warranty_date.strftime('%d.%m.%Y') if self.warranty_date else '',
|
||||||
|
'color': str(self.color) if self.color else '',
|
||||||
|
'note': self.note or '',
|
||||||
|
'partner_id': self.partner_id.name if self.partner_id else '',
|
||||||
|
'partner_ref': self.partner_ref or '',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Tags hinzufügen (falls vorhanden)
|
||||||
|
if hasattr(self, 'tag_ids') and self.tag_ids:
|
||||||
|
# Komma-separierte Liste für Template
|
||||||
|
values['tags'] = ', '.join(self.tag_ids.mapped('name'))
|
||||||
|
# DokuWiki Bullet-Liste für Tabellenzellen
|
||||||
|
values['tags_list'] = ' '.join(self.tag_ids.mapped('name')) if self.tag_ids else ''
|
||||||
|
# Anzahl der Tags
|
||||||
|
values['tags_count'] = str(len(self.tag_ids))
|
||||||
|
else:
|
||||||
|
values['tags'] = ''
|
||||||
|
values['tags_list'] = ''
|
||||||
|
values['tags_count'] = '0'
|
||||||
|
|
||||||
|
# ows.machine Felder hinzufügen (falls verknüpft)
|
||||||
|
if self.ows_machine_id:
|
||||||
|
machine = self.ows_machine_id
|
||||||
|
# Nur existierende Felder hinzufügen
|
||||||
|
ows_machine_fields = {}
|
||||||
|
|
||||||
|
# Standard-Felder von ows.machine
|
||||||
|
if hasattr(machine, 'name') and machine.name:
|
||||||
|
ows_machine_fields['ows_machine_id.name'] = machine.name
|
||||||
|
if hasattr(machine, 'model') and machine.model:
|
||||||
|
ows_machine_fields['ows_machine_id.model'] = machine.model
|
||||||
|
if hasattr(machine, 'serial_no') and machine.serial_no:
|
||||||
|
ows_machine_fields['ows_machine_id.serial_no'] = machine.serial_no
|
||||||
|
if hasattr(machine, 'location') and machine.location:
|
||||||
|
ows_machine_fields['ows_machine_id.location'] = machine.location
|
||||||
|
if hasattr(machine, 'note'):
|
||||||
|
ows_machine_fields['ows_machine_id.note'] = machine.note or ''
|
||||||
|
if hasattr(machine, 'ows_category') and machine.ows_category:
|
||||||
|
ows_machine_fields['ows_machine_id.category'] = machine.ows_category
|
||||||
|
if hasattr(machine, 'ows_category_icon') and machine.ows_category_icon:
|
||||||
|
ows_machine_fields['ows_machine_id.category_icon'] = machine.ows_category_icon
|
||||||
|
|
||||||
|
values.update(ows_machine_fields)
|
||||||
|
|
||||||
|
# Bild-Upload und Referenz (falls vorhanden)
|
||||||
|
if self.image_1920:
|
||||||
|
wiki_status_id = self._get_wiki_status_id()
|
||||||
|
# Media-ID mit gleicher Namespace-Struktur wie Pages (DokuWiki Best Practice)
|
||||||
|
IrConfigParameter = self.env['ir.config_parameter'].sudo()
|
||||||
|
equipment_namespace = IrConfigParameter.get_param(
|
||||||
|
'dokuwiki.equipment_namespace',
|
||||||
|
default='werkstatt:ausstattung'
|
||||||
|
)
|
||||||
|
# Area-Name normalisieren (wie bei Pages)
|
||||||
|
area_name = self._normalize_wiki_name(self.ows_area_id.name) if self.ows_area_id else 'unbekannt'
|
||||||
|
# Bild ins Wiki hochladen (erkenne Extension aus Bytes)
|
||||||
|
try:
|
||||||
|
import base64
|
||||||
|
import imghdr
|
||||||
|
|
||||||
|
# image_1920 ist base64-kodiert, in bytes umwandeln für XML-RPC
|
||||||
|
image_bytes = base64.b64decode(self.image_1920)
|
||||||
|
|
||||||
|
# Typ/Extension erkennen (jpeg -> .jpg)
|
||||||
|
img_type = None
|
||||||
|
try:
|
||||||
|
img_type = imghdr.what(None, image_bytes)
|
||||||
|
except Exception:
|
||||||
|
img_type = None
|
||||||
|
|
||||||
|
if img_type == 'jpeg':
|
||||||
|
ext = '.jpg'
|
||||||
|
elif img_type:
|
||||||
|
ext = f'.{img_type}'
|
||||||
|
elif image_bytes.startswith(b'%PDF'):
|
||||||
|
ext = '.pdf'
|
||||||
|
else:
|
||||||
|
ext = '.bin'
|
||||||
|
|
||||||
|
media_id = f"{equipment_namespace}:{area_name}:{wiki_status_id}{ext}"
|
||||||
|
|
||||||
|
dokuwiki_client = self.env['dokuwiki.client']
|
||||||
|
dokuwiki_client.upload_media(media_id, image_bytes, overwrite=True)
|
||||||
|
|
||||||
|
# DokuWiki Image-Syntax: {{namespace:file.jpg?300}}
|
||||||
|
values['image'] = f"{{{{:{media_id}?100}}}}"
|
||||||
|
values['image_large'] = f"{{{{:{media_id}}}}}"
|
||||||
|
values['image_id'] = media_id
|
||||||
|
_logger.info(f"Bild hochgeladen: {media_id}")
|
||||||
|
except Exception as e:
|
||||||
|
_logger.error(f"Fehler beim Bild-Upload: {e}", exc_info=True)
|
||||||
|
values['image'] = ''
|
||||||
|
values['image_large'] = ''
|
||||||
|
values['image_id'] = ''
|
||||||
|
else:
|
||||||
|
values['image'] = ''
|
||||||
|
values['image_large'] = ''
|
||||||
|
values['image_id'] = ''
|
||||||
|
|
||||||
|
# View-Typ spezifische Werte
|
||||||
|
if view_type == 'area':
|
||||||
|
values['view_type'] = 'Bereich'
|
||||||
|
values['view_name'] = self.ows_area_id.name if self.ows_area_id else 'Unbekannt'
|
||||||
|
else:
|
||||||
|
values['view_type'] = 'Einsatzzweck'
|
||||||
|
values['view_name'] = 'TODO: Einsatzzweck'
|
||||||
|
|
||||||
|
return values
|
||||||
|
|
||||||
|
def _generate_wiki_main_page_content_fallback(self, view_type='area'):
|
||||||
|
"""
|
||||||
|
Fallback-Methode: Generiert hart-codierten Wiki-Markup-Inhalt,
|
||||||
|
wenn c_template.txt nicht verfügbar ist.
|
||||||
|
Dieser Content wird in odoo-status/ gespeichert und ist nur lesbar.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
view_type (str): 'area' oder 'purpose'
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Wiki-Markup-Inhalt für odoo-status Seite
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
|
||||||
|
# Header je nach View-Typ
|
||||||
|
if view_type == 'area':
|
||||||
|
view_name = self.ows_area_id.name if self.ows_area_id else "Unbekannt"
|
||||||
|
view_label = "Bereich"
|
||||||
|
else:
|
||||||
|
# Später für Einsatzzweck
|
||||||
|
view_name = "TODO: Einsatzzweck"
|
||||||
|
view_label = "Einsatzzweck"
|
||||||
|
|
||||||
|
content = f"""====== {self.name} - Odoo Status ======
|
||||||
|
|
||||||
|
**{view_label}:** {view_name}
|
||||||
|
**Kategorie:** {self.category_id.name if self.category_id else 'Keine'}
|
||||||
|
**Seriennummer:** {self.serial_no or 'Keine'}
|
||||||
|
**Modell:** {self.model or 'Keine'}
|
||||||
|
**Status:** {self.status_id.name if self.status_id else 'Unbekannt'} {self.status_id.smiley if self.status_id and self.status_id.smiley else ''}
|
||||||
|
|
||||||
|
===== Technische Daten =====
|
||||||
|
|
||||||
|
**Hersteller:** {self.partner_id.name if self.partner_id else 'Unbekannt'}
|
||||||
|
**Standort:** {self.location or 'Nicht angegeben'}
|
||||||
|
**Kosten:** {self.cost if self.cost else 'Nicht angegeben'}
|
||||||
|
**Garantie bis:** {self.warranty_date.strftime('%d.%m.%Y') if self.warranty_date else 'Keine'}
|
||||||
|
|
||||||
|
===== Notizen =====
|
||||||
|
|
||||||
|
{self.note or 'Keine Notizen'}
|
||||||
|
|
||||||
|
----
|
||||||
|
//Diese Seite wird automatisch von Odoo generiert und ist nur lesbar.//
|
||||||
|
//Letzte Synchronisation: {datetime.now().strftime('%d.%m.%Y %H:%M')} //
|
||||||
|
"""
|
||||||
|
return content
|
||||||
|
|
||||||
|
def _generate_wiki_main_page_content(self, view_type='area'):
|
||||||
|
"""
|
||||||
|
Generiert den Wiki-Markup-Inhalt für die Odoo-Status-Seite.
|
||||||
|
Verwendet c_template.txt aus DokuWiki falls verfügbar, sonst Fallback.
|
||||||
|
Diese Seite wird in odoo-status/ gespeichert und ist nur lesbar.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
view_type (str): 'area' oder 'purpose'
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Wiki-Markup-Inhalt für odoo-status Seite
|
||||||
|
"""
|
||||||
|
return self._render_template_from_wiki(view_type)
|
||||||
|
|
||||||
|
def _generate_wiki_doku_page_content(self):
|
||||||
|
"""
|
||||||
|
DEPRECATED: Diese Methode wird nicht mehr verwendet.
|
||||||
|
Odoo erstellt keine Benutzer-Dokumentationsseiten mehr.
|
||||||
|
Benutzer erstellen diese Seiten manuell durch Klick auf den Link in der Übersichtstabelle.
|
||||||
|
"""
|
||||||
|
_logger.warning("_generate_wiki_doku_page_content() wurde aufgerufen - diese Methode ist deprecated!")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def sync_to_dokuwiki(self):
|
||||||
|
"""
|
||||||
|
Synchronisiert Equipment-Daten zum DokuWiki.
|
||||||
|
|
||||||
|
WICHTIG: Erstellt NUR die Odoo-Status-Seite in odoo-status/ Namespace!
|
||||||
|
Benutzer-Dokumentationsseiten werden NICHT erstellt - diese erstellen Benutzer
|
||||||
|
manuell durch Klick auf den Link in der Übersichtstabelle.
|
||||||
|
|
||||||
|
Erstellt/aktualisiert:
|
||||||
|
- wiki_doku_id beim ersten Sync (falls noch nicht gesetzt)
|
||||||
|
- Odoo-Status-Seite (odoo-status:wiki_doku_id) - wird immer aktualisiert
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True bei Erfolg
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
|
||||||
|
if not self.ows_area_id:
|
||||||
|
raise UserError("Bereich muss gesetzt sein für Wiki-Synchronisation!")
|
||||||
|
|
||||||
|
if not self.name:
|
||||||
|
raise UserError("Equipment-Name muss gesetzt sein für Wiki-Synchronisation!")
|
||||||
|
|
||||||
|
# wiki_status_id beim ersten Sync setzen (aus Equipment-Namen und comp_serial_no generieren)
|
||||||
|
if not self.wiki_status_id:
|
||||||
|
generated_status_id = self._get_wiki_status_id()
|
||||||
|
if not generated_status_id:
|
||||||
|
raise UserError("wiki_status_id konnte nicht generiert werden!")
|
||||||
|
self.write({'wiki_status_id': generated_status_id})
|
||||||
|
_logger.info(f"Wiki-Status-ID für {self.name} gesetzt: {generated_status_id}")
|
||||||
|
|
||||||
|
# wiki_doku_id beim ersten Sync setzen (aus Equipment-Namen generieren, editierbar)
|
||||||
|
if not self.wiki_doku_id:
|
||||||
|
generated_doku_id = self._get_wiki_doku_id()
|
||||||
|
if not generated_doku_id:
|
||||||
|
generated_doku_id = self._normalize_wiki_name(self.name)
|
||||||
|
self.write({'wiki_doku_id': generated_doku_id})
|
||||||
|
_logger.info(f"Wiki-Doku-ID für {self.name} gesetzt: {generated_doku_id}")
|
||||||
|
|
||||||
|
dokuwiki_client = self.env['dokuwiki.client']
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Odoo-Status-Seite erstellen/aktualisieren (immer beim Sync)
|
||||||
|
status_page_id = self._get_wiki_status_page_id()
|
||||||
|
status_content = self._generate_wiki_main_page_content(view_type='area')
|
||||||
|
|
||||||
|
dokuwiki_client.create_page(
|
||||||
|
status_page_id,
|
||||||
|
status_content,
|
||||||
|
f"Synchronisiert von Odoo: {self.name}"
|
||||||
|
)
|
||||||
|
_logger.info(f"Odoo-Status-Seite für {self.name} aktualisiert: {status_page_id}")
|
||||||
|
|
||||||
|
# WICHTIG: Benutzer-Dokumentationsseiten werden NICHT erstellt!
|
||||||
|
# Diese werden nur in der Übersichtstabelle verlinkt und von Benutzern
|
||||||
|
# manuell erstellt durch Klick auf den roten Link.
|
||||||
|
|
||||||
|
# Sync-Status aktualisieren
|
||||||
|
self.write({
|
||||||
|
'wiki_synced': True,
|
||||||
|
'wiki_last_sync': fields.Datetime.now(),
|
||||||
|
})
|
||||||
|
|
||||||
|
_logger.info(f"Equipment {self.name} erfolgreich zu DokuWiki synchronisiert")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
_logger.error(f"Fehler bei Wiki-Synchronisation für {self.name}: {e}")
|
||||||
|
self.write({'wiki_synced': False})
|
||||||
|
raise UserError(f"Wiki-Synchronisation fehlgeschlagen: {e}")
|
||||||
|
|
||||||
|
def action_sync_to_dokuwiki(self):
|
||||||
|
"""
|
||||||
|
Button-Action für manuelle Wiki-Synchronisation.
|
||||||
|
"""
|
||||||
|
for record in self:
|
||||||
|
record.sync_to_dokuwiki()
|
||||||
|
|
||||||
|
return {
|
||||||
|
'type': 'ir.actions.client',
|
||||||
|
'tag': 'display_notification',
|
||||||
|
'params': {
|
||||||
|
'title': _('Wiki-Synchronisation'),
|
||||||
|
'message': _('Equipment wurde erfolgreich zum Wiki synchronisiert.'),
|
||||||
|
'type': 'success',
|
||||||
|
'sticky': False,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def action_open_wiki(self):
|
||||||
|
"""
|
||||||
|
Button-Action zum Öffnen der Wiki-Seite im Browser.
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
|
||||||
|
if not self.wiki_page_url:
|
||||||
|
raise UserError("Wiki-URL nicht verfügbar. Bitte erst zum Wiki synchronisieren.")
|
||||||
|
|
||||||
|
return {
|
||||||
|
'type': 'ir.actions.act_url',
|
||||||
|
'url': self.wiki_page_url,
|
||||||
|
'target': 'new',
|
||||||
|
}
|
||||||
|
|
||||||
|
def write(self, vals):
|
||||||
|
"""
|
||||||
|
Override write um automatische Synchronisation zu triggern.
|
||||||
|
"""
|
||||||
|
result = super().write(vals)
|
||||||
|
|
||||||
|
# Felder die eine Synchronisation auslösen
|
||||||
|
sync_fields = {'name', 'serial_no', 'ows_area_id', 'category_id', 'status_id',
|
||||||
|
'model', 'partner_id', 'partner_ref', 'location', 'note', 'image_1920', 'tag_ids'}
|
||||||
|
|
||||||
|
# Flag ob Übersichtstabelle aktualisiert werden soll
|
||||||
|
should_update_overview = False
|
||||||
|
|
||||||
|
if sync_fields & set(vals.keys()):
|
||||||
|
# Nur synchronisieren wenn auto_sync aktiviert und bereits synced
|
||||||
|
for record in self:
|
||||||
|
if record.wiki_auto_sync and record.wiki_synced:
|
||||||
|
try:
|
||||||
|
record.sync_to_dokuwiki()
|
||||||
|
should_update_overview = True
|
||||||
|
except Exception as e:
|
||||||
|
_logger.warning(f"Automatische Wiki-Sync fehlgeschlagen: {e}")
|
||||||
|
# Nicht abbrechen, nur loggen
|
||||||
|
else:
|
||||||
|
# Sync-Status zurücksetzen
|
||||||
|
record.write({'wiki_synced': False})
|
||||||
|
|
||||||
|
# Optional: Übersichtstabelle aktualisieren
|
||||||
|
if should_update_overview:
|
||||||
|
IrConfigParameter = self.env['ir.config_parameter'].sudo()
|
||||||
|
auto_update_overview = IrConfigParameter.get_param(
|
||||||
|
'dokuwiki.auto_update_overview_table',
|
||||||
|
default='False'
|
||||||
|
)
|
||||||
|
|
||||||
|
if auto_update_overview.lower() == 'true':
|
||||||
|
try:
|
||||||
|
self.env['maintenance.equipment'].action_sync_overview_table()
|
||||||
|
_logger.info("Übersichtstabelle automatisch aktualisiert")
|
||||||
|
except Exception as e:
|
||||||
|
_logger.warning(f"Automatische Übersichtstabellen-Sync fehlgeschlagen: {e}")
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# Übersichtstabelle (DataTable)
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
def _generate_overview_table_row(self, column_data_template):
|
||||||
|
"""
|
||||||
|
Generiert eine DokuWiki DataTable Zeile für dieses Equipment mit konfigurierbaren Spalten.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
column_data_template (str): Template mit Platzhaltern, z.B. "{status_smiley}|{partner_id}|{model}"
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Wiki-Markup für eine Tabellenzeile
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
|
||||||
|
# Template-Werte vorbereiten (nutzt bestehende Platzhalter-Engine)
|
||||||
|
values = self._prepare_template_values(view_type='area')
|
||||||
|
|
||||||
|
# Platzhalter im Template ersetzen
|
||||||
|
rendered_data = column_data_template
|
||||||
|
for key, value in values.items():
|
||||||
|
placeholder = '{' + key + '}'
|
||||||
|
rendered_data = rendered_data.replace(placeholder, str(value or '-'))
|
||||||
|
|
||||||
|
# Pipe-separierte Werte in DokuWiki-Tabellenzeile umwandeln
|
||||||
|
columns = rendered_data.split('|')
|
||||||
|
row = '| ' + ' | '.join(columns) + ' |'
|
||||||
|
|
||||||
|
return row
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def action_sync_overview_table(self):
|
||||||
|
"""
|
||||||
|
Aktualisiert die Übersichtstabelle in DokuWiki mit allen Equipment-Einträgen.
|
||||||
|
Verwendet konfigurierbare Spalten aus Systemparametern (kein Template mehr nötig).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Ergebnis-Dictionary mit 'total', 'success', 'error_messages'
|
||||||
|
"""
|
||||||
|
IrConfigParameter = self.env['ir.config_parameter'].sudo()
|
||||||
|
dokuwiki_client = self.env['dokuwiki.client']
|
||||||
|
|
||||||
|
# Config Parameter (Defaults werden via _register_hook automatisch gesetzt)
|
||||||
|
overview_page_id = IrConfigParameter.get_param('dokuwiki.overview_page_id')
|
||||||
|
overview_title = IrConfigParameter.get_param('dokuwiki.overview_title')
|
||||||
|
overview_columns = IrConfigParameter.get_param('dokuwiki.overview_columns')
|
||||||
|
overview_column_data = IrConfigParameter.get_param('dokuwiki.overview_column_data')
|
||||||
|
|
||||||
|
# Validierung
|
||||||
|
if not all([overview_page_id, overview_title, overview_columns, overview_column_data]):
|
||||||
|
raise UserError("DokuWiki Übersichtstabellen-Parameter nicht konfiguriert! Bitte Modul neu installieren.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Alle Equipment mit ows_area_id laden (sortiert nach Bereich, dann Name)
|
||||||
|
equipment_records = self.search([
|
||||||
|
('ows_area_id', '!=', False)
|
||||||
|
], order='ows_area_id, name')
|
||||||
|
|
||||||
|
if not equipment_records:
|
||||||
|
raise UserError("Keine Equipment-Einträge mit Bereich gefunden!")
|
||||||
|
|
||||||
|
# Tabellenzeilen generieren
|
||||||
|
table_rows = []
|
||||||
|
for equipment in equipment_records:
|
||||||
|
try:
|
||||||
|
row = equipment._generate_overview_table_row(overview_column_data)
|
||||||
|
table_rows.append(row)
|
||||||
|
except Exception as e:
|
||||||
|
_logger.warning(f"Fehler beim Generieren der Zeile für {equipment.name}: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Rows zusammenfügen
|
||||||
|
table_rows_markup = '\n'.join(table_rows)
|
||||||
|
|
||||||
|
# Spalten-Header generieren (aus overview_columns)
|
||||||
|
column_headers = overview_columns.split('|')
|
||||||
|
header_row = '^ ' + ' ^ '.join(column_headers) + ' ^'
|
||||||
|
|
||||||
|
# Sync-Zeitstempel
|
||||||
|
sync_datetime = datetime.now().strftime('%d.%m.%Y %H:%M:%S')
|
||||||
|
|
||||||
|
# Komplette Seite zusammenbauen (ohne Template)
|
||||||
|
overview_content = f"""====== {overview_title} ======
|
||||||
|
|
||||||
|
Diese Seite wird automatisch aktualisiert.
|
||||||
|
|
||||||
|
**Letztes Update:** {sync_datetime}
|
||||||
|
|
||||||
|
<datatable Equipment-Übersicht>
|
||||||
|
{header_row}
|
||||||
|
{table_rows_markup}
|
||||||
|
</datatable>
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
**Hinweis:** Diese Tabelle ist interaktiv:
|
||||||
|
* Klicke auf die Spaltenköpfe zum Sortieren
|
||||||
|
* Nutze das Suchfeld zum Filtern
|
||||||
|
* Klicke auf die Links in der Spalte "Name" für Details
|
||||||
|
|
||||||
|
**Status:** 🙂 Maschine/Gerät in gutem Zustand, 🙁 - Maschine/Gerät ist Defekt, ❌ Maschine/Gerät wurde ausgemustert
|
||||||
|
|
||||||
|
**Sicherheit:** 🟢 **keine** Einweisungspflicht, 🟡 keine **explizite** Einweisungspflicht, 🔴 explizite Einweisungspflicht
|
||||||
|
|
||||||
|
(Für Einweisungstermine bitte den Thekendienst befragen oder trage dich [[https://hobbyhimmel.de/so-gehts/einweisungen/|hier]] im Kalender für einen Termin ein)
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Seite in DokuWiki speichern
|
||||||
|
dokuwiki_client.create_page(
|
||||||
|
overview_page_id,
|
||||||
|
overview_content,
|
||||||
|
f"Automatisches Update der Übersichtstabelle ({len(table_rows)} Equipment)"
|
||||||
|
)
|
||||||
|
|
||||||
|
_logger.info(f"✓ Übersichtstabelle aktualisiert: {len(table_rows)} Equipment in {overview_page_id}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
'total': len(equipment_records),
|
||||||
|
'success': len(table_rows),
|
||||||
|
'error_messages': '',
|
||||||
|
'overview_url': dokuwiki_client.get_wiki_url(overview_page_id),
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"Fehler beim Aktualisieren der Übersichtstabelle: {str(e)}"
|
||||||
|
_logger.error(error_msg)
|
||||||
|
return {
|
||||||
|
'total': 0,
|
||||||
|
'success': 0,
|
||||||
|
'error_messages': error_msg,
|
||||||
|
'overview_url': '',
|
||||||
|
}
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def action_clear_template_cache(self):
|
||||||
|
"""
|
||||||
|
Leert den Template-Cache und erzwingt ein Neuladen aller Templates aus DokuWiki.
|
||||||
|
Nützlich nach Template-Änderungen im Wiki.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Notification mit Anzahl geleerte Cache-Einträge
|
||||||
|
"""
|
||||||
|
global _template_cache
|
||||||
|
|
||||||
|
count = len(_template_cache)
|
||||||
|
cache_keys = list(_template_cache.keys())
|
||||||
|
_template_cache.clear()
|
||||||
|
|
||||||
|
_logger.info(f"Template-Cache geleert: {count} Einträge ({', '.join(cache_keys)})")
|
||||||
|
|
||||||
|
return {
|
||||||
|
'type': 'ir.actions.client',
|
||||||
|
'tag': 'display_notification',
|
||||||
|
'params': {
|
||||||
|
'title': _('Template-Cache geleert'),
|
||||||
|
'message': _(f'{count} Template(s) aus dem Cache entfernt. Nächster Sync lädt Templates neu.'),
|
||||||
|
'type': 'success',
|
||||||
|
'sticky': False,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def action_reset_wiki_sync_status(self):
|
||||||
|
"""
|
||||||
|
Setzt den Wiki-Synchronisationsstatus für alle Equipment zurück.
|
||||||
|
Nützlich nach Namespace-Änderungen oder wenn alle Equipment neu synchronisiert werden sollen.
|
||||||
|
|
||||||
|
Setzt zurück:
|
||||||
|
- wiki_synced auf False
|
||||||
|
- wiki_last_sync auf NULL
|
||||||
|
|
||||||
|
Optional (auskommentiert):
|
||||||
|
- wiki_doku_id auf NULL (nur wenn IDs komplett neu generiert werden sollen)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Notification mit Anzahl zurückgesetzter Equipment
|
||||||
|
"""
|
||||||
|
equipment_records = self.search([
|
||||||
|
('wiki_synced', '=', True)
|
||||||
|
])
|
||||||
|
|
||||||
|
count = len(equipment_records)
|
||||||
|
|
||||||
|
if count == 0:
|
||||||
|
return {
|
||||||
|
'type': 'ir.actions.client',
|
||||||
|
'tag': 'display_notification',
|
||||||
|
'params': {
|
||||||
|
'title': _('Wiki-Sync Reset'),
|
||||||
|
'message': _('Keine synchronisierten Equipment gefunden.'),
|
||||||
|
'type': 'warning',
|
||||||
|
'sticky': False,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Reset durchführen
|
||||||
|
equipment_records.write({
|
||||||
|
'wiki_synced': False,
|
||||||
|
'wiki_last_sync': False,
|
||||||
|
'wiki_status_id': False, # Status-ID neu generieren
|
||||||
|
# wiki_doku_id wird NICHT zurückgesetzt - ist manuell editierbar!
|
||||||
|
})
|
||||||
|
|
||||||
|
_logger.info(f"Wiki-Sync-Status für {count} Equipment zurückgesetzt")
|
||||||
|
|
||||||
|
return {
|
||||||
|
'type': 'ir.actions.client',
|
||||||
|
'tag': 'display_notification',
|
||||||
|
'params': {
|
||||||
|
'title': _('Wiki-Sync Reset'),
|
||||||
|
'message': _(f'{count} Equipment wurden zurückgesetzt und müssen neu synchronisiert werden.'),
|
||||||
|
'type': 'success',
|
||||||
|
'sticky': False,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from odoo import fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class MaintenanceEquipmentStatus(models.Model):
|
||||||
|
"""
|
||||||
|
Extension of the maintenance.equipment.status model to add DokuWiki smiley support.
|
||||||
|
|
||||||
|
This class extends the standard Odoo maintenance equipment status model to include
|
||||||
|
a smiley field that can be used for DokuWiki integration. The smiley field allows
|
||||||
|
administrators to associate DokuWiki emoticon syntax with each maintenance equipment
|
||||||
|
status, enabling visual representation of equipment status in DokuWiki documentation.
|
||||||
|
|
||||||
|
The smiley field accepts DokuWiki emoticon syntax (e.g., ':-)', ':-(', ':-D', etc.)
|
||||||
|
which can be used to display appropriate emoticons when exporting or displaying
|
||||||
|
equipment status information in DokuWiki format.
|
||||||
|
|
||||||
|
This extension is particularly useful for workshops or maintenance departments that
|
||||||
|
use DokuWiki as their documentation platform and want to have a visual representation
|
||||||
|
of equipment status alongside textual information.
|
||||||
|
"""
|
||||||
|
_inherit = 'maintenance.equipment.status'
|
||||||
|
|
||||||
|
smiley = fields.Char(
|
||||||
|
string='Smiley',
|
||||||
|
help='DokuWiki Smiley für diesen Status (z.B. :-) oder :-()'
|
||||||
|
)
|
||||||
54
open_workshop_dokuwiki/models/res_config_settings.py
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import logging
|
||||||
|
from odoo import api, models
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ResConfigSettings(models.TransientModel):
|
||||||
|
_inherit = 'res.config.settings'
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _register_hook(self):
|
||||||
|
"""
|
||||||
|
Hook der bei jedem Modul-Load (auch bei Update) ausgeführt wird.
|
||||||
|
Initialisiert fehlende DokuWiki-Parameter.
|
||||||
|
"""
|
||||||
|
super()._register_hook()
|
||||||
|
self._init_dokuwiki_parameters()
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _init_dokuwiki_parameters(self):
|
||||||
|
"""
|
||||||
|
Initialisiert DokuWiki-Parameter beim Modul-Install/-Update falls sie nicht existieren.
|
||||||
|
Wird automatisch beim Laden des Moduls aufgerufen.
|
||||||
|
"""
|
||||||
|
IrConfigParameter = self.env['ir.config_parameter'].sudo()
|
||||||
|
|
||||||
|
# Default-Parameter
|
||||||
|
defaults = {
|
||||||
|
'dokuwiki.url': 'https://wiki.hobbyhimmel.de',
|
||||||
|
'dokuwiki.user': 'odoo.odoo',
|
||||||
|
'dokuwiki.password': 'CHANGE_ME',
|
||||||
|
'dokuwiki.equipment_namespace': 'werkstatt:ausstattung', # NEU: Basis-Namespace für Equipment
|
||||||
|
'dokuwiki.central_documentation_namespace': 'werkstatt:ausstattung:odoo-status', # NEU: Namespace für Odoo-generierte Status-Seiten
|
||||||
|
'dokuwiki.overview_page_id': 'werkstatt:ausstattung:uebersicht', # Seiten-ID für Übersichtstabelle
|
||||||
|
'dokuwiki.overview_title': 'Geräte & Maschinen - Übersicht',
|
||||||
|
'dokuwiki.overview_columns': 'Name|Zustand|Sicherheit|Bereich|Standort|Bild',
|
||||||
|
'dokuwiki.overview_column_data': '[[{wiki_page_id}|{name}]]|{status_smiley}|{ows_machine_id.category_icon}|{ows_area}|{location}|{image}',
|
||||||
|
'dokuwiki.auto_update_overview_table': 'False', # Automatische Übersichtstabellen-Aktualisierung bei Equipment-Änderungen
|
||||||
|
}
|
||||||
|
|
||||||
|
# Nur fehlende Parameter anlegen
|
||||||
|
count_created = 0
|
||||||
|
count_existing = 0
|
||||||
|
for key, value in defaults.items():
|
||||||
|
if not IrConfigParameter.get_param(key):
|
||||||
|
IrConfigParameter.set_param(key, value)
|
||||||
|
count_created += 1
|
||||||
|
_logger.info(f"DokuWiki parameter '{key}' initialized with default value")
|
||||||
|
else:
|
||||||
|
count_existing += 1
|
||||||
|
|
||||||
|
if count_created > 0:
|
||||||
|
_logger.info(f"DokuWiki parameters: {count_created} created, {count_existing} already existing")
|
||||||
2
open_workshop_dokuwiki/security/ir.model.access.csv
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
|
access_equipment_wiki_sync_wizard,equipment.wiki.sync.wizard,model_equipment_wiki_sync_wizard,base.group_user,1,1,1,1
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="maintenance_equipment_status_form_view" model="ir.ui.view">
|
||||||
|
<field name="name">maintenance.equipment.status.form.dokuwiki</field>
|
||||||
|
<field name="model">maintenance.equipment.status</field>
|
||||||
|
<field name="inherit_id" ref="maintenance_equipment_status.maintenance_equipment_status_view_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<field name="name" position="after">
|
||||||
|
<field name="smiley" placeholder="z.B. :-) oder :-("/>
|
||||||
|
</field>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="maintenance_equipment_status_tree_view" model="ir.ui.view">
|
||||||
|
<field name="name">maintenance.equipment.status.tree.dokuwiki</field>
|
||||||
|
<field name="model">maintenance.equipment.status</field>
|
||||||
|
<field name="inherit_id" ref="maintenance_equipment_status.maintenance_equipment_status_view_tree"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<field name="name" position="after">
|
||||||
|
<field name="smiley" optional="show"/>
|
||||||
|
</field>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
120
open_workshop_dokuwiki/views/maintenance_equipment_views.xml
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<!-- Equipment Tree View Extension - Wiki Link -->
|
||||||
|
<record id="maintenance_equipment_view_tree_dokuwiki" model="ir.ui.view">
|
||||||
|
<field name="name">maintenance.equipment.tree.dokuwiki</field>
|
||||||
|
<field name="model">maintenance.equipment</field>
|
||||||
|
<field name="inherit_id" ref="maintenance.hr_equipment_view_tree"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<!-- Wiki Doku-Link nach dem Namen -->
|
||||||
|
<field name="name" position="after">
|
||||||
|
<field name="wiki_doku_link" widget="html" string="Wiki Doku" optional="show"/>
|
||||||
|
<field name="wiki_status_link" widget="html" string="Wiki Status" optional="hide"/>
|
||||||
|
</field>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Equipment Form View Extension -->
|
||||||
|
<record id="maintenance_equipment_view_form_dokuwiki" model="ir.ui.view">
|
||||||
|
<field name="name">maintenance.equipment.form.dokuwiki</field>
|
||||||
|
<field name="model">maintenance.equipment</field>
|
||||||
|
<field name="inherit_id" ref="maintenance.hr_equipment_view_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
|
||||||
|
<!-- Bild-Feld oben rechts neben den Smart Buttons -->
|
||||||
|
<xpath expr="//sheet/div[@name='button_box']" position="before">
|
||||||
|
<field name="image_1920" widget="image" class="oe_avatar" options="{'preview_image': 'image_1920'}"/>
|
||||||
|
</xpath>
|
||||||
|
|
||||||
|
<!-- Smart Button im Header -->
|
||||||
|
<xpath expr="//div[@name='button_box']" position="inside">
|
||||||
|
<button name="action_open_wiki"
|
||||||
|
type="object"
|
||||||
|
class="oe_stat_button"
|
||||||
|
icon="fa-book"
|
||||||
|
invisible="not wiki_page_url">
|
||||||
|
<div class="o_field_widget o_stat_info">
|
||||||
|
<span class="o_stat_text">Wiki</span>
|
||||||
|
<span class="o_stat_text text-success" invisible="not wiki_synced">Synchronisiert</span>
|
||||||
|
<span class="o_stat_text text-warning" invisible="wiki_synced">Nicht synchronisiert</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</xpath>
|
||||||
|
|
||||||
|
<!-- Wiki-Felder in neuem Tab -->
|
||||||
|
<xpath expr="//notebook" position="inside">
|
||||||
|
<page string="Wiki" name="wiki">
|
||||||
|
<group>
|
||||||
|
<group string="Wiki Dokumentations-Seite (Benutzer-Doku)" colspan="2">
|
||||||
|
<field name="wiki_doku_id" string="Seitenname"/>
|
||||||
|
<field name="wiki_page_url" widget="url" string="🔗 Zur Doku-Seite" class="oe_link"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<group string="Wiki Status-Seite (Odoo-Daten, automatisch)">
|
||||||
|
<field name="wiki_status_id" string="Seitenname" readonly="1"/>
|
||||||
|
</group>
|
||||||
|
<group string="Synchronisations-Status">
|
||||||
|
<field name="wiki_synced" readonly="1"/>
|
||||||
|
<field name="wiki_last_sync" readonly="1"/>
|
||||||
|
<field name="wiki_auto_sync"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<group>
|
||||||
|
<group string="Aktionen">
|
||||||
|
<button name="action_sync_to_dokuwiki"
|
||||||
|
string="Zum Wiki synchronisieren"
|
||||||
|
type="object"
|
||||||
|
class="btn-primary"
|
||||||
|
icon="fa-refresh"/>
|
||||||
|
<button name="action_open_wiki"
|
||||||
|
string="Wiki im Browser öffnen"
|
||||||
|
type="object"
|
||||||
|
class="btn-secondary"
|
||||||
|
icon="fa-external-link"
|
||||||
|
invisible="not wiki_page_url"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<group string="Hilfe">
|
||||||
|
<div class="alert alert-info" role="alert">
|
||||||
|
<h4 class="alert-heading">📚 DokuWiki-Integration</h4>
|
||||||
|
<p>
|
||||||
|
Dieses Equipment ist mit DokuWiki verbunden. Die Synchronisation erstellt <strong>zwei unterschiedliche Seiten</strong>:
|
||||||
|
</p>
|
||||||
|
<hr/>
|
||||||
|
<h5>📊 Wiki Status Seite (automatisch, nur lesbar)</h5>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Zweck:</strong> Enthält aktuelle Odoo-Daten (Status, Standort, Hersteller etc.)</li>
|
||||||
|
<li><strong>Format:</strong> werkstatt:ausstattung:odoo-status:[name-seriennummer]</li>
|
||||||
|
<li><strong>Eindeutigkeit:</strong> Jedes Equipment hat eine eigene Status-Seite</li>
|
||||||
|
<li><strong>Update:</strong> Wird bei jeder Synchronisation automatisch aktualisiert</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h5>📖 Wiki Dokumentations Seite (manuell, editierbar)</h5>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Zweck:</strong> Benutzer-Dokumentation, Anleitungen, Tipps</li>
|
||||||
|
<li><strong>Format:</strong> werkstatt:ausstattung:[bereich]:[doku-name]</li>
|
||||||
|
<li><strong>Besonderheit:</strong> Kann von mehreren Equipment geteilt werden (z.B. "akkuschrauber")</li>
|
||||||
|
<li><strong>Erstellung:</strong> Wird NICHT von Odoo erstellt - Benutzer erstellen diese manuell</li>
|
||||||
|
</ul>
|
||||||
|
<hr/>
|
||||||
|
<p class="mb-0">
|
||||||
|
<strong>💡 Tipp:</strong> Die Status-Seite kann mit dem include-Plugin in die Dokumentations-Seite eingebunden werden:<br/>
|
||||||
|
<code>{{page>werkstatt:ausstattung:odoo-status:[name-seriennummer]}}</code>
|
||||||
|
</p>
|
||||||
|
<hr/>
|
||||||
|
<p class="mb-0">
|
||||||
|
<strong>⚙️ Automatische Synchronisation:</strong> Wenn aktiviert, werden Änderungen automatisch zur Status-Seite übertragen.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</group>
|
||||||
|
</page>
|
||||||
|
</xpath>
|
||||||
|
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
2
open_workshop_dokuwiki/wizard/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from . import equipment_wiki_sync_wizard
|
||||||
157
open_workshop_dokuwiki/wizard/equipment_wiki_sync_wizard.py
Normal file
|
|
@ -0,0 +1,157 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import logging
|
||||||
|
from odoo import api, fields, models, _
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class EquipmentWikiSyncWizard(models.TransientModel):
|
||||||
|
_name = 'equipment.wiki.sync.wizard'
|
||||||
|
_description = 'Massen-Synchronisation von Equipment zu DokuWiki'
|
||||||
|
|
||||||
|
sync_mode = fields.Selection([
|
||||||
|
('all', 'Alle Equipment'),
|
||||||
|
('area', 'Nach Bereich filtern'),
|
||||||
|
('unsynced', 'Nur nicht synchronisierte'),
|
||||||
|
('overview_table', 'Übersichtstabelle aktualisieren'),
|
||||||
|
('reset_sync', 'Sync-Status zurücksetzen'),
|
||||||
|
], string='Synchronisations-Modus', default='all', required=True)
|
||||||
|
|
||||||
|
ows_area_id = fields.Many2one(
|
||||||
|
'ows.machine.area',
|
||||||
|
string='Bereich',
|
||||||
|
help='Nur Equipment aus diesem Bereich synchronisieren'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Statistik-Felder (nach Sync)
|
||||||
|
total_count = fields.Integer(string='Gefunden', readonly=True)
|
||||||
|
success_count = fields.Integer(string='Erfolgreich', readonly=True)
|
||||||
|
error_count = fields.Integer(string='Fehler', readonly=True)
|
||||||
|
error_messages = fields.Text(string='Fehlermeldungen', readonly=True)
|
||||||
|
overview_url = fields.Char(string='Übersichtsseite URL', readonly=True)
|
||||||
|
|
||||||
|
@api.onchange('sync_mode')
|
||||||
|
def _onchange_sync_mode(self):
|
||||||
|
"""Bereich-Feld nur bei 'area' Modus anzeigen"""
|
||||||
|
if self.sync_mode != 'area':
|
||||||
|
self.ows_area_id = False
|
||||||
|
|
||||||
|
def action_sync_equipment(self):
|
||||||
|
"""
|
||||||
|
Hauptaktion: Equipment synchronisieren basierend auf gewähltem Modus
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
|
||||||
|
# Übersichtstabelle-Modus: Schneller Update ohne Equipment-Iteration
|
||||||
|
if self.sync_mode == 'overview_table':
|
||||||
|
return self._sync_overview_table()
|
||||||
|
|
||||||
|
# Reset-Modus: Sync-Status zurücksetzen
|
||||||
|
if self.sync_mode == 'reset_sync':
|
||||||
|
return self.env['maintenance.equipment'].action_reset_wiki_sync_status()
|
||||||
|
|
||||||
|
# Equipment-Liste basierend auf Modus ermitteln
|
||||||
|
domain = []
|
||||||
|
|
||||||
|
if self.sync_mode == 'area':
|
||||||
|
if not self.ows_area_id:
|
||||||
|
raise UserError("Bitte einen Bereich auswählen!")
|
||||||
|
domain.append(('ows_area_id', '=', self.ows_area_id.id))
|
||||||
|
elif self.sync_mode == 'unsynced':
|
||||||
|
domain.append(('wiki_synced', '=', False))
|
||||||
|
|
||||||
|
# Equipment finden
|
||||||
|
equipment_records = self.env['maintenance.equipment'].search(domain)
|
||||||
|
|
||||||
|
if not equipment_records:
|
||||||
|
raise UserError("Keine Equipment-Einträge gefunden, die synchronisiert werden können!")
|
||||||
|
|
||||||
|
total = len(equipment_records)
|
||||||
|
success = 0
|
||||||
|
errors = 0
|
||||||
|
error_list = []
|
||||||
|
|
||||||
|
_logger.info(f"Starte Wiki-Synchronisation für {total} Equipment-Einträge")
|
||||||
|
|
||||||
|
# Jeden Equipment-Eintrag synchronisieren
|
||||||
|
for equipment in equipment_records:
|
||||||
|
try:
|
||||||
|
# Prüfen ob Bereich gesetzt ist
|
||||||
|
if not equipment.ows_area_id:
|
||||||
|
error_msg = f"{equipment.name}: Kein Bereich gesetzt"
|
||||||
|
error_list.append(error_msg)
|
||||||
|
errors += 1
|
||||||
|
_logger.warning(error_msg)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Standard-Synchronisation (erstellt nur Odoo-Status-Seiten)
|
||||||
|
equipment.sync_to_dokuwiki()
|
||||||
|
success += 1
|
||||||
|
_logger.info(f"✓ {equipment.name} synchronisiert")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"{equipment.name}: {str(e)}"
|
||||||
|
error_list.append(error_msg)
|
||||||
|
errors += 1
|
||||||
|
_logger.error(f"✗ {error_msg}")
|
||||||
|
|
||||||
|
# Statistik speichern
|
||||||
|
self.write({
|
||||||
|
'total_count': total,
|
||||||
|
'success_count': success,
|
||||||
|
'error_count': errors,
|
||||||
|
'error_messages': '\n'.join(error_list) if error_list else 'Keine Fehler',
|
||||||
|
})
|
||||||
|
|
||||||
|
# Ergebnis-View anzeigen
|
||||||
|
return {
|
||||||
|
'type': 'ir.actions.act_window',
|
||||||
|
'name': 'Wiki-Synchronisation Ergebnis',
|
||||||
|
'res_model': 'equipment.wiki.sync.wizard',
|
||||||
|
'view_mode': 'form',
|
||||||
|
'res_id': self.id,
|
||||||
|
'target': 'new',
|
||||||
|
'context': {'show_result': True},
|
||||||
|
}
|
||||||
|
|
||||||
|
def _sync_overview_table(self):
|
||||||
|
"""
|
||||||
|
Aktualisiert die Übersichtstabelle in DokuWiki.
|
||||||
|
Ruft die Methode im maintenance.equipment Model auf.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Action zum Anzeigen des Ergebnisses
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
|
||||||
|
_logger.info("Starte Übersichtstabellen-Synchronisation")
|
||||||
|
|
||||||
|
# Equipment Model aufrufen
|
||||||
|
result = self.env['maintenance.equipment'].action_sync_overview_table()
|
||||||
|
|
||||||
|
# Statistik speichern
|
||||||
|
self.write({
|
||||||
|
'total_count': result.get('total', 0),
|
||||||
|
'success_count': result.get('success', 0),
|
||||||
|
'error_count': result.get('total', 0) - result.get('success', 0),
|
||||||
|
'error_messages': result.get('error_messages', 'Keine Fehler'),
|
||||||
|
'overview_url': result.get('overview_url', ''),
|
||||||
|
})
|
||||||
|
|
||||||
|
# Ergebnis-View anzeigen
|
||||||
|
return {
|
||||||
|
'type': 'ir.actions.act_window',
|
||||||
|
'name': 'Übersichtstabelle aktualisiert',
|
||||||
|
'res_model': 'equipment.wiki.sync.wizard',
|
||||||
|
'view_mode': 'form',
|
||||||
|
'res_id': self.id,
|
||||||
|
'target': 'new',
|
||||||
|
'context': {'show_result': True},
|
||||||
|
}
|
||||||
|
|
||||||
|
def action_clear_template_cache(self):
|
||||||
|
"""
|
||||||
|
Leert den Template-Cache (delegiert an maintenance.equipment).
|
||||||
|
"""
|
||||||
|
return self.env['maintenance.equipment'].action_clear_template_cache()
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<!-- Wizard Form View -->
|
||||||
|
<record id="equipment_wiki_sync_wizard_form" model="ir.ui.view">
|
||||||
|
<field name="name">equipment.wiki.sync.wizard.form</field>
|
||||||
|
<field name="model">equipment.wiki.sync.wizard</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Wiki-Synchronisation">
|
||||||
|
<group invisible="context.get('show_result', False)">
|
||||||
|
<group>
|
||||||
|
<field name="sync_mode" widget="radio"/>
|
||||||
|
<field name="ows_area_id" invisible="sync_mode != 'area'" required="sync_mode == 'area'"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<!-- Ergebnis-Anzeige -->
|
||||||
|
<group invisible="not context.get('show_result', False)">
|
||||||
|
<group>
|
||||||
|
<field name="total_count"/>
|
||||||
|
<field name="success_count"/>
|
||||||
|
<field name="error_count"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="overview_url" widget="url" string="Übersichtsseite" invisible="not overview_url"/>
|
||||||
|
<field name="error_messages" widget="text" colspan="2" invisible="error_count == 0"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<button string="Synchronisieren"
|
||||||
|
name="action_sync_equipment"
|
||||||
|
type="object"
|
||||||
|
class="btn-primary"
|
||||||
|
invisible="context.get('show_result', False)"/>
|
||||||
|
<button string="Template-Cache leeren"
|
||||||
|
name="action_clear_template_cache"
|
||||||
|
type="object"
|
||||||
|
class="btn-warning"
|
||||||
|
invisible="context.get('show_result', False)"
|
||||||
|
help="Leert den Template-Cache und erzwingt Neuladen beim nächsten Sync"/>
|
||||||
|
<button string="Schließen"
|
||||||
|
special="cancel"
|
||||||
|
class="btn-secondary"/>
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Menu-Action -->
|
||||||
|
<record id="action_equipment_wiki_sync_wizard" model="ir.actions.act_window">
|
||||||
|
<field name="name">Wiki-Synchronisation</field>
|
||||||
|
<field name="res_model">equipment.wiki.sync.wizard</field>
|
||||||
|
<field name="view_mode">form</field>
|
||||||
|
<field name="target">new</field>
|
||||||
|
<field name="context">{}</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Menu-Eintrag unter Wartung → Konfiguration -->
|
||||||
|
<menuitem id="menu_equipment_wiki_sync"
|
||||||
|
name="Wiki-Synchronisation"
|
||||||
|
parent="maintenance.menu_maintenance_configuration"
|
||||||
|
action="action_equipment_wiki_sync_wizard"
|
||||||
|
sequence="99"/>
|
||||||
|
</odoo>
|
||||||