Compare commits

...

8 Commits

49 changed files with 835 additions and 4165 deletions

View File

@ -1,6 +1,6 @@
# Open Workshop (open_workshop ows) # Open Workshop (open_workshop ows)
Dieses Odoo v13.0 Modul erweitert das POS- und Kontakt-Modul um Funktionen für offene Werkstätten (FabLabs, Makerspaces etc.) und dient der Verwaltung von Maschinen, Naschinen Einweisungen Produkten, Maschinen Nutzungsprodukten und Zugangsberechtigungen zu den Maschinen. Dieses Odoo v18.0 Modul erweitert das POS- und Kontakt-Modul um Funktionen für offene Werkstätten (FabLabs, Makerspaces etc.) und dient der Verwaltung von Maschinen, Naschinen Einweisungen Produkten, Maschinen Nutzungsprodukten und Zugangsberechtigungen zu den Maschinen.
## Funktionen ## Funktionen
@ -28,40 +28,10 @@ Dieses Odoo v13.0 Modul erweitert das POS- und Kontakt-Modul um Funktionen für
## Installation ## Installation
1. Dieses Modul in den Custom-Addons-Ordner kopieren 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. 2. Im Odoo Backend unter Apps installieren
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 ## 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 ## ToDos
- Bearbeitung der Maschinenfreigaben im Backend - Bearbeitung der Maschinenfreigaben im Backend
- Automatische Erstellung von `mail.message` bei manueller Freigabe - Automatische Erstellung von `mail.message` bei manueller Freigabe

View File

@ -1,5 +1,3 @@
from . import models from . import models
from . import controllers from . import controllers
from . import post_init_hook
# damit run_migration sichtbar ist:
run_migration = post_init_hook.run_migration

View File

@ -1,9 +1,9 @@
{ {
'name': 'POS Open Workshop', 'name': 'POS Open Workshop',
'license': 'AGPL-3', 'license': 'AGPL-3',
'version': '13.0.1.0.0', 'version': '18.0.1.0.0',
'summary': 'Erstellt Maschinenfreigaben basierend auf POS-Einweisungsprodukten', 'summary': 'Erstellt Maschinenfreigaben basierend auf POS-Einweisungsprodukten',
'depends': ['base','product','sale','contacts','point_of_sale'], 'depends': ['base', 'account','product','sale','contacts','point_of_sale'],
'author': 'matthias.lotz', 'author': 'matthias.lotz',
'category': 'Point of Sale', 'category': 'Point of Sale',
'data': [ 'data': [
@ -13,26 +13,27 @@
'views/machine_area_views.xml', 'views/machine_area_views.xml',
'views/machine_views.xml', 'views/machine_views.xml',
'views/res_partner_view.xml', 'views/res_partner_view.xml',
'views/assets.xml',
'data/data.xml', 'data/data.xml',
], ],
'qweb': [
'static/src/xml/ows_briefing_details.xml',
'static/src/xml/ows_briefing_details_edit.xml',
'static/src/xml/ows_pos_order_selector.xml',
'static/src/xml/ows_machine_sidebar.xml',
'static/src/xml/ows_pos_machine_access_view.xml',
],
'installable': True, 'installable': True,
'assets': { 'assets': {
'point_of_sale.assets': [ 'web.assets_backend': [
'static/src/js/machine_access_sidebar.js', 'open_workshop/static/src/css/category_color.css',
'static/src/css/pos.css', ],
'point_of_sale._assets_pos': [
'open_workshop/static/src/css/pos.css',
'open_workshop/static/src/js/ows_machine_access_list.js',
'open_workshop/static/src/js/ows_pos_customer_sidebar.js',
'open_workshop/static/src/js/ows_pos_sidebar.js',
'open_workshop/static/src/js/ows_product_screen_template_patch.js',
'open_workshop/static/src/xml/ows_machine_access_list.xml',
'open_workshop/static/src/xml/ows_pos_customer_sidebar.xml',
'open_workshop/static/src/xml/ows_pos_sidebar.xml',
'open_workshop/static/src/xml/ows_product_screen_template_patch.xml',
], ],
}, },
'post_init_hook': 'run_migration',
'description': """ 'description': """
Diese App erstellt Maschinenfreigaben basierend auf POS-Einweisungsprodukten. Diese App erstellt Maschinenfreigaben basierend auf POS-Einweisungsprodukten.
Die App ist für den Einsatz in der Odoo-Version 13.0 konzipiert. Die App ist für den Einsatz in der Odoo-Version 18.0 konzipiert.
""", """,
} }

View File

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

View File

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

View File

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

View File

@ -1,38 +0,0 @@
import csv
import random
# Beispielsweise 50 Kunden mit Namen und E-Mail
partners = env['res.partner'].search(
[('customer_rank', '>', 0), ('is_company', '=', False)],
limit=50
)
with open('/home/odoo/custom_addons/open_workshop/demo/demo_partners.csv', 'w', newline='') as f:
writer = csv.writer(f)
writer.writerow([
'id',
'name',
'street',
'zip',
'city',
'phone',
'email',
'company_type',
'customer_rank',
'supplier_rank'
])
for idx, partner in enumerate(partners, start=1):
partner_id = f'res_partner_demo_{idx}'
writer.writerow([
partner_id,
partner.name or '',
partner.street or '',
partner.zip or '',
partner.city or '',
partner.phone or '',
partner.email or '',
partner.company_type or 'person',
partner.customer_rank,
partner.supplier_rank,
])

View File

