Compare commits

...

38 Commits

Author SHA1 Message Date
550cdac1eb fix: Set company_id in equipment migration to fix serial number generation
- Add default company_id to equipment records during migration
- Fixes 'Generate Serial Number' button not working for migrated equipment
- Equipment without company_id could not generate serial numbers
2025-12-11 19:22:52 +01:00
b68ac293c8 added qrcode for equipment modul 2025-12-11 19:19:02 +01:00
42db76a9c7 feat: Improve equipment creation flow and UX
- Made location field readonly in maintenance.equipment view
- Hide equipment_id field in machine form (auto-created)
- Name and serial_no now directly editable in machine form
- Area is now required field
- Location automatically synced from area to equipment
- Equipment auto-created on machine save with all fields
- Renamed menu from 'Maschinen' to 'Ausrüstung'
- Improved user flow: No need to switch between views

Changes:
- Added maintenance_equipment_views.xml for readonly location
- Updated create() to always auto-create equipment
- Enhanced write() to sync name/serial_no changes to equipment
- Menu labels updated to 'Ausrüstung'
2025-12-07 21:33:06 +01:00
f87755e7d0 Merge branch '18.0-MultiModulOpenWorkshop' into 18.0
Integration of maintenance.equipment with automated migration workflow
2025-12-07 21:10:35 +01:00
8fb58c744e feat: Migrate to maintenance.equipment with automated OpenUpgrade workflow
- Integrated maintenance.equipment using _inherits pattern in open_workshop_base
- Removed duplicate fields (code, description, storage_location, purchase_price, purchase_date)
- Delegated equipment management to OCA maintenance module
- Added Smart Button UI pattern for equipment details
- Implemented automated migration workflow:
  * SQL script renames module open_workshop → open_workshop_base
  * Pre-migration: Installs maintenance, adds equipment_id column
  * Post-migration: Migrates 23 machines with proper JSONB names and locations
- Restored old open_workshop module (installable=False) for DB compatibility
- Updated CI/CD workflow with migration steps
- Area mapping corrected: area.name → equipment.location
- JSONB handling: Using SQL jsonb_build_object() for proper storage
- Serial numbers: From old code field or generated as OWS-{id}

Tested and verified:
 23 machines successfully migrated
 JSONB names extractable: name->>'de_DE' and name->>'en_US'
 Locations correctly mapped: Fablab, Holzbereich, etc.
 equipment_id linkage functional
2025-12-07 21:09:35 +01:00
ceb8af7e48 Fix: POS Template - duplicate key error in ows_pos_customer_sidebar
- t-key fallback: order.uid || order.id || order_index
- Verhindert 'undefined' duplicate key error im POS
- Asset-Bundle korrigiert: point_of_sale._assets_pos
2025-12-07 17:18:09 +01:00
71c6ba56ed Kapitel 3+4: POS-Code in separates Modul ausgelagert
- Neues Modul open_workshop_pos erstellt
- POS JavaScript, XML Templates und CSS verschoben
- open_workshop_base bereinigt: keine POS-Abhängigkeit mehr
- open_workshop_base Version 18.0.1.0.4
- open_workshop_pos Version 18.0.1.0.0
- Kategorie von 'Point of Sale' zu 'Manufacturing' geändert
- Alle Template-Referenzen aktualisiert (open_workshop_base → open_workshop_pos)

Module getestet und erfolgreich installiert in hh18 Datenbank.
2025-12-07 16:43:51 +01:00
744b7b3234 Merge branch '18.0-MultiModulOpenWorkshop': Migration open_workshop → open_workshop_base 2025-12-07 16:01:51 +01:00
3619526af0 Migration: open_workshop → open_workshop_base
- Modul umbenannt von open_workshop zu open_workshop_base
- Alle Referenzen im Code aktualisiert (Templates, Views, Assets)
- SQL-Migrationsskript für automatische DB-Migration erstellt
- post_init_hook hinzugefügt
- Version auf 18.0.1.0.3 erhöht
- Vorbereitung für modulare Architektur (Base, POS, API)

Fixes für Gitea Action Integration:
- SQL-Skript in open_workshop_base/migrations/
- Alter open_workshop/ Ordner entfernt
- Migrations-Workflow getestet auf hh18
2025-12-07 15:45:03 +01:00
bb3d1bf7c9 removed unused files 2025-12-07 15:00:15 +01:00
bf605539fa final feature request 2 2025-12-07 14:53:06 +01:00
12d5902e3c final feature request 1 2025-12-07 14:50:36 +01:00
977aa2d1b3 final feature request 2025-12-07 14:46:09 +01:00
58c7e8f258 Open Workshop Multi Module Refactoring 2025-12-07 14:03:32 +01:00
7cd458b72f This now a multi modul odoo repository, open_workshop moved to open_workshop_base 2025-12-06 20:45:29 +01:00
dff2de1755 FIX: pos: wenn auf - gedrückt wird um eine Order zu entfernen, wurde nicht
die Order nicht gefunden. -> behoben
- IMP: "einfachere" Debug Möglichkeit mit odoo-18-dev container. README.md in .vscode
  hinzugefügt.
2025-10-25 14:10:25 +02:00
d56ae65b56 fixed missing dependency 2025-10-24 20:15:44 +02:00
bc0459ab9b IMP: added work_email to admin employee 2025-10-11 11:23:50 +02:00
7230bcb6f8 fix: anonymize_for_testsystem now supports archived admin account 2025-10-10 19:56:49 +02:00
f07f9dd8b3 Fix Odoo 18 compatibility: Enable notebook widget and fix context references
- Uncomment and activate notebook widget in machine form view
- Fix context reference from 'active_id' to 'id' for Odoo 18 compatibility
- Remove duplicate category_icon field in form group
- Clean up XML structure and indentation
- Enable "Nutzungsprodukte" and "Einweisungsprodukte" tabs with editable lists
- Resolve access rights inconsistency error for product_ids field

Fixes module upgrade error: "Field 'active_id' does not exist in model 'ows.machine'"
2025-10-09 22:17:09 +02:00
5c7fd4330d removed debugpy 2025-10-07 18:27:40 +02:00
ab696db035 Duplicate Warning Patch 2025-10-07 17:34:34 +02:00
1069630e86 FIX: Add VS Code debugging configuration and fix pos_order.py
- Added .vscode/launch.json with remote debugging setup for Odoo development
- Configured debugpy attachment on port 4338 with path mappings
- Added workspace settings for Python development with Odoo
- Fixed issues in pos_order.py for improved POS functionality
- Enhanced development workflow with breakpoint support for custom code and Odoo core
2025-10-06 19:54:24 +02:00
558dff276a debugpy option 2025-10-05 20:52:04 +02:00
e0a9205fea [fix] res.partner.remove.duplicate.bank.warning 2025-08-07 19:30:26 +02:00
24baff2a86 [fix] typo fehler 2025-08-07 19:26:37 +02:00
62dbf92b36 [fix] unknown res.partner.remove.duplicate.bank.warning 2025-08-07 19:21:34 +02:00
bbb5181a74 Merge pull request 'fix for duplicate_bank_partner_ids in account modul' (#9) from 18.0-target into 18.0
Reviewed-on: #9
2025-07-13 14:37:07 +02:00
05f9ef0990 fix for duplicate_bank_partner_ids in account modul 2025-07-13 14:32:50 +02:00
33db478c72 Merge pull request 'removeCurrentOrder() fix' (#8) from 18.0-target into 18.0
Reviewed-on: #8
2025-07-06 14:54:50 +02:00
7e8840f2a5 removeCurrentOrder() fix 2025-07-06 14:53:37 +02:00
d4a835f178 Merge pull request '18.0-target' (#7) from 18.0-target into 18.0
Reviewed-on: #7
2025-07-01 22:08:25 +02:00
0fe8417602 running POS ows machine access and customer sidebar 2025-07-01 21:57:51 +02:00
1f59e16b26 working with pos sidebar, but no content 2025-06-28 21:58:58 +02:00
59e4b19dee almost done for POS 2025-06-28 18:49:49 +02:00
021d01efe6 working merge with open_workshop 17.0. Missing Traings view in Machine Backend. No POS Machine sidebar. 2025-06-28 15:31:59 +02:00
f4216d790c added todo 2025-05-02 08:06:03 +02:00
eb17894a13 minimale Version von open workshop v16.0 für migration 2025-05-01 15:33:35 +02:00
135 changed files with 5368 additions and 4270 deletions

View 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
View File

@ -1,8 +0,0 @@
ODOO_VERSION=13.0
CONTAINER_NAME_EXTENSION=13_dev
ODOO_PORT=9013
DB_HOST=hobbyhimmel_odoo_13_dev_db
DB_PORT=5432
DB_USER=odoo
DB_PASSWORD=odoo
DB_NAME=hobbyhimmel

289
.vscode/.jslintrc vendored Normal file
View 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
View 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 OdooCore 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 DevContainerFunktion oder die DebugKonfigurationen 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 WorkspaceVerzeichnis `/mnt/extra-addons/open_workshop`.
Vorteil: Du arbeitest direkt im Container (kein lokales Kopieren der CoreSourcen nötig) und Breakpoints funktionieren zuverlässig.
3) Debugging (Attach)
- Wenn du als DevContainer verbunden bist, verwende die DebugKonfiguration "Odoo Attach (container)" (port 5678).
- Wenn du lokal arbeitest und den HostPort benutzt, verwende "Odoo Attach (host)" (port 4338).
4) PfadMapping
- 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 OdooCores 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 DebugModus 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 DevImage 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
View 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
View 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
View 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": []
}
]
}

View 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 | 520 € | sehr gut |
| Odoo self-hosted | ~020 € | 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

View File

@ -0,0 +1,320 @@
# 0. Migration: open_workshop → open_workshop_base ✅ ABGESCHLOSSEN
**Status: ERLEDIGT (08.12.2025)**
Die Migration wurde erfolgreich durchgeführt:
- ✅ SQL-Migration: Modul umbenannt in Datenbank
- ✅ Pre-Migration: maintenance.equipment Integration vorbereitet
- ✅ Post-Migration: 23 Maschinen zu maintenance.equipment migriert
- ✅ _inherits Pattern implementiert (ows.machine → maintenance.equipment)
- ✅ Automatische Migration in CI/CD Pipeline integriert
- ✅ UX verbessert: Equipment wird automatisch erstellt
- ✅ Location readonly und auto-sync von area
- ✅ Menü umbenannt zu "Ausrüstung"
**Technische Details:**
- Alte Felder entfernt: code, description, storage_location, purchase_price, purchase_date
- Delegation an maintenance.equipment: name, serial_no, location, cost, effective_date
- JSONB für mehrsprachige Namen (de_DE, en_US)
- Area → Location Synchronisation automatisch
---
# Open Workshop FEATURE PLAN
## Vollständige Modularisierung + WordPress REST API Integration (API = Pflicht)
Dieses Dokument ist die **finale, vollständig korrigierte Version** des Feature-Plans.
Es kombiniert alle Inhalte der vorherigen Dokumente, **konsolidiert, bereinigt und vereinheitlicht**,
und stellt klar:
# ⭐ Die API ist ein **Pflichtbestandteil** der finalen Architektur.
Das Dokument ist **eigenständig** und enthält alles, was ein Entwickler oder KI-Agent benötigt,
um das gesamte Open-Workshop-System korrekt modularisiert aufzubauen.
---
# 1. Gesamtziel
Das Projekt *Open Workshop* soll in eine moderne, klare und skalierbare Architektur überführt werden:
- **Odoo (intern)** als Verwaltungs-Backend für Maschinen, Einweisungen, POS, Maintenance, Events.
- **WordPress (extern)** als öffentliche Website mit hoher Performance und SEO.
- **REST API (Pflicht!)** als Kommunikationsschicht zwischen beiden Systemen.
Damit entsteht:
```
WordPress (öffentlich)
| REST API (nur lesend)
Odoo (intern, geschützt)
```
Diese Architektur vermeidet:
- Performance-Probleme durch DSL-Uplink
- Sicherheitsrisiken von Odoo im Internet
- Hohe Kosten für Odoo.sh Benutzerlizenzen
---
# 2. Aktuelle Modulstruktur (Stand: 08.12.2025)
```
open_workshop/
├── open_workshop/ # ✅ Alt-Modul (installable=False, Kompatibilität)
├── open_workshop_base/ # ✅ FERTIG Backend, _inherits maintenance.equipment
├── open_workshop_pos/ # ✅ FERTIG POS-Integration (JS, XML, UI)
└── open_workshop_api/ # ⏳ GEPLANT REST API für WordPress
```
**Entfernt:** open_workshop_maintenance (verworfen - Funktionalität direkt in open_workshop_base integriert)
---
# 3. Modul: open_workshop_base ✅ FERTIG
**Status: PRODUKTIV (18.0.1.0.4)**
Enthält:
- ✅ `ows.machine` mit _inherits zu `maintenance.equipment`
- ✅ `ows.machine.area` (Bereiche mit JSONB-Namen)
- ✅ `ows.machine.product` (Nutzungsprodukte)
- ✅ `ows.machine.training` (Einweisungsprodukte)
- ✅ `ows.machine.access` (Zugriffsfreigaben)
- ✅ Automatische Equipment-Erstellung bei Maschinenanlage
- ✅ Area → Location Synchronisation
- ✅ Sicherheitsregeln (ir.model.access)
- ✅ Backend-UI: Form, Tree, Search/Filter
- ✅ Smart Button zu Equipment-Details
- ✅ Integration mit OCA maintenance Modulen
**Abhängigkeiten:**
- base, account, hr, product, sale, contacts, **maintenance**
**Maintenance Integration:**
- Statt separatem Modul: Direkte Integration via _inherits
- Equipment-Felder: name, serial_no, location, cost, effective_date
- OWS-Felder: category, area_id, product_ids, training_ids
---
# 4. Modul: open_workshop_pos ✅ FERTIG
**Status: PRODUKTIV**
Vollständig separiertes POS-Modul mit:
### JS-Module:
- ✅ ows_machine_access_list.js
- ✅ ows_pos_customer_sidebar.js
- ✅ ows_pos_sidebar.js
- ✅ ows_product_screen_template_patch.js
### XML-Templates:
- ✅ QWeb Templates für POS-UI
- ✅ Maschinenfreigaben-Widget
- ✅ Customer Sidebar mit Zugriffsstatus
### Assets:
```json
'point_of_sale.assets': [
'open_workshop_pos/static/src/js/**/*.js',
'open_workshop_pos/static/src/xml/**/*.xml',
'open_workshop_pos/static/src/css/**/*.css'
]
```
**Abhängigkeiten:**
- open_workshop_base
- point_of_sale
**Integration:**
- Zeigt Maschinenzugriffe im POS
- Filterung nach Bereichen
- Live-Status der Freigaben
---
# 5. Modul: open_workshop_api ⏳ GEPLANT
**Status: NOCH NICHT IMPLEMENTIERT**
Dieses Modul stellt die **REST API** bereit, über die WordPress öffentlich verfügbare Daten abholt.
## 5.1 Ziele
- Minimaler externer Zugriff (nur API-Endpunkte)
- JSON-Ausgabe für WordPress
- Keine Odoo-Website erforderlich
- Odoo selbst bleibt **nicht öffentlich erreichbar**
## 5.2 API-Endpunkte
Pflicht:
```
GET /api/v1/machines
GET /api/v1/machine/<id>
```
Empfohlen:
```
GET /api/v1/areas
GET /api/v1/files/<attachment_id>
GET /api/v1/events (später für Kurse)
```
## 5.3 JSON Beispiel
```json
{
"id": 12,
"name": "Formatkreissäge",
"status": "available",
"area": "Holzwerkstatt",
"image_url": "https://odoo.example.org/api/v1/file/29",
"manual_url": "https://odoo.example.org/api/v1/file/55",
"training_required": true
}
```
## 5.4 Sicherheitsmechanismen
- CORS nur für WordPress-Domain erlauben
- Optionaler Token im Header (`Authorization: Bearer <token>`)
- Kein Zugriff auf `/web`, `/api/web`, `/pos`
- Rate Limiting via Reverse Proxy
---
# 6. WordPress Integration ⏳ GEPLANT
**Status: VORBEREITET (API noch nicht implementiert)**
WordPress bleibt die öffentliche Website.
## 6.1 Warum?
- Sehr hohe Performance (CDN, Caching)
- SEO-optimiert
- Keine Belastung deiner Internetleitung
- Keine Sicherheitsrisiken im internen Odoo
- Keine Lizenzkosten
## 6.2 WordPress Plugin (bereitgestellt)
Das Plugin:
- Ruft Odoo-API ab
- Rendert Maschinenliste via Shortcode
- Konsumiert JSON-Daten
- Unterstützt Token-Auth
Beispiel:
```
[openworkshop_machines]
```
---
# 7. Finaler Architektur-Plan
```
Internet
|
WordPress (Frontend, CDN)
|
fetch JSON via REST API
|
----------------------------
| API Gateway (open_workshop_api)
| Exposed only: /api/v1/*
----------------------------
|
Odoo Backend (LAN/VPN)
Maschinen, POS, Benutzer, Einweisungen
```
**Kein direkter Zugriff auf Odoo-Weboberfläche!**
---
# 8. Sicherheit
- Reverse Proxy (Traefik/Nginx) muss alle Odoo-Backoffice-URLS sperren
- Nur `/api/v1/*` darf öffentlich sein
- HTTPS erzwingen
- Rate-Limit + Firewall-Regeln
- Token-Authentifizierung optional zusätzlich einsetzbar
---
# 9. Vorteile dieser Architektur
**WordPress bleibt ultraschnell** (1000 Besucher/Tag problemlos)
**Odoo bleibt sicher** hinter Firewall
**Keine Benutzerkosten** → alle Ehrenamtlichen können intern mitarbeiten
**Modular, wartbar, zukunftssicher**
**API erlaubt feine Steuerung**, welche Daten öffentlich sind
**Keine Last auf DSL-Leitung** (WordPress hostet extern)
**Maintenance-Integration** ohne Extra-Modul direkt in Base
---
# 10. Implementierungsstand (08.12.2025)
| Schritt | Status | Details |
|---------|--------|---------|
| 1. open_workshop_base | ✅ **FERTIG** | Version 18.0.1.0.4, produktiv |
| 2. open_workshop_pos | ✅ **FERTIG** | POS-Integration komplett |
| 3. ~~open_workshop_maintenance~~ | ❌ **VERWORFEN** | Direkt in Base integriert |
| 4. Maintenance Integration | ✅ **FERTIG** | _inherits Pattern implementiert |
| 5. Migration Workflow | ✅ **FERTIG** | SQL + Python, CI/CD integriert |
| 6. open_workshop_api | ⏳ **GEPLANT** | REST API für WordPress |
| 7. WordPress Plugin | ⏳ **GEPLANT** | Frontend-Integration |
---
# 11. Nächste Schritte
1. **open_workshop_api** entwickeln
- REST Controller implementieren
- JSON-Serializer für machines/areas
- CORS und Security konfigurieren
- Token-Auth optional hinzufügen
2. **WordPress Plugin** anpassen
- API-Endpunkte konfigurieren
- Shortcode-Rendering
- Caching implementieren
3. **Testing & Deployment**
- API-Tests schreiben
- Reverse Proxy konfigurieren
- Performance-Tests
- Go-Live vorbereiten
---
# 12. Endfazit
Diese Architektur hat sich bewährt:
- ✅ **Technisch korrekt** _inherits Pattern statt separatem Modul
- ✅ **Performant** Maintenance.equipment als Single Source of Truth
- ✅ **Sicher** API-Layer trennt intern/extern
- ✅ **Kostenoptimiert** Keine Odoo.sh Lizenzen nötig
- ✅ **Langfristig erweiterbar** Modularer Aufbau
- ✅ **Produktiv im Einsatz** Migration erfolgreich abgeschlossen
Die API ist **zentrales Zukunftsmodul** für die WordPress-Integration.
**Letztes Update: 08.12.2025**

