Compare commits

...

105 Commits

Author SHA1 Message Date
1644f756cd Merge pull request '13.0_dev-target' (#5) from 13.0_dev-target into 13.0_dev
Reviewed-on: #5
2025-06-26 17:42:24 +02:00
6f8f788d9d merge with open_workshop 17.0 2025-06-26 17:34:13 +02:00
2d806ae333 update machine model 2025-06-25 21:41:03 +02:00
e9db046765 fixed pos receipt print, machine-access-sidebar is now invisible 2025-05-18 08:15:08 +00:00
31b4f7e7a2 [FIX] open_workshop: Mehrere Maschinenfreigaben pro Einweisungsprodukt im POS
All checks were successful
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Successful in 4m48s
Beim Kauf eines Einweisungsprodukts wurden bisher nur eine Maschinenfreigabe erstellt,
selbst wenn das Produkt mehreren Maschinen zugeordnet war.
Dieser Fix passt _process_order an, sodass alle zugehörigen Maschinen erfasst und
ggf. neue Freigaben für den Kunden erstellt werden.

+ Nutzung von defaultdict zur besseren Produkt-Maschine-Zuordnung
+ Klares Logging zur Nachvollziehbarkeit
+ Verhindert doppelte Freigaben
2025-05-06 17:19:57 +00:00
b40e2f7837 Reihenfolge in Kontakte geändert (Tabs)
All checks were successful
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Successful in 4m45s
2025-05-04 19:27:31 +00:00
ea6d809020 modified Contact View to more Odoo Style 2025-05-04 18:47:47 +00:00
63337a28bd Issue Template hinzugefügt
All checks were successful
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Successful in 4m39s
2025-05-02 09:31:24 +00:00
a19be91685 alle nicht odoo nutzer sind als Kontakt archiviert
All checks were successful
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Successful in 4m40s
2025-05-02 08:13:35 +00:00
da4cd0ba5c für das Testsystem wurden alle Angestellten archiviert und Admin in Testsystem umbenannt
All checks were successful
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Successful in 4m45s
2025-05-02 07:45:28 +00:00
76b1dc29f2 todo.md aktualisiert
All checks were successful
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Successful in 4m39s
2025-05-01 16:15:47 +02:00
0b45c9df62 Merge remote-tracking branch 'origin/13.0_dev' into 13.0_dev
All checks were successful
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Successful in 4m36s
2025-05-01 05:49:12 +00:00
92696827d3 removed display: grid in clientlist-screen 2025-05-01 05:37:04 +00:00
6c2c0479c8 .gitea/workflows/odoo-restore-open_workshop_install.yaml aktualisiert
All checks were successful
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Successful in 4m40s
2025-04-23 21:46:02 +02:00
9574e3675e test cron
Some checks failed
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Failing after 3s
2025-04-23 21:38:01 +02:00
5498b20ea5 fixed inverse unrelated fiels deletion, added tests
All checks were successful
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Successful in 4m40s
2025-04-23 17:43:31 +00:00
ef46a18937 hübsch
All checks were successful
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Successful in 4m48s
2025-04-16 20:29:12 +00:00
a159b88691 ows_machine_sidebar.xml is messy 2025-04-16 18:18:31 +00:00
93468530af no error in clientscreen anymore
All checks were successful
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Successful in 4m38s
2025-04-15 23:32:38 +00:00
3e0abbf184 update aus clientlist
All checks were successful
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Successful in 4m41s
2025-04-13 09:24:33 +00:00
9b16baf196 machine access sidebar funktioniert 2025-04-13 07:32:59 +00:00
fb96a54c2d not working 2025-04-13 06:21:31 +00:00
26b48adf92 clean up
All checks were successful
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Successful in 4m36s
2025-04-12 11:50:57 +00:00
1f15d83f63 fixed password
All checks were successful
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Successful in 4m48s
2025-04-12 11:21:10 +00:00
7c63482590 fixed enviroment vars
Some checks failed
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Failing after 1m7s
2025-04-12 11:14:24 +00:00
9be3e65212 logging
Some checks failed
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Failing after 1m14s
2025-04-12 11:08:45 +00:00
7dbd9e8f12 neuer Versuch uninstall modules via rpc
Some checks failed
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Failing after 1m24s
2025-04-12 11:00:05 +00:00
42faa68903 alter table vvow_einweisungen
Some checks failed
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Failing after 4m37s
2025-04-12 08:04:30 +00:00
2c84a2b5ff fixed network
Some checks failed
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Failing after 4m41s
2025-04-12 06:50:08 +00:00
30a42307f7 fixed volume name
Some checks failed
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Failing after 4m44s
2025-04-12 06:36:42 +00:00
2410446d02 next try vvow_einweisungen uninstall
Some checks failed
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Failing after 4m47s
2025-04-12 06:09:58 +00:00
e8a9afcde2 removed uninstall vvow_einweisungen
All checks were successful
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Successful in 4m36s
2025-04-11 19:18:58 +00:00
9fa2fb726c Added sleep before vvow_einweisungen uninstall.
Some checks failed
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Failing after 6m52s
2025-04-11 17:53:10 +00:00
334a261674 revert test gpg
Some checks failed
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Failing after 4m49s
2025-04-11 17:38:19 +00:00
2a471aa8f8 test gpg
Some checks failed
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Failing after 37s
2025-04-11 17:10:20 +00:00
aa9275c44d type fix 2
Some checks failed
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Failing after 4m58s
2025-04-11 16:58:14 +00:00
d0277fd998 typo fix
Some checks failed
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Failing after 4m28s
2025-04-11 16:51:26 +00:00
03c7e5fff8 Import Machine to Product and Training Relation
Some checks failed
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Failing after 4m33s
2025-04-11 16:41:09 +00:00
d60c0df45b logging
Some checks failed
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Failing after 4m52s
2025-04-11 16:13:01 +00:00
ad11530ea4 return
All checks were successful
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Successful in 1m11s
2025-04-11 15:55:30 +00:00
9248efde09 logging
Some checks failed
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Failing after 45s
2025-04-11 15:53:38 +00:00
d9b361548a fixed missing blank
Some checks failed
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Failing after 47s
2025-04-11 15:50:55 +00:00
54e7b08184 added env again
Some checks failed
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Failing after 46s
2025-04-11 15:44:50 +00:00
5bb0052f48 fixed git clone
Some checks failed
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Failing after 48s
2025-04-11 15:40:09 +00:00
8681d8e6a3 added gitea user information for repository
Some checks failed
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Failing after 43s
2025-04-11 15:14:40 +00:00
64a7bb3b3b login into gita
Some checks failed
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Failing after 37s
2025-04-11 15:01:19 +00:00
804da2cc08 changed order
Some checks failed
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Failing after 40s
2025-04-10 22:27:14 +00:00
e511ed797d unverschlüsselt
Some checks failed
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Failing after 35s
2025-04-10 22:21:56 +00:00
bf1c9ea88c test
Some checks failed
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Failing after 33s
2025-04-10 22:13:34 +00:00
4d42f8b8e2 gpg hack
Some checks failed
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Failing after 30s
2025-04-10 21:58:24 +00:00
4b45f2b282 gnugpg2
Some checks failed
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Failing after 30s
2025-04-10 21:28:11 +00:00
3acafcbdd5 pinenty-mode
Some checks failed
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Failing after 28s
2025-04-10 21:11:13 +00:00
628fd5e477 added gpg_passphrase
Some checks failed
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Failing after 34s
2025-04-10 21:09:55 +00:00
f2809c7780 pinentry-mode
Some checks failed
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Failing after 29s
2025-04-10 21:03:00 +00:00
cab16d3b92 fixed home folder
Some checks failed
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Failing after 41s
2025-04-10 20:58:36 +00:00
0810eca271 fixed path
Some checks failed
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Failing after 35s
2025-04-10 20:52:18 +00:00
14b48d1e78 fixed path
Some checks failed
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Failing after 30s
2025-04-10 20:49:37 +00:00
f75d5cbbd7 Fixed ssh user
Some checks failed
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Failing after 35s
2025-04-10 20:46:22 +00:00
bfb5b7e686 modified: scripts/odoo-restore.sh
Some checks failed
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Failing after 26s
2025-04-10 20:34:35 +00:00
cf33bc9a82 fixed enviroment vars
Some checks failed
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Failing after 28s
2025-04-10 20:21:47 +00:00
9540f80525 bessere Fehler logging
Some checks failed
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Failing after 27s
2025-04-10 20:16:57 +00:00
41fc6259aa Added error codes
Some checks failed
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Failing after 29s
2025-04-10 20:13:39 +00:00
1cb6d30fcc fixed chmod
Some checks failed
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Failing after 29s
2025-04-10 20:08:09 +00:00
a60947de85 added gitea action gpg and sftp download
Some checks failed
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Failing after 29s
2025-04-10 20:03:36 +00:00
1aa6bdd851 install nano into gitea action docker
Some checks failed
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Failing after 29s
2025-04-10 19:25:47 +00:00
c03a5c8263 comment out all_inkl_host
Some checks failed
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Failing after 23s
2025-04-10 19:08:31 +00:00
9065e2316c added .env
Some checks failed
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Failing after 27s
2025-04-10 19:03:13 +00:00
449d55ce1e fix in gitea action
Some checks failed
odoo-backup-restore / run-odoo-backup-in-docker (push) Failing after 3s
2025-04-10 17:00:18 +00:00
0d84c55c80 update gitea action 2025-04-10 16:56:02 +00:00
3b35a56ca1 added uninstall scripts 2025-04-10 16:52:35 +00:00
ba4bc5dc8d fixed import_machine_prdoucts, prepared for gitea actions 2025-04-09 18:56:57 +00:00
a5e14a07d7 added import skript for mapping machine to products and trainings 2025-04-08 21:29:07 +00:00
d5d99b4d8d fixed res.parnter update ows.user 2025-04-08 21:18:51 +00:00
7f4e7183ec fixed Automatische Felder Migration für Maschinenfreigaben in res.partner bei der Installation 2025-04-08 18:14:57 +00:00
f4a586f2db models/ows_models.py aktualisiert 2025-04-08 10:05:00 +02:00
0818daf530 scripts/uninstall_old_modules.py aktualisiert 2025-04-07 16:01:20 +02:00
a04109906c scripts/uninstall_old_modules.py aktualisiert 2025-04-07 15:57:17 +02:00
67361724a6 Add uninstall old modules script 2025-04-07 15:41:45 +02:00
8ccfebb399 Deinstallation von vvow_einweisungen bei Installation 2025-04-07 15:23:48 +02:00
c0b64c6238 README.md aktualisiert 2025-04-07 12:43:12 +02:00
5cc8ab7113 removed predefined data(einweisung), added fix_missing_parnters for migration db purpose 2025-04-06 20:08:05 +00:00
96a463561f moved some helper scripts 2025-04-06 19:24:14 +00:00
95f3046196 added migration for vvow_* einweisungs fields in res.partner 2025-04-06 19:16:13 +00:00
0454aebc95 diese Version lässt sich mit einer integrierten mirgration auf eine bestehende DB installieren, es fehlen noch die Einweisungen 2025-04-06 16:55:58 +00:00
e14531fa7f Einfache Zuordnung Maschine - Einweisung und Maschine - Nutzung 2025-04-06 11:48:32 +00:00
312dffdad0 working pos again 2025-04-05 20:04:46 +00:00
a317315bb2 product and categories import as xml from hobbyhimmel db 2025-04-05 16:36:38 +00:00
dbfd702a53 demo daten 2025-04-05 13:58:23 +00:00
72c837fe1f added imported demo data from hobbyhimmel db 2025-04-05 11:49:07 +00:00
9bfea2b586 traingsprodukte und nutzungsprodukte als Liste 2025-04-05 10:38:33 +00:00
a8aa4798b1 added ows.user 2025-04-05 05:59:36 +00:00
a608f18b3f Fixed Briefing view in POS 2025-04-03 19:31:28 +00:00
fb4c58e021 Fixed Briefing view in POS 2025-04-03 19:31:13 +00:00
8a6d72fb19 Maschinen Ansicht zeigt nun auch die dazugehörigen Einweisungsprodukte 2025-04-03 19:24:44 +00:00
de95446686 bessere Ansicht in POS der Einweisungen, kompakter 2025-04-03 18:14:42 +00:00
5363620682 einspaltiges Einweisungen im Backend 2025-04-03 16:20:38 +00:00
7bc1a47651 working modification in backend 2025-04-03 15:49:37 +00:00
75dbe50037 dynamisches Einweisungslayout funktioniert. 2025-04-02 19:01:07 +00:00
63932ec8a7 changed color to color_hex 2025-04-02 18:10:52 +00:00
6e70c95a7c fixed update client details 2025-04-02 16:38:04 +00:00
e39837f09c merge vvow_pos 2025-04-02 14:56:42 +00:00
7b25645e0a Dateien aus dem alten vvow_pos hinzugefügt 2025-04-01 17:51:55 +00:00
bbdca32b59 POS funktioniert nun mit dynamischen Einweisungen aus open_workshop 2025-03-30 17:35:31 +00:00
b52efaf563 fixed xml load order and removed debugpy 2025-03-29 17:26:10 +00:00
MaPaLo76
9ae8b095da intial version of open workshop v13.0 2025-03-29 17:28:05 +01:00
55 changed files with 5494 additions and 225 deletions

8
.env Normal file
View File

@ -0,0 +1,8 @@
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

@ -0,0 +1,69 @@
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

@ -0,0 +1,154 @@
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,7 +121,6 @@ celerybeat.pid
*.sage.py *.sage.py
# Environments # Environments
.env
.venv .venv
env/ env/
venv/ venv/

44
Checkliste.md Normal file
View File

@ -0,0 +1,44 @@
# ✅ 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,3 +1,78 @@
# open_workshop # Open Workshop (open_workshop ows)
Dieses Odoo v13.0 Modul erweitert das POS- und Kontakt-Modul um Funktionen für offene Werkstätten (FabLabs, Makerspaces etc.) und dient der Verwaltung von Maschinen, Naschinen Einweisungen Produkten, Maschinen Nutzungsprodukten und Zugangsberechtigungen zu den Maschinen.
## 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 +1,5 @@
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,28 +1,38 @@
{ {
'name': 'Open Workshop', 'name': 'POS Open Workshop',
'version': '1.0', 'license': 'AGPL-3',
'author': 'Dein Name / Deine Organisation', 'version': '13.0.1.0.0',
'category': 'Custom', 'summary': 'Erstellt Maschinenfreigaben basierend auf POS-Einweisungsprodukten',
'summary': 'Verwaltung von Maschinen & Einweisungen in einer offenen Werkstatt', 'depends': ['base','product','sale','contacts','point_of_sale'],
'description': """ 'author': 'matthias.lotz',
Dieses Modul fügt zwei neue Modelle hinzu: 'category': 'Point of Sale',
- 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/open_workshop_machine_views.xml', 'views/machine_product_training_views.xml',
'views/open_workshop_training_views.xml', 'views/menu_views.xml',
'views/res_partner_views.xml', 'views/machine_area_views.xml',
'views/machine_views.xml',
'views/res_partner_view.xml',
'views/assets.xml',
'data/data.xml',
],
'qweb': [
'static/src/xml/ows_briefing_details.xml',
'static/src/xml/ows_briefing_details_edit.xml',
'static/src/xml/ows_pos_order_selector.xml',
'static/src/xml/ows_machine_sidebar.xml',
'static/src/xml/ows_pos_machine_access_view.xml',
], ],
'installable': True, 'installable': True,
'application': True, 'assets': {
'point_of_sale.assets': [
'static/src/js/machine_access_sidebar.js',
'static/src/css/pos.css',
],
},
'post_init_hook': 'run_migration',
'description': """
Diese App erstellt Maschinenfreigaben basierend auf POS-Einweisungsprodukten.
Die App ist für den Einsatz in der Odoo-Version 13.0 konzipiert.
""",
} }

3
controllers/__init__.py Normal file
View File

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

11
controllers/pos_access.py Normal file
View File

@ -0,0 +1,11 @@
# 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)

153
data/data.xml Normal file
View File

@ -0,0 +1,153 @@
<odoo>
<!-- Bereiche -->
<record id="area_fablab" model="ows.machine.area">
<field name="name">Fablab</field>
<field name="color_hex">#008000</field>
</record>
<record id="area_holz" model="ows.machine.area">
<field name="name">Holzbereich</field>
<field name="color_hex">#ff0000</field>
</record>
<record id="area_metall" model="ows.machine.area">
<field name="name">Metallbereich</field>
<field name="color_hex">#0000ff</field>
</record>
<record id="area_elektronik" model="ows.machine.area">
<field name="name">Elektronikbereich</field>
<field name="color_hex">#ffff00</field>
</record>
<!-- Maschinen im Fablab -->
<record id="machine_sabako_laser" model="ows.machine">
<field name="name">Sabako Laser</field>
<field name="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>

View File

@ -1,9 +0,0 @@
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

51
demo/demo_partners.csv Normal file
View File

@ -0,0 +1,51 @@
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

2
demo/export.py Normal file
View File

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

38
demo/export_partner.py Normal file
View File

@ -0,0 +1,38 @@
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,
])

31
fix_missing_partners.py Normal file
View File

@ -0,0 +1,31 @@
# -*- 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

@ -0,0 +1,177 @@
<?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>

2
helper/export.sh Executable file
View File

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

View File

@ -0,0 +1,20 @@
# /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)

26
helper/export_products.py Normal file
View File

@ -0,0 +1,26 @@
# /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

@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-
# /opt/odoo/odoo/odoo-bin shell -d <alte datebase> < export_products_and_categories.py
import xml.etree.ElementTree as ET
def xml_safe_id(name):
return name.lower().replace(' ', '_').replace('/', '_').replace('(', '').replace(')', '').replace('-', '_')
def export_categories_and_products(env):
root = ET.Element("odoo")
# Export Kategorien
categories = env['product.category'].search([
('name', 'in', ['Einweisungen', 'Maschinennutzung'])
])
for cat in categories:
record = ET.SubElement(root, "record", {
"id": f"cat_{xml_safe_id(cat.name)}",
"model": "product.category"
})
ET.SubElement(record, "field", name="name").text = cat.name
if cat.parent_id:
ET.SubElement(record, "field", name="parent_id", attrib={"ref": f"cat_{xml_safe_id(cat.parent_id.name)}"})
# Export Produkte
products = env['product.product'].search([
('categ_id.name', 'in', ['Einweisungen', 'Maschinennutzung'])
])
for product in products:
record = ET.SubElement(root, "record", {
"id": f"prod_{xml_safe_id(product.name)}",
"model": "product.product"
})
ET.SubElement(record, "field", name="name").text = product.name or ''
ET.SubElement(record, "field", name="default_code").text = product.default_code or ''
ET.SubElement(record, "field", name="list_price").text = str(product.list_price or 0.0)
ET.SubElement(record, "field", name="available_in_pos").text = "True"
if product.categ_id:
ET.SubElement(record, "field", name="categ_id", attrib={"ref": f"cat_{xml_safe_id(product.categ_id.name)}"})
tree = ET.ElementTree(root)
tree.write("data_product_and_categories.xml", encoding="utf-8", xml_declaration=True)
print("✅ XML export saved to data_product_and_categories.xml")
# Automatischer Start in odoo-bin shell
if 'env' in globals():
export_categories_and_products(env)

1
helper/install.sh Executable file
View File

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

View File

@ -0,0 +1,50 @@
<odoo>
<record id="machine_prusa_training_prod_einweisung_3d_drucker_prusa" model="ows.machine.training">
<field name="machine_id" ref="machine_prusa"/>
<field name="training_id" ref="prod_einweisung_3d_drucker_prusa"/>
</record>
<record id="machine_formatkreissaege_usage_prod_formatkreissäge_1_minute" model="ows.machine.product">
<field name="machine_id" ref="machine_formatkreissaege"/>
<field name="product_id" ref="prod_formatkreissäge_1_minute"/>
</record>
<record id="machine_bandsaege_holz_usage_prod_bandsäge_1_minute" model="ows.machine.product">
<field name="machine_id" ref="machine_bandsaege_holz"/>
<field name="product_id" ref="prod_bandsäge_1_minute"/>
</record>
<record id="machine_bandsaege_holz_training_prod_einweisung_bandsäge" model="ows.machine.training">
<field name="machine_id" ref="machine_bandsaege_holz"/>
<field name="training_id" ref="prod_einweisung_bandsäge"/>
</record>
<record id="machine_kreissaege_metall_usage_prod_formatkreissäge_1_minute" model="ows.machine.product">
<field name="machine_id" ref="machine_kreissaege_metall"/>
<field name="product_id" ref="prod_formatkreissäge_1_minute"/>
</record>
<record id="machine_bandsaege_metall_usage_prod_bandsäge_1_minute" model="ows.machine.product">
<field name="machine_id" ref="machine_bandsaege_metall"/>
<field name="product_id" ref="prod_bandsäge_1_minute"/>
</record>
<record id="machine_bandsaege_metall_training_prod_einweisung_bandsäge" model="ows.machine.training">
<field name="machine_id" ref="machine_bandsaege_metall"/>
<field name="training_id" ref="prod_einweisung_bandsäge"/>
</record>
<record id="machine_drehbank_usage_prod_drehbank_1_minute" model="ows.machine.product">
<field name="machine_id" ref="machine_drehbank"/>
<field name="product_id" ref="prod_drehbank_1_minute"/>
</record>
<record id="machine_drehbank_training_prod_einweisung_drehbank" model="ows.machine.training">
<field name="machine_id" ref="machine_drehbank"/>
<field name="training_id" ref="prod_einweisung_drehbank"/>
</record>
<record id="machine_fraese_usage_prod_cnc_fräse_1_minute" model="ows.machine.product">
<field name="machine_id" ref="machine_fraese"/>
<field name="product_id" ref="prod_cnc_fräse_1_minute"/>
</record>
<record id="machine_fraese_usage_prod_fräse___deckel_1_minute" model="ows.machine.product">
<field name="machine_id" ref="machine_fraese"/>
<field name="product_id" ref="prod_fräse___deckel_1_minute"/>
</record>
<record id="machine_fraese_training_prod_einweisung_metallfräse" model="ows.machine.training">
<field name="machine_id" ref="machine_fraese"/>
<field name="training_id" ref="prod_einweisung_metallfräse"/>
</record>
</odoo>

1
helper/update.sh Executable file
View File

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

View File

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

View File

@ -1,12 +0,0 @@
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)

578
models/ows_models.py Normal file
View File

@ -0,0 +1,578 @@
# -*- 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')

49
models/pos_order.py Normal file
View File

@ -0,0 +1,49 @@
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

View File

@ -1,11 +0,0 @@
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'
)

View File

@ -1,29 +0,0 @@
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'
)

58
post_init_hook.py Normal file
View File

@ -0,0 +1,58 @@
# -*- 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

@ -0,0 +1,41 @@
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

@ -0,0 +1,79 @@
# 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)

1
scripts/install-odoo.sh Executable file
View File

@ -0,0 +1 @@
/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

61
scripts/odoo-restore.sh Normal file
View File

@ -0,0 +1,61 @@
#!/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"

59
scripts/uninstall_rpc.py Executable file
View File

@ -0,0 +1,59 @@
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,4 +1,8 @@
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_open_workshop_machine_user,access_open_workshop_machine_user,model_open_workshop_machine,base.group_user,1,1,1,1 access_ows_machine_access_user,ows.machine.access,model_ows_machine_access,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_user,ows.machine,model_ows_machine,base.group_user,1,1,1,1
access_ows_machine_product_user,ows.machine.product,model_ows_machine_product,base.group_user,1,1,1,1
access_ows_machine_training_user,access_ows_machine_training_user,model_ows_machine_training,base.group_user,1,1,1,1
access_ows_machine_area,ows.machine.area,model_ows_machine_area,base.group_user,1,1,1,1
access_ows_user,ows.user,model_ows_user,base.group_user,1,1,1,1
access_ows_machine_training,ows.machine.training,model_ows_machine_training,base.group_user,1,1,1,1

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

2625
static/src/css/pos.css Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,132 @@
/* 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

@ -0,0 +1,72 @@
<?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

@ -0,0 +1,64 @@
<?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

@ -0,0 +1,88 @@
<?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

@ -0,0 +1,70 @@
<?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>

1
tests/__init__.py Normal file
View File

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

138
tests/test_res_partner.py Normal file
View File

@ -0,0 +1,138 @@
## 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)

4
todo.md Normal file
View File

@ -0,0 +1,4 @@
[ ] 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)
[ ]

13
views/assets.xml Normal file
View File

@ -0,0 +1,13 @@
<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

@ -0,0 +1,38 @@
<!-- 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

@ -0,0 +1,49 @@
<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>

69
views/machine_views.xml Normal file
View File

@ -0,0 +1,69 @@
<!-- 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>

81
views/menu_views.xml Normal file
View File

@ -0,0 +1,81 @@
<!-- 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

@ -1,54 +0,0 @@
<?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

@ -1,48 +0,0 @@
<?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>

111
views/res_partner_view.xml Normal file
View File

@ -0,0 +1,111 @@
<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

@ -1,30 +0,0 @@
<?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>