Compare commits

..

No commits in common. "13.0_dev" and "main" have entirely different histories.

55 changed files with 225 additions and 5494 deletions

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

View File

@ -1,69 +0,0 @@
name: POS Test Feedback
about: Rückmeldung zu einem Test des POS-Systems geben
title: "[Feedback] "
labels: [feedback, test]
assignees: []
body:
- type: markdown
attributes:
value: |
## 🧪 POS-Test: Feedbackformular
Bitte gib uns Rückmeldung zu den einzelnen Funktionen. Beschreibe ggf. Probleme oder Auffälligkeiten.
- type: textarea
id: sonstiges
attributes:
label: Sonstiges Feedback oder Fehler
description: Alles andere, was dir beim Test aufgefallen ist (z.B. Layout, Ladezeiten, Fehlermeldungen).
- type: textarea
id: nutzerauswahl
attributes:
label: Nutzer auswählen
description: Funktioniert die Auswahl des Nutzers im POS wie erwartet?
placeholder: z.B. Nutzer nicht auffindbar, Anzeige langsam etc.
- type: textarea
id: haftung
attributes:
label: Haftungsausschluss prüfen
description: Wird der Haftungsausschluss korrekt angezeigt bzw. berücksichtigt?
- type: textarea
id: maschinenfreigabe
attributes:
label: Maschinenfreigabe prüfen
description: Wird korrekt angezeigt, ob der Nutzer eine Einweisung für eine Maschine hat?
- type: textarea
id: abrechnung
attributes:
label: Abrechnung (Bargeld / SumUp)
description: Funktioniert die Abrechnung für den Nutzer?
- type: textarea
id: coupon
attributes:
label: Coupons (erstellen / einlösen)
description: Funktionieren Erstellen und Einlösen von Coupons korrekt?
- type: textarea
id: nutzerdaten
attributes:
label: Nutzerdaten aktualisieren / Haftungsausschluss abwählen
description: Lassen sich Nutzerdaten wie RFID oder der Haftungsausschluss korrekt ändern?
- type: textarea
id: einweisung
attributes:
label: Einweisung verkaufen / prüfen
description: Lässt sich eine Einweisung verkaufen und wird sie korrekt zugewiesen?
- type: textarea
id: backend
attributes:
label: Backend-Funktionen
description: Können Maschinen und Einweisungen im Backend wie erwartet verwaltet werden?

View File