View 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.“*

View 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

View File

@ -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

View File

@ -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.
""",
}

View 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
})

View 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'],
}

View File

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
from . import maintenance_equipment
from . import res_company

View File

@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
from odoo import models, fields
class MaintenanceEquipment(models.Model):
_inherit = 'maintenance.equipment'
qr_code = fields.Binary("QR Code")
comp_serial_no = fields.Char("Inventory Serial No", tracking=True)
serial_no = fields.Char('Mfg. Serial Number', copy=False)
def action_print_qrcode_layout(self):
action = self.env['ir.actions.act_window']._for_xml_id('aspl_equipment_qrcode_generator.action_open_label_layout_equipment')
action['context'] = {'default_equipment_ids': self.ids}
return action
def generate_serial_no(self):
for equipment_id in self:
if not equipment_id.comp_serial_no:
company_id = equipment_id.company_id.id
sequence_id = self.env['ir.sequence'].search(
[('name', '=', 'Equipment Company Sequence'), ('company_id', '=', company_id)])
if sequence_id:
data = sequence_id._next()
equipment_id.write({
'comp_serial_no': data
})

View 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

View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import custom_qrcode_generator

View File

@ -0,0 +1,191 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<template id="report_equipment_simple_label2x7">
<t t-set="qrcode_size" t-value="'height:14mm'"/>
<t t-set="table_style" t-value="'width:97mm;height:37.1mm;margin:inherit;' + table_style"/>
<td t-att-style="make_invisible and 'visibility:hidden;'" >
<div class="o_label_full" t-att-style="table_style">
<div class="o_label_name">
<strong t-field="equipment.name"/>
</div>
<div class="o_label_data">
<div class="text-center o_label_right_column">
<t t-if="equipment.qr_code">
<div t-field="equipment.qr_code" t-options="{'widget': 'image'}" style="width: 130px;margin-top: -40px;"/>
</t>
</div>
<div class="text-left" style="line-height:normal;word-wrap: break-word;">
<span class="text-nowrap" t-field="equipment.serial_no"/>
<div class="o_label_extra_data">
<span t-field="equipment.comp_serial_no"/>
</div>
<t t-if="equipment.warranty_date">
<strong t-field="equipment.warranty_date"/>
</t>
</div>
<div class="o_label_clear"></div>
</div>
</div>
</td>
</template>
<template id="report_equipment_simple_label4x7">
<t t-set="barcode_size" t-value="'width:80px;'"/>
<t t-set="table_style" t-value="'width:47mm;height:37.1mm;margin:inherit;' + table_style"/>
<td t-att-style="make_invisible and 'visibility:hidden;'" >
<div class="o_label_full" t-att-style="table_style">
<div class="o_label_name">
<strong t-field="equipment.name"/>
</div>
<div class= "text-center o_label_right_column">
<t t-if="equipment.qr_code">
<div t-field="equipment.qr_code" t-options="{'widget': 'image'}" style="width:95px;padding:0px;margin-top:-10px"/>
</t>
</div>
<div class="text-left o_label_left_column" style="line-height:normal;word-wrap: break-word;">
<div class="o_label_data">
<strong t-field="equipment.serial_no"/>
<span t-field="equipment.comp_serial_no"/>
<t t-if="equipment.warranty_date">
<strong t-field="equipment.warranty_date"/>
</t>
</div>
</div>
</div>
</td>
</template>
<template id="report_equipment_simple_label2x5">
<t t-set="barcode_size" t-value="'width:38mm;height:14mm;margin-top:50px;'"/>
<t t-set="table_style"
t-value="'width:97mm;height:50mm;font-size: 12px;border: 2px solid black;table-layout: fixed;'"/>
<td>
<table t-att-style="table_style">
<tr>
<td style="width:25%;">Name</td>
<td style="width:5%">:</td>
<td colspan="2" style="width:70%;word-wrap: break-word;">
<t t-esc="equipment.name"/>
</td>
</tr>
<tr>
<td style="width:25%">Model</td>
<td style="width:5%">:</td>
<td style="width:35%;word-wrap: break-word;">
<t t-esc="equipment.model"/>
</td>
<td rowspan="5" style="width:35%">
<t t-if="equipment.name">
<div t-field="equipment.qr_code" t-options="{'widget': 'image'}"
style="width:30mm;height:10mm;"/>
</t>
</td>
</tr>
<tr>
<td style="width:25%">Mfg Serial</td>
<td style="width:5%">:</td>
<td style="width:35%;font-size:10px;word-wrap: break-word;">
<t t-esc="equipment.serial_no"/>
</td>
</tr>
<tr>
<td style="width:25%">Serial</td>
<td style="width:5%">:</td>
<td style="width:35%;word-wrap: break-word;">
<t t-esc="equipment.comp_serial_no"/>
</td>
</tr>
<tr>
<td style="width:25%">Warranty Date</td>
<td style="width:5%">:</td>
<td style="width:35%;word-wrap: break-word;">
<t t-esc="equipment.warranty_date"/>
</td>
<!-- <td style="width:35%;"></td> -->
</tr>
<tr>
<td style="width:25%"></td>
<td style="width:5%"></td>
<td style="width:35%;word-wrap: break-word;"></td>
</tr>
</table>
</td>
</template>
<template id="report_quipmentlabel">
<t t-call="web.html_container">
<t t-if="columns and rows">
<t t-if="columns == 2 and rows == 7">
<t t-set="padding_page" t-value="'padding: 14mm 3mm'"/>
<t t-set="report_to_call" t-value="'aspl_equipment_qrcode_generator.report_equipment_simple_label2x7'"/>
</t>
<t t-if="columns == 4 and rows == 7">
<t t-set="padding_page" t-value="'padding: 14mm 3mm'"/>
<t t-set="report_to_call" t-value="'aspl_equipment_qrcode_generator.report_equipment_simple_label4x7'"/>
</t>
<t t-if="columns == 2 and rows == 5">
<t t-set="padding_page" t-value="'padding: 14mm 3mm'"/>
<t t-set="report_to_call" t-value="'aspl_equipment_qrcode_generator.report_equipment_simple_label2x5'"/>
</t>
<t t-foreach="range(page_numbers)" t-as="page">
<div class="o_label_sheet" t-att-style="padding_page">
<table class="my-0 table table-sm table-borderless">
<t t-foreach="range(rows)" t-as="row">
<tr>
<t t-foreach="range(columns)" t-as="column">
<t t-if="equipment_data">
<t t-set="current_data" t-value="equipment_data.popitem()"/>
<t t-set="equipment" t-value="current_data[0]"/>
<t t-set="table_style" t-value="'border: 1px solid black'"/>
<t t-call="{{report_to_call}}"/>
</t>
</t>
</tr>
</t>
</table>
</div>
</t>
</t>
</t>
</template>
<template id="maintenance_quip">
<t t-call="web.basic_layout">
<div class="page">
<t t-call="aspl_equipment_qrcode_generator.report_quipmentlabel">
<t t-set="products" t-value="products"/>
</t>
</div>
</t>
</template>
<record id="paperformat_label_sheet_qrcode" model="report.paperformat">
<field name="name">A4 Label Sheet</field>
<field name="default" eval="True"/>
<field name="format">A4</field>
<field name="page_height">0</field>
<field name="page_width">0</field>
<field name="orientation">Portrait</field>
<field name="margin_top">0</field>
<field name="margin_bottom">0</field>
<field name="margin_left">0</field>
<field name="margin_right">0</field>
<field name="disable_shrinking" eval="True"/>
<field name="dpi">96</field>
</record>
<record id="report_equipment_label" model="ir.actions.report">
<field name="name">Equipment QR-code (PDF)</field>
<field name="model">maintenance.equipment</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">aspl_equipment_qrcode_generator.maintenance_quip</field>
<field name="report_file">aspl_equipment_qrcode_generator.maintenance_quip</field>
<field name="paperformat_id" ref="aspl_equipment_qrcode_generator.paperformat_label_sheet_qrcode"/>
<field name="print_report_name">'Products Labels - %s' % (object.name)</field>
<field name="binding_model_id" eval="False"/>
<field name="binding_type">report</field>
</record>
</data>
</odoo>

View File

@ -0,0 +1,84 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
import math
from io import BytesIO
import qrcode
from odoo import models
def generate_qr_code(value):
qr = qrcode.QRCode(
version=1,
error_correction=qrcode.constants.ERROR_CORRECT_L,
box_size=20,
border=4,
)
qr.add_data(value)
qr.make(fit=True)
img = qr.make_image()
temp = BytesIO()
img.save(temp, format="PNG")
qr_img = base64.b64encode(temp.getvalue())
return qr_img
def _prepare_data(env, data):
equipment_label_layout_id = env['equipment.label.layout'].browse(data['equipment_label_layout_id'])
equipment_dict = {}
equipment_ids = equipment_label_layout_id.equipment_ids
for equipment in equipment_ids:
if not equipment.name:
continue
equipment_dict[equipment] = 1
combine_equipment_detail = ""
# Generate Equipment Redirect LInk
url = env['ir.config_parameter'].sudo().get_param('web.base.url')
menuId = env.ref('maintenance.menu_equipment_form').sudo().id
actionId = env.ref('maintenance.hr_equipment_action').sudo().id
equipment_link = url + '/web#id=' + str(equipment.id) + '&menu_id=' + str(menuId) + '&action=' + str(
actionId) + '&model=maintenance.equipment&view_type=form'
# Prepare main Equipment Detail
main_equipment_detail = ""
main_equipment_detail = main_equipment_detail.join(
"Name: " + str(equipment.name) + "\n" +
"Model: " + str(equipment.model) + "\n" +
"Mfg serial no: " + str(equipment.serial_no) + "\n"
"Warranty Exp. Date: " +str(equipment.warranty_date) + "\n"
"Category: " +str(equipment.category_id.name)
)
# main_equipment_detail = equipment_link + '\n' + '\n' + main_equipment_detail
# Prepare Child Equipment Detail
combine_equipment_detail = main_equipment_detail
combine_equipment_detail += '\n' + '\n' + equipment_link
# Generate Qr Code depends on Details
qr_image = generate_qr_code(combine_equipment_detail)
equipment.write({
'qr_code': qr_image
})
env.cr.commit()
page_numbers = (len(equipment_ids) - 1) // (equipment_label_layout_id.rows * equipment_label_layout_id.columns) + 1
dict_equipment = {
'rows': equipment_label_layout_id.rows,
'columns': equipment_label_layout_id.columns,
'page_numbers': page_numbers,
'equipment_data': equipment_dict
}
return dict_equipment
class ReportProductTemplateLabel(models.AbstractModel):
_name = 'report.aspl_equipment_qrcode_generator.maintenance_quip'
_description = 'Equipment QR-code Report'
def _get_report_values(self, docids, data):
return _prepare_data(self.env, data)

View File

@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_equipment_label_layout access.equipment_label_layout model_equipment_label_layout 1 1 1 1

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 563 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 362 KiB

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

View File

@ -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>

View File

@ -0,0 +1,3 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import equipment_label_layout

View File

@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import _, api, fields, models
class EquipmentLabelLayout(models.TransientModel):
_name = 'equipment.label.layout'
_description = 'Choose the sheet layout to print the labels'
print_format = fields.Selection([
('2x5', '2 x 5'),
('2x7', '2 x 7'),
('4x7', '4 x 7')], string="Format", default='2x5', required=True)
equipment_ids = fields.Many2many('maintenance.equipment')
rows = fields.Integer(compute='_compute_dimensions')
columns = fields.Integer(compute='_compute_dimensions')
@api.depends('print_format')
def _compute_dimensions(self):
for wizard in self:
if 'x' in wizard.print_format:
columns, rows = wizard.print_format.split('x')[:2]
wizard.columns = int(columns)
wizard.rows = int(rows)
else:
wizard.columns, wizard.rows = 1, 1
def process_label(self):
xml_id = 'aspl_equipment_qrcode_generator.report_equipment_label'
data = {
'equipment_label_layout_id':self.id
}
return self.env.ref(xml_id).report_action(None, data=data)

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="equipment_label_layout_form" model="ir.ui.view">
<field name="name">equipment.label.layout.form</field>
<field name="model">equipment.label.layout</field>
<field name="mode">primary</field>
<field name="arch" type="xml">
<form>
<group>
<group>
<field name="print_format" widget="radio"/>
</group>
</group>
<footer>
<button name="process_label" string="Confirm" type="object" class="btn-primary"/>
<button string="Discard" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
<record id="action_open_label_layout_equipment" model="ir.actions.act_window">
<field name="name">Choose Labels Layout</field>
<field name="res_model">equipment.label.layout</field>
<field name="view_ids"
eval="[(5, 0, 0),
(0, 0, {'view_mode': 'form', 'view_id': ref('equipment_label_layout_form')})]" />
<field name="target">new</field>
</record>
</odoo>

View File

@ -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 id name street zip city phone email company_type customer_rank supplier_rank
2 res_partner_demo_1 AAAA Max Mustermann Musterstraße 1 person 15 0
3 res_partner_demo_2 Benjamin Winter person 1 0
4 res_partner_demo_3 Martin Berthelon Fabrikstr. 3 73728 Esslingen martin.berthelon@hotmail.fr person 15 0
5 res_partner_demo_4 Aaron Christ Hohewartstraße 46 70469 Stuttgart christ.aaron@web.de person 14 0
6 res_partner_demo_5 Aaron Dörr Riegeläckerstr. 60 71229 Leonberg aaron_doerr@web.de person 33 0
7 res_partner_demo_6 Aaron Gale Chopinstr. 20 70195 Stuttgart 015172165290 aarongale1@live.com person 4 0
8 res_partner_demo_7 Aaron Zimmermann Heinrichstr. 15 38106 Braunschweig 016091647469 person 1 0
9 res_partner_demo_8 Abalrahman Alsadi Bachstr. 29 70563 Stuttgart abdulrahman.m.saadi@gmail.com person 1 0
10 res_partner_demo_9 Abdullah Zengin Engelbertstr. 124 70499 Stuttgart person 3 0
11 res_partner_demo_10 Abdussamed Korkmaz Bertha-von-Suttner-Straße 1 74366 Kirchheim Am Neckar korkmaz.abdussamed@gmail.com person 1 0
12 res_partner_demo_11 Achim Brendle Oberwiesenstraße 45 70619 Stuttgart 7114797505 achim.brendle@web.de person 2 0
13 res_partner_demo_12 Achim Jatkowski Hummelstr. 38 70569 Stuttgart 017621512316 achim.jatkowski@gmail.com person 1 0
14 res_partner_demo_13 Achim Jung Kurt Tucholsky Str. 6 71254 Ditzingen 07156174013 acjung@web.de person 1 0
15 res_partner_demo_14 Achim Kelbel Vivaldiweg 6 70195 Stuttgart a.kelbel@t-online.de person 2 0
16 res_partner_demo_15 Achim Kramer Reinsburger 172 70197 Stuttgart achim@zibra.de person 1 0
17 res_partner_demo_16 Adalbert Zeisl Bachstr. 20 71364 Winnenden 07195-2092884 betz1000@gmx.de person 2 0
18 res_partner_demo_17 Adalina Schäfer Sancenbacherstr. 26 74538 Rosengarten 015778855550 lina_max_schaefer@gmx.de person 1 0
19 res_partner_demo_18 Adam Riegel Marabustr. 35 / 84 70378 Stuttgart 0711 532082 person 1 0
20 res_partner_demo_19 Adam Swais Obertürkheimerstr. 54 73733 Esslingen adamswais@web.de person 1 0
21 res_partner_demo_20 Adela Spulber Obere Bismarck Str. 97 70197 Stuttgart person 1 0
22 res_partner_demo_21 Adem Uzun Liesel-Bach-Str. 54 71034 Böblingen 015251690873 adem.uzun2@gmail.com person 1 0
23 res_partner_demo_22 Adnan Djekic Vesoulerstr. 33 70839 Gerlingen 01724227468 adnandjekic@alice-dsl.net person 1 0
24 res_partner_demo_23 Adrian Berres Bärgstadter Str. 90 63928 Gehenbühl a.berres@gmx.de person 1 0
25 res_partner_demo_24 Adrian Lanksweirt Heidestraße 6 70469 Stuttgart adrian.lanksweirt@gmail.com person 1 0
26 res_partner_demo_25 Adrian Popov Hallerstr. 42 90419 Nürnberg +4915114305751 adrinuernberg@gmail.com person 2 0
27 res_partner_demo_26 Agnes Krettek Seyfferstr. 62 70187 Stuttgart agneskrettek@gmail.com person 1 0
28 res_partner_demo_27 Ahmad Taijan Rümelinstr 69 70191 Stuttgart person 2 0
29 res_partner_demo_28 Aileen Becker Eichendorffstr. 4 73630 Remshalden 015780645637 aileen.becker@gmx.de person 87 0
30 res_partner_demo_29 Ailey Simpson Eierstraße 44 A 70199 Stuttgart aileywsimpson@gmail.com person 1 0
31 res_partner_demo_30 Akira Mitsu Fritz-Ulrich-Weg 5 70567 Stuttgart mitsuakira0914@gmail.com person 5 0
32 res_partner_demo_31 Aksel Özdemir Rotebühlstraße 53 70178 Stuttgart aksel.oezdemir@gmx.de person 2 0
33 res_partner_demo_32 Albert Ebenbichler Am Backhaus 9 73666 Boltmannsweiler 01726101655 info@albert-ebenbichler.com person 1 0
34 res_partner_demo_33 Albert Kaupp Waldäckerstr. 10 70435 Stuttgart 0711 8263232 albert.kaupp@online.de person 2 0
35 res_partner_demo_34 Albrecht Barth Klopstockstr. 39 70193 Stuttgart albrecht.barth@web.de person 3 0
36 res_partner_demo_35 Albrecht Schlayer Im Netzbrunnen 17 70825 K-Münchingen aws1308@gmail.com person 1 0
37 res_partner_demo_36 Alec Dobler Kräherwald 251 70193 Stuttgart person 1 0
38 res_partner_demo_37 Alejandro Cano Perez Burgstallstraße 66 70199 Stuttgart cano.perez@gmx.de person 2 0
39 res_partner_demo_38 Alejandro Rodriguez Im Hirschwinkel 1 76297 Stutensee 015771409317 ralexei95@yahoo.de person 1 0
40 res_partner_demo_39 Alejandro Zarza Aguado Reinsburgstr. 152 70197 Stuttgart 017628401435 11alex96@gmail.com person 1 0
41 res_partner_demo_40 Aleksandar Vasić Lothringer Str. 5 70435 Stuttgart aleksvasic@web.de person 3 0
42 res_partner_demo_41 Alen Minasyan Kastanienallee 41/1 71638 Ludwigsburg bidilik@gmx.de person 1 0
43 res_partner_demo_42 Alex Olenberg Theodor-Rottschildstr. 25 73760 Stuttgart person 26 0
44 res_partner_demo_43 Alex Schaut Braunenbergweg 9 70806 Kornwestheim 07154 16530 aschaut@gmx.de person 3 0
45 res_partner_demo_44 Alexander Adloff Charlottenstraße 2 74074 Heilbronn alexadloff@gmx.de person 3 0
46 res_partner_demo_45 Alexander Bauer Im Himmel 20 70569 Stuttgart 071172237601 ab.312@icloud.com person 1 0
47 res_partner_demo_46 Alexander Blendl Neckarstr. 8 70736 Fellbach blendl.alex@gmail.com person 4 0
48 res_partner_demo_47 Alexander Borshov Schellingstraße 24 71277 Rutesheim aborshov@gmail.com person 1 0
49 res_partner_demo_48 Alexander Bosch Osterwiesenstr. 37 70794 Filderstadt bosch-alexander@web.de person 1 0
50 res_partner_demo_49 Alexander Braig Holzgrund Str. 25 70806 Kornwestheim a.braig84@gmx.de person 17 0
51 res_partner_demo_50 Alexander Carolus Kornbergstr. 23 70176 Stuttgart alexander.carolus person 1 0

View File

@ -1,2 +0,0 @@
/opt/odoo/odoo/odoo-bin shell -d hobbyhimmel < /home/odoo/custom_addons/open_workshop/demo/export_partner.py

View File

@ -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,
])

View File

@ -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)

View File

@ -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>

View File

@ -1,2 +0,0 @@
/opt/odoo/odoo/odoo-bin shell -d hobbyhimmel < /home/odoo/custom_addons/open_workshop/data/export_products_and_categories.py

View File

@ -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)

View File

@ -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'
])

View File

@ -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)

View File

@ -1 +0,0 @@
/opt/odoo/odoo/odoo-bin -d hobbyhimmel13_dev -i open_workshop --stop-after-init

View File

@ -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>

View File

@ -1 +0,0 @@
/opt/odoo/odoo/odoo-bin -d hobbyhimmel --update=open_workshop --dev=all --stop-after-init

2
log/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore

View File

@ -0,0 +1,300 @@
<?php
/**
* Plugin Name: OpenWorkshop Odoo API
* Plugin URI: https://hobbyhimmel.de/
* Description: Bindet Daten aus Odoo (OpenWorkshop) per REST-API in WordPress ein. Stellt u.a. den Shortcode [openworkshop_machines] bereit.
* Version: 0.1.0
* Author: HobbyHimmel / Matthias Lotz
* License: GPLv3
* License URI: https://www.gnu.org/licenses/gpl-3.0.html
* Text Domain: openworkshop-odoo-api
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class OpenWorkshop_Odoo_API_Plugin {
const OPTION_GROUP = 'openworkshop_odoo_api_options';
const OPTION_NAME = 'openworkshop_odoo_api_settings';
public function __construct() {
add_action( 'admin_menu', array( $this, 'register_settings_page' ) );
add_action( 'admin_init', array( $this, 'register_settings' ) );
add_shortcode( 'openworkshop_machines', array( $this, 'shortcode_machines' ) );
}
/**
* Register settings page under Settings OpenWorkshop Odoo API
*/
public function register_settings_page() {
add_options_page(
__( 'OpenWorkshop Odoo API', 'openworkshop-odoo-api' ),
__( 'OpenWorkshop Odoo API', 'openworkshop-odoo-api' ),
'manage_options',
'openworkshop-odoo-api',
array( $this, 'render_settings_page' )
);
}
/**
* Register settings (base URL + optional token)
*/
public function register_settings() {
register_setting(
self::OPTION_GROUP,
self::OPTION_NAME,
array(
'type' => 'array',
'sanitize_callback' => array( $this, 'sanitize_settings' ),
'default' => array(
'base_url' => '',
'api_token' => '',
'machines_endpoint' => '/api/v1/machines',
),
)
);
add_settings_section(
'openworkshop_odoo_api_main',
__( 'Odoo API Einstellungen', 'openworkshop-odoo-api' ),
'__return_false',
'openworkshop-odoo-api'
);
add_settings_field(
'base_url',
__( 'Odoo Basis-URL', 'openworkshop-odoo-api' ),
array( $this, 'render_field_base_url' ),
'openworkshop-odoo-api',
'openworkshop_odoo_api_main'
);
add_settings_field(
'machines_endpoint',
__( 'Endpoint für Maschinenliste', 'openworkshop-odoo-api' ),
array( $this, 'render_field_machines_endpoint' ),
'openworkshop-odoo-api',
'openworkshop_odoo_api_main'
);
add_settings_field(
'api_token',
__( 'API Token (optional)', 'openworkshop-odoo-api' ),
array( $this, 'render_field_api_token' ),
'openworkshop-odoo-api',
'openworkshop_odoo_api_main'
);
}
public function sanitize_settings( $input ) {
$output = array();
$output['base_url'] = isset( $input['base_url'] )
? esc_url_raw( rtrim( $input['base_url'], '/' ) )
: '';
$output['machines_endpoint'] = isset( $input['machines_endpoint'] )
? sanitize_text_field( $input['machines_endpoint'] )
: '/api/v1/machines';
$output['api_token'] = isset( $input['api_token'] )
? sanitize_text_field( $input['api_token'] )
: '';
return $output;
}
public function get_settings() {
$settings = get_option( self::OPTION_NAME, array() );
$defaults = array(
'base_url' => '',
'api_token' => '',
'machines_endpoint' => '/api/v1/machines',
);
return wp_parse_args( $settings, $defaults );
}
public function render_settings_page() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$settings = $this->get_settings();
?>
<div class="wrap">
<h1><?php esc_html_e( 'OpenWorkshop Odoo API Einstellungen', 'openworkshop-odoo-api' ); ?></h1>
<form method="post" action="options.php">
<?php
settings_fields( self::OPTION_GROUP );
do_settings_sections( 'openworkshop-odoo-api' );
submit_button();
?>
</form>
<h2><?php esc_html_e( 'Shortcodes', 'openworkshop-odoo-api' ); ?></h2>
<p><?php esc_html_e( 'Maschinenliste anzeigen:', 'openworkshop-odoo-api' ); ?></p>
<code>[openworkshop_machines]</code>
<p><?php esc_html_e( 'Optional können Attribute verwendet werden, um z. B. eine andere Anzahl von Maschinen anzuzeigen.', 'openworkshop-odoo-api' ); ?></p>
</div>
<?php
}
public function render_field_base_url() {
$settings = $this->get_settings();
?>
<input type="text"
name="<?php echo esc_attr( self::OPTION_NAME ); ?>[base_url]"
value="<?php echo esc_attr( $settings['base_url'] ); ?>"
class="regular-text"
placeholder="https://odoo.example.org" />
<p class="description">
<?php esc_html_e( 'Basis-URL deiner Odoo-Instanz (ohne Slash am Ende). Die API-Route wird daran angehängt.', 'openworkshop-odoo-api' ); ?>
</p>
<?php
}
public function render_field_machines_endpoint() {
$settings = $this->get_settings();
?>
<input type="text"
name="<?php echo esc_attr( self::OPTION_NAME ); ?>[machines_endpoint]"
value="<?php echo esc_attr( $settings['machines_endpoint'] ); ?>"
class="regular-text"
placeholder="/api/v1/machines" />
<p class="description">
<?php esc_html_e( 'Relativer Pfad zum Maschinen-Endpoint. Standard: /api/v1/machines', 'openworkshop-odoo-api' ); ?>
</p>
<?php
}
public function render_field_api_token() {
$settings = $this->get_settings();
?>
<input type="text"
name="<?php echo esc_attr( self::OPTION_NAME ); ?>[api_token]"
value="<?php echo esc_attr( $settings['api_token'] ); ?>"
class="regular-text" />
<p class="description">
<?php esc_html_e( 'Optionaler API-Token, der im Authorization-Header gesendet wird (Bearer &lt;token&gt;).', 'openworkshop-odoo-api' ); ?>
</p>
<?php
}
/**
* Shortcode: [openworkshop_machines]
* Attributes:
* limit Anzahl der Einträge (optional)
*/
public function shortcode_machines( $atts ) {
$atts = shortcode_atts(
array(
'limit' => 0,
),
$atts,
'openworkshop_machines'
);
$data = $this->fetch_machines();
if ( is_wp_error( $data ) ) {
return '<p>' . esc_html__( 'Fehler beim Laden der Maschinendaten aus Odoo.', 'openworkshop-odoo-api' ) . '</p>';
}
if ( ! is_array( $data ) || empty( $data ) ) {
return '<p>' . esc_html__( 'Keine Maschinen gefunden.', 'openworkshop-odoo-api' ) . '</p>';
}
$limit = intval( $atts['limit'] );
if ( $limit > 0 ) {
$data = array_slice( $data, 0, $limit );
}
ob_start();
?>
<div class="openworkshop-machines">
<table class="openworkshop-machines-table">
<thead>
<tr>
<th><?php esc_html_e( 'Name', 'openworkshop-odoo-api' ); ?></th>
<th><?php esc_html_e( 'Bereich', 'openworkshop-odoo-api' ); ?></th>
<th><?php esc_html_e( 'Status', 'openworkshop-odoo-api' ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $data as $machine ) : ?>
<tr>
<td>
<?php echo isset( $machine['name'] ) ? esc_html( $machine['name'] ) : ''; ?>
</td>
<td>
<?php echo isset( $machine['area'] ) ? esc_html( $machine['area'] ) : ''; ?>
</td>
<td>
<?php echo isset( $machine['status'] ) ? esc_html( $machine['status'] ) : ''; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php
return ob_get_clean();
}
/**
* Calls the Odoo API and returns decoded JSON or WP_Error.
*/
protected function fetch_machines() {
$settings = $this->get_settings();
if ( empty( $settings['base_url'] ) ) {
return new WP_Error( 'openworkshop_no_base_url', __( 'Keine Odoo Basis-URL konfiguriert.', 'openworkshop-odoo-api' ) );
}
$url = $settings['base_url'] . $settings['machines_endpoint'];
$args = array(
'timeout' => 10,
'headers' => array(
'Accept' => 'application/json',
),
);
if ( ! empty( $settings['api_token'] ) ) {
$args['headers']['Authorization'] = 'Bearer ' . $settings['api_token'];
}
$response = wp_remote_get( $url, $args );
if ( is_wp_error( $response ) ) {
return $response;
}
$code = wp_remote_retrieve_response_code( $response );
$body = wp_remote_retrieve_body( $response );
if ( $code < 200 || $code >= 300 ) {
return new WP_Error( 'openworkshop_bad_status', sprintf( 'HTTP %d', $code ) );
}
$data = json_decode( $body, true );
if ( json_last_error() !== JSON_ERROR_NONE ) {
return new WP_Error( 'openworkshop_json_error', json_last_error_msg() );
}
return $data;
}
}
function openworkshop_odoo_api_bootstrap() {
static $instance = null;
if ( $instance === null ) {
$instance = new OpenWorkshop_Odoo_API_Plugin();
}
return $instance;
}
openworkshop_odoo_api_bootstrap();

View File

@ -0,0 +1,30 @@
=== OpenWorkshop Odoo API ===
Contributors: hobbyhimmel
Tags: odoo, api, openworkshop, integration
Requires at least: 5.8
Tested up to: 6.6
Stable tag: 0.1.0
License: GPLv3
License URI: https://www.gnu.org/licenses/gpl-3.0.html
Dieses Plugin bindet Maschinendaten aus einer Odoo/OpenWorkshop-Installation via REST-API in WordPress ein.
== Beschreibung ==
Das Plugin stellt u.a. den Shortcode [openworkshop_machines] bereit, der eine einfache Maschinenliste
auf Basis eines JSON-Endpunkts in Odoo rendert.
In den Einstellungen kann die Basis-URL der Odoo-Instanz, der Endpoint (z.B. /api/v1/machines) sowie ein
optionaler API-Token hinterlegt werden.
== Installation ==
1. ZIP in WordPress unter Plugins → Installieren → Plugin hochladen hochladen.
2. Aktivieren.
3. Unter Einstellungen → OpenWorkshop Odoo API die Basis-URL und den Endpoint konfigurieren.
4. Den Shortcode [openworkshop_machines] in einer Seite oder einem Beitrag einfügen.
== Changelog ==
= 0.1.0 =
* Erste Version.

View File

@ -0,0 +1,11 @@
{
"folders": [
{
"path": "."
},
{
"path": "../../../usr/lib/python3/dist-packages/odoo"
}
],
"settings": {}
}

View File

@ -0,0 +1,3 @@
from . import models
from . import controllers

View File

@ -0,0 +1,39 @@
{
'name': 'POS Open Workshop',
'license': 'AGPL-3',
'version': '18.0.1.0.1',
'summary': 'Erstellt Maschinenfreigaben basierend auf POS-Einweisungsprodukten',
'depends': ['base', 'account', 'hr','product','sale','contacts','point_of_sale'],
'author': 'matthias.lotz',
'category': 'Point of Sale',
'data': [
'security/ir.model.access.csv',
'views/machine_product_training_views.xml',
'views/menu_views.xml',
'views/machine_area_views.xml',
'views/machine_views.xml',
'views/res_partner_view.xml',
'data/data.xml',
],
'installable': False, # Wird durch open_workshop_base ersetzt
'assets': {
'web.assets_backend': [
'open_workshop/static/src/css/category_color.css',
],
'point_of_sale._assets_pos': [
'open_workshop/static/src/css/pos.css',
'open_workshop/static/src/js/ows_machine_access_list.js',
'open_workshop/static/src/js/ows_pos_customer_sidebar.js',
'open_workshop/static/src/js/ows_pos_sidebar.js',
'open_workshop/static/src/js/ows_product_screen_template_patch.js',
'open_workshop/static/src/xml/ows_machine_access_list.xml',
'open_workshop/static/src/xml/ows_pos_customer_sidebar.xml',
'open_workshop/static/src/xml/ows_pos_sidebar.xml',
'open_workshop/static/src/xml/ows_product_screen_template_patch.xml',
],
},
'description': """
Diese App erstellt Maschinenfreigaben basierend auf POS-Einweisungsprodukten.
Die App ist für den Einsatz in der Odoo-Version 18.0 konzipiert.
""",
}

View File

@ -1,11 +1,14 @@
# Datei: controllers/pos_access.py
from odoo import http
from odoo.http import request
class OpenWorkshopPOSController(http.Controller):
@http.route('/open_workshop/partner_access', type='json', auth='user')
def get_partner_machine_access(self, partner_id):
def get_partner_machine_access(self, **kwargs):
partner_id = kwargs.get('params', {}).get('partner_id')
if not partner_id:
return {"error": "Missing partner_id"}
Machine = request.env['ows.machine'].sudo()
return Machine.get_access_list_grouped(partner_id)

View File

@ -8,6 +8,42 @@ import logging
_logger = logging.getLogger(__name__)
_logger.info("✅ ows_models.py geladen")
class HREmployee(models.Model):
_inherit = 'hr.employee'
@api.model
def anonymize_for_testsystem(self):
"""Benennt Admin-Angestellten um und archiviert alle anderen für das Testsystem."""
admin_user = self.env['res.users'].search([('name', '=', 'Administrator')], limit=1)
if not admin_user:
_logger.error("[OWS] Administrator-Benutzer nicht gefunden!")
return
_logger.info(f"[OWS] Administrator-Benutzer gefunden: {admin_user.name} (ID: {admin_user.id})")
# Suche auch archivierte Employees
admin_employee = self.with_context(active_test=False).search([('user_id', '=', admin_user.id)], limit=1)
if admin_employee:
# Administrator-Employee reaktivieren und umbenennen
admin_employee.write({
'name': 'TESTSYSTEM',
'job_title': 'Testumgebung',
'work_email': 'office@hobbyhimmel.de',
'work_phone': False,
'active': True, # Reaktivieren falls archiviert
})
_logger.info(f"[OWS] Admin-Angestellter reaktiviert und umbenannt: {admin_employee.name} (ID: {admin_employee.id})")
else:
_logger.warning("[OWS] Kein Angestellter für Administrator gefunden.")
return
# Alle anderen Angestellten archivieren (auch bereits archivierte berücksichtigen)
other_employees = self.with_context(active_test=False).search([('id', '!=', admin_employee.id)])
other_employees.write({'active': False})
_logger.info("[OWS] %d Angestellte archiviert.", len(other_employees))
class ResPartner(models.Model):
_inherit = 'res.partner'
_logger.info("✅ ows ResPartner geladen")
@ -141,32 +177,50 @@ class ResPartner(models.Model):
def _compute_machine_access_html(self):
areas = self.env['ows.machine.area'].search([], order="name")
for partner in self:
html = "<div style='display: grid; grid-template-columns: repeat(3, 1fr); gap: 2rem;'>"
html = ""
for area in areas:
html += f"<div class='o_group' style='margin-bottom: 1em;'>"
html += f"<table class='o_group o_inner_group'>"
html += f"<thead><tr><th>{area.name}</th><th></th><th>Datum</th><th>Gültig bis</th></tr></thead><tbody>"
html += f"""
<div class="o_form_sheet">
<h3 class="o_form_label">{area.name}</h3>
<table class="table table-sm table-bordered o_form_table">
<thead>
<tr>
<th>Maschine</th>
<th>Status</th>
<th>Datum</th>
<th>Gültig bis</th>
</tr>
</thead>
<tbody>
"""
machines = self.env['ows.machine'].search([('area_id', '=', area.id)], order="name")
for machine in machines:
access = self.env['ows.machine.access'].search([
('partner_id', '=', partner.id),
('machine_id', '=', machine.id),
], limit=1)
icon = "<span class='fa fa-check text-success'></span>" if access else "<span class='fa fa-times text-danger'></span>"
date_granted = access.date_granted.strftime('%Y-%m-%d') if access and access.date_granted else ""
date_expiry = access.date_expiry.strftime('%Y-%m-%d') if access and access.date_expiry else ""
icon = '<span class="text-success fa fa-check"/>' if access else '<span class="text-danger fa fa-times"/>'
date_granted = access.date_granted.strftime('%Y-%m-%d') if access and access.date_granted else "-"
date_expiry = access.date_expiry.strftime('%Y-%m-%d') if access and access.date_expiry else "-"
html += f"""
<tr>
<td class='o_td_label'><label>{machine.name}</label></td>
<td class='o_td_field'>{icon}</td>
<td class='o_td_field'>{date_granted}</td>
<td class='o_td_field'>{date_expiry}</td>
</tr>
<tr>
<td>{machine.name}</td>
<td>{icon}</td>
<td>{date_granted}</td>
<td>{date_expiry}</td>
</tr>
"""
html += "</tbody></table></div>"
html += "</div>"
partner.machine_access_html = html
@api.model
def migrate_existing_partners(self):
"""
@ -283,6 +337,31 @@ class ResPartner(models.Model):
_logger.info(f"[OWS Migration] ✅ Maschinenfreigaben erstellt: {count_created}")
self.env.cr.commit()
@api.model
def archive_partners_without_users(self):
"""
Archiviert alle Partner (res.partner), die keine Benutzer (res.users) sind.
"""
Partner = self.env['res.partner']
User = self.env['res.users']
# IDs aller Partner, die ein Benutzerkonto haben
user_partner_ids = User.search([]).mapped('partner_id').ids
# Alle Partner ohne Benutzerkonto
partners_to_archive = Partner.search([
('id', 'not in', user_partner_ids),
('active', '=', True),
])
count = len(partners_to_archive)
partners_to_archive.write({'active': False})
for p in partners_to_archive:
_logger.debug(f"[OWS] Archiviert Partner: {p.name} (ID {p.id})")
_logger.info(f"[OWS] Archiviert {count} Partner ohne Benutzerkonto.")
self.env.cr.commit()
class OwsUser(models.Model):
_name = 'ows.user'
@ -307,15 +386,57 @@ class OwsUser(models.Model):
]
AVAILABLE_COLORS = [
('#000000', 'schwarz'),
('#ff0000', 'Rot'),
('#E91E63', 'Pink'),
('#9C27B0', 'Lila'),
('#3F51B5', 'Indigo'),
('#0000ff', 'Blau'),
('#008000', 'Grün'),
('#ffff00', 'Gelb'),
('#FF9800', 'Orange'),
('#795548', 'Braun'),
('#ffffff', 'Weiss'),
]
class OwsMachineArea(models.Model):
_name = 'ows.machine.area'
_table = "ows_machine_area"
_table = 'ows_machine_area'
_description = 'OWS: Maschinenbereich'
_order = 'name'
name = fields.Char(required=True, translate=True)
#color = fields.Integer(string="Farbe")
color_hex = fields.Char(string="Farbe (Hex)", help="Hex-Farbcode wie #FF0000 für Rot")
name = fields.Char(string="Name", required=True, translate=True)
color_hex = fields.Selection(
selection=AVAILABLE_COLORS,
string="Farbe (Hex)",
required=True,
)
color_hex_value = fields.Char(
string="Farbcode",
compute='_compute_color_hex_value',
store=False
)
color_name = fields.Char(
string="Farbname",
compute='_compute_color_name',
store=False
)
@api.depends('color_hex')
def _compute_color_hex_value(self):
for rec in self:
rec.color_hex_value = rec.color_hex or ''
@api.depends('color_hex')
def _compute_color_name(self):
label_dict = dict(AVAILABLE_COLORS)
for rec in self:
rec.color_name = label_dict.get(rec.color_hex, 'Unbekannt')
class OwsMachine(models.Model):
@ -325,13 +446,37 @@ class OwsMachine(models.Model):
name = fields.Char(required=True, translate=True)
code = fields.Char(required=True, help="Eindeutiger Kurzcode, z.B. 'lasercutter'")
category = fields.Selection([
('green', 'Kategorie 1: grün'),
('yellow', 'Kategorie 2: gelb'),
('red', 'Kategorie 3: rot'),
], string="Sicherheitskategorie", required=True, default='red', help="Sicherheitsrelevante Maschinenkategorie:\n"
"- grün: keine Einweisungspflicht\n"
"- gelb: empfohlene Einweisung\n"
"- rot: Einweisung zwingend erforderlich")
category_icon = fields.Char(string="Kategorie-Symbol", compute="_compute_category_icon", store=False)
@api.depends('category')
def _compute_category_icon(self):
for rec in self:
icon_map = {
'green': '🟢',
'yellow': '🟡',
'red': '🔴',
}
rec.category_icon = icon_map.get(rec.category, '')
description = fields.Text()
active = fields.Boolean(default=True)
area_id = fields.Many2one('ows.machine.area', string='Bereich')
area_id = fields.Many2one('ows.machine.area', string='Bereich', help="Bereich, in dem die Maschine oder das Gerät steht.")
product_ids = fields.One2many('ows.machine.product', 'machine_id', string="Nutzungsprodukte")
product_names = fields.Char(string="Nutzungsprodukte Liste", compute="_compute_product_using_names", store=False,)
product_names = fields.Char(string="Liste der Nutzungsprodukte", compute="_compute_product_using_names", store=False,)
training_ids = fields.One2many('ows.machine.training', 'machine_id', string="Einweisungsprodukte")
training_names = fields.Char(string="Einweisungsprodukte Liste", compute="_compute_product_training_names", store=False,)
training_names = fields.Char(string="Liste der Einweisungsprodukte", compute="_compute_product_training_names", store=False,)
storage_location = fields.Char(string="Lagerort", help="Lagerort der Maschine oder des Geräts.")
purchase_price = fields.Float(string="Kaufpreis", help="Kaufpreis der Maschine oder des Geräts.")
purchase_date = fields.Date(string="Kaufdatum", help="Kaufdatum der Maschine oder des Geräts.")
@api.depends('product_ids.product_id.name')
def _compute_product_using_names(self):
@ -354,12 +499,34 @@ class OwsMachine(models.Model):
@api.model
def get_access_list_grouped(self, partner_id):
"""
Gibt eine gruppierte Liste von Maschinenzugängen für einen bestimmten Partner zurück. Diese Funktion wird in
Odoo POS Frontend verwendet um die Ansicht zu erzeugen auf Welche Maschinen der Partner Zugriff hat.
Für einen gegebenen Partner (über die partner_id) werden alle Maschinenbereiche (areas) abgefragt.
Für jeden Bereich wird geprüft, auf welche Maschinen der Partner Zugriff hat. Das Ergebnis wird
als Liste von Bereichen mit jeweils zugehörigen Maschinen und Zugriffsstatus zurückgegeben.
Zusätzlich werden sicherheitsrelevante Informationen des Partners (wie Sicherheitsunterweisung,
Sicherheits-ID, RFID-Karte und Geburtstag) aus dem zugehörigen ows_user ermittelt und mitgeliefert.
Args:
partner_id (int): Die ID des Partners, für den die Zugriffsübersicht erstellt werden soll.
Returns:
dict: Ein Dictionary mit folgenden Schlüsseln:
- 'access_by_area': Liste von Bereichen mit Maschinen und Zugriffsstatus.
- 'security_briefing': Sicherheitsunterweisung des Nutzers (bool oder False).
- 'security_id': Sicherheits-ID des Nutzers (str oder '').
- 'rfid_card': RFID-Kartennummer des Nutzers (str oder '').
- 'birthday': Geburtstag des Nutzers (str oder '').
"""
partner = self.env['res.partner'].browse(partner_id)
areas = self.env['ows.machine.area'].search([], order="name")
_logger.info("🔍 Maschinenbereiche: %s", areas.mapped('name'))
_logger.info("🔍 Partner_id: %s", partner_id)
res = []
_logger.info("Access RPC called with partner_id=%s", partner_id)
access_by_area = []
for area in areas:
machines = self.search([('area_id', '=', area.id)], order="name")
machines = self.search([('area_id', '=', area.id), ('category', '=', 'red')], order="name")
machine_list = []
for machine in machines:
has_access = bool(self.env['ows.machine.access'].search([
@ -370,12 +537,22 @@ class OwsMachine(models.Model):
'name': machine.name,
'has_access': has_access,
})
res.append({
'area': area.name,
'color_hex': area.color_hex or '#000000',
'machines': machine_list
})
return res
if machine_list:
access_by_area.append({
'area': area.name,
'color_hex': area.color_hex or '#000000',
'machines': machine_list
})
user = partner.ows_user_id[:1]
return {
'access_by_area': access_by_area,
'security_briefing': user.security_briefing if user else False,
'security_id': user.security_id if user else '',
'rfid_card': user.rfid_card if user else '',
'birthday': user.birthday if user else '',
}
class OwsMachineAccess(models.Model):
@ -398,7 +575,7 @@ class OwsMachineAccess(models.Model):
class OwsMachineProduct(models.Model):
_name = 'ows.machine.product'
_table = 'ows_machine_product'
_description = 'OWS: Zurordnung Produkt der Nutzung zur die Maschine'
_description = 'OWS: Zuordnung Produkt der Nutzung zu der Maschine'
product_id = fields.Many2one('product.product', required=True, domain=[('available_in_pos', '=', True)], ondelete='cascade')
machine_id = fields.Many2one('ows.machine', required=True, ondelete='cascade')
@ -406,7 +583,7 @@ class OwsMachineProduct(models.Model):
class OwsMachineTraining(models.Model):
_name = 'ows.machine.training'
_table = 'ows_machine_training'
_description = 'OWS: Zurordnung Produkt der Einweisung zur die Maschine'
_description = 'OWS: Zuordnung Produkt der Einweisung zu der Maschine'
training_id = fields.Many2one('product.product', required=True, domain=[('available_in_pos', '=', True)], ondelete='cascade')
machine_id = fields.Many2one('ows.machine', required=True, ondelete='cascade')

View File

@ -1,12 +1,13 @@
from odoo import models, fields, api
from collections import defaultdict
#import debugpy
import logging
_logger = logging.getLogger(__name__)
_logger.info("✅ pos_order.py geladen")
_logger.info("✅ pos_order.py geladen Test 2")
# debugpy.listen(("0.0.0.0", 5678))
#debugpy.listen(("0.0.0.0", 5678))
print("✅ debugpy wartet auf Verbindung (Port 5678) ...")
# Optional: Starte erst, wenn VS Code verbunden ist
#debugpy.wait_for_client()
@ -14,15 +15,15 @@ print("✅ debugpy wartet auf Verbindung (Port 5678) ...")
class PosOrder(models.Model):
_inherit = 'pos.order'
def _process_order(self, order, draft, existing_order):
pos_order_id = super(PosOrder, self)._process_order(order, draft, existing_order)
def _process_order(self, order, existing_order):
_logger.info("🚨 DEBUG: _process_order wurde aufgerufen mit order: %s", order.get('name', 'unbekannt'))
pos_order_id = super(PosOrder, self)._process_order(order, existing_order)
pos_order = self.browse(pos_order_id)
training_products = self.env['ows.machine.training'].search([])
product_map = {
tp.training_id.product_tmpl_id.id: tp.machine_id.id
for tp in training_products
}
product_map = defaultdict(list)
for tp in training_products:
product_map[tp.training_id.product_tmpl_id.id].append(tp.machine_id.id)
partner = pos_order.partner_id
if not partner:
@ -31,15 +32,13 @@ class PosOrder(models.Model):
for line in pos_order.lines:
product_tmpl_id = line.product_id.product_tmpl_id.id
machine_id = product_map.get(product_tmpl_id)
_logger.info("🔍 Prüfe Produkt %s → Maschine ID: %s", line.product_id.display_name, machine_id)
if machine_id:
machine_ids = product_map.get(product_tmpl_id, [])
_logger.info("🔍 Prüfe Produkt %s → Maschinen IDs: %s", line.product_id.display_name, machine_ids)
for machine_id in machine_ids:
already_exists = self.env['ows.machine.access'].search([
('partner_id', '=', partner.id),
('machine_id', '=', machine_id)
])
], limit=1)
if not already_exists:
self.env['ows.machine.access'].create({
'partner_id': partner.id,

View 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;
}

View File

@ -0,0 +1,57 @@
.custompane {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
.ows-sidebar {
flex: 1 1 auto;
width: 220px;
display: flex;
flex-direction: column;
overflow: hidden;
}
.order-entry:hover { cursor: pointer; }
.order-entry.selected { background-color: #007bff; color: white; }
.ows-customer-list {
flex: 1 1 auto;
overflow-y: auto;
min-height: 0; /* notwendig für Scrollbar */
}
.client-details-grid {
flex-shrink: 0;
max-height: 60%;
overflow-y: auto;
background: #fff;
}
.pos *::-webkit-scrollbar {
width: 8px;
height:8px;
}
.sidebar-line {
display: flex;
justify-content: space-between;
gap: 0.5em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 0.2em 0;
}
.sidebar-date {
flex-shrink: 0;
}
.sidebar-name {
flex-shrink: 1;
text-align: right;
overflow: hidden;
text-overflow: ellipsis;
}

View File

@ -0,0 +1,72 @@
// @odoo-module ows_machine_access_list.js
import { Component, useState } from "@odoo/owl";
import { useBus } from "@web/core/utils/hooks";
import { usePos } from "@point_of_sale/app/store/pos_hook";
import { rpc } from "@web/core/network/rpc";
import { registry } from "@web/core/registry";
export class OwsMachineAccessList extends Component {
static template = 'open_workshop.OwsMachineAccessList';
setup() {
this.pos = usePos();
this.state = useState({
client: null,
grouped_accesses: [],
security_briefing: false,
security_id: '',
rfid_card: '',
birthday: '',
});
// 🔁 Reagiere auf Partnerwechsel über den Odoo-Bus
useBus(this.env.bus, 'partner-changed', () => {
this.updateAccessList();
});
// 🔃 Beim Mounten initiale Daten laden
this.updateAccessList();
}
async updateAccessList() {
const order = this.pos.get_order();
const partner = order?.get_partner?.();
this.state.client = partner || null;
if (!partner) {
this.state.grouped_accesses = [];
this.state.security_briefing = false;
this.state.security_id = '';
this.state.rfid_card = '';
this.state.birthday = '';
return;
}
try {
const data = await rpc("/open_workshop/partner_access", {
params: { partner_id: partner.id },
});
this.state.grouped_accesses = data.access_by_area || [];
this.state.security_briefing = data.security_briefing;
this.state.security_id = data.security_id;
this.state.rfid_card = data.rfid_card;
this.state.birthday = data.birthday;
} catch (error) {
console.error("Fehler beim Laden der Einweisungen:", error);
this.state.grouped_accesses = [];
this.state.security_briefing = false;
this.state.security_id = '';
this.state.rfid_card = '';
this.state.birthday = '';
}
}
}
registry.category("templates").add("open_workshop.OwsMachineAccessList", OwsMachineAccessList);

View File

@ -0,0 +1,54 @@
// @odoo-module ows_pos_customer_sidebar.js
import { Component } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";
import { usePos } from "@point_of_sale/app/store/pos_hook";
import { _t } from "@web/core/l10n/translation";
import { ask } from "@web/core/confirmation_dialog/confirmation_dialog";
export class OwsPosCustomerSidebar extends Component {
static template = "open_workshop.OwsPosCustomerSidebar";
setup() {
this.pos = usePos(); // ✅ Holt dir Zugriff auf den zentralen POS-Store
this.dialog = useService("dialog");
}
addOrder() {
this.pos.add_new_order(); // ✅ Neue Order wird aktive Order
this.pos.showScreen("ProductScreen");
this.pos.selectPartner();
this.env.bus.trigger('partner-changed'); // ✅ Event manuell auslösen
}
async removeCurrentOrder() {
this.pos.onDeleteOrder(this.pos.get_order())
}
openTicketScreen() {
this.pos.showScreen("TicketScreen");
}
// 🔧 FIXED: Zugriff auf Order-Liste korrigiert
getFilteredOrderList() {
return this.pos.get_open_orders();
}
getDate(order) {
const date = new Date(order.date_order);
const dd = String(date.getDate()).padStart(2, '0');
const mm = String(date.getMonth() + 1).padStart(2, '0');
const hh = String(date.getHours()).padStart(2, '0');
const mi = String(date.getMinutes()).padStart(2, '0');
return `${dd}.${mm}. ${hh}:${mi}`;
}
getPartner(order) {
return order.get_partner()?.name || "Kein Kunde";
}
selectOrder(order) {
this.pos.set_order(order);
this.env.bus.trigger('partner-changed');
}
}

View File

@ -0,0 +1,11 @@
// ows_pos_sidebar.js
// @odoo-module
import { Component } from "@odoo/owl";
import { OwsPosCustomerSidebar } from "./ows_pos_customer_sidebar";
import { OwsMachineAccessList } from "./ows_machine_access_list";
export class OwsPosSidebar extends Component {
static template = "open_workshop.OwsPosSidebar";
static components = { OwsPosCustomerSidebar, OwsMachineAccessList };
}

View File

@ -0,0 +1,15 @@
// product_screen_template_patch.js
// @odoo-module
import { registry } from "@web/core/registry";
import { ProductScreen } from "@point_of_sale/app/screens/product_screen/product_screen";
import { OwsPosSidebar } from "./ows_pos_sidebar";
class OwsProductScreen extends ProductScreen {
static components = Object.assign({}, ProductScreen.components, {
OwsPosSidebar,
});
}
registry.category("pos_screens").remove("ProductScreen");
registry.category("pos_screens").add("ProductScreen", OwsProductScreen);

View File

@ -0,0 +1,76 @@
<t t-name="open_workshop.OwsMachineAccessList">
<div class="client-details-grid p-2 small">
<!-- ✅ Sicherheitsbereich -->
<t t-if="state.client">
<div class="client-details-header">
<ul>
<li><span class="client-details-label">Einweisungen</span></li>
</ul>
<div class="client-details-area border" t-att-style="'border: solid 3px #ffffff; margin: 5px;'">
<ul>
<li class="client-detail">
<span class="detail client-details-vvow_briefing"></span>
<span class="briefinglabel">Werkstatt</span>
</li>
<li class="client-detail">
<t t-if="!state.security_briefing">
<span class="detail client-details-vvow_briefing_error"></span>
</t>
<t t-if="state.security_briefing">
<span class="detail client-details-vvow_briefing"></span>
</t>
<span class="briefinglabel">Haftungsausschluss</span>
</li>
<t t-if="!state.security_briefing">
<li class="client-detail">
<ul class="subpoints">
<span class="detail client-details-vvow_sec_briefing_error">Bitte Prüfen‼</span>
</ul>
</li>
</t>
<t t-if="state.security_briefing">
<ul class="subpoints">
<li class="client-detail">
<span class="label">Id:</span>
<span class="detail client-details-vvow_security_id">
<t t-esc="state.security_id || 'N/A'" />
</span>
</li>
<li class="client-detail">
<span class="label">Geburtstag:</span>
<span class="detail client-details-vvow_security_id">
<t t-esc="state.birthday || 'N/A'" />
</span>
</li>
</ul>
</t>
</ul>
</div>
</div>
</t>
<!-- ✅ Maschinenliste: immer sichtbar, gefiltert -->
<t t-foreach="state.grouped_accesses" t-as="area" t-key="area.area">
<t t-if="area.machines.length > 0">
<div class="client-details-area" t-att-style="'border: solid 3px ' + area.color_hex + '; margin: 5px;'">
<ul>
<t t-foreach="area.machines" t-as="machine" t-key="machine.name">
<li class="client-detail">
<span t-attf-class="detail {{ machine.has_access ? 'client-details-vvow_briefing' : 'client-details-vvow_briefing_error' }}">
<t t-esc="machine.has_access ? '✅' : '❌'" />
</span>
<span class="briefinglabel"><t t-esc="machine.name"/></span>
</li>
</t>
</ul>
</div>
</t>
</t>
</div>
</t>

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="open_workshop.OwsPosCustomerSidebar" owl="1">
<div class="ows-sidebar p-2 bg-light border-end h-100">
<div class="ows-sidebar-header mb-2 d-flex justify-content-between align-items-center">
<button class="btn btn-secondary" t-on-click="openTicketScreen">Orders</button>
<div>
<button class="btn btn-sm btn-success me-1" t-on-click="addOrder">+</button>
<button class="btn btn-sm btn-danger" t-on-click="removeCurrentOrder"></button>
</div>
</div>
<div class="ows-customer-list overflow-auto">
<t t-foreach="getFilteredOrderList()" t-as="order" t-key="order.uid">
<!--div class="order-entry p-1 rounded mb-1 border"
t-att-class="order === pos.get_order() ? 'bg-primary text-white' : 'bg-white'"
t-on-click="() => selectOrder(order)"-->
<div t-att-class="'order-entry' + (order === pos.get_order() ? ' selected' : '')" t-on-click="() => this.selectOrder(order)">
<div class="sidebar-line">
<span class="sidebar-date"><t t-esc="getDate(order)"/></span>
<span class="sidebar-name" t-att-title="getPartner(order)"><t t-esc="getPartner(order)"/></span>
</div>
</div>
</t>
</div>
</div>
</t>
</templates>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="open_workshop.OwsPosSidebar" owl="1">
<div class="custompane d-flex flex-column border-end bg-200">
<OwsPosCustomerSidebar />
<OwsMachineAccessList />
</div>
</t>
</templates>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.ProductScreen" t-inherit="point_of_sale.ProductScreen" t-inherit-mode="extension">
<xpath expr="//div[contains(@class, 'leftpane')]" position="before">
<OwsPosSidebar />
</xpath>
</t>
</templates>

View File

@ -0,0 +1,8 @@
<odoo>
<template id="assets_open_workshop" inherit_id="point_of_sale._assets_pos">
<xpath expr="." position="inside">
<script type="text/javascript" src="/open_workshop/static/src/js/machine_access_sidebar.js"/>
<link rel="stylesheet" type="text/css" href="/open_workshop/static/src/css/pos.css"/>
</xpath>
</template>
</odoo>

View File

@ -4,7 +4,7 @@
<record id="action_machine_area_list" model="ir.actions.act_window">
<field name="name">Maschinenbereiche</field>
<field name="res_model">ows.machine.area</field>
<field name="view_mode">tree,form</field>
<field name="view_mode">list,form</field>
</record>
<!-- Menüpunkt unter Maschinen > Konfiguration -->
@ -15,10 +15,11 @@
<field name="name">ows.machine.area.tree</field>
<field name="model">ows.machine.area</field>
<field name="arch" type="xml">
<tree>
<list>
<field name="name"/>
<field name="color_hex" widget="color_picker"/>
</tree>
<field name="color_hex_value" string="Farbe (Hex)"/>
<field name="color_name" string="Farbname"/>
</list>
</field>
</record>
@ -30,7 +31,8 @@
<form string="Maschinenbereich">
<group>
<field name="name"/>
<field name="color_hex" widget="color_picker"/>
<field name="color_hex"/>
<field name="color_name" readonly="1"/>
</group>
</form>
</field>

View File

@ -4,10 +4,10 @@
<field name="name">ows.machine.product.tree</field>
<field name="model">ows.machine.product</field>
<field name="arch" type="xml">
<tree editable="bottom">
<list editable="bottom">
<field name="machine_id"/>
<field name="product_id" domain="[('categ_id.name', '=', 'Maschinennutzung')]"/>
</tree>
</list>
</field>
</record>
@ -16,10 +16,10 @@
<field name="name">ows.machine.training.tree</field>
<field name="model">ows.machine.training</field>
<field name="arch" type="xml">
<tree editable="bottom">
<list editable="bottom">
<field name="machine_id"/>
<field name="training_id" domain="[('categ_id.name', '=', 'Einweisungen')]"/>
</tree>
<field name="training_id" domain="[('categ_id.name', 'in', ['Einweisungen', 'Kurse'])]"/>
</list>
</field>
</record>
@ -27,7 +27,7 @@
<record id="action_machine_product" model="ir.actions.act_window">
<field name="name">Maschinen-Nutzungsprodukte</field>
<field name="res_model">ows.machine.product</field>
<field name="view_mode">tree</field>
<field name="view_mode">list</field>
<field name="view_id" ref="view_machine_product_tree"/>
<field name="help" type="html">
<p>Verwalte die Zuordnung von Maschinen zu Nutzungsprodukten.</p>
@ -38,7 +38,7 @@
<record id="action_machine_training" model="ir.actions.act_window">
<field name="name">Maschinen-Einweisungsprodukte</field>
<field name="res_model">ows.machine.training</field>
<field name="view_mode">tree</field>
<field name="view_mode">list</field>
<field name="view_id" ref="view_machine_training_tree"/>
<field name="help" type="html">
<p>Verwalte die Zuordnung von Maschinen zu Einweisungsprodukten.</p>

View File

@ -0,0 +1,68 @@
<!-- machine_views.xml -->
<odoo>
<!-- Maschinen Listenansicht -->
<record id="view_machine_tree" model="ir.ui.view">
<field name="name">ows.machine.tree</field>
<field name="model">ows.machine</field>
<field name="arch" type="xml">
<list>
<field name="category_icon" string="⚙" readonly="1"/>
<field name="name"/>
<field name="category"/>
<field name="code"/>
<field name="area_id" widget="many2one_color"/>
<field name="product_names"/>
<field name="training_names"/>
<field name="storage_location"/>
<field name="purchase_price"/>
<field name="purchase_date"/>
<field name="active"/>
</list>
</field>
</record>
<!-- Maschinen Formularansicht -->
<record id="view_machine_form" model="ir.ui.view">
<field name="name">ows.machine.form</field>
<field name="model">ows.machine</field>
<field name="arch" type="xml">
<form string="Maschine">
<sheet>
<group>
<field name="category_icon" string="⚙" readonly="1"/>
<field name="name"/>
<field name="category"/>
<field name="code"/>
<field name="area_id"/>
</group>
<group>
<field name="description"/>
<field name="storage_location"/>
<field name="purchase_price"/>
<field name="purchase_date"/>
<field name="active"/>
</group>
<!-- Notebook für Produkte und Einweisungen -->
<notebook>
<page string="Nutzungsprodukte">
<field name="product_ids" context="{'default_machine_id': id}">
<list editable="bottom">
<field name="product_id" domain="[('categ_id.name', '=', 'Maschinennutzung')]" />
</list>
</field>
</page>
<page string="Einweisungsprodukte">
<field name="training_ids" context="{'default_machine_id': id}">
<list editable="bottom">
<field name="training_id" domain="[('categ_id.name', 'in', ['Einweisungen', 'Kurse'])]" />
</list>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
</odoo>

View File

@ -4,14 +4,14 @@
<record id="action_machine_list" model="ir.actions.act_window">
<field name="name">Maschinen</field>
<field name="res_model">ows.machine</field>
<field name="view_mode">tree,form</field>
<field name="view_mode">list,form</field>
</record>
<!-- Trainingsprodukt-Liste -->
<record id="action_training_product_list" model="ir.actions.act_window">
<field name="name">Einweisungs-Produkte</field>
<field name="res_model">ows.machine.product</field>
<field name="view_mode">tree,form</field>
<field name="view_mode">list,form</field>
</record>
<!-- Menüstruktur -->
@ -59,10 +59,10 @@
<field name="name">ows.machine.product.tree</field>
<field name="model">ows.machine.product</field>
<field name="arch" type="xml">
<tree>
<list>
<field name="product_id"/>
<field name="machine_id"/>
</tree>
</list>
</field>
</record>

View File

@ -0,0 +1,127 @@
<odoo>
<!-- Entfernt die Partner-Warnung (Duplicate Bank Accounts) in res.partner
<record id="patch_res_partner_duplicate_warning" model="ir.ui.view">
<field name="name">res.partner.remove.duplicate.bank.warning</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="account.view_partner_property_form"/>
<field name="priority" eval="99"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='warning_tax' and @class='alert alert-warning oe_edit_only']" position="replace"/>
</field>
</record>-->
<!-- Entfernt die Bankkonto-Warnung in res.partner.bank
<record id="patch_res_partner_bank_duplicate_warning" model="ir.ui.view">
<field name="name">res.partner.bank.remove.duplicate.warning</field>
<field name="model">res.partner.bank</field>
<field name="inherit_id" ref="account.view_partner_bank_form_inherit_account"/>
<field name="priority" eval="99"/>
<field name="arch" type="xml">
<xpath expr="//div[@class='alert alert-warning']" position="replace"/>
</field>
</record> -->
<!-- Zentrale View für alle drei Tabs in garantierter Reihenfolge -->
<record id="view_partner_form_inherit_open_workshop_tabs" model="ir.ui.view">
<field name="name">res.partner.form.ows.tabs</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form"/>
<field name="priority" eval="20"/>
<field name="arch" type="xml">
<xpath expr="//notebook" position="inside">
<page name="ows_machine_access" string="Offene Werkstatt (Hobbyhimmel)">
<!-- EINWEISUNG: Zwei Felder nebeneinander -->
<group name="container_row_2" string="Sicherheitseinweisung" col="2">
<field name="security_briefing"/>
<field name="security_id"/>
</group>
<!-- MASCHINENFREIGABEN: Volle Breite -->
<group string="Maschinenfreigaben" col="2">
<field name="machine_access_ids" colspan="2" context="{'default_partner_id': id}" nolabel="1">
<list>
<field name="machine_id"/>
<field name="date_granted"/>
<field name="date_expiry"/>
<field name="granted_by_pos"/>
</list>
</field>
</group>
<!-- ÜBERSICHT: Volle Breite -->
<group string="Maschinenfreigaben Übersicht" >
<field name="machine_access_html" colspan="2" readonly="1" widget="html" nolabel="1"/>
</group>
</page>
</xpath>
</field>
</record>
<!-- Geburtstag direkt nach der USt-ID -->
<record id="view_partner_form_inherit_ows_birthday" model="ir.ui.view">
<field name="name">res.partner.form.ows.birthday</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form"/>
<field name="priority" eval="15"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='vat']" position="after">
<field name="birthday"/>
</xpath>
</field>
</record>
<!-- List View Anpassung -->
<record id="ows_userList_inherit" model="ir.ui.view">
<field name="name">res.partner.ows.tree</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_tree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='vat']" position="after">
<field name="create_date" optional="show"/>
<field name="security_briefing" optional="show"/>
<field name="security_id" optional="show"/>
<field name="rfid_card" optional="show"/>
<field name="category_id" widget="many2many_tags"/>
</xpath>
<xpath expr="//field[@name='vat']" position="replace">
<field name="vat" invisible="1"/>
</xpath>
<xpath expr="//field[@name='email']" position="replace">
<field name="email" invisible="1"/>
</xpath>
<xpath expr="//field[@name='phone']" position="replace">
<field name="phone" invisible="1"/>
</xpath>
<xpath expr="//field[@name='state_id']" position="replace">
<field name="state_id" invisible="1"/>
</xpath>
<xpath expr="//field[@name='country_id']" position="replace">
<field name="country_id" invisible="1"/>
</xpath>
</field>
</record>
<!-- Standardwerte setzen (company_type = person) -->
<record id="view_partner_form_inherit" model="ir.ui.view">
<field name="name">res.partner.form.inherit.default_person</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form"/>
<field name="arch" type="xml">
<field name="company_type" position="attributes">
<attribute name="default">person</attribute>
</field>
</field>
</record>
<!-- Optional: Kontakte-Action, falls gebraucht -->
<record id="contacts.action_contacts" model="ir.actions.act_window">
<field name="view_mode">list,kanban,form,activity</field>
</record>
<record id="contacts.action_contacts_view_kanban" model="ir.actions.act_window.view">
<field name="sequence" eval="1"/>
</record>
<record id="contacts.action_contacts_view_tree" model="ir.actions.act_window.view">
<field name="sequence" eval="0"/>
</record>
</odoo>