@ -1,31 +0,0 @@
# -*- coding: utf-8 -*-
from odoo import SUPERUSER_ID
from odoo.api import Environment
import logging
_logger = logging.getLogger(__name__)
MISSING_PARTNERS = [
6534, 1594, 4700, 6557, 5392, 4960, 5226, 6535, 4666
]
def insert_missing_partners(cr, registry):
env = Environment(cr, SUPERUSER_ID, {})
for partner_id in MISSING_PARTNERS:
cr.execute("""
INSERT INTO res_partner (
id, name, customer_rank, create_uid, create_date, write_uid, write_date
)
VALUES (%s, %s, 1, %s, now(), %s, now())
ON CONFLICT (id) DO NOTHING;
""", (partner_id, f"Fehlender Partner {partner_id}", SUPERUSER_ID, SUPERUSER_ID))
cr.execute("SELECT setval('res_partner_id_seq', (SELECT MAX(id) FROM res_partner));")
_logger.info(f"[OWS Repair] {len(MISSING_PARTNERS)} fehlende Partner hinzugefügt.")
cr.commit()
# Automatischer Start in odoo-bin shell
if 'env' in globals():
insert_missing_partners(env.cr, env.registry)

View File

@ -1,177 +0,0 @@
<?xml version='1.0' encoding='utf-8'?>
<odoo>
<record id="cat_einweisungen" model="product.category">
<field name="name">Einweisungen</field>
</record>
<record id="cat_maschinennutzung" model="product.category">
<field name="name">Maschinennutzung</field>
</record>
<record id="prod_3d_druck_30_minuten" model="product.product">
<field name="name">3D Druck (30 Minuten)</field>
<field name="default_code" />
<field name="list_price">0.25</field>
<field name="available_in_pos">True</field>
<field ref="cat_maschinennutzung" name="categ_id" />
</record>
<record id="prod_bandschleifer_1_minute" model="product.product">
<field name="name">Bandschleifer (1 Minute)</field>
<field name="default_code" />
<field name="list_price">0.1</field>
<field name="available_in_pos">True</field>
<field ref="cat_maschinennutzung" name="categ_id" />
</record>
<record id="prod_bandsäge_1_minute" model="product.product">
<field name="name">Bandsäge (1 Minute)</field>
<field name="default_code" />
<field name="list_price">0.1</field>
<field name="available_in_pos">True</field>
<field ref="cat_maschinennutzung" name="categ_id" />
</record>
<record id="prod_cnc_fräse_1_minute" model="product.product">
<field name="name">CNC Fräse (1 Minute)</field>
<field name="default_code" />
<field name="list_price">0.1</field>
<field name="available_in_pos">True</field>
<field ref="cat_maschinennutzung" name="categ_id" />
</record>
<record id="prod_cnc_sicherheitseinweisung" model="product.product">
<field name="name">CNC Sicherheitseinweisung</field>
<field name="default_code" />
<field name="list_price">25.0</field>
<field name="available_in_pos">True</field>
<field ref="cat_einweisungen" name="categ_id" />
</record>
<record id="prod_drehbank_1_minute" model="product.product">
<field name="name">Drehbank (1 Minute)</field>
<field name="default_code" />
<field name="list_price">0.1</field>
<field name="available_in_pos">True</field>
<field ref="cat_maschinennutzung" name="categ_id" />
</record>
<record id="prod_einweisung_3d_drucker_delta" model="product.product">
<field name="name">Einweisung 3D Drucker Delta</field>
<field name="default_code" />
<field name="list_price">15.0</field>
<field name="available_in_pos">True</field>
<field ref="cat_einweisungen" name="categ_id" />
</record>
<record id="prod_einweisung_3d_drucker_prusa" model="product.product">
<field name="name">Einweisung 3D Drucker Prusa</field>
<field name="default_code" />
<field name="list_price">20.0</field>
<field name="available_in_pos">True</field>
<field ref="cat_einweisungen" name="categ_id" />
</record>
<record id="prod_einweisung_bandsäge" model="product.product">
<field name="name">Einweisung Bandsäge</field>
<field name="default_code" />
<field name="list_price">15.0</field>
<field name="available_in_pos">True</field>
<field ref="cat_einweisungen" name="categ_id" />
</record>
<record id="prod_einweisung_drehbank" model="product.product">
<field name="name">Einweisung Drehbank</field>
<field name="default_code" />
<field name="list_price">20.0</field>
<field name="available_in_pos">True</field>
<field ref="cat_einweisungen" name="categ_id" />
</record>
<record id="prod_einweisung_fks" model="product.product">
<field name="name">Einweisung FKS</field>
<field name="default_code" />
<field name="list_price">20.0</field>
<field name="available_in_pos">True</field>
<field ref="cat_einweisungen" name="categ_id" />
</record>
<record id="prod_einweisung_hobel" model="product.product">
<field name="name">Einweisung Hobel</field>
<field name="default_code" />
<field name="list_price">15.0</field>
<field name="available_in_pos">True</field>
<field ref="cat_einweisungen" name="categ_id" />
</record>
<record id="prod_einweisung_laser" model="product.product">
<field name="name">Einweisung Laser</field>
<field name="default_code" />
<field name="list_price">15.0</field>
<field name="available_in_pos">True</field>
<field ref="cat_einweisungen" name="categ_id" />
</record>
<record id="prod_einweisung_metallfräse" model="product.product">
<field name="name">Einweisung Metallfräse</field>
<field name="default_code" />
<field name="list_price">20.0</field>
<field name="available_in_pos">True</field>
<field ref="cat_einweisungen" name="categ_id" />
</record>
<record id="prod_einweisung_schweißgerät" model="product.product">
<field name="name">Einweisung Schweißgerät</field>
<field name="default_code" />
<field name="list_price">10.0</field>
<field name="available_in_pos">True</field>
<field ref="cat_einweisungen" name="categ_id" />
</record>
<record id="prod_einweisung_in_maschinelle_holzverbindungen" model="product.product">
<field name="name">Einweisung in maschinelle Holzverbindungen</field>
<field name="default_code" />
<field name="list_price">15.0</field>
<field name="available_in_pos">True</field>
<field ref="cat_einweisungen" name="categ_id" />
</record>
<record id="prod_formatkreissäge_1_minute" model="product.product">
<field name="name">Formatkreissäge (1 Minute)</field>
<field name="default_code" />
<field name="list_price">0.1</field>
<field name="available_in_pos">True</field>
<field ref="cat_maschinennutzung" name="categ_id" />
</record>
<record id="prod_fräse___deckel_1_minute" model="product.product">
<field name="name">Fräse - Deckel (1 Minute)</field>
<field name="default_code" />
<field name="list_price">0.1</field>
<field name="available_in_pos">True</field>
<field ref="cat_maschinennutzung" name="categ_id" />
</record>
<record id="prod_hobel_1_minute" model="product.product">
<field name="name">Hobel (1 Minute)</field>
<field name="default_code" />
<field name="list_price">0.1</field>
<field name="available_in_pos">True</field>
<field ref="cat_maschinennutzung" name="categ_id" />
</record>
<record id="prod_laser_aktivminute" model="product.product">
<field name="name">Laser (Aktivminute)</field>
<field name="default_code" />
<field name="list_price">0.7000000000000001</field>
<field name="available_in_pos">True</field>
<field ref="cat_maschinennutzung" name="categ_id" />
</record>
<record id="prod_sandstrahlbox_1_minute" model="product.product">
<field name="name">Sandstrahlbox (1 Minute)</field>
<field name="default_code" />
<field name="list_price">0.2</field>
<field name="available_in_pos">True</field>
<field ref="cat_maschinennutzung" name="categ_id" />
</record>
<record id="prod_schweißgerät_1_minute" model="product.product">
<field name="name">Schweißgerät (1 Minute)</field>
<field name="default_code" />
<field name="list_price">0.2</field>
<field name="available_in_pos">True</field>
<field ref="cat_maschinennutzung" name="categ_id" />
</record>
<record id="prod_schweißkabine_eigenes_schweißgerät___1_minute" model="product.product">
<field name="name">Schweißkabine (eigenes Schweißgerät - 1 Minute)</field>
<field name="default_code" />
<field name="list_price">0.1</field>
<field name="available_in_pos">True</field>
<field ref="cat_maschinennutzung" name="categ_id" />
</record>
<record id="prod_sonstige_dienstleistungen_nutzung" model="product.product">
<field name="name">Sonstige Dienstleistungen/Nutzung</field>
<field name="default_code" />
<field name="list_price">1.0</field>
<field name="available_in_pos">True</field>
<field ref="cat_maschinennutzung" name="categ_id" />
</record>
</odoo>