@ -1,154 +0,0 @@
name: odoo-restore-open_workshop-install
on:
schedule:
- cron: "43 5 * * *" # Dieser Job wird täglich um 05:43 Uhr UTC ausgeführt.
push:
branches:
- 13.0_dev
env:
URL_RESTORE: http://hobbybackend2.fritz.box:9013
jobs:
run-odoo-backup-in-docker:
runs-on: ["hobbybackend2", "ubuntu-latest"] # Gibt an, dass der Job entweder auf 'hobbybackend2' und 'ubuntu-latest' läuft.
steps:
- name: Checkout the repository
# Dieser Schritt holt den Code des Repositories in die Action-Umgebung.
# Dies ist notwendig, um auf Dateien im Repository wie Skripte oder Konfigurationen zugreifen zu können.
uses: actions/checkout@v3
- name: Load environment variables from .env
run: |
set -a
source .env
set +a
# Schreibe die Variablen in $GITHUB_ENV für die Verwendung in der YAML
while IFS= read -r line; do
if [[ ! -z "$line" && "$line" != \#* ]]; then
echo "$line" >> $GITHUB_ENV
fi
done < .env
- name: Set up Docker container
# Dieser Schritt startet einen Docker-Container basierend auf dem "ubuntu:latest" Image.
# Er läuft im Hintergrund (`-d`), und wir verwenden `tail -f /dev/null`, um den Container laufend zu halten.
# Danach werden benötigte Pakete wie `gnupg`, `openssh-client`, `curl` und `sshpass` installiert.
run: |
docker run -d --name ${{ github.workflow }} ubuntu:latest tail -f /dev/null
docker exec ${{ github.workflow }} apt-get update
docker exec ${{ github.workflow }} apt-get install -y gnupg2 openssh-client curl sshpass nano
- name: Copy SSH keys to the container
# In diesem Schritt werden SSH-Schlüssel aus den GitHub Secrets in temporäre Dateien geschrieben.
# Diese Dateien werden dann in den Docker-Container kopiert, um SSH-Zugriff innerhalb des Containers zu ermöglichen.
# Schließlich werden die Berechtigungen des privaten Schlüssels (id_rsa) auf 600 gesetzt.
run: |
docker exec ${{ github.workflow }} mkdir -p /root/.ssh
echo "${{ secrets.OPENSSH_PRIVATE_KEY }}" > ssh_private_key
echo "${{ secrets.OPENSSH_PUBLIC_KEY }}" > ssh_public_key
docker cp ssh_private_key ${{ github.workflow }}:/root/.ssh/id_rsa
docker cp ssh_public_key ${{ github.workflow }}:/root/.ssh/id_rsa.pub
docker exec ${{ github.workflow }} chmod 600 /root/.ssh/id_rsa
- name: Copy PGP keys to the container
# Hier werden die GPG-Schlüssel (PGP-Schlüssel) in den Container kopiert, um Backups verschlüsseln zu können.
# Die GPG-Schlüssel werden ebenfalls aus den GitHub Secrets entnommen und temporär in Dateien gespeichert,
# die anschließend importiert werden.
run: |
echo "${{ secrets.PGP_PRIVATE_KEY }}" > pgp_private_key.asc
echo "${{ secrets.PGP_PUBLIC_KEY }}" > pgp_public_key.asc
docker cp pgp_private_key.asc ${{ github.workflow }}:/root/pgp_private_key.asc
docker cp pgp_public_key.asc ${{ github.workflow }}:/root/pgp_public_key.asc
docker exec ${{ github.workflow }} gpg --batch --import /root/pgp_private_key.asc
docker exec ${{ github.workflow }} gpg --import /root/pgp_public_key.asc
rm pgp_private_key.asc pgp_public_key.asc
- name: Add Host to Known Hosts
# In diesem Schritt wird der SSH-Host-Schlüssel des Remote-Servers (ALL_INKL_HOST) zum "known_hosts"-File
# des Containers hinzugefügt, um SSH-Verbindungen zum Remote-Server zu ermöglichen, ohne dass Warnungen angezeigt werden.
# Der Host-Schlüssel wird mit `ssh-keyscan` gesammelt und in `known_hosts` eingetragen.
run: |
echo "This is the host: ${{ secrets.ALL_INKL_HOST }}"
docker exec ${{ github.workflow }} bash -c "mkdir -p /root/.ssh && touch /root/.ssh/known_hosts && chmod 600 /root/.ssh/known_hosts"
docker exec ${{ github.workflow }} bash -c "ssh-keyscan -H '${{ secrets.ALL_INKL_HOST }}' >> /root/.ssh/known_hosts"
docker exec ${{ github.workflow }} bash -c "cat /root/.ssh/known_hosts"
- name: Run the restore script in Docker container
# In diesem Schritt wird das Skript `odoo-restore.sh` in den Container kopiert und dort ausgeführt.
# Die für das Skript notwendigen Umgebungsvariablen werden mitgegeben.
# Das Skript entpackt das Backup-Archiv.
# Das Skript löscht die bisherige Datenbank und fügt die neue Datenbank wieder ein.
# Anschließend wird das Odoo filestore wiederhergestellt.
run: |
docker cp ./scripts/odoo-restore.sh ${{ github.workflow }}:/root/odoo-restore.sh
docker exec ${{ github.workflow }} chmod +x /root/odoo-restore.sh
docker exec -e ADMIN_PASSWORD=${{ secrets.ODOO_ADMIN_PASSWORD }} \
-e sftp_host=${{ secrets.SFTP_HOST }} \
-e sftp_user=${{ secrets.SFTP_USER }} \
-e sftp_password=${{ secrets.SFTP_PASSWD }} \
-e gpg_password=${{ secrets.GPG_PASSPHRASE }} \
${{ github.workflow }} /bin/bash -c "bash /root/odoo-restore.sh '${{ env.URL_RESTORE }}'"
- name: Stop and remove Docker container
# Nachdem das Restore abgeschlossen ist, wird der Docker-Container gestoppt und entfernt,
# um keine Ressourcen auf dem Host unnötig zu verbrauchen.
run: |
docker stop ${{ github.workflow }}
docker rm ${{ github.workflow }}
- name: Clone or update custom_addons open_workshop repository
run: |
echo "Container Name Extension: ${{ env.CONTAINER_NAME_EXTENSION }}"
docker exec hobbyhimmel_odoo_${{ env.CONTAINER_NAME_EXTENSION }} /bin/bash -c "
git config --global pull.ff only && \
if [ ! -d /home/odoo/custom_addons/open_workshop ]; then
git clone https://gitea:${{ secrets.BUILD_ACTION }}@gitea.lan.hobbyhimmel.de/hobbyhimmel/open_workshop.git /home/odoo/custom_addons/open_workshop;
else
cd /home/odoo/custom_addons/open_workshop && \
git remote set-url origin https://gitea:${{ secrets.BUILD_ACTION }}@gitea.lan.hobbyhimmel.de/hobbyhimmel/open_workshop.git && \
git fetch && git checkout ${{ env.ODOO_VERSION }}_dev && git pull;
fi"
- name: Uninstall vvow_pos
run: |
docker exec -e ODOO_URL=${{ env.URL_RESTORE }} \
-e ODOO_DB=${{ env.DB_NAME }} \
-e ODOO_USERNAME=${{ secrets.ODOO_HOBBYHIMMEL_ADMIN }} \
-e ODOO_PASSWORD=${{ secrets.ODOO_HOBBYHIMMEL_ADMIN_PASSWORD }} \
hobbyhimmel_odoo_${{ env.CONTAINER_NAME_EXTENSION }} \
/bin/bash -c "
cd /home/odoo/custom_addons/open_workshop/scripts && \
python3 uninstall_rpc.py vvow_pos"
- name: Fix Database hobbyhimmel with fix_missing_pos_partner.py
# In diesem Schritt wird das Skript `fix_missing_pos_partner.py` ausgeführt, um sicherzustellen, dass alle Partner in der Datenbank korrekt sind.
run: |
docker exec hobbyhimmel_odoo_${{ env.CONTAINER_NAME_EXTENSION }} /bin/bash -c "
cd /home/odoo/custom_addons/open_workshop/scripts && \
/opt/odoo/odoo/odoo-bin shell -d ${{ env.DB_NAME }} < fix_missing_pos_partner.py"
- name: Install open_workshop
# Install open_workshop and run necessary migrations
run: |
docker exec hobbyhimmel_odoo_${{ env.CONTAINER_NAME_EXTENSION }} /bin/bash -c "
/opt/odoo/odoo/odoo-bin -d ${{ env.DB_NAME }} -i open_workshop --stop-after-init"
docker restart hobbyhimmel_odoo_${{ env.CONTAINER_NAME_EXTENSION }}
- name: Import Machine to Product and Training relation
run: |
docker exec hobbyhimmel_odoo_${{ env.CONTAINER_NAME_EXTENSION }} /bin/bash -c "
cd /home/odoo/custom_addons/open_workshop/scripts && \
/opt/odoo/odoo/odoo-bin shell -d ${{ env.DB_NAME }} < import_machine_products.py"
- name: Uninstall vvow_einweisungen
run: |
docker exec -e ODOO_URL=${{ env.URL_RESTORE }} \
-e ODOO_DB=${{ env.DB_NAME }} \
-e ODOO_USERNAME=${{ secrets.ODOO_HOBBYHIMMEL_ADMIN }} \
-e ODOO_PASSWORD=${{ secrets.ODOO_HOBBYHIMMEL_ADMIN_PASSWORD }} \
hobbyhimmel_odoo_${{ env.CONTAINER_NAME_EXTENSION }} \
/bin/bash -c "
cd /home/odoo/custom_addons/open_workshop/scripts && \
python3 uninstall_rpc.py vvow_einweisungen"

1
.gitignore vendored
View File

@ -121,6 +121,7 @@ celerybeat.pid
*.sage.py *.sage.py
# Environments # Environments
.env
.venv .venv
env/ env/
venv/ venv/

View File

@ -1,44 +0,0 @@
# ✅ OpenWorkshop Test Checkliste
## 🔹 1. Migration
- [ ] `migrate_existing_partners()` erzeugt zu jedem Partner genau einen `ows.user`.
- [ ] `migrate_existing_partners()` übernimmt korrekt alte `vvow_*`-Felder.
- [ ] `migrate_machine_access_from_old_fields()` erstellt korrekte Einträge in `ows.machine.access`.
- [ ] `migrate_machine_access_from_old_fields()` übernimmt das Änderungsdatum aus `mail.tracking.value`.
## 🔹 2. Kontakte Backend
- [ ] Beim Anlegen eines neuen Partners wird automatisch ein `ows.user` angelegt.
- [ ] Änderungen an Geburtstag, RFID, Haftung in Partner-Formular schreiben korrekt in `ows.user`.
- [ ] Die Werte aus `ows.user` werden korrekt im Partnerformular angezeigt (via `compute`).
- [ ] Das HTML-Widget mit Maschinenfreigaben (`machine_access_html`) wird korrekt dargestellt.
## 🔹 3. POS-Integration
- [ ] Felder aus `ows.user` (Geburtstag, RFID etc.) erscheinen im POS-Kunden-Popup.
- [ ] Maschinenfreigaben erscheinen im POS-Layout korrekt gruppiert nach Bereichen.
- [ ] Farben der Maschinenbereiche werden korrekt aus `color_hex` übernommen.
## 🔹 4. Maschinenverwaltung
- [ ] Maschinen-Formular zeigt Nutzungs- und Einweisungsprodukte korrekt an.
- [ ] Drop-downs in den Produktlisten zeigen nur Produkte der richtigen Kategorie.
- [ ] Neue Zuordnungen können direkt in den Tree-Ansichten editiert werden.
- [ ] Filter greifen korrekt (Maschinennutzung / Einweisungen).
## 🔹 5. Menüstruktur
- [ ] Menüeinträge "Nutzungsprodukte" und "Einweisungsprodukte" erscheinen unter Konfiguration > Maschinen.
- [ ] Klick auf "Alle Maschinen" öffnet die erwartete Listenansicht.
## 🔹 6. CSV/XML Demo-/Initialdaten
- [ ] Maschinenbereiche (`ows.machine.area`) sind korrekt aus `data.xml` geladen.
- [ ] Maschinen und ihre Produkt-Zuordnungen sind vollständig.
- [ ] Kategorien und Produkte sind korrekt verknüpft (`product.category`, `product.product`).
## 🔹 7. Systemweite Konsistenz
- [ ] Es gibt keine doppelten `ows.user`-Einträge.
- [ ] Kein Partner existiert ohne zugehörigen `ows.user`.
- [ ] `res.partner.ows_user_id` ist immer gefüllt.
## 🔹 8. Technische Qualität
- [ ] Kein `@api.depends('id')` mehr vorhanden.
- [ ] Commit wird in Migrationsfunktionen korrekt gesetzt (`self.env.cr.commit()`).
- [ ] Keine toten `vvow_*` Felder mehr im Modell (wenn auf ows.user umgestellt).
- [ ] post-init und pre-load Skripte laufen fehlerfrei bei Neuinstallation.

View File

@ -1,78 +1,3 @@
# Open Workshop (open_workshop ows) # open_workshop
Dieses Odoo v13.0 Modul erweitert das POS- und Kontakt-Modul um Funktionen für offene Werkstätten (FabLabs, Makerspaces etc.) und dient der Verwaltung von Maschinen, Naschinen Einweisungen Produkten, Maschinen Nutzungsprodukten und Zugangsberechtigungen zu den Maschinen.
## Funktionen
### Erweiterungen an Kontakten (res.partner)
- Geburtstagsfeld, RFID-Karte, Haftungsausschluss usw. ausgelagert nach `ows.user`
- Automatische Erstellung des `ows.user`-Eintrags beim Anlegen eines Kontakts
- Übersichtliche Darstellung aller Maschinenfreigaben im Odoo Kontaktformular
### Maschinen und Bereiche
- Modell `ows.machine` mit Gruppierung nach Bereichen (`ows.machine.area`)
- Farblich kodierte Bereiche (Hex-Wert aus Datenbank) welche zur Darstellung im POS verwendet werden
### Einweisungen und Nutzungen
- Modelle `ows.machine.training` und `ows.machine.product`
- Konfigurierbare Produkte für Einweisung/Nutzung direkt im Backend
- Zuweisung von Nutzungsprodukten zu Maschinen
- Zuweisung von Einweisungsprodukten zu Maschinen
### Maschinenfreigaben
- Modell `ows.machine.access` verknüpft Partner und Maschine
- Darstellung im POS als tabellarische Übersicht mit Anzeige für eine bestehende Einweisung / Nutzungsberechtigung
- Anzeige im POS-Kundendetailsansicht innerhalb der Kundendetailsansicht
- Anzeige im Odoo Kontak Modul der Maschineneinweisungen
## Installation
1. Dieses Modul in den Custom-Addons-Ordner kopieren
2. Vor der Installation von open_worshop muss vvow_pos deinstalliert werden. Die Funktionalität von vvow_pos wird durch open_workshop ersetzt und erweitert.
3. ggf. muss die alte Datenbank manuell migiriert werden, es gibt ca 9 gelöscht res.partner auf die Verweise aus POS bestehen. Diese res.parnter müssen wieder hergestellt werden. Dazu gibt es ein Skript unter
```folder
scripts/fix_missing_pos_partner.py
```
```bash
opt/odoo/odoo/odoo-bin shell -d hobbyhimmel < scrpts/fix_missing_pos_partner.py
```
4. Odoo starten mit:
```bash
odoo-bin -d deine_datenbank -u open_workshop
```
5. Alternativ im Backend unter Apps installieren
## Automatische Migrationen
Beim ersten Laden des Moduls werden folgende Migrationen durchgeführt:
- Bestehende `res.partner` erhalten automatisch `ows.user`-Eintrag (inkl. Übernahme alter Felder wie vvow_birthday, vvow_security, vvow_security_id, vvow_rfid.
- Alte Felder mit Maschinenfreigaben (`vvow_holz_*`, `vvow_metall_*`, `vvow_fablab_*`) werden in `ows.machine.access` übertragen
- inkl. Übernahme des Änderungsdatum aus `mail.message` wann der Nutzer die Einweisung erhalten hat (ist noch fehlerhaft)
## Entwicklerhinweise
### post_init_hook
Die Datei `post_init_hook.py` ruft automatisch nach der Installation folgende Methoden auf:
```python
res.partner.migrate_existing_partners()
res.partner.migrate_machine_access_from_old_fields()
```
### Datenimport
- Maschinenbereiche, Maschinen werden über `.xml`-Dateien in `data/` geladen
- Die Zuordnung von Maschine zu Einweisungsprodukten und Nutzungsprodukten muss derzeit noch manuell erstellt werden. Ein skript dafür folgt.
## ToDos
- Bearbeitung der Maschinenfreigaben im Backend
- Automatische Erstellung von `mail.message` bei manueller Freigabe
- Integration von Fristen (z.B. Ablaufdatum Einweisung)
## Autoren
- Matthias Lotz
## Lizenz
AGPL-3.0 oder später
---
Letzte Aktualisierung: 06.04.2025
Das ist ein Odoo 18.0 Modul für die Verwaltung von Maschinen, Einweisungen für Nutzer in einer offenen Werkstatt / Fablab

View File

@ -1,5 +1 @@
from . import models 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 +1,28 @@
{ {
'name': 'POS Open Workshop', 'name': 'Open Workshop',
'license': 'AGPL-3', 'version': '1.0',
'version': '13.0.1.0.0', 'author': 'Dein Name / Deine Organisation',
'summary': 'Erstellt Maschinenfreigaben basierend auf POS-Einweisungsprodukten', 'category': 'Custom',
'depends': ['base','product','sale','contacts','point_of_sale'], 'summary': 'Verwaltung von Maschinen & Einweisungen in einer offenen Werkstatt',
'author': 'matthias.lotz', 'description': """
'category': 'Point of Sale', Dieses Modul fügt zwei neue Modelle hinzu:
- Maschinen (mit Metadaten)
- Maschineneinweisungen (Verknüpfung zwischen Partner und Maschine)
Außerdem wird das Partnerformular erweitert, damit man direkt sieht,
für welche Maschinen ein Partner bereits eingewiesen ist.
""",
'depends': [
'base',
'point_of_sale',
],
'data': [ 'data': [
'security/ir.model.access.csv', 'security/ir.model.access.csv',
'views/machine_product_training_views.xml', 'views/open_workshop_machine_views.xml',
'views/menu_views.xml', 'views/open_workshop_training_views.xml',
'views/machine_area_views.xml', 'views/res_partner_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, 'installable': True,
'assets': { 'application': True,
'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

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

View File

@ -1,11 +0,0 @@
# 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):
Machine = request.env['ows.machine'].sudo()
return Machine.get_access_list_grouped(partner_id)

View File

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

9
data/maschine.csv Normal file
View File

@ -0,0 +1,9 @@
Name,Maschinen-Code,Standort,Active,Beschreibung
Standbohrmaschine,M-0001,Allgemein,1,Beschreibung 1
Kantenschleifer,M-0001,Holz,1,Beschreibung 1
Tischfräse,M-0001,Holz,1,Beschreibung 1
Felder Kreissäge,M-0001,Holz,1,Beschreibung 1
Felder Abricht-Dickenhobel,M-0001,Holz,1,Beschreibung 1
Felder Fräse,M-0001,Holz,1,Beschreibung 1
Felder Bandsäge,M-0001,Holz,1,Beschreibung 1
Drechselbank,M-0001,Holz,1,Beschreibung 1
1 Name Maschinen-Code Standort Active Beschreibung
2 Standbohrmaschine M-0001 Allgemein 1 Beschreibung 1
3 Kantenschleifer M-0001 Holz 1 Beschreibung 1
4 Tischfräse M-0001 Holz 1 Beschreibung 1
5 Felder Kreissäge M-0001 Holz 1 Beschreibung 1
6 Felder Abricht-Dickenhobel M-0001 Holz 1 Beschreibung 1
7 Felder Fräse M-0001 Holz 1 Beschreibung 1
8 Felder Bandsäge M-0001 Holz 1 Beschreibung 1
9 Drechselbank M-0001 Holz 1 Beschreibung 1

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

View File

@ -1,5 +1,4 @@
from . import ows_models from . import machine
from . import pos_order from . import training
from . import res_partner

12
models/machine.py Normal file
View File

@ -0,0 +1,12 @@
from odoo import models, fields, api
class OpenWorkshopMachine(models.Model):
_name = 'open.workshop.machine'
_description = 'Open Workshop Machine'
name = fields.Char(string='Name', required=True)
code = fields.Char(string='Maschinen-Code', help='Interner Code oder Inventar-Nr.')
location = fields.Char(string='Standort')
description = fields.Text(string='Beschreibung')
active = fields.Boolean(default=True)

View File

@ -1,578 +0,0 @@
# -*- 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)
_logger.info(f"[OWS] Administrator-Benutzer gefunden: {admin_user.name} (ID: {admin_user.id})")
admin_employee = self.search([('user_id', '=', admin_user.id)], limit=1)
if admin_employee:
admin_employee.write({
'name': 'TESTSYSTEM',
'job_title': 'Testumgebung',
'work_email': False,
'work_phone': False,
})
_logger.info("[OWS] Admin-Angestellter wurde umbenannt.")
else:
_logger.warning("[OWS] Kein Angestellter für user_admin gefunden.")
# Alle anderen Angestellten archivieren
other_employees = self.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 = "<div class='tab-content'><div class='tab-pane active' id='machine_access_tab'>"
html += "<div class='o_group'>"
for area in areas:
html += "<table class='o_group o_inner_group o_group_col_6'><tbody>"
# Bereichsüberschrift
html += f"""
<tr>
<td colspan="3" style="width: 100%;">
<div class="o_horizontal_separator">{area.name}</div>
</td>
</tr>
<tr>
<td class="o_td_label"><label class="o_form_label"></label></td>
<td class="o_td_label"><label class="o_form_label">Datum</label></td>
<td class="o_td_label"><label class="o_form_label">Gültig bis</label></td>
</tr>
"""
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 "-"
html += f"""
<tr>
<td class="o_td_label"><label class="o_form_label">{icon} {machine.name}</label></td>
<td class="o_td_field">{date_granted}</td>
<td class="o_td_field">{date_expiry}</td>
</tr>
"""
html += "</tbody></table>"
html += "</div></div></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'),
('#1f1f1f', 'Grau'),
]
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):
_name = 'ows.machine'
_table = 'ows_machine'
_description = 'OWS: Maschine'
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(string="Gerätebeschreibung", translate=True, help="Beschreibung der Maschine oder des Geräts.")
active = fields.Boolean(string="Aktive", default=True, help="Ist die Maschine oder das Gerät aktiv? Inaktive Maschinen werden nicht mehr in der POS-Ansicht angezeigt.")
area_id = fields.Many2one('ows.machine.area', string='Bereich', help="Bereich in der Werkstatt, in dem die Maschine oder das Gerät steht.")
product_ids = fields.One2many('ows.machine.product', 'machine_id', string="Nutzungsprodukte", help="Dies ist das zugehörige Produkt, falls die Maschine oder das Geräte eine zeitliche Nutzungsgebühr hat.")
product_names = fields.Char(string="Liste der Nutzungsprodukte", compute="_compute_product_using_names", store=False,)
training_ids = fields.One2many('ows.machine.training', 'machine_id', string="Einweisungsprodukte")
training_names = fields.Char(string="Liste der Einweisungsprodukte", compute="_compute_product_training_names", store=False,)
storage_location = fields.Char(string="Lagerort", help="Lagerort der Maschine oder des Geräts.")
purchase_price = fields.Float(string="Kaufpreis", help="Kaufpreis der Maschine oder des Geräts.")
purchase_date = fields.Date(string="Kaufdatum", help="Kaufdatum der Maschine oder des Geräts.")
@api.depends('product_ids.product_id.name')
def _compute_product_using_names(self):
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)
_sql_constraints = [
('code_unique', 'unique(code)', 'Maschinencode muss eindeutig sein.')
]
def name_get(self):
return [(rec.id, f"{rec.name} ({rec.code})") for rec in self]
@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")
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: Zurordnung Produkt der Nutzung zur die 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: Zurordnung Produkt der Einweisung zur die 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,49 +0,0 @@
from odoo import models, fields, api
from collections import defaultdict
#import debugpy
import logging
_logger = logging.getLogger(__name__)
_logger.info("✅ pos_order.py geladen")
# 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, draft, existing_order):
pos_order_id = super(PosOrder, self)._process_order(order, draft, 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

11
models/res_partner.py Normal file
View File

@ -0,0 +1,11 @@
from odoo import models, fields
class ResPartner(models.Model):
_inherit = 'res.partner'
machine_training_ids = fields.One2many(
'open.workshop.machine.training',
'partner_id',
string='Maschineneinweisungen'
)

29
models/training.py Normal file
View File

@ -0,0 +1,29 @@
from odoo import models, fields, api
class OpenWorkshopMachineTraining(models.Model):
_name = 'open.workshop.machine.training'
_description = 'Machine Training for Partners'
partner_id = fields.Many2one(
'res.partner',
string='Partner',
required=True,
ondelete='cascade'
)
machine_id = fields.Many2one(
'open.workshop.machine',
string='Maschine',
required=True,
ondelete='restrict'
)
training_date = fields.Datetime(
string='Datum der Einweisung',
default=fields.Datetime.now
)
notes = fields.Text(string='Notizen')
trainer_id = fields.Many2one(
'res.users',
string='Einweiser/In'
)

View File

@ -1,58 +0,0 @@
# -*- coding: utf-8 -*-
from odoo import SUPERUSER_ID
from odoo.api import Environment
#from . import import_machine_products
import logging
_logger = logging.getLogger(__name__)
def run_migration(cr, registry):
"""
Wird nach der Modulinstallation automatisch ausgeführt.
Migriert vorhandene res.partner-Einträge zu ows.user.
"""
env = Environment(cr, SUPERUSER_ID, {})
_logger.info("[OWS] Starte automatische Partner-Migration bei Modulinstallation...")
try:
env['res.partner'].migrate_existing_partners()
_logger.info("[OWS] Automatische Partner-Migration abgeschlossen.")
except Exception as e:
_logger.error(f"[OWS] Fehler bei automatischer Partner-Migration: {e}")
try:
env['res.partner'].migrate_machine_access_from_old_fields()
_logger.info("[OWS] Automatische Felder-Migration für Maschinenfreigaben in res.partner.")
except Exception as e:
_logger.error(f"[OWS] Fehler bei automatischer Felder-Migration: {e}")
# Testsystem-Anpassungen (Admin umbenennen + andere archivieren)
try:
env['hr.employee'].anonymize_for_testsystem()
_logger.info("[OWS] Testsystem-Anpassung der Mitarbeiter abgeschlossen.")
except Exception as e:
_logger.error(f"[OWS] Fehler bei Testsystem-Anpassung der Mitarbeiter: {e}")
# Archivierung aller Kontakte die keinen User Account haben
try:
env['res.partner'].archive_partners_without_users()
_logger.info("[OWS] Testsystem-Anpassung der Kontakte abgeschlossen.")
except Exception as e:
_logger.error(f"[OWS] Fehler bei Testsystem-Anpassung der Kontakte: {e}")
#import_machine_products.run_import(cr, registry)
''' Funktioniert nicht:
try:
module = env['ir.module.module'].search([('name', '=', 'vvow_einweisungen')], limit=1)
if module and module.state != 'uninstalled':
_logger.info("[OWS] Deinstalliere altes Modul vvow_einweisungen...")
module.button_immediate_uninstall()
except Exception as e:
_logger.error(f"[OWS] Fehler bei deinstallieren von vvow_einweisungen: {e}")
'''

View File

@ -1,41 +0,0 @@
from odoo import SUPERUSER_ID
def fix_missing_pos_order_partners(cr, registry):
"""
Findet POS-Bestellungen mit fehlendem Partner und fügt Dummy-Partner
direkt per SQL mit der passenden ID ein.
"""
cr.execute("""
SELECT DISTINCT partner_id FROM pos_order
WHERE partner_id IS NOT NULL
AND partner_id NOT IN (SELECT id FROM res_partner)
""")
missing_ids = [row[0] for row in cr.fetchall()]
print(f"Superuser: {SUPERUSER_ID}")
if not missing_ids:
print("✅ Keine fehlenden Partner gefunden.")
return
print(f"🚧 Fehlende Partner-IDs: {missing_ids}")
# Direkter SQL-Insert für res_partner
for pid in missing_ids:
print(f" Erzeuge Dummy-Partner mit ID {pid}")
cr.execute("""
INSERT INTO res_partner (id, name, customer_rank, create_uid, create_date, write_uid, write_date)
VALUES (%s, %s, %s, %s, now(), %s, now())
ON CONFLICT (id) DO NOTHING
""", (pid, f"Fehlender Partner {pid}", 1, SUPERUSER_ID, SUPERUSER_ID))
# Sequenz zurücksetzen, um ID-Kollision zu verhindern
cr.execute("""
SELECT setval('res_partner_id_seq', (SELECT MAX(id) FROM res_partner))
""")
print("✅ Alle fehlenden Partner ergänzt.")
# Automatischer Start in odoo-bin shell
if 'env' in globals():
fix_missing_pos_order_partners(env.cr, env.registry)
env.cr.commit()

View File

@ -1,79 +0,0 @@
# import_machine_products.py
# odoo-bin shell -d deine_datenbank < import_machine_products.py
import logging
from odoo import api, SUPERUSER_ID
_logger = logging.getLogger(__name__)
# Mapping von product_id > machine_id
TRAINING_MAPPING = {
'16':'1',
'11':'2',
'10':'4',
'117':'5',
'14':'6',
'12':'7',
'15':'8',
'118':'10',
'118':'11',
'118':'12',
'18':'15',
'18':'16',
'13':'18',
'17':'19',
}
# Mapping von product_id > machine_id
USAGE_MAPPING = {
'50':'1',
'49':'2',
'49':'3',
'49':'4',
'53':'5',
'52':'7',
'55':'8',
'59':'15',
'60':'15',
'59':'16',
'60':'16',
'57':'18',
'58':'19',
}
def run_import(cr, registry):
env = api.Environment(cr, SUPERUSER_ID, {})
machine_model = env['ows.machine']
product_model = env['product.product']
training_model = env['ows.machine.training']
usage_model = env['ows.machine.product']
created_training = 0
created_usage = 0
for product_id, machine_id in TRAINING_MAPPING.items():
machine = machine_model.search([('id', '=', machine_id)], limit=1)
product = product_model.search([('id', '=', product_id)], limit=1)
if machine and product:
training_model.create({
'machine_id': machine.id,
'training_id': product.id,
})
created_training += 1
for product_id, machine_id in USAGE_MAPPING.items():
machine = machine_model.search([('id', '=', machine_id)], limit=1)
product = product_model.search([('id', '=', product_id)], limit=1)
if machine and product:
usage_model.create({
'machine_id': machine.id,
'product_id': product.id,
})
created_usage += 1
_logger.info(f"[OWS Import] ✅ Trainings-Zuordnungen erstellt: {created_training}, Nutzungs-Zuordnungen erstellt: {created_usage}")
env.cr.commit()
# Automatischer Start in odoo-bin shell
if 'env' in globals():
run_import(env.cr, env.registry)

View File

@ -1 +0,0 @@
/opt/odoo/odoo/odoo-bin -d hobbyhimmel13_dev -i base,sale,pos_time_based_products,wk_coupons,pos_coupons,open_workshop --stop-after-init --load-language=de_DE

View File

@ -1,61 +0,0 @@
#!/bin/bash
BACKUP_DIR=/root # Verzeichnis, in dem das Backup temporär gespeichert wird.
#MONITOR_URL='https://cronitor.link/p/c6debfe4e00f46fe9eb430ab9d2b2588/7PPIpd' # Cronitor-URL zur Überwachung des Skriptstatus.
ODOO_DATABASE=hobbyhimmel # Der Name der Odoo-Datenbank, die wiederhergestellt werden soll.
DEFAULT_URL="http://hobbybackend2.fritz.box:9013" # Standard-URL für die Odoo-Datenbank-Backup-API.
URL="${1:-$DEFAULT_URL}" # Falls ein Argument übergeben wird, wird es als URL genutzt, sonst bleibt die Standard-URL.
BACKUP_NAME=${ODOO_DATABASE}.latest.zip # Der Name der Backup-Datei, wobei das aktuelle Datum an den Namen angehängt wird.
remote_directory=/www/htdocs/${sftp_user}/odoo # Das Verzeichnis auf dem Remote-SFTP-Server, in dem die Backups gespeichert werden.
# Function to report failure to Cronitor with step
# Diese Funktion meldet einen Fehler an Cronitor und beendet das Skript.
# Parameter: $1 - Der Schrittname, der beim Fehlschlag gemeldet wird.
report_failure() {
local step=$1 # Schrittname wird als erster Parameter übergeben.
echo "Error during $step" # Meldet den Fehler.
#curl "$MONITOR_URL?state=fail&message=$step" # Meldet den Fehlerstatus an Cronitor.
exit 1 # Beendet das Skript mit Exit-Code 1 (Fehler).
}
echo "Restoring Odoo database from backup..." # Meldet den Beginn der Wiederherstellung der Odoo-Datenbank.
echo "Verbindungsdaten: ${sftp_password}, ssh-${sftp_user}, ${sftp_host}"
cd /root # Wechselt in das Home-Verzeichnis des Benutzers.
# Holt die verschlüsselte Backup-Datei vom Remote-SFTP-Server in das lokale Verzeichnis.
sshpass -p "${sftp_password}" sftp -i ~/.ssh/id_rsa ssh-${sftp_user}@${sftp_host}:${remote_directory} <<EOF
get ${BACKUP_NAME}.gpg
get ${BACKUP_NAME}
EOF
# Prüft, ob der vorherige Befehl erfolgreich war, sonst bricht das Skript ab.
if [ $? -ne 0 ]; then
report_failure "SFTP transfer"
fi
cd /root
# Backup entschlüsseln -> Funktioniert nicht, weil im Passwort ein ´ enthalten ist
#rm ${BACKUP_NAME}
#gpg --batch --yes --passphrase "{$gpg_password}" --output "${BACKUP_DIR}/${BACKUP_NAME}" --decrypt "${BACKUP_DIR}/${BACKUP_NAME}.gpg"
echo "Admin password: ${ADMIN_PASSWORD}" # Meldet das Admin-Passwort.
echo "Database name: ${ODOO_DATABASE}" # Meldet den Namen der Odoo-Datenbank.
echo "Backup name: ${BACKUP_NAME}" # Meldet den Namen der Backup-Datei.
echo "URL: ${URL}" # Meldet die URL der Odoo-Datenbank-Backup-API.
echo "Backup directory: ${BACKUP_DIR}" # Meldet das Verzeichnis, in dem das Backup gespeichert wird.
echo "Delete database: ${ODOO_DATABASE}" # Meldet, dass die Datenbank gelöscht wird.
curl -X POST -s \
-F "master_pwd=${ADMIN_PASSWORD}" \
-F "name=${ODOO_DATABASE}" \
${URL}/web/database/drop || report_failure "Database deletion"
echo "Restoring database..." # Meldet den Beginn der Wiederherstellung der Datenbank.
curl \
-F "master_pwd=${ADMIN_PASSWORD}" \
-F "name=${ODOO_DATABASE}" \
-F "backup_file=@${BACKUP_DIR}/${BACKUP_NAME}" \
-F "backup_format=zip" \
-F "copy=true" \
${URL}/web/database/restore || report_failure "Database restoration"
# If everything was successful, report completion
# Meldet den erfolgreichen Abschluss des Skripts an Cronitor.
#curl "$MONITOR_URL?state=complete"

View File

@ -1,59 +0,0 @@
import sys
import os
import xmlrpc.client
# ----------------------
# KONFIGURATION AUS UMGEBUNG
# ----------------------
url = os.getenv("ODOO_URL", "http://localhost:8069")
db = os.getenv("ODOO_DB", "hobbyhimmel")
username = os.getenv("ODOO_USERNAME")
password = os.getenv("ODOO_PASSWORD")
# ----------------------
# PRÜFUNG DER KONFIGURATION
# ----------------------
if not all([url, db, username, password]):
print("❌ Fehler: ODOO_URL, ODOO_DB, ODOO_USERNAME und ODOO_PASSWORD müssen als Umgebungsvariablen gesetzt sein.")
sys.exit(1)
# ----------------------
# PARAMETER PRÜFEN
# ----------------------
if len(sys.argv) != 2:
print("❌ Fehler: Modulname muss als Parameter übergeben werden.")
print("👉 Beispiel: python3 uninstall_rpc.py vvow_einweisungen")
sys.exit(1)
module_name = sys.argv[1]
# ----------------------
# AUTHENTIFIZIERUNG
# ----------------------
common = xmlrpc.client.ServerProxy(f"{url}/xmlrpc/2/common")
uid = common.authenticate(db, username, password, {})
if not uid:
print("❌ Anmeldung fehlgeschlagen. Bitte Zugangsdaten prüfen.")
sys.exit(1)
# ----------------------
# MODUL SUCHEN & DEINSTALLIEREN
# ----------------------
models = xmlrpc.client.ServerProxy(f"{url}/xmlrpc/2/object")
ids = models.execute_kw(db, uid, password,
'ir.module.module', 'search',
[[['name', '=', module_name], ['state', '=', 'installed']]],
{'limit': 1}
)
if ids:
print(f"📦 Deinstalliere Modul: {module_name}")
models.execute_kw(db, uid, password,
'ir.module.module', 'button_immediate_uninstall',
[ids]
)
print("✅ Deinstallation abgeschlossen.")
else:
print(f" Modul '{module_name}' ist nicht installiert oder nicht vorhanden.")

View File

@ -1,8 +1,4 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink 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_open_workshop_machine_user,access_open_workshop_machine_user,model_open_workshop_machine,base.group_user,1,1,1,1
access_ows_machine_user,ows.machine,model_ows_machine,base.group_user,1,1,1,1 access_open_workshop_training_user,access_open_workshop_training_user,model_open_workshop_machine_training,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 access_open_workshop_machine_user ows.machine.access access_open_workshop_machine_user model_ows_machine_access model_open_workshop_machine base.group_user 1 1 1 1
3 access_ows_machine_user access_open_workshop_training_user ows.machine access_open_workshop_training_user model_ows_machine model_open_workshop_machine_training base.group_user 1 1 1 1
4
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

File diff suppressed because it is too large Load Diff

View File

@ -1,132 +0,0 @@
/* This file is based on the original code from the OrderWidget in Odoo Point of Sale module (screen.js).
* It has been modified to create a sidebar that displays machine access information for the selected customer.*/
odoo.define('open_workshop.machine_access_sidebar', function (require) {
"use strict";
const DUMMY_PARTNER = {
id: 1,
name: "AAAA Max Mustermann",
security_briefing: false,
security_id: null,
create_date: null,
};
var rpc = require('web.rpc');
var screens = require('point_of_sale.screens');
var chrome = require('point_of_sale.chrome');
var core = require('web.core');
var QWeb = core.qweb;
var MachineAccessSidebar = screens.ScreenWidget.extend({
template: 'MachineAccessSidebar',
init: function(parent, options) {
this._super(parent, options);
this.partner = null;
this.pos.bind('change:selectedOrder', this.bind_order_events, this);
},
show: function() {
this._super();
this.render_access();
},
update_machine_access: function(partner) {
console.log("🔁 Sidebar aktualisiert Maschinenfreigaben für Partner:", partner);
this.partner = partner;
this.render_access();
},
render_access: function() {
var self = this;
var partner = this.partner || DUMMY_PARTNER;
rpc.query({
model: 'ows.machine',
method: 'get_access_list_grouped',
args: [partner.id],
}).then(function (result) {
partner.create_date = partner.create_date && partner.create_date.substring(0, 10);
var html = QWeb.render('PartnerMachineAccessList', {
areas: result.access_by_area || [],
partner: partner,
});
self.$('.access-content').html(html);
});
},
bind_order_events: function () {
var order = this.pos.get_order();
if (!order) return;
/*order.unbind('change:client', this);
order.bind('change:client', this, function () {
this.update_machine_access(order.get_client());
});*/
this.update_machine_access(order.get_client());
}
});
// Sidebar aktualisieren bei Klick im ClientListScreenWidget (Vorschau)
screens.ClientListScreenWidget.include({
show: function () {
this._super();
$('.order-selector').hide(); // ← wird hier versteckt
},
hide: function () {
this._super();
$('.order-selector').show(); // ← beim Verlassen wieder zeigen
},
display_client_details: function (visibility, partner, clickpos) {
this._super(visibility, partner, clickpos);
try {
if (partner && typeof partner === 'object' && 'id' in partner) {
var sidebar = this.pos.chrome.sidebar_widget;
if (sidebar) {
console.log("👤 ClientListScreen: Vorschau für", partner.name);
sidebar.update_machine_access(partner);
}
}
} catch (e) {
console.warn("⚠️ Fehler beim Update der Sidebar nach Kundenauswahl:", e);
}
}
});
chrome.Chrome.include({
build_widgets: function () {
this._super();
var sidebar = new MachineAccessSidebar(this, {});
this.sidebar_widget = sidebar;
sidebar.appendTo(this.$el);
//this.pos.bind('change:selectedOrder', sidebar.bind_order_events, sidebar);
if (this.pos.get_order()) {
sidebar.bind_order_events();
}
}
});
});
odoo.define('open_workshop.models', function (require) {
"use strict";
var models = require('point_of_sale.models');
var field_utils = require('web.field_utils');
models.load_fields('res.partner', 'create_date');
models.load_fields('res.partner', 'birthday');
models.load_fields('res.partner', 'security_briefing');
models.load_fields('res.partner', 'security_id');
models.load_fields('res.partner', 'rfid_card');
});

View File

@ -1,72 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<!-- overwrite client details (view) -->
<t t-extend="ClientDetails">
<t t-jquery=".client-details-left" t-operation="replace">
<div class='client-details-left'>
<div class='client-detail'>
<span class='label'>Address</span>
<t t-if='partner.address'>
<span class='detail client-address'>
<t t-esc='partner.address' />
</span>
</t>
<t t-if='!partner.address'>
<span class='detail client-address empty'>N/A</span>
</t>
</div>
<div class='client-detail'>
<span class='label'>Email</span>
<t t-if='partner.email'>
<span class='detail client-email'>
<t t-esc='partner.email' />
</span>
</t>
<t t-if='!partner.email'>
<span class='detail client-email empty'>N/A</span>
</t>
</div>
<div class='client-detail'>
<span class='label'>Geburtstag</span>
<t t-if='partner.birthday'>
<span class='detail client-vvow_birthday'>
<t t-esc='partner.birthday' />
</span>
</t>
<t t-if='!partner.birthday'>
<span class='detail client-vvow_birthday empty'>N/A</span>
</t>
</div>
<div class='client-detail'>
<span class='label'>Phone</span>
<t t-if='partner.phone'>
<span class='detail client-phone'>
<t t-esc='partner.phone' />
</span>
</t>
<t t-if='!partner.phone'>
<span class='detail client-phone empty'>N/A</span>
</t>
</div>
<div t-attf-class='client-detail #{widget.pos.pricelists.length &lt;= 1 ? "oe_hidden" : ""}'>
<span class='label'>Pricelist</span>
<t t-if='partner.property_product_pricelist'>
<span class='detail property_product_pricelist'>
<t t-esc='partner.property_product_pricelist[1]'/>
</span>
</t>
<t t-if='!partner.property_product_pricelist'>
<span class='detail property_product_pricelist empty'>N/A</span>
</t>
</div>
<div class='client-detail'>
<t t-if='!partner.security_briefing'><span class='detail client-details-vvow_sec_briefing_error'>Haftungsausschluss prüfen!</span></t>
</div>
</div>
</t>
<t t-jquery=".client-details-right" t-operation="replace"/>
</t>
</templates>

View File

@ -1,64 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<!-- overwrite client details (view) -->
<t t-extend="ClientDetailsEdit">
<t t-jquery=".client-details-left" t-operation="replace">
<div class='client-details-left'>
<div class='client-detail'>
<span class='label'>Street</span>
<input class='detail client-address-street' name='street' t-att-value='partner.street || ""' placeholder='Street'></input>
</div>
<div class='client-detail'>
<span class='label'>Postcode</span>
<input class='detail client-address-zip' name='zip' t-att-value='partner.zip || ""' placeholder='ZIP'></input>
</div>
<div class='client-detail'>
<span class='label'>City</span>
<input class='detail client-address-city' name='city' t-att-value='partner.city || ""' placeholder='City'></input>
</div>
<div class='client-detail'>
<span class='label'>Country</span>
<select class='detail client-address-country needsclick' name='country_id'>
<option value=''>None</option>
<t t-foreach='widget.pos.countries' t-as='country'>
<option t-att-value='country.id' t-att-selected="partner.country_id ? ((country.id === partner.country_id[0]) ? true : undefined) : undefined">
<t t-esc='country.name'/>
</option>
</t>
</select>
</div>
<div class='client-detail'>
<span class='label'>Email</span>
<input class='detail client-email' name='email' type='email' t-att-value='partner.email || ""'></input>
</div>
<div class='client-detail'>
<span class='label'>Phone</span>
<input class='detail client-phone' name='phone' type='tel' t-att-value='partner.phone || ""'></input>
</div>
<div class='client-detail'>
<span class='label'>Geburtstag</span>
<input class='detail client-birthday' name='birthday' type='date' t-att-value='partner.birthday || ""'></input>
</div>
<div t-attf-class='client-detail #{widget.pos.pricelists.length &lt;= 1 ? "oe_hidden" : ""}'>
<span class='label'>Pricelist</span>
<select class='detail needsclick' name='property_product_pricelist'>
<t t-foreach='widget.pos.pricelists' t-as='pricelist'>
<option t-att-value='pricelist.id' t-att-selected="partner.property_product_pricelist ? (pricelist.id === partner.property_product_pricelist[0] ? true : undefined) : undefined">
<t t-esc='pricelist.display_name'/>
</option>
</t>
</select>
</div>
<div class='client-detail'>
<span class='label'>Haftungsauschschluß</span>
<select class='detail client-vvow_security_briefing-states needsclick' name='security_briefing'>
<option value='true' t-att-selected="partner.security_briefing ? true : undefined">Ja</option>
<option value='' t-att-selected="!partner.security_briefing ? true: undefined">Nein</option>
</select>
</div>
</div>
</t>
<t t-jquery=".client-details-right" t-operation="replace"/>
</t>
</templates>

View File

@ -1,88 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="MachineAccessSidebar" owl="1">
<div class="machine-access-sidebar">
<div class="access-content">
</div>
</div>
</t>
<t t-name="PartnerMachineAccessList">
<div class="client-details-grid">
<div class="client-details-header">
<ul>
<li><span class="client-details-label">Einweisungen</span></li>
</ul>
<div class="client-details-area" 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="!partner.security_briefing">
<span class="detail client-details-vvow_briefing_error"></span>
</t>
<t t-if="partner.security_briefing">
<span class="detail client-details-vvow_briefing"></span>
</t>
<span class="briefinglabel">Haftungsausschluss</span>
</li>
<t t-if="!partner.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="partner.security_briefing">
<ul class="subpoints">
<li class="client-detail">
<span class="label">Id:</span>
<t t-if="partner.security_id">
<span class="detail client-details-vvow_security_id"><t t-esc="partner.security_id"/></span>
</t>
<t t-if="!partner.security_id">
<span class="detail client-details-vvow_security_id">N/A</span>
</t>
</li>
<li class="client-detail">
<span class="label">Erstellt:</span>
<t t-if="partner.create_date">
<span class="detail client-details-vvow_security_id"><t t-esc="partner.create_date"/></span>
</t>
<t t-if="!partner.create_date">
<span class="detail client-vvow_birthday">N/A</span>
</t>
</li>
</ul>
</t>
</ul>
</div>
</div>
<t t-foreach="areas" t-as="area">
<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">
<li class="client-detail">
<t t-if="!machine.has_access">
<span class="detail client-details-vvow_briefing_error"></span>
</t>
<t t-if="machine.has_access">
<span class="detail client-details-vvow_briefing"></span>
</t>
<span class="briefinglabel"><t t-esc="machine.name"/></span>
</li>
</t>
</ul>
</div>
</t>
</div>
</t>
</templates>

View File

@ -1,70 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<!-- Test replace logo -->
<t t-extend="Chrome">
<t t-jquery=".pos-branding" t-operation="replace">
<img style="max-height:48px; max-width: 100%; width:auto" src="/web/binary/company_logo" alt="HOBBYHIMMEL"/>
</t>
</t>
<!-- Remove old order-selector container -->
<t t-extend="Chrome">
<t t-jquery=".placeholder-OrderSelectorWidget" t-operation="replace">
</t>
</t>
<!-- insert order-selector container in new position -->
<t t-extend="Chrome">
<t t-jquery=".pos" t-operation="prepend">
<div class="placeholder-OrderSelectorWidget"></div>
</t>
</t>
<!-- completly overwrite OrderSelectorWidget -->
<t t-name="OrderSelectorWidget">
<div class="order-selector" >
<div class="new-order-button">
<span class="order-button square neworder-button">
<i class='fa fa-plus' role="img" aria-label="New order" title="New order"/>
</span>
<span class="order-button square deleteorder-button">
<i class='fa fa-minus' role="img" aria-label="Delete order" title="Delete order"/>
</span>
</div>
<span class="orders touch-scrollable">
<t t-foreach="widget.pos.get_order_list()" t-as="order">
<t t-if="order === widget.pos.get_order()">
<span class="order-button select-order selected" t-att-title="order.sequence_number" t-att-data-uid="order.uid">
<span class="order-time">
<t t-esc="moment(order.creation_date).format('HH:mm')"/>
</span>
<span class="order-customer">
<t t-if="order.get_client()">
<t t-esc="order.get_client().name" />
</t>
<t t-if="!order.get_client()">
?
</t>
</span>
</span>
</t>
<t t-if="order !== widget.pos.get_order()">
<span class="order-button select-order" t-att-title="order.sequence_number" t-att-data-uid="order.uid">
<span class="order-time">
<t t-esc="moment(order.creation_date).format('HH:mm')"/>
</span>
<span class="order-customer">
<t t-if="order.get_client()">
<t t-esc="order.get_client().name" />
</t>
<t t-if="!order.get_client()">
?
</t>
</span>
</span>
</t>
</t>
</span>
</div>
</t>
</templates>

View File

@ -1 +0,0 @@
from . import test_res_partner

View File

@ -1,138 +0,0 @@
## Testausführung
# odoo-bin -d deine_testdatenbank --test-enable --init open_workshop
## oder:
# /opt/odoo/odoo/odoo-bin -d hobbyhimmel --test-enable --test-tags /test_res_partner.py --stop-after-init --http-port=8070 --log-level=debug
# /opt/odoo/odoo/odoo-bin -d hobbyhimmel --test-enable --stop-after-init --test-tags open_workshop --log-level=debug --update open_workshop --http-port=8070
from odoo.tests.common import TransactionCase
from odoo.tests import tagged
import logging
_logger = logging.getLogger(__name__)
@tagged('post_install', 'open_workshop', 'vvow')
class TestResPartnerVvowFields(TransactionCase):
def setUp(self):
super().setUp()
self.partner_model = self.env['res.partner']
self.user_model = self.env['ows.user']
def test_create_with_vvow_fields_creates_user(self):
partner = self.partner_model.create({
'name': 'Max Test',
'vvow_birthday': '1990-01-01',
'vvow_rfid_card': 'ABC12345',
'vvow_security_briefing': True,
'vvow_security_id': 'HB-001',
})
user = partner.ows_user_id
self.assertEqual(len(user), 1)
self.assertEqual(user.birthday.isoformat(), '1990-01-01')
self.assertEqual(user.rfid_card, 'ABC12345')
self.assertTrue(user.security_briefing)
self.assertEqual(user.security_id, 'HB-001')
# Check that Fassade korrekt ausgerechnet ist
self.assertEqual(partner.birthday.isoformat(), '1990-01-01')
self.assertEqual(partner.rfid_card, 'ABC12345')
self.assertTrue(partner.security_briefing)
self.assertEqual(partner.security_id, 'HB-001')
def test_write_vvow_fields_updates_user_and_fassade(self):
partner = self.partner_model.create({'name': 'Update Tester'})
user = partner.ows_user_id
partner.write({
'vvow_birthday': '1985-12-31',
'vvow_rfid_card': 'NEW123',
'vvow_security_briefing': False,
'vvow_security_id': 'NEU-SEC',
})
self.assertEqual(user.birthday.isoformat(), '1985-12-31')
self.assertEqual(user.rfid_card, 'NEW123')
self.assertFalse(user.security_briefing)
self.assertEqual(user.security_id, 'NEU-SEC')
# Sicherstellen, dass die berechneten Felder auch aktualisiert wurden
self.assertEqual(partner.birthday.isoformat(), '1985-12-31')
self.assertEqual(partner.rfid_card, 'NEW123')
self.assertFalse(partner.security_briefing)
self.assertEqual(partner.security_id, 'NEU-SEC')
def test_change_fassade_field_updates_user(self):
partner = self.partner_model.create({
'name': 'Change Fassade',
'vvow_security_briefing': True,
})
user = partner.ows_user_id
# Ändere security_briefing (die Fassade)
partner.write({'security_briefing': False})
# Muss sich im ows.user widerspiegeln
self.assertFalse(user.security_briefing)
# vvow-Sync: vvow bleibt wie gehabt (wird nicht überschrieben)
self.assertTrue(partner.vvow_security_briefing)
def test_change_and_read_consistency(self):
partner = self.partner_model.create({
'name': 'Read-Change-Test',
'vvow_security_briefing': True,
})
user = partner.ows_user_id
# Ändere security_briefing über Fassade
partner.write({'security_briefing': False})
self.assertFalse(user.security_briefing)
self.assertFalse(partner.security_briefing)
# Lies vvow_* separat aus sollte weiterhin den ursprünglichen vvow-Wert zeigen
self.assertTrue(partner.vvow_security_briefing)
# Ändere vvow_* erneut jetzt sollte alles wieder gesetzt werden
partner.write({'vvow_security_briefing': True})
self.assertTrue(partner.security_briefing)
self.assertTrue(user.security_briefing)
def test_inverse_does_not_clear_unrelated_fields(self):
_logger.info("🔍 Starte Test für Feldsynchronisation")
# Schritt 1: Partner mit vvow_* Feldern erzeugen → soll automatisch ows.user erzeugen
partner = self.partner_model.create({
'name': 'Unabhängigkeits-Test',
'vvow_birthday': '1995-05-05',
'vvow_rfid_card': 'ABC999',
'vvow_security_briefing': True,
'vvow_security_id': 'SEC-XYZ',
})
_logger.debug("✅ Partner angelegt: %s", partner.name)
# Validierung: es wurde ein ows.user erzeugt
self.assertEqual(len(partner.ows_user_id), 1, "Kein ows.user erzeugt")
user = partner.ows_user_id
_logger.debug("👤 ows.user: %s", user.read(['birthday', 'security_id']))
# Sicherstellen, dass alle Felder korrekt gesetzt wurden
self.assertEqual(user.birthday.isoformat(), '1995-05-05')
self.assertEqual(user.rfid_card, 'ABC999')
self.assertTrue(user.security_briefing)
self.assertEqual(user.security_id, 'SEC-XYZ')
# Schritt 2: Nur ein Feld über die Fassade ändern (inverse wird getriggert)
partner.write({'security_briefing': False})
_logger.info("✏️ security_briefing auf False gesetzt")
# Schritt 3: DB-Cache leeren
user.flush()
user.invalidate_cache()
_logger.debug("📦 user nach write: %s", user.read(['birthday', 'security_id']))
# Schritt 4: Sicherstellen, dass die anderen Felder NICHT gelöscht wurden
self.assertEqual(user.birthday.isoformat(), '1995-05-05')
self.assertEqual(user.rfid_card, 'ABC999')
self.assertEqual(user.security_id, 'SEC-XYZ')
self.assertFalse(user.security_briefing)

View File

@ -1,4 +0,0 @@
[ ] Help System
[ ] Möglichkeit, Einweisungen manuell zu setzen?
[ ] Möglichkeit, Einweisungen von Personen im Backend zurückzusetzen (geht im Moment nur über die Datenbank direkt)
[ ]

View File

@ -1,13 +0,0 @@
<odoo>
<data>
<template id="assets" inherit_id="point_of_sale.assets">
<xpath expr="." position="inside">
<script type="text/javascript" src="/open_workshop/static/src/js/machine_access_sidebar.js"/>
<template id="machine_access_template" name="Maschinenfreigaben Template" src="/open_workshop/static/src/xml/ows_machine_sidebar.xml"/>
</xpath>
<xpath expr="//link[@href='/point_of_sale/static/src/css/pos.css']" position="replace">
<link rel="stylesheet" type="text/css" href="/open_workshop/static/src/css/pos.css"/>
</xpath>
</template>
</data>
</odoo>

View File

@ -1,38 +0,0 @@
<!-- 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">tree,form</field>
</record>
<!-- Menüpunkt unter Maschinen > Konfiguration -->
<menuitem id="menu_machine_area" name="Bereiche" parent="menu_machine_config" action="open_workshop.action_machine_area_list" sequence="30"/>
<!-- Listenansicht -->
<record id="view_machine_area_tree" model="ir.ui.view">
<field name="name">ows.machine.area.tree</field>
<field name="model">ows.machine.area</field>
<field name="arch" type="xml">
<tree>
<field name="name"/>
<field name="color_hex" widget="color_picker"/>
</tree>
</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" widget="color_picker"/>
</group>
</form>
</field>
</record>
</odoo>

View File

@ -1,49 +0,0 @@
<odoo>
<!-- Tree View: Nutzungsprodukte -->
<record id="view_machine_product_tree" model="ir.ui.view">
<field name="name">ows.machine.product.tree</field>
<field name="model">ows.machine.product</field>
<field name="arch" type="xml">
<tree editable="bottom">
<field name="machine_id"/>
<field name="product_id" domain="[('categ_id.name', '=', 'Maschinennutzung')]"/>
</tree>
</field>
</record>
<!-- Tree View: Einweisungsprodukte -->
<record id="view_machine_training_tree" model="ir.ui.view">
<field name="name">ows.machine.training.tree</field>
<field name="model">ows.machine.training</field>
<field name="arch" type="xml">
<tree editable="bottom">
<field name="machine_id"/>
<field name="training_id" domain="[('categ_id.name', '=', 'Einweisungen')]"/>
</tree>
</field>
</record>
<!-- Action: Nutzungsprodukte -->
<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_id" ref="view_machine_product_tree"/>
<field name="help" type="html">
<p>Verwalte die Zuordnung von Maschinen zu Nutzungsprodukten.</p>
</field>
</record>
<!-- Action: Einweisungsprodukte -->
<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_id" ref="view_machine_training_tree"/>
<field name="help" type="html">
<p>Verwalte die Zuordnung von Maschinen zu Einweisungsprodukten.</p>
</field>
</record>
</odoo>

View File

@ -1,69 +0,0 @@
<!-- 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">
<tree>
<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"/>
</tree>
</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="name"/>
<field name="category"/>
<field name="category_icon" string="⚙" readonly="1"/>
<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>
<!-- Neue -->
<notebook>
<page string="Nutzungsprodukte">
<field name="product_ids" context="{'default_machine_id': active_id}">
<tree editable="bottom">
<field name="product_id" domain="[('categ_id.name', '=', 'Maschinennutzung')]" />
</tree>
</field>
</page>
<page string="Einweisungsprodukte">
<field name="training_ids" context="{'default_machine_id': active_id}">
<tree editable="bottom">
<field name="training_id" domain="[('categ_id.name', 'in', ['Einweisungen', 'Kurse'])]" />
</tree>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
</odoo>

View File

@ -1,81 +0,0 @@
<!-- menu_views.xml -->
<odoo>
<!-- Maschinenliste -->
<record id="action_machine_list" model="ir.actions.act_window">
<field name="name">Maschinen</field>
<field name="res_model">ows.machine</field>
<field name="view_mode">tree,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>
</record>
<!-- Menüstruktur -->
<!-- Oberstes Menü -->
<menuitem id="menu_machine_root"
name="Maschinen"
sequence="10"/>
<!-- Konfigurationsebene -->
<menuitem id="menu_machine_config"
name="Konfiguration"
parent="menu_machine_root"
sequence="10"/>
<!-- Menüpunkt: Maschinenliste (klickbar) -->
<menuitem id="menu_machine_list_action"
name="Alle Maschinen"
parent="menu_machine_config"
action="open_workshop.action_machine_list"
sequence="10"/>
<!-- Menücontainer: Zuordnungen -->
<menuitem id="menu_machine_list"
name="Zuordnungen"
parent="menu_machine_config"
sequence="20"/>
<!-- Untermenü: Nutzungsprodukte -->
<menuitem id="menu_machine_product"
name="Nutzungsprodukte"
parent="menu_machine_list"
action="action_machine_product"
sequence="10"/>
<!-- Untermenü: Einweisungsprodukte -->
<menuitem id="menu_machine_training"
name="Einweisungsprodukte"
parent="menu_machine_list"
action="action_machine_training"
sequence="20"/>
<!-- List & Form Views für training.product -->
<record id="view_training_product_tree" model="ir.ui.view">
<field name="name">ows.machine.product.tree</field>
<field name="model">ows.machine.product</field>
<field name="arch" type="xml">
<tree>
<field name="product_id"/>
<field name="machine_id"/>
</tree>
</field>
</record>
<record id="view_training_product_form" model="ir.ui.view">
<field name="name">ows.machine.product.form</field>
<field name="model">ows.machine.product</field>
<field name="arch" type="xml">
<form string="Einweisungs-Produkt">
<group>
<field name="product_id"/>
<field name="machine_id"/>
</group>
</form>
</field>
</record>
</odoo>

View File

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="action_open_workshop_machine" model="ir.actions.act_window">
<field name="name">Maschinen</field>
<field name="res_model">open.workshop.machine</field>
<field name="view_mode">list,form</field>
<field name="help">Verwaltung aller Maschinen in der offenen Werkstatt</field>
</record>
<menuitem id="open_workshop_main_menu"
name="Offene Werkstatt"
sequence="10"/>
<menuitem id="open_workshop_menu_machine"
name="Maschinen"
parent="open_workshop_main_menu"
action="action_open_workshop_machine"
sequence="20"/>
<record id="view_open_workshop_machine_tree" model="ir.ui.view">
<field name="name">open.workshop.machine.tree</field>
<field name="model">open.workshop.machine</field>
<field name="arch" type="xml">
<list string="Maschinen">
<field name="name"/>
<field name="code"/>
<field name="description"/>
<field name="location"/>
<field name="active"/>
</list>
</field>
</record>
<record id="view_open_workshop_machine_form" model="ir.ui.view">
<field name="name">open.workshop.machine.form</field>
<field name="model">open.workshop.machine</field>
<field name="arch" type="xml">
<form string="Maschine">
<sheet>
<group>
<field name="name"/>
<field name="code"/>
<field name="location"/>
<field name="active"/>
</group>
<group>
<field name="description"/>
</group>
</sheet>
</form>
</field>
</record>
</odoo>

View File

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="action_open_workshop_training" model="ir.actions.act_window">
<field name="name">Maschineneinweisungen</field>
<field name="res_model">open.workshop.machine.training</field>
<field name="view_mode">list,form</field>
<field name="help">Verwaltung aller Maschineneinweisungen (Partner -> Maschine)</field>
</record>
<menuitem id="open_workshop_menu_training"
name="Einweisungen"
parent="open_workshop_main_menu"
action="action_open_workshop_training"
sequence="30"/>
<record id="view_open_workshop_training_tree" model="ir.ui.view">
<field name="name">open.workshop.machine.training.tree</field>
<field name="model">open.workshop.machine.training</field>
<field name="arch" type="xml">
<list string="Maschineneinweisungen">
<field name="partner_id"/>
<field name="machine_id"/>
<field name="training_date"/>
</list>
</field>
</record>
<record id="view_open_workshop_training_form" model="ir.ui.view">
<field name="name">open.workshop.machine.training.form</field>
<field name="model">open.workshop.machine.training</field>
<field name="arch" type="xml">
<form string="Maschineneinweisung">
<sheet>
<group>
<field name="partner_id"/>
<field name="machine_id"/>
<field name="training_date"/>
<field name="trainer_id"/>
</group>
<group>
<field name="notes"/>
</group>
</sheet>
</form>
</field>
</record>
</odoo>

View File

@ -1,111 +0,0 @@
<odoo>
<!-- 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="10"/>
<field name="arch" type="xml">
<xpath expr="//page[@name='sales_purchases']" position="before">
<!-- Tab 1: HOBBYHIMMEL Basis -->
<page name="ows_basic" string="HOBBYHIMMEL Basis">
<group name="container_row_2">
<group string="Sicherheit">
<field name="security_briefing"/>
<field name="security_id"/>
</group>
<group string="Zugang">
<field name="rfid_card"/>
</group>
</group>
</page>
<!-- Tab 2: HOBBYHIMMEL Einweisungen (HTML) -->
<page name="ows_machine_access_html" string="HOBBYHIMMEL Einweisungen">
<field name="machine_access_html" readonly="1" widget="html"/>
</page>
<!-- Tab 3: Einweisungen (Liste) -->
<page name="ows_machine_access_list" string="Einweisungen (Liste)">
<field name="machine_access_ids">
<tree>
<field name="machine_id"/>
<field name="date_granted"/>
<field name="date_expiry"/>
<field name="granted_by_pos"/>
</tree>
</field>
</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">tree,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

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_partner_form_open_workshop_inherit" model="ir.ui.view">
<field name="name">res.partner.form.open.workshop.inherit</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form"/>
<field name="arch" type="xml">
<xpath expr="//notebook" position="inside">
<page string="Maschineneinweisungen">
<field name="machine_training_ids">
<list editable="bottom">
<field name="machine_id"/>
<field name="training_date"/>
<field name="trainer_id"/>
</list>
<form>
<group>
<field name="machine_id"/>
<field name="training_date"/>
<field name="trainer_id"/>
<field name="notes"/>
</group>
</form>
</field>
</page>
</xpath>
</field>
</record>
</odoo>