View File

@ -1,6 +1,6 @@
# Open Workshop (open_workshop ows)
Dieses Odoo v13.0 Modul erweitert das POS- und Kontakt-Modul um Funktionen für offene Werkstätten (FabLabs, Makerspaces etc.) und dient der Verwaltung von Maschinen, Naschinen Einweisungen Produkten, Maschinen Nutzungsprodukten und Zugangsberechtigungen zu den Maschinen.
Dieses Odoo v18.0 Modul erweitert das POS- und Kontakt-Modul um Funktionen für offene Werkstätten (FabLabs, Makerspaces etc.) und dient der Verwaltung von Maschinen, Naschinen Einweisungen Produkten, Maschinen Nutzungsprodukten und Zugangsberechtigungen zu den Maschinen.
## Funktionen
@ -28,40 +28,10 @@ Dieses Odoo v13.0 Modul erweitert das POS- und Kontakt-Modul um Funktionen für
## Installation
1. Dieses Modul in den Custom-Addons-Ordner kopieren
2. Vor der Installation von open_worshop muss vvow_pos deinstalliert werden. Die Funktionalität von vvow_pos wird durch open_workshop ersetzt und erweitert.
3. ggf. muss die alte Datenbank manuell migiriert werden, es gibt ca 9 gelöscht res.partner auf die Verweise aus POS bestehen. Diese res.parnter müssen wieder hergestellt werden. Dazu gibt es ein Skript unter
```folder
scripts/fix_missing_pos_partner.py
```
```bash
opt/odoo/odoo/odoo-bin shell -d hobbyhimmel < scrpts/fix_missing_pos_partner.py
```
4. Odoo starten mit:
```bash
odoo-bin -d deine_datenbank -u open_workshop
```
5. Alternativ im Backend unter Apps installieren
## Automatische Migrationen
Beim ersten Laden des Moduls werden folgende Migrationen durchgeführt:
- Bestehende `res.partner` erhalten automatisch `ows.user`-Eintrag (inkl. Übernahme alter Felder wie vvow_birthday, vvow_security, vvow_security_id, vvow_rfid.
- Alte Felder mit Maschinenfreigaben (`vvow_holz_*`, `vvow_metall_*`, `vvow_fablab_*`) werden in `ows.machine.access` übertragen
- inkl. Übernahme des Änderungsdatum aus `mail.message` wann der Nutzer die Einweisung erhalten hat (ist noch fehlerhaft)
2. Im Odoo Backend unter Apps installieren
## Entwicklerhinweise
### post_init_hook
Die Datei `post_init_hook.py` ruft automatisch nach der Installation folgende Methoden auf:
```python
res.partner.migrate_existing_partners()
res.partner.migrate_machine_access_from_old_fields()
```
### Datenimport
- Maschinenbereiche, Maschinen werden über `.xml`-Dateien in `data/` geladen
- Die Zuordnung von Maschine zu Einweisungsprodukten und Nutzungsprodukten muss derzeit noch manuell erstellt werden. Ein skript dafür folgt.
## ToDos
- Bearbeitung der Maschinenfreigaben im Backend
- Automatische Erstellung von `mail.message` bei manueller Freigabe

View 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)