View File

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

View File

@ -1,20 +0,0 @@
# /opt/odoo/odoo/odoo-bin shell -d <alte datebase> < export_categories.py
import csv
from odoo import api, SUPERUSER_ID
import os
categories = env['product.category'].search([('name', 'in', ['Einweisungen', 'Maschinennutzung'])])
file_path = os.path.join(os.getcwd(), 'product_category.csv')
with open(file_path, 'w', newline='') as csvfile:
writer = csv.writer(csvfile)
writer.writerow(['id', 'name', 'parent_id/id'])
for cat in categories:
xml_id = f"open_workshop.cat_{cat.name.lower().replace(' ', '_')}"
parent_id = cat.parent_id and f"base.{cat.parent_id.xml_id}" or ''
writer.writerow([xml_id, cat.name, parent_id])
# Aufruf in odoo shell z.B.:
# env = odoo.api.Environment(cr, SUPERUSER_ID, {})
# export_categories(env)

View File

@ -1,26 +0,0 @@
# /opt/odoo/odoo/odoo-bin shell -d <alte datebase> < export_products.py
import csv
from odoo import api, SUPERUSER_ID
import os
# Kategorien suchen
category_names = ['Einweisungen', 'Maschinennutzung']
categories = env['product.category'].search([('name', 'in', category_names)])
products = env['product.product'].search([('categ_id', 'in', categories.ids)])
file_path = os.path.join(os.getcwd(), 'product_product.csv')
with open(file_path, 'w', newline='') as csvfile:
writer = csv.writer(csvfile)
writer.writerow(['id', 'name', 'default_code', 'list_price', 'categ_id/id', 'available_in_pos'])
for prod in products:
cat_xml_id = f"open_workshop.cat_{prod.categ_id.name.lower().replace(' ', '_')}"
xml_id = f"open_workshop.prod_{prod.default_code or prod.name.lower().replace(' ', '_')}"
writer.writerow([
xml_id,
prod.name,
prod.default_code or '',
prod.list_price,
cat_xml_id,
'1' if prod.available_in_pos else '0'
])

View File

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

View File

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

View File

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

View File

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

2
log/.gitignore vendored Normal file
View File

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

View File