View File

@ -0,0 +1,43 @@
{
'name': 'Open Workshop Base',
'license': 'AGPL-3',
'version': '18.0.1.0.4', # Migration läuft bei 18.0.1.0.4
'summary': 'Kern-Modul für Maschinenfreigaben - vereinfachte Equipment-Integration',
'depends': ['base', 'account', 'hr', 'product', 'sale', 'contacts', 'maintenance'],
'author': 'matthias.lotz',
'category': 'Manufacturing',
'data': [
'security/ir.model.access.csv',
'views/machine_product_training_views.xml',
'views/menu_views.xml',
'views/machine_area_views.xml',
'views/machine_views.xml',
'views/maintenance_equipment_views.xml',
'views/res_partner_view.xml',
'data/data.xml',
],
'installable': True,
'assets': {
'web.assets_backend': [
'open_workshop_base/static/src/css/category_color.css',
],
},
'description': """
Open Workshop Base - Kernmodul
================================
Dieses Modul stellt die Basis-Funktionalität für Open Workshop bereit:
* Maschinenmodelle (ows.machine) nutzt maintenance.equipment als Single Source of Truth
* Bereiche (ows.machine.area)
* Einweisungslogik und Freigaben
* Produktverknüpfungen für Einweisungen
* Backend-UI (Form, Tree, Kanban)
* Automatische Migration bestehender Maschinen zu maintenance.equipment
Für POS-Integration installiere: open_workshop_pos
Für API-Zugriff installiere: open_workshop_api (erforderlich für WordPress Frontend)
""",
'post_init_hook': 'post_init_hook',
}

View File

@ -0,0 +1,3 @@
# Datei: open_workshop/controllers/__init__.py
from . import pos_access

View File

@ -0,0 +1,14 @@
from odoo import http
from odoo.http import request
class OpenWorkshopPOSController(http.Controller):
@http.route('/open_workshop/partner_access', type='json', auth='user')
def get_partner_machine_access(self, **kwargs):
partner_id = kwargs.get('params', {}).get('partner_id')
if not partner_id:
return {"error": "Missing partner_id"}
Machine = request.env['ows.machine'].sudo()
return Machine.get_access_list_grouped(partner_id)

View File

@ -0,0 +1,153 @@
<odoo>
<!-- Bereiche -->
<record id="area_fablab" model="ows.machine.area">
<field name="name">Fablab</field>
<field name="color_hex">#008000</field>
</record>
<record id="area_holz" model="ows.machine.area">
<field name="name">Holzbereich</field>
<field name="color_hex">#ff0000</field>
</record>
<record id="area_metall" model="ows.machine.area">
<field name="name">Metallbereich</field>
<field name="color_hex">#0000ff</field>
</record>
<record id="area_elektronik" model="ows.machine.area">
<field name="name">Elektronikbereich</field>
<field name="color_hex">#ffff00</field>
</record>
<!-- Maschinen im Fablab -->
<record id="machine_sabako_laser" model="ows.machine">
<field name="name">Sabako Laser</field>
<field name="serial_no">sabako_laser</field>
<field name="area_id" ref="area_fablab"/>
</record>
<record id="machine_prusa" model="ows.machine">
<field name="name">Prusa</field>
<field name="serial_no">prusa</field>
<field name="area_id" ref="area_fablab"/>
</record>
<record id="machine_prusa_mmu" model="ows.machine">
<field name="name">Prusa MMU</field>
<field name="serial_no">prusa_mmu</field>
<field name="area_id" ref="area_fablab"/>
</record>
<record id="machine_3d_delta" model="ows.machine">
<field name="name">3D Delta</field>
<field name="serial_no">3d_delta</field>
<field name="area_id" ref="area_fablab"/>
</record>
<record id="machine_cnc_beamicon" model="ows.machine">
<field name="name">CNC Beamicon</field>
<field name="serial_no">cnc_beamicon</field>
<field name="area_id" ref="area_fablab"/>
</record>
<!-- Maschinen im Holzbereich -->
<record id="machine_formatkreissaege" model="ows.machine">
<field name="name">Formatkreissäge</field>
<field name="serial_no">formatkreissaege</field>
<field name="area_id" ref="area_holz"/>
</record>
<record id="machine_bandsaege_holz" model="ows.machine">
<field name="name">Bandsäge</field>
<field name="serial_no">bandsaege_holz</field>
<field name="area_id" ref="area_holz"/>
</record>
<record id="machine_abrichte" model="ows.machine">
<field name="name">Abricht Dickenhobel</field>
<field name="serial_no">dickenhobel</field>
<field name="area_id" ref="area_holz"/>
</record>
<record id="machine_drechselbank" model="ows.machine">
<field name="name">Drechselbank</field>
<field name="serial_no">drechselbank</field>
<field name="area_id" ref="area_holz"/>
</record>
<record id="machine_festool_domino" model="ows.machine">
<field name="name">Festool Domino Fräse</field>
<field name="serial_no">festool_domino</field>
<field name="area_id" ref="area_holz"/>
</record>
<record id="machine_maffel_duo" model="ows.machine">
<field name="name">Maffel Duo Dübler</field>
<field name="serial_no">maffel_duo</field>
<field name="area_id" ref="area_holz"/>
</record>
<record id="machine_lamello" model="ows.machine">
<field name="name">Lamello Zeta P2</field>
<field name="serial_no">lamello_zeta_p2</field>
<field name="area_id" ref="area_holz"/>
</record>
<!-- Maschinen im Metallbereich -->
<record id="machine_kreissaege_metall" model="ows.machine">
<field name="name">Kreissäge</field>
<field name="serial_no">kreissaege_metall</field>
<field name="area_id" ref="area_metall"/>
</record>
<record id="machine_bandsaege_metall" model="ows.machine">
<field name="name">Bandsäge</field>
<field name="serial_no">bandsaege_metall</field>
<field name="area_id" ref="area_metall"/>
</record>
<record id="machine_mig_mag" model="ows.machine">
<field name="name">MIG/MAG Schweißgeräte</field>
<field name="serial_no">mig_mag</field>
<field name="area_id" ref="area_metall"/>
</record>
<record id="machine_wig" model="ows.machine">
<field name="name">WIG Schweißgerät</field>
<field name="serial_no">wig</field>
<field name="area_id" ref="area_metall"/>
</record>
<record id="machine_schweissen" model="ows.machine">
<field name="name">Schweißen allgemein</field>
<field name="serial_no">schweissen_allgemein</field>
<field name="area_id" ref="area_metall"/>
</record>
<record id="machine_drehbank" model="ows.machine">
<field name="name">Drehbank</field>
<field name="serial_no">drehbank</field>
<field name="area_id" ref="area_metall"/>
</record>
<record id="machine_fraese" model="ows.machine">
<field name="name">Fräse</field>
<field name="serial_no">fraese</field>
<field name="area_id" ref="area_metall"/>
</record>
<record id="machine_abkantbank" model="ows.machine">
<field name="name">Abkantbank</field>
<field name="serial_no">abkantbank</field>
<field name="area_id" ref="area_metall"/>
</record>
<!-- Maschine im Elektronikbereich -->
<record id="machine_loetkolben" model="ows.machine">
<field name="name">Lötkolben</field>
<field name="serial_no">loetkolben</field>
<field name="area_id" ref="area_elektronik"/>
</record>
</odoo>