@ -8,6 +8,33 @@ import logging
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
_logger.info("✅ ows_models.py geladen") _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): class ResPartner(models.Model):
_inherit = 'res.partner' _inherit = 'res.partner'
_logger.info("✅ ows ResPartner geladen") _logger.info("✅ ows ResPartner geladen")
@ -141,32 +168,50 @@ class ResPartner(models.Model):
def _compute_machine_access_html(self): def _compute_machine_access_html(self):
areas = self.env['ows.machine.area'].search([], order="name") areas = self.env['ows.machine.area'].search([], order="name")
for partner in self: for partner in self:
html = "<div style='display: grid; grid-template-columns: repeat(3, 1fr); gap: 2rem;'>" html = ""
for area in areas: for area in areas:
html += f"<div class='o_group' style='margin-bottom: 1em;'>" html += f"""
html += f"<table class='o_group o_inner_group'>" <div class="o_form_sheet">
html += f"<thead><tr><th>{area.name}</th><th></th><th>Datum</th><th>Gültig bis</th></tr></thead><tbody>" <h3 class="o_form_label">{area.name}</h3>
<table class="table table-sm table-bordered o_form_table">
<thead>
<tr>
<th>Maschine</th>
<th>Status</th>
<th>Datum</th>
<th>Gültig bis</th>
</tr>
</thead>
<tbody>
"""
machines = self.env['ows.machine'].search([('area_id', '=', area.id)], order="name") machines = self.env['ows.machine'].search([('area_id', '=', area.id)], order="name")
for machine in machines: for machine in machines:
access = self.env['ows.machine.access'].search([ access = self.env['ows.machine.access'].search([
('partner_id', '=', partner.id), ('partner_id', '=', partner.id),
('machine_id', '=', machine.id), ('machine_id', '=', machine.id),
], limit=1) ], limit=1)
icon = "<span class='fa fa-check text-success'></span>" if access else "<span class='fa fa-times text-danger'></span>" icon = '<span class="text-success fa fa-check"/>' if access else '<span class="text-danger fa fa-times"/>'
date_granted = access.date_granted.strftime('%Y-%m-%d') if access and access.date_granted else "" date_granted = access.date_granted.strftime('%Y-%m-%d') if access and access.date_granted else "-"
date_expiry = access.date_expiry.strftime('%Y-%m-%d') if access and access.date_expiry else "" date_expiry = access.date_expiry.strftime('%Y-%m-%d') if access and access.date_expiry else "-"
html += f""" html += f"""
<tr> <tr>
<td class='o_td_label'><label>{machine.name}</label></td> <td>{machine.name}</td>
<td class='o_td_field'>{icon}</td> <td>{icon}</td>
<td class='o_td_field'>{date_granted}</td> <td>{date_granted}</td>
<td class='o_td_field'>{date_expiry}</td> <td>{date_expiry}</td>
</tr> </tr>
""" """
html += "</tbody></table></div>" html += "</tbody></table></div>"
html += "</div>"
partner.machine_access_html = html partner.machine_access_html = html
@api.model @api.model
def migrate_existing_partners(self): def migrate_existing_partners(self):
""" """
@ -284,6 +329,31 @@ class ResPartner(models.Model):
_logger.info(f"[OWS Migration] ✅ Maschinenfreigaben erstellt: {count_created}") _logger.info(f"[OWS Migration] ✅ Maschinenfreigaben erstellt: {count_created}")
self.env.cr.commit() 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): class OwsUser(models.Model):
_name = 'ows.user' _name = 'ows.user'
_description = 'OWS: Benutzerdaten' _description = 'OWS: Benutzerdaten'
@ -307,15 +377,57 @@ class OwsUser(models.Model):
] ]
AVAILABLE_COLORS = [
('#000000', 'schwarz'),
('#ff0000', 'Rot'),
('#E91E63', 'Pink'),
('#9C27B0', 'Lila'),
('#3F51B5', 'Indigo'),
('#0000ff', 'Blau'),
('#008000', 'Grün'),
('#ffff00', 'Gelb'),
('#FF9800', 'Orange'),
('#795548', 'Braun'),
('#ffffff', 'Weiss'),
]
class OwsMachineArea(models.Model): class OwsMachineArea(models.Model):
_name = 'ows.machine.area' _name = 'ows.machine.area'
_table = "ows_machine_area" _table = 'ows_machine_area'
_description = 'OWS: Maschinenbereich' _description = 'OWS: Maschinenbereich'
_order = 'name' _order = 'name'
name = fields.Char(required=True, translate=True) name = fields.Char(string="Name", required=True, translate=True)
#color = fields.Integer(string="Farbe")
color_hex = fields.Char(string="Farbe (Hex)", help="Hex-Farbcode wie #FF0000 für Rot") 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): class OwsMachine(models.Model):
@ -325,13 +437,37 @@ class OwsMachine(models.Model):
name = fields.Char(required=True, translate=True) name = fields.Char(required=True, translate=True)
code = fields.Char(required=True, help="Eindeutiger Kurzcode, z.B. 'lasercutter'") 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() description = fields.Text()
active = fields.Boolean(default=True) active = fields.Boolean(default=True)
area_id = fields.Many2one('ows.machine.area', string='Bereich') area_id = fields.Many2one('ows.machine.area', string='Bereich', help="Bereich, in dem die Maschine oder das Gerät steht.")
product_ids = fields.One2many('ows.machine.product', 'machine_id', string="Nutzungsprodukte") product_ids = fields.One2many('ows.machine.product', 'machine_id', string="Nutzungsprodukte")
product_names = fields.Char(string="Nutzungsprodukte Liste", compute="_compute_product_using_names", store=False,) product_names = fields.Char(string="Liste der Nutzungsprodukte", compute="_compute_product_using_names", store=False,)
training_ids = fields.One2many('ows.machine.training', 'machine_id', string="Einweisungsprodukte") training_ids = fields.One2many('ows.machine.training', 'machine_id', string="Einweisungsprodukte")
training_names = fields.Char(string="Einweisungsprodukte Liste", compute="_compute_product_training_names", store=False,) training_names = fields.Char(string="Liste der Einweisungsprodukte", compute="_compute_product_training_names", store=False,)
storage_location = fields.Char(string="Lagerort", help="Lagerort der Maschine oder des Geräts.")
purchase_price = fields.Float(string="Kaufpreis", help="Kaufpreis der Maschine oder des Geräts.")
purchase_date = fields.Date(string="Kaufdatum", help="Kaufdatum der Maschine oder des Geräts.")
@api.depends('product_ids.product_id.name') @api.depends('product_ids.product_id.name')
def _compute_product_using_names(self): def _compute_product_using_names(self):
@ -354,12 +490,34 @@ class OwsMachine(models.Model):
@api.model @api.model
def get_access_list_grouped(self, partner_id): 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") areas = self.env['ows.machine.area'].search([], order="name")
_logger.info("🔍 Maschinenbereiche: %s", areas.mapped('name')) _logger.info("Access RPC called with partner_id=%s", partner_id)
_logger.info("🔍 Partner_id: %s", partner_id) access_by_area = []
res = []
for area in areas: for area in areas:
machines = self.search([('area_id', '=', area.id)], order="name") machines = self.search([('area_id', '=', area.id), ('category', '=', 'red')], order="name")
machine_list = [] machine_list = []
for machine in machines: for machine in machines:
has_access = bool(self.env['ows.machine.access'].search([ has_access = bool(self.env['ows.machine.access'].search([
@ -370,12 +528,22 @@ class OwsMachine(models.Model):
'name': machine.name, 'name': machine.name,
'has_access': has_access, 'has_access': has_access,
}) })
res.append({ if machine_list:
access_by_area.append({
'area': area.name, 'area': area.name,
'color_hex': area.color_hex or '#000000', 'color_hex': area.color_hex or '#000000',
'machines': machine_list 'machines': machine_list
}) })
return res
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): class OwsMachineAccess(models.Model):
@ -398,7 +566,7 @@ class OwsMachineAccess(models.Model):
class OwsMachineProduct(models.Model): class OwsMachineProduct(models.Model):
_name = 'ows.machine.product' _name = 'ows.machine.product'
_table = 'ows_machine_product' _table = 'ows_machine_product'
_description = 'OWS: Zurordnung Produkt der Nutzung zur die Maschine' _description = 'OWS: Zuordnung Produkt der Nutzung zu der Maschine'
product_id = fields.Many2one('product.product', required=True, domain=[('available_in_pos', '=', True)], ondelete='cascade') product_id = fields.Many2one('product.product', required=True, domain=[('available_in_pos', '=', True)], ondelete='cascade')
machine_id = fields.Many2one('ows.machine', required=True, ondelete='cascade') machine_id = fields.Many2one('ows.machine', required=True, ondelete='cascade')
@ -406,7 +574,7 @@ class OwsMachineProduct(models.Model):
class OwsMachineTraining(models.Model): class OwsMachineTraining(models.Model):
_name = 'ows.machine.training' _name = 'ows.machine.training'
_table = 'ows_machine_training' _table = 'ows_machine_training'
_description = 'OWS: Zurordnung Produkt der Einweisung zur die Maschine' _description = 'OWS: Zuordnung Produkt der Einweisung zu der Maschine'
training_id = fields.Many2one('product.product', required=True, domain=[('available_in_pos', '=', True)], ondelete='cascade') training_id = fields.Many2one('product.product', required=True, domain=[('available_in_pos', '=', True)], ondelete='cascade')
machine_id = fields.Many2one('ows.machine', required=True, ondelete='cascade') machine_id = fields.Many2one('ows.machine', required=True, ondelete='cascade')

View File

@ -1,4 +1,5 @@
from odoo import models, fields, api from odoo import models, fields, api
from collections import defaultdict
#import debugpy #import debugpy
import logging import logging
@ -19,10 +20,9 @@ class PosOrder(models.Model):
pos_order = self.browse(pos_order_id) pos_order = self.browse(pos_order_id)
training_products = self.env['ows.machine.training'].search([]) training_products = self.env['ows.machine.training'].search([])
product_map = { product_map = defaultdict(list)
tp.training_id.product_tmpl_id.id: tp.machine_id.id for tp in training_products:
for tp in training_products product_map[tp.training_id.product_tmpl_id.id].append(tp.machine_id.id)
}
partner = pos_order.partner_id partner = pos_order.partner_id
if not partner: if not partner:
@ -31,15 +31,13 @@ class PosOrder(models.Model):
for line in pos_order.lines: for line in pos_order.lines:
product_tmpl_id = line.product_id.product_tmpl_id.id product_tmpl_id = line.product_id.product_tmpl_id.id
machine_id = product_map.get(product_tmpl_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)
_logger.info("🔍 Prüfe Produkt %s → Maschine ID: %s", line.product_id.display_name, machine_id) for machine_id in machine_ids:
if machine_id:
already_exists = self.env['ows.machine.access'].search([ already_exists = self.env['ows.machine.access'].search([
('partner_id', '=', partner.id), ('partner_id', '=', partner.id),
('machine_id', '=', machine_id) ('machine_id', '=', machine_id)
]) ], limit=1)
if not already_exists: if not already_exists:
self.env['ows.machine.access'].create({ self.env['ows.machine.access'].create({
'partner_id': partner.id, 'partner_id': partner.id,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,9 @@
.category-color-circle {
display: inline-block;
width: 14px;
height: 14px;
border-radius: 50%;
margin-left: 8px;
vertical-align: middle;
border: 1px solid #444;
}

File diff suppressed because it is too large Load Diff

View File

@ -1,132 +0,0 @@
/* This file is based on the original code from the OrderWidget in Odoo Point of Sale module (screen.js).
* It has been modified to create a sidebar that displays machine access information for the selected customer.*/
odoo.define('open_workshop.machine_access_sidebar', function (require) {
"use strict";
const DUMMY_PARTNER = {
id: 1,
name: "AAAA Max Mustermann",
security_briefing: false,
security_id: null,
create_date: null,
};
var rpc = require('web.rpc');
var screens = require('point_of_sale.screens');
var chrome = require('point_of_sale.chrome');
var core = require('web.core');
var QWeb = core.qweb;
var MachineAccessSidebar = screens.ScreenWidget.extend({
template: 'MachineAccessSidebar',
init: function(parent, options) {
this._super(parent, options);
this.partner = null;
this.pos.bind('change:selectedOrder', this.bind_order_events, this);
},
show: function() {
this._super();
this.render_access();
},
update_machine_access: function(partner) {
console.log("🔁 Sidebar aktualisiert Maschinenfreigaben für Partner:", partner);
this.partner = partner;
this.render_access();
},
render_access: function() {
var self = this;
var partner = this.partner || DUMMY_PARTNER;
rpc.query({
model: 'ows.machine',
method: 'get_access_list_grouped',
args: [partner.id],
}).then(function (result) {
partner.create_date = partner.create_date && partner.create_date.substring(0, 10);
var html = QWeb.render('PartnerMachineAccessList', {
areas: result || [],
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 @@
// @odoo-module ows_machine_access_list.js
import { Component, useState } from "@odoo/owl";
import { useBus } from "@web/core/utils/hooks";
import { usePos } from "@point_of_sale/app/store/pos_hook";
import { rpc } from "@web/core/network/rpc";
import { registry } from "@web/core/registry";
export class OwsMachineAccessList extends Component {
static template = 'open_workshop.OwsMachineAccessList';
setup() {
this.pos = usePos();
this.state = useState({
client: null,
grouped_accesses: [],
security_briefing: false,
security_id: '',
rfid_card: '',
birthday: '',
});
// 🔁 Reagiere auf Partnerwechsel über den Odoo-Bus
useBus(this.env.bus, 'partner-changed', () => {
this.updateAccessList();
});
// 🔃 Beim Mounten initiale Daten laden
this.updateAccessList();
}
async updateAccessList() {
const order = this.pos.get_order();
const partner = order?.get_partner?.();
this.state.client = partner || null;
if (!partner) {
this.state.grouped_accesses = [];
this.state.security_briefing = false;
this.state.security_id = '';
this.state.rfid_card = '';
this.state.birthday = '';
return;
}
try {
const data = await rpc("/open_workshop/partner_access", {
params: { partner_id: partner.id },
});
this.state.grouped_accesses = data.access_by_area || [];
this.state.security_briefing = data.security_briefing;
this.state.security_id = data.security_id;
this.state.rfid_card = data.rfid_card;
this.state.birthday = data.birthday;
} catch (error) {
console.error("Fehler beim Laden der Einweisungen:", error);
this.state.grouped_accesses = [];
this.state.security_briefing = false;
this.state.security_id = '';
this.state.rfid_card = '';
this.state.birthday = '';
}
}
}
registry.category("templates").add("open_workshop.OwsMachineAccessList", OwsMachineAccessList);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,88 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="MachineAccessSidebar" owl="1">
<div class="machine-access-sidebar">
<div class="access-content">
</div>
</div>
</t>
<t t-name="PartnerMachineAccessList">
<div class="client-details-grid">
<div class="client-details-header">
<ul>
<li><span class="client-details-label">Einweisungen</span></li>
</ul>
<div class="client-details-area" t-att-style="'border: solid 3px #ffffff; margin: 5px;'">
<ul>
<li class="client-detail">
<span class="detail client-details-vvow_briefing"></span>
<span class="briefinglabel">Werkstatt</span>
</li>
<li class="client-detail">
<t t-if="!partner.security_briefing">
<span class="detail client-details-vvow_briefing_error"></span>
</t>
<t t-if="partner.security_briefing">
<span class="detail client-details-vvow_briefing"></span>
</t>
<span class="briefinglabel">Haftungsausschluss</span>
</li>
<t t-if="!partner.security_briefing">
<li class="client-detail">
<ul class="subpoints"><span class="detail client-details-vvow_sec_briefing_error">Bitte Prüfen‼</span></ul>
</li>
</t>
<t t-if="partner.security_briefing">
<ul class="subpoints">
<li class="client-detail">
<span class="label">Id:</span>
<t t-if="partner.security_id">
<span class="detail client-details-vvow_security_id"><t t-esc="partner.security_id"/></span>
</t>
<t t-if="!partner.security_id">
<span class="detail client-details-vvow_security_id">N/A</span>
</t>
</li>
<li class="client-detail">
<span class="label">Erstellt:</span>
<t t-if="partner.create_date">
<span class="detail client-details-vvow_security_id"><t t-esc="partner.create_date"/></span>
</t>
<t t-if="!partner.create_date">
<span class="detail client-vvow_birthday">N/A</span>
</t>
</li>
</ul>
</t>
</ul>
</div>
</div>
<t t-foreach="areas" t-as="area">
<div class="client-details-area" t-att-style="'border: solid 3px ' + area.color_hex + '; margin: 5px;'">
<ul>
<t t-foreach="area.machines" t-as="machine">
<li class="client-detail">
<t t-if="!machine.has_access">
<span class="detail client-details-vvow_briefing_error"></span>
</t>
<t t-if="machine.has_access">
<span class="detail client-details-vvow_briefing"></span>
</t>
<span class="briefinglabel"><t t-esc="machine.name"/></span>
</li>
</t>
</ul>
</div>
</t>
</div>
</t>
</templates>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

104
todo.md
View File

@ -1,3 +1,101 @@
[ ] Help System # 🎯 Ziel des Moduls „Open Workshop“
[ ] Möglichkeit, Einweisungen manuell zu setzen? Das Modul Open Workshop ist für den Einsatz in einer offenen Werkstatt / einem FabLab gedacht, in dem Kund:innen selbstständig Maschinen nutzen können, aber nur, wenn bestimmte Voraussetzungen erfüllt sind (z.B. Einweisung). Das Ziel ist es, Sicherheit, Zugriffssteuerung, Selbstverwaltung und Abrechnung in einer Odoo-gestützten Umgebung zu ermöglichen insbesondere im POS (Point-of-Sale)-Modul.
[ ]
#🛠️ Funktionaler Rahmen & Umgebung
## Betriebsumgebung
Odoo in mehreren Versionen (aktuell Fokus auf Odoo 18.0)
Docker-basierte Installation mit Entwicklungs-, Test- und Produktivinstanzen
PostgreSQL-Datenbank
Gitea zur Versionierung und CI (z.B. act_runner)
POS läuft in einem speziellen Frontend mit festen Ansichten und Touch-Bedienung
Nutzung durch Personal & Werkstattnutzer:innen auf verschiedenen Geräten
Kiosksysteme für Anmeldung, Einweisung, ggf. auch Selbstbedienung
Interne Systeme z.T. über *.lan.hobbyhimmel.de zugänglich
# 🔐 Kernfunktionen des Moduls „Open Workshop“
## Maschinenverwaltung
Maschinenmodell mit Zuordnung zu Bereichen (machine areas)
Darstellung aller Maschinen gruppiert nach Bereich im POS
Maschinenbereiche enthalten u.a. Farbe (hex), Beschreibung etc.
Upload von Dokumenten und Bildern möglich
## Zugriffsverwaltung / Einweisungen
Modell ows.machine.access steuert, welche:r Partner:in auf welche Maschine zugreifen darf
Einweisungen sind Produkte in Odoo, deren Kauf automatisch eine machine.access-Eintragung erzeugt
Kunden können mehrfach eingewiesen werden (z.B. bei mehreren Maschinen in einem Kurs)
Darstellung im POS über grünes Häkchen / rotes Kreuz
## Sicherheitsinformationen
Modell ows.user verknüpft 1:1 mit res.partner, enthält:
Geburtstag
RFID-Card-ID
Sicherheitsunterweisung (bool)
Sicherheits-ID
Synchronisation zwischen res.partner-Fassadenfeldern und ows.user
## POS-Integration
Erweiterung der POS-Ansicht:
Rechte Spalte zeigt dauerhaft Maschinenfreigaben an
Dynamische Darstellung pro Partner
Gruppiert nach Maschinenbereichen
Farben und Styles aus der DB
Buttons zur Partnerauswahl und Order-Wechsel
Maschinennutzungsprodukte nur sichtbar, wenn Kunde eingewiesen ist
Einweisungsvideos können verlinkt sein
## Datenpflege und Tests
Produkte aus Kategorien „Einweisungen“ und „Maschinennutzung“ werden im Modul als fest installierte Daten gepflegt
Strukturierte Tests für res.partner und ows.user (Erstellen, Lesen, Schreiben, Sync)
# 🧩 Mögliche Erweiterungsbereiche (aus deinen bisherigen Ideen)
Dokumentation & Tutorials per DokuWiki-Integration
Selbstregistrierung von Nutzer:innen (Kioskmodus)
Zeiterfassung an Maschinen per RFID/IoT (z.B. IoT-Box oder AMC3301)
Verwaltung von Maschinen-Verbrauchsmaterialien
Kalenderbuchung & Eventintegration (für Einweisungstermine)
Automatisiertes Backup & Restore von POS-Umgebungen via CI
Nutzung von OWL-Komponenten (ab v17) zur modernen POS-Darstellung
# 📦 Modulstruktur und Philosophie
Das Modul ist modular und gut in Odoo integriert
Wo möglich, nutzt du bestehende Odoo-Modelle (res.partner, product.template, pos.order)
Zusätzliche Modelle (ows.machine, ows.machine.access, ows.user, ows.machine.area) kapseln Werkstatt-spezifische Logik
Fokus auf Datensicherheit, Nachvollziehbarkeit und einfache Erweiterbarkeit
Frontend-Anpassungen (POS) sind tief integriert, ohne das Standardverhalten zu sehr zu stören

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,93 +1,81 @@
<!-- res_partner_view.xml -->
<odoo> <odoo>
<!-- View-Erweiterung für res.partner: Tab mit HTML-Tabelle <data>
Der Inhalt wird in der Methode _compute_machine_access_html() generiert.
Diese Methode wird in der Klasse res.partner definiert in der Datei models/ows_models.py. <!-- Entfernt die Partner-Warnung (Duplicate Bank Accounts) in res.partner -->
Die Methode wird aufgerufen, wenn das Partnerformular geöffnet wird. <record id="patch_res_partner_duplicate_warning" model="ir.ui.view">
Die HTML-Tabelle wird in der Variable machine_access_html gespeichert. <field name="name">res.partner.remove.duplicate.bank.warning</field>
Die Variable wird in der View angezeigt.
-->
<record id="view_partner_form_inherit_open_workshop_html" model="ir.ui.view">
<field name="name">res.partner.form.ows.machine.access.html</field>
<field name="model">res.partner</field> <field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form"/> <field name="inherit_id" ref="account.view_partner_property_form"/>
<field name="priority" eval="99"/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<notebook position="inside"> <xpath expr="//div[@class='alert alert-warning']" position="replace"/>
<page string="HOBBYHIMMEL Einweisungen">
<field name="machine_access_html" readonly="1" widget="html"/>
</page>
</notebook>
</field> </field>
</record> </record>
<!-- Teil 1: Maschinenfreigaben-Tabelle --> <!-- Entfernt die Bankkonto-Warnung in res.partner.bank -->
<record id="view_partner_form_inherit_open_workshop" model="ir.ui.view"> <record id="patch_res_partner_bank_duplicate_warning" model="ir.ui.view">
<field name="name">res.partner.form.ows.machine.access</field> <field name="name">res.partner.bank.remove.duplicate.warning</field>
<field name="model">res.partner.bank</field>
<field name="inherit_id" ref="account.view_partner_bank_form_inherit_account"/>
<field name="priority" eval="99"/>
<field name="arch" type="xml">
<xpath expr="//div[@class='alert alert-warning']" position="replace"/>
</field>
</record>
</data>
<!-- 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="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form"/> <field name="inherit_id" ref="base.view_partner_form"/>
<field name="priority" eval="20"/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<notebook position="inside"> <xpath expr="//notebook" position="inside">
<page string="Einweisungen (Liste)"> <page name="ows_machine_access" string="Offene Werkstatt (Hobbyhimmel)">
<field name="machine_access_ids"> <!-- EINWEISUNG: Zwei Felder nebeneinander -->
<tree> <group name="container_row_2" string="Sicherheitseinweisung" col="2">
<field name="security_briefing"/>
<field name="security_id"/>
</group>
<!-- MASCHINENFREIGABEN: Volle Breite -->
<group string="Maschinenfreigaben" col="2">
<field name="machine_access_ids" colspan="2" context="{'default_partner_id': id}" nolabel="1">
<list>
<field name="machine_id"/> <field name="machine_id"/>
<field name="date_granted"/> <field name="date_granted"/>
<field name="date_expiry"/> <field name="date_expiry"/>
<field name="granted_by_pos"/> <field name="granted_by_pos"/>
</tree> </list>
</field> </field>
</page> </group>
</notebook>
</field>
</record>
<!-- Teil 2: HOBBYHIMMEL Basis (ows_user_id Felder) --> <!-- ÜBERSICHT: Volle Breite -->
<record id="view_partner_form_inherit_ows_user" model="ir.ui.view"> <group string="Maschinenfreigaben Übersicht" >
<field name="name">res.partner.form.ows.user</field> <field name="machine_access_html" colspan="2" readonly="1" widget="html" nolabel="1"/>
</group>
</page>
</xpath>
</field>
</record>
<!-- Geburtstag direkt nach der USt-ID -->
<record id="view_partner_form_inherit_ows_birthday" model="ir.ui.view">
<field name="name">res.partner.form.ows.birthday</field>
<field name="model">res.partner</field> <field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form"/> <field name="inherit_id" ref="base.view_partner_form"/>
<field name="priority" eval="15"/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<!-- Geburtstag direkt unter USt-ID -->
<xpath expr="//field[@name='vat']" position="after"> <xpath expr="//field[@name='vat']" position="after">
<field name="birthday"/> <field name="birthday"/>
</xpath> </xpath>
<!-- Eigene Seite "Basis" nach der Verkaufsseite -->
<xpath expr="//page[@name='sales_purchases']" position="after">
<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>
</xpath>
</field> </field>
</record> </record>
<record id="contacts.action_contacts" model="ir.actions.act_window">
<field name="view_mode">tree,kanban,form,activity</field> <!-- List View Anpassung -->
</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"/>
<!--tree default_order="create_date desc"/-->
</record>
<!-- Action to set default view to list view for Contacts
<record id="contacts.action_contacts" model="ir.actions.act_window">
<field name="name">Contacts</field>
<field name="res_model">res.partner</field>
<field name="view_mode">tree,kanban,form</field>
<field name="view_id" ref="base.view_partner_tree"/>
</record>
-->
<record id="ows_userList_inherit" model="ir.ui.view"> <record id="ows_userList_inherit" model="ir.ui.view">
<field name="name">res.partner.ows.tree</field> <field name="name">res.partner.ows.tree</field>
<field name="model">res.partner</field> <field name="model">res.partner</field>
@ -99,7 +87,6 @@
<field name="security_id" optional="show"/> <field name="security_id" optional="show"/>
<field name="rfid_card" optional="show"/> <field name="rfid_card" optional="show"/>
<field name="category_id" widget="many2many_tags"/> <field name="category_id" widget="many2many_tags"/>
</xpath> </xpath>
<xpath expr="//field[@name='vat']" position="replace"> <xpath expr="//field[@name='vat']" position="replace">
<field name="vat" invisible="1"/> <field name="vat" invisible="1"/>
@ -118,6 +105,8 @@
</xpath> </xpath>
</field> </field>
</record> </record>
<!-- Standardwerte setzen (company_type = person) -->
<record id="view_partner_form_inherit" model="ir.ui.view"> <record id="view_partner_form_inherit" model="ir.ui.view">
<field name="name">res.partner.form.inherit.default_person</field> <field name="name">res.partner.form.inherit.default_person</field>
<field name="model">res.partner</field> <field name="model">res.partner</field>
@ -127,5 +116,16 @@
<attribute name="default">person</attribute> <attribute name="default">person</attribute>
</field> </field>
</field> </field>
</record> </record>
<!-- Optional: Kontakte-Action, falls gebraucht -->
<record id="contacts.action_contacts" model="ir.actions.act_window">
<field name="view_mode">list,kanban,form,activity</field>
</record>
<record id="contacts.action_contacts_view_kanban" model="ir.actions.act_window.view">
<field name="sequence" eval="1"/>
</record>
<record id="contacts.action_contacts_view_tree" model="ir.actions.act_window.view">
<field name="sequence" eval="0"/>
</record>
</odoo> </odoo>