View File

@ -0,0 +1,111 @@
# -*- coding: utf-8 -*-
import logging
from datetime import date
from odoo import SUPERUSER_ID, api
_logger = logging.getLogger(__name__)
def migrate(cr, version):
"""
Post-Migration: Migriere bestehende ows.machine Daten zu maintenance.equipment
Area wird in equipment.location gespeichert (nicht mehr in category!)
"""
_logger.info("=== Post-Migration: Daten-Migration zu equipment ===")
env = api.Environment(cr, SUPERUSER_ID, {})
# 1. Zähle zu migrierende Maschinen
cr.execute("SELECT COUNT(*) FROM ows_machine WHERE equipment_id IS NULL")
machines_to_migrate = cr.fetchone()[0]
_logger.info(f"Found {machines_to_migrate} machines to migrate")
if machines_to_migrate == 0:
_logger.info("No machines to migrate - skipping")
return
_logger.info("Creating maintenance.equipment entries...")
# 2. Migriere alle Maschinen die noch kein equipment haben
cr.execute("""
SELECT
id, name, area_id,
active, create_uid, write_uid, create_date, write_date
FROM ows_machine
WHERE equipment_id IS NULL
""")
machines = cr.fetchall()
migrated_count = 0
# Default Company ermitteln
cr.execute("SELECT id FROM res_company ORDER BY id LIMIT 1")
default_company_id = cr.fetchone()[0] if cr.rowcount > 0 else None
for machine in machines:
(m_id, m_name_jsonb, m_area_id,
m_active, m_create_uid, m_write_uid, m_create_date, m_write_date) = machine
# Name aus JSONB extrahieren (de_DE oder en_US)
m_name = None
if m_name_jsonb:
m_name = m_name_jsonb.get('de_DE') or m_name_jsonb.get('en_US')
# Serial number generieren
m_serial_no = f"OWS-{m_id}"
# Location aus Area-Name ermitteln
location = None
if m_area_id:
# Hole Area-Name (JSONB!) und nutze als location
cr.execute("SELECT name FROM ows_machine_area WHERE id = %s", (m_area_id,))
area_result = cr.fetchone()
if area_result and area_result[0]:
area_name_jsonb = area_result[0]
if area_name_jsonb:
location = area_name_jsonb.get('de_DE') or area_name_jsonb.get('en_US')
# Equipment erstellen (name als JSONB!)
# Fallback: Wenn m_name leer, nutze serial_no
display_name = m_name if m_name and m_name.strip() else m_serial_no
# Verwende SQL direkt für korrektes JSONB handling
try:
cr.execute("""
INSERT INTO maintenance_equipment
(name, serial_no, location, cost, effective_date, equipment_assign_to,
company_id, active, create_uid, write_uid, create_date, write_date)
VALUES
(jsonb_build_object('de_DE', %s, 'en_US', %s), %s, %s, %s, %s, 'other',
%s, %s, %s, %s, %s, %s)
RETURNING id
""", (display_name, display_name, m_serial_no,
location, 0.0, date.today(),
default_company_id,
m_active if m_active is not None else True,
m_create_uid, m_write_uid, m_create_date, m_write_date))
equipment_id = cr.fetchone()[0]
# equipment_id in ows_machine setzen
cr.execute("""
UPDATE ows_machine
SET equipment_id = %s
WHERE id = %s
""", (equipment_id, m_id))
migrated_count += 1
except Exception as e:
_logger.error(f"Failed to migrate machine {m_id}: {e}")
# 3. Validierung
cr.execute("SELECT COUNT(*) FROM ows_machine WHERE equipment_id IS NULL")
remaining = cr.fetchone()[0]
if remaining > 0:
_logger.warning(f"⚠️ {remaining} machines could not be migrated!")
else:
_logger.info(f"✅ Successfully migrated {migrated_count} machines to equipment")
_logger.info("Post-migration completed")

View 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")

View 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;

View File

@ -0,0 +1,5 @@
from . import ows_models
from . import pos_order

View File

@ -0,0 +1,675 @@
# -*- coding: utf-8 -*-
# ows_models.py
# Part of Odoo Open Workshop
from odoo import models, fields, api
from markupsafe import escape as html_escape
import logging
_logger = logging.getLogger(__name__)
_logger.info("✅ ows_models.py geladen")
class HREmployee(models.Model):
_inherit = 'hr.employee'
@api.model
def anonymize_for_testsystem(self):
"""Benennt Admin-Angestellten um und archiviert alle anderen für das Testsystem."""
admin_user = self.env['res.users'].search([('name', '=', 'Administrator')], limit=1)
if not admin_user:
_logger.error("[OWS] Administrator-Benutzer nicht gefunden!")
return
_logger.info(f"[OWS] Administrator-Benutzer gefunden: {admin_user.name} (ID: {admin_user.id})")
# Suche auch archivierte Employees
admin_employee = self.with_context(active_test=False).search([('user_id', '=', admin_user.id)], limit=1)
if admin_employee:
# Administrator-Employee reaktivieren und umbenennen
admin_employee.write({
'name': 'TESTSYSTEM',
'job_title': 'Testumgebung',
'work_email': 'office@hobbyhimmel.de',
'work_phone': False,
'active': True, # Reaktivieren falls archiviert
})
_logger.info(f"[OWS] Admin-Angestellter reaktiviert und umbenannt: {admin_employee.name} (ID: {admin_employee.id})")
else:
_logger.warning("[OWS] Kein Angestellter für Administrator gefunden.")
return
# Alle anderen Angestellten archivieren (auch bereits archivierte berücksichtigen)
other_employees = self.with_context(active_test=False).search([('id', '!=', admin_employee.id)])
other_employees.write({'active': False})
_logger.info("[OWS] %d Angestellte archiviert.", len(other_employees))
class ResPartner(models.Model):
_inherit = 'res.partner'
_logger.info("✅ ows ResPartner geladen")
ows_user_id = fields.One2many('ows.user', 'partner_id', string="OWS Benutzerdaten")
# Alte Felder (weiterhin sichtbar für externe Programme)
birthday = fields.Date(
string="Geburtstag",
compute='_compute_ows_user_fields',
inverse='_inverse_birthday',
store=False
)
rfid_card = fields.Text(
string="RFID Card ID",
compute='_compute_ows_user_fields',
inverse='_inverse_rfid_card',
store=False
)
security_briefing = fields.Boolean(
string="Haftungsausschluss",
compute='_compute_ows_user_fields',
inverse='_inverse_security_briefing',
store=False
)
security_id = fields.Text(
string="Haftungsausschluss ID",
compute='_compute_ows_user_fields',
inverse='_inverse_security_id',
store=False
)
# Neue direkte vvow_* Felder
vvow_birthday = fields.Date(string="Geburtstag (vvow)")
vvow_rfid_card = fields.Text(string="RFID Card ID (vvow)")
vvow_security_briefing = fields.Boolean(string="Haftungsausschluss (vvow)")
vvow_security_id = fields.Text(string="Haftungsausschluss ID (vvow)")
@api.depends('ows_user_id')
def _compute_ows_user_fields(self):
for partner in self:
user = partner.ows_user_id[0] if partner.ows_user_id else False
partner.birthday = user.birthday if user else False
partner.rfid_card = user.rfid_card if user else False
partner.security_briefing = user.security_briefing if user else False
partner.security_id = user.security_id if user else False
def _inverse_birthday(self):
for partner in self:
user = partner.ows_user_id[0] if partner.ows_user_id else False
if user:
user.birthday = partner.birthday
def _inverse_rfid_card(self):
for partner in self:
user = partner.ows_user_id[0] if partner.ows_user_id else False
if user:
user.rfid_card = partner.rfid_card
def _inverse_security_briefing(self):
for partner in self:
user = partner.ows_user_id[0] if partner.ows_user_id else False
if user:
user.security_briefing = partner.security_briefing
def _inverse_security_id(self):
for partner in self:
user = partner.ows_user_id[0] if partner.ows_user_id else False
if user:
user.security_id = partner.security_id
@api.model_create_multi
def create(self, vals_list):
partners = super().create(vals_list)
for vals, partner in zip(vals_list, partners):
self.env['ows.user'].create({
'partner_id': partner.id,
'birthday': vals.get('birthday') or vals.get('vvow_birthday'),
'rfid_card': vals.get('rfid_card') or vals.get('vvow_rfid_card'),
'security_briefing': vals.get('security_briefing') or vals.get('vvow_security_briefing'),
'security_id': vals.get('security_id') or vals.get('vvow_security_id'),
})
return partners
def write(self, vals):
res = super().write(vals)
for partner in self:
user = partner.ows_user_id[0] if partner.ows_user_id else False
if not user:
continue
# Synchronisation alt -> user
if 'birthday' in vals:
user.birthday = vals['birthday']
if 'rfid_card' in vals:
user.rfid_card = vals['rfid_card']
if 'security_briefing' in vals:
user.security_briefing = vals['security_briefing']
if 'security_id' in vals:
user.security_id = vals['security_id']
# Synchronisation vvow_* -> user + alt
if 'vvow_birthday' in vals:
user.birthday = vals['vvow_birthday']
partner.birthday = vals['vvow_birthday']
if 'vvow_rfid_card' in vals:
user.rfid_card = vals['vvow_rfid_card']
partner.rfid_card = vals['vvow_rfid_card']
if 'vvow_security_briefing' in vals:
user.security_briefing = vals['vvow_security_briefing']
partner.security_briefing = vals['vvow_security_briefing']
if 'vvow_security_id' in vals:
user.security_id = vals['vvow_security_id']
partner.security_id = vals['vvow_security_id']
return res
machine_access_ids = fields.One2many(
'ows.machine.access',
'partner_id',
string='Maschinenfreigaben'
)
machine_access_html = fields.Html(
string="Maschinenfreigabe",
compute="_compute_machine_access_html",
sanitize=False
)
@api.depends('machine_access_ids')
def _compute_machine_access_html(self):
areas = self.env['ows.machine.area'].search([], order="name")
for partner in self:
html = ""
for area in areas:
html += f"""
<div class="o_form_sheet">
<h3 class="o_form_label">{area.name}</h3>
<table class="table table-sm table-bordered o_form_table">
<thead>
<tr>
<th>Maschine</th>
<th>Status</th>
<th>Datum</th>
<th>Gültig bis</th>
</tr>
</thead>
<tbody>
"""
machines = self.env['ows.machine'].search([('area_id', '=', area.id)], order="name")
for machine in machines:
access = self.env['ows.machine.access'].search([
('partner_id', '=', partner.id),
('machine_id', '=', machine.id),
], limit=1)
icon = '<span class="text-success fa fa-check"/>' if access else '<span class="text-danger fa fa-times"/>'
date_granted = access.date_granted.strftime('%Y-%m-%d') if access and access.date_granted else "-"
date_expiry = access.date_expiry.strftime('%Y-%m-%d') if access and access.date_expiry else "-"
html += f"""
<tr>
<td>{machine.name}</td>
<td>{icon}</td>
<td>{date_granted}</td>
<td>{date_expiry}</td>
</tr>
"""
html += "</tbody></table></div>"
partner.machine_access_html = html
@api.model
def migrate_existing_partners(self):
"""
Erstellt für alle vorhandenen res.partner einen ows.user,
wenn noch keiner existiert, und übernimmt alte vvow_* Felder.
Führt am Ende automatisch einen commit durch.
Verwendung in der odoo shell:
env['res.partner'].migrate_existing_partners()
env['ows.user'].search_count([])
env['ows.user'].search([]).mapped('partner_id.name')
"""
migrated = 0
skipped = 0
partners = self.env['res.partner'].search([])
for partner in partners:
if partner.ows_user_id:
skipped += 1
continue
# Werte lesen (werden evtl. durch _inherit hinzugefügt)
vals = {
'partner_id': partner.id,
'birthday': getattr(partner, 'vvow_birthday', False),
'rfid_card': getattr(partner, 'vvow_rfid_card', False),
'security_briefing': getattr(partner, 'vvow_security_briefing', False),
'security_id': getattr(partner, 'vvow_security_id', False),
}
self.env['ows.user'].create(vals)
migrated += 1
_logger.info(f"Erzeuge ows.user für Partner {partner.id} ({partner.name}) mit Werten: {vals}")
_logger.info(f"[OWS Migration] ✅ Migriert: {migrated}, ❌ Übersprungen: {skipped}")
# 🔐 Commit am Ende
self.env.cr.commit()
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': "Migration abgeschlossen",
'message': f"{migrated} Partner migriert, {skipped} übersprungen.",
'sticky': False,
}
}
@api.model
def migrate_machine_access_from_old_fields(self):
"""
Migriert alte vvow_* Boolean-Felder in strukturierte ows.machine.access Einträge.
Das Freigabe-Datum wird aus dem Änderungsverlauf (mail.message.tracking_value_ids) extrahiert.
"""
mapping = [
('vvow_holz_sander', 'bandschleifer'),
('vvow_holz_felder_bandsaw', 'bandsaege_holz'),
('vvow_holz_felder_jointer', 'dickenhobel'),
('vvow_holz_felder_mill', 'felder_fraese'),
('vvow_holz_felder_tablesaw', 'formatkreissaege'),
('vvow_holz_mill', 'tischfraese'),
('vvow_holz_lathe', 'drechselbank'),
('vvow_holz_domino', 'festool_domino'),
('vvow_holz_duoduebler', 'maffel_duo'),
('vvow_holz_lamello', 'lamello_zeta_p2'),
('vvow_fablab_laser_sabko', 'sabako_laser'),
('vvow_fablab_3dprint_delta', '3d_delta'),
('vvow_fablab_3dprint_prusa', 'prusa'),
('vvow_fablab_3dprint_prusa_mmu', 'prusa_mmu'),
('vvow_fablab_cnc_beamicon', 'cnc_beamicon'),
('vvow_metall_welding_mig', 'mig_mag'),
('vvow_metall_welding_wig', 'wig'),
('vvow_metall_welding_misc', 'schweissen_allgemein'),
('vvow_metall_lathe', 'drehbank'),
('vvow_metall_bandsaw', 'bandsaege_metall'),
('vvow_metall_mill_fp2', 'fraese'),
('vvow_metall_bende', 'abkantbank'),
('vvow_metall_chop_saw', 'kreissaege_metall'),
]
MachineAccess = self.env['ows.machine.access']
MailMessage = self.env['mail.message']
TrackingValue = self.env['mail.tracking.value']
count_created = 0
for partner in self.search([]):
for field_name, machine_code in mapping:
if not getattr(partner, field_name, False):
continue
machine = self.env['ows.machine'].search([('code', '=', machine_code)], limit=1)
if not machine:
continue
# Änderungsverlauf durchsuchen: Wann wurde das Feld auf True gesetzt?
tracking = TrackingValue.search([
('field', '=', field_name),
('field_type', '=', 'boolean'),
('new_value_integer', '=', 1),
('mail_message_id.model', '=', 'res.partner'),
('mail_message_id.res_id', '=', partner.id),
], order='id ASC', limit=1)
date_granted = tracking.mail_message_id.date if tracking else fields.Datetime.now()
if not MachineAccess.search([('partner_id', '=', partner.id), ('machine_id', '=', machine.id)]):
MachineAccess.create({
'partner_id': partner.id,
'machine_id': machine.id,
'date_granted': date_granted,
})
count_created += 1
_logger.info(f"[OWS Migration] ✅ Maschinenfreigaben erstellt: {count_created}")
self.env.cr.commit()
@api.model
def archive_partners_without_users(self):
"""
Archiviert alle Partner (res.partner), die keine Benutzer (res.users) sind.
"""
Partner = self.env['res.partner']
User = self.env['res.users']
# IDs aller Partner, die ein Benutzerkonto haben
user_partner_ids = User.search([]).mapped('partner_id').ids
# Alle Partner ohne Benutzerkonto
partners_to_archive = Partner.search([
('id', 'not in', user_partner_ids),
('active', '=', True),
])
count = len(partners_to_archive)
partners_to_archive.write({'active': False})
for p in partners_to_archive:
_logger.debug(f"[OWS] Archiviert Partner: {p.name} (ID {p.id})")
_logger.info(f"[OWS] Archiviert {count} Partner ohne Benutzerkonto.")
self.env.cr.commit()
class OwsUser(models.Model):
_name = 'ows.user'
_description = 'OWS: Benutzerdaten'
_table = 'ows_user'
_logger.info("✅ ows_user geladen")
partner_id = fields.Many2one(
'res.partner',
required=True,
ondelete='cascade',
string='Kontakt'
)
birthday = fields.Date('Geburtstag')
rfid_card = fields.Text('RFID Card ID')
security_briefing = fields.Boolean('Haftungsausschluss', default=False)
security_id = fields.Text('Haftungsausschluss ID')
_sql_constraints = [
('partner_unique', 'unique(partner_id)', 'Jeder Partner darf nur einen OWS-Datensatz haben.')
]
AVAILABLE_COLORS = [
('#000000', 'schwarz'),
('#ff0000', 'Rot'),
('#E91E63', 'Pink'),
('#9C27B0', 'Lila'),
('#3F51B5', 'Indigo'),
('#0000ff', 'Blau'),
('#008000', 'Grün'),
('#ffff00', 'Gelb'),
('#FF9800', 'Orange'),
('#795548', 'Braun'),
('#ffffff', 'Weiss'),
]
class OwsMachineArea(models.Model):
_name = 'ows.machine.area'
_table = 'ows_machine_area'
_description = 'OWS: Maschinenbereich'
_order = 'name'
name = fields.Char(string="Name", required=True, translate=True)
color_hex = fields.Selection(
selection=AVAILABLE_COLORS,
string="Farbe (Hex)",
required=True,
)
color_hex_value = fields.Char(
string="Farbcode",
compute='_compute_color_hex_value',
store=False
)
color_name = fields.Char(
string="Farbname",
compute='_compute_color_name',
store=False
)
@api.depends('color_hex')
def _compute_color_hex_value(self):
for rec in self:
rec.color_hex_value = rec.color_hex or ''
@api.depends('color_hex')
def _compute_color_name(self):
label_dict = dict(AVAILABLE_COLORS)
for rec in self:
rec.color_name = label_dict.get(rec.color_hex, 'Unbekannt')
class OwsMachine(models.Model):
"""
Open Workshop Maschine - nutzt maintenance.equipment als Single Source of Truth.
Delegation Pattern (_inherits):
- Beim Erstellen wird automatisch ein maintenance.equipment erstellt
- Felder wie name, serial_no, cost etc. werden direkt von equipment übernommen
- Keine Datenduplizierung!
"""
_name = 'ows.machine'
_table = 'ows_machine'
_description = 'OWS: Maschine'
_inherits = {'maintenance.equipment': 'equipment_id'}
# PFLICHT: Verknüpfung zu maintenance.equipment (Single Source of Truth)
equipment_id = fields.Many2one(
'maintenance.equipment',
required=True,
ondelete='cascade',
string='Equipment',
help='Verknüpfung zum Maintenance Equipment (Single Source of Truth für name, code, Preis, etc.)'
)
# Delegierte Felder (kommen automatisch von equipment via _inherits):
# - name (equipment.name)
# - serial_no → wird als 'code' verwendet (siehe @api.depends unten)
# - cost → purchase_price
# - effective_date → purchase_date
# - location → storage_location (wird von area_id synchronisiert, readonly via View)
# - note → description
# - category_id → Wird mit area_id synchronisiert
# OWS-spezifische Felder (nur in ows.machine!)
category = fields.Selection([
('green', 'Kategorie 1: grün'),
('yellow', 'Kategorie 2: gelb'),
('red', 'Kategorie 3: rot'),
], string="Sicherheitskategorie", required=True, default='red', help="Sicherheitsrelevante Maschinenkategorie:\n"
"- grün: keine Einweisungspflicht\n"
"- gelb: empfohlene Einweisung\n"
"- rot: Einweisung zwingend erforderlich")
category_icon = fields.Char(string="Kategorie-Symbol", compute="_compute_category_icon", store=False)
@api.depends('category')
def _compute_category_icon(self):
for rec in self:
icon_map = {
'green': '🟢',
'yellow': '🟡',
'red': '🔴',
}
rec.category_icon = icon_map.get(rec.category, '')
area_id = fields.Many2one('ows.machine.area', string='Bereich', help="Bereich, in dem die Maschine oder das Gerät steht.")
product_ids = fields.One2many('ows.machine.product', 'machine_id', string="Nutzungsprodukte")
product_names = fields.Char(string="Liste der Nutzungsprodukte", compute="_compute_product_using_names", store=False,)
training_ids = fields.One2many('ows.machine.training', 'machine_id', string="Einweisungsprodukte")
training_names = fields.Char(string="Liste der Einweisungsprodukte", compute="_compute_product_training_names", store=False,)
@api.depends('product_ids.product_id.name')
def _compute_product_using_names(self):
for machine in self:
names = machine.product_ids.mapped('product_id.name')
machine.product_names = ", ".join(names)
@api.depends('training_ids.training_id.name')
def _compute_product_training_names(self):
for machine in self:
names = machine.training_ids.mapped('training_id.name')
machine.training_names = ", ".join(names)
# Keine eigenen SQL Constraints - Equipment hat bereits unique constraint für serial_no
@api.model
def create(self, vals):
"""
Beim Erstellen einer ows.machine:
1. Equipment IMMER automatisch erstellen
2. Area Location synchronisieren
3. serial_no und name vom User übernehmen
"""
# Equipment IMMER automatisch erstellen
equipment_vals = {
'name': vals.get('name', 'Neue Maschine'),
'serial_no': vals.get('serial_no', False),
}
# Area → Location Mapping für Equipment
if 'area_id' in vals and vals['area_id']:
area = self.env['ows.machine.area'].browse(vals['area_id'])
if area and area.name:
equipment_vals['location'] = area.name
equipment = self.env['maintenance.equipment'].create(equipment_vals)
vals['equipment_id'] = equipment.id
return super(OwsMachine, self).create(vals)
def write(self, vals):
"""
Bei Updates:
1. Area Location synchronisieren
2. Name/Serial_no Equipment synchronisieren
"""
# Area → Location Mapping
if 'area_id' in vals and vals['area_id']:
area = self.env['ows.machine.area'].browse(vals['area_id'])
if area and area.name:
vals['location'] = area.name
# Name/Serial_no an Equipment weiterleiten
equipment_vals = {}
if 'name' in vals:
equipment_vals['name'] = vals['name']
if 'serial_no' in vals:
equipment_vals['serial_no'] = vals['serial_no']
if equipment_vals and self.equipment_id:
self.equipment_id.write(equipment_vals)
return super(OwsMachine, self).write(vals)
def name_get(self):
result = []
for rec in self:
name = rec.name or 'Unbenannt'
if rec.serial_no: # Kommt direkt von equipment via _inherits
name = f"[{rec.serial_no}] {name}"
result.append((rec.id, name))
return result
def action_open_equipment(self):
"""Smart Button: Öffnet die Equipment-Detailansicht"""
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'name': 'Equipment Details',
'res_model': 'maintenance.equipment',
'res_id': self.equipment_id.id,
'view_mode': 'form',
'target': 'current',
}
@api.model
def get_access_list_grouped(self, partner_id):
"""
Gibt eine gruppierte Liste von Maschinenzugängen für einen bestimmten Partner zurück. Diese Funktion wird in
Odoo POS Frontend verwendet um die Ansicht zu erzeugen auf Welche Maschinen der Partner Zugriff hat.
Für einen gegebenen Partner (über die partner_id) werden alle Maschinenbereiche (areas) abgefragt.
Für jeden Bereich wird geprüft, auf welche Maschinen der Partner Zugriff hat. Das Ergebnis wird
als Liste von Bereichen mit jeweils zugehörigen Maschinen und Zugriffsstatus zurückgegeben.
Zusätzlich werden sicherheitsrelevante Informationen des Partners (wie Sicherheitsunterweisung,
Sicherheits-ID, RFID-Karte und Geburtstag) aus dem zugehörigen ows_user ermittelt und mitgeliefert.
Args:
partner_id (int): Die ID des Partners, für den die Zugriffsübersicht erstellt werden soll.
Returns:
dict: Ein Dictionary mit folgenden Schlüsseln:
- 'access_by_area': Liste von Bereichen mit Maschinen und Zugriffsstatus.
- 'security_briefing': Sicherheitsunterweisung des Nutzers (bool oder False).
- 'security_id': Sicherheits-ID des Nutzers (str oder '').
- 'rfid_card': RFID-Kartennummer des Nutzers (str oder '').
- 'birthday': Geburtstag des Nutzers (str oder '').
"""
partner = self.env['res.partner'].browse(partner_id)
areas = self.env['ows.machine.area'].search([], order="name")
_logger.info("Access RPC called with partner_id=%s", partner_id)
access_by_area = []
for area in areas:
machines = self.search([('area_id', '=', area.id), ('category', '=', 'red')], order="name")
machine_list = []
for machine in machines:
has_access = bool(self.env['ows.machine.access'].search([
('partner_id', '=', partner_id),
('machine_id', '=', machine.id),
], limit=1))
machine_list.append({
'name': machine.name,
'has_access': has_access,
})
if machine_list:
access_by_area.append({
'area': area.name,
'color_hex': area.color_hex or '#000000',
'machines': machine_list
})
user = partner.ows_user_id[:1]
return {
'access_by_area': access_by_area,
'security_briefing': user.security_briefing if user else False,
'security_id': user.security_id if user else '',
'rfid_card': user.rfid_card if user else '',
'birthday': user.birthday if user else '',
}
class OwsMachineAccess(models.Model):
_name = 'ows.machine.access'
_table = 'ows_machine_access'
_description = 'OWS: Maschinenfreigabe'
_order = 'partner_id, machine_id'
partner_id = fields.Many2one('res.partner', required=True, ondelete='cascade')
machine_id = fields.Many2one('ows.machine', required=True)
date_granted = fields.Date(default=fields.Date.today)
date_expiry = fields.Date(string="Ablaufdatum")
granted_by_pos = fields.Boolean(default=True)
_sql_constraints = [
('partner_machine_unique', 'unique(partner_id, machine_id)', 'Der Kunde hat diese Freigabe bereits.')
]
class OwsMachineProduct(models.Model):
_name = 'ows.machine.product'
_table = 'ows_machine_product'
_description = 'OWS: Zuordnung Produkt der Nutzung zu der Maschine'
product_id = fields.Many2one('product.product', required=True, domain=[('available_in_pos', '=', True)], ondelete='cascade')
machine_id = fields.Many2one('ows.machine', required=True, ondelete='cascade')
class OwsMachineTraining(models.Model):
_name = 'ows.machine.training'
_table = 'ows_machine_training'
_description = 'OWS: Zuordnung Produkt der Einweisung zu der Maschine'
training_id = fields.Many2one('product.product', required=True, domain=[('available_in_pos', '=', True)], ondelete='cascade')
machine_id = fields.Many2one('ows.machine', required=True, ondelete='cascade')

View File

@ -0,0 +1,50 @@
from odoo import models, fields, api
from collections import defaultdict
#import debugpy
import logging
_logger = logging.getLogger(__name__)
_logger.info("✅ pos_order.py geladen Test 2")
#debugpy.listen(("0.0.0.0", 5678))
print("✅ debugpy wartet auf Verbindung (Port 5678) ...")
# Optional: Starte erst, wenn VS Code verbunden ist
#debugpy.wait_for_client()
class PosOrder(models.Model):
_inherit = 'pos.order'
def _process_order(self, order, existing_order):
_logger.info("🚨 DEBUG: _process_order wurde aufgerufen mit order: %s", order.get('name', 'unbekannt'))
pos_order_id = super(PosOrder, self)._process_order(order, existing_order)
pos_order = self.browse(pos_order_id)
training_products = self.env['ows.machine.training'].search([])
product_map = defaultdict(list)
for tp in training_products:
product_map[tp.training_id.product_tmpl_id.id].append(tp.machine_id.id)
partner = pos_order.partner_id
if not partner:
_logger.info("🟡 POS-Bestellung ohne Partner keine Freigabe möglich")
return pos_order_id
for line in pos_order.lines:
product_tmpl_id = line.product_id.product_tmpl_id.id
machine_ids = product_map.get(product_tmpl_id, [])
_logger.info("🔍 Prüfe Produkt %s → Maschinen IDs: %s", line.product_id.display_name, machine_ids)
for machine_id in machine_ids:
already_exists = self.env['ows.machine.access'].search([
('partner_id', '=', partner.id),
('machine_id', '=', machine_id)
], limit=1)
if not already_exists:
self.env['ows.machine.access'].create({
'partner_id': partner.id,
'machine_id': machine_id,
'granted_by_pos': True
})
_logger.info("✅ Maschinenfreigabe erstellt: %s für %s", machine_id, partner.name)
return pos_order_id

View File

@ -0,0 +1,8 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_ows_machine_access_user,ows.machine.access,model_ows_machine_access,base.group_user,1,1,1,1
access_ows_machine_user,ows.machine,model_ows_machine,base.group_user,1,1,1,1
access_ows_machine_product_user,ows.machine.product,model_ows_machine_product,base.group_user,1,1,1,1
access_ows_machine_training_user,access_ows_machine_training_user,model_ows_machine_training,base.group_user,1,1,1,1
access_ows_machine_area,ows.machine.area,model_ows_machine_area,base.group_user,1,1,1,1
access_ows_user,ows.user,model_ows_user,base.group_user,1,1,1,1
access_ows_machine_training,ows.machine.training,model_ows_machine_training,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_ows_machine_access_user ows.machine.access model_ows_machine_access base.group_user 1 1 1 1
3 access_ows_machine_user ows.machine model_ows_machine base.group_user 1 1 1 1
4 access_ows_machine_product_user ows.machine.product model_ows_machine_product base.group_user 1 1 1 1
5 access_ows_machine_training_user access_ows_machine_training_user model_ows_machine_training base.group_user 1 1 1 1
6 access_ows_machine_area ows.machine.area model_ows_machine_area base.group_user 1 1 1 1
7 access_ows_user ows.user model_ows_user base.group_user 1 1 1 1
8 access_ows_machine_training ows.machine.training model_ows_machine_training base.group_user 1 1 1 1

View 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;
}

View 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>

View File

@ -0,0 +1,40 @@
<!-- machine_area_views.xml -->
<odoo>
<!-- Action zum Anzeigen der Bereiche -->
<record id="action_machine_area_list" model="ir.actions.act_window">
<field name="name">Maschinenbereiche</field>
<field name="res_model">ows.machine.area</field>
<field name="view_mode">list,form</field>
</record>
<!-- Menüpunkt unter Maschinen > Konfiguration -->
<menuitem id="menu_machine_area" name="Bereiche" parent="menu_machine_config" action="open_workshop_base.action_machine_area_list" sequence="30"/>
<!-- Listenansicht -->
<record id="view_machine_area_tree" model="ir.ui.view">
<field name="name">ows.machine.area.tree</field>
<field name="model">ows.machine.area</field>
<field name="arch" type="xml">
<list>
<field name="name"/>
<field name="color_hex_value" string="Farbe (Hex)"/>
<field name="color_name" string="Farbname"/>
</list>
</field>
</record>
<!-- Formularansicht -->
<record id="view_machine_area_form" model="ir.ui.view">
<field name="name">ows.machine.area.form</field>
<field name="model">ows.machine.area</field>
<field name="arch" type="xml">
<form string="Maschinenbereich">
<group>
<field name="name"/>
<field name="color_hex"/>
<field name="color_name" readonly="1"/>
</group>
</form>
</field>
</record>
</odoo>

Some files were not shown because too many files have changed in this diff Show More