Compare commits

..

13 Commits

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

+ Nutzung von defaultdict zur besseren Produkt-Maschine-Zuordnung
+ Klares Logging zur Nachvollziehbarkeit
+ Verhindert doppelte Freigaben
2025-05-06 17:19:57 +00:00
b40e2f7837 Reihenfolge in Kontakte geändert (Tabs)
All checks were successful
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Successful in 4m45s
2025-05-04 19:27:31 +00:00
ea6d809020 modified Contact View to more Odoo Style 2025-05-04 18:47:47 +00:00
63337a28bd Issue Template hinzugefügt
All checks were successful
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Successful in 4m39s
2025-05-02 09:31:24 +00:00
a19be91685 alle nicht odoo nutzer sind als Kontakt archiviert
All checks were successful
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Successful in 4m40s
2025-05-02 08:13:35 +00:00
da4cd0ba5c für das Testsystem wurden alle Angestellten archiviert und Admin in Testsystem umbenannt
All checks were successful
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Successful in 4m45s
2025-05-02 07:45:28 +00:00
76b1dc29f2 todo.md aktualisiert
All checks were successful
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Successful in 4m39s
2025-05-01 16:15:47 +02:00
0b45c9df62 Merge remote-tracking branch 'origin/13.0_dev' into 13.0_dev
All checks were successful
odoo-restore-open_workshop-install / run-odoo-backup-in-docker (push) Successful in 4m36s
2025-05-01 05:49:12 +00:00
92696827d3 removed display: grid in clientlist-screen 2025-05-01 05:37:04 +00:00
35 changed files with 4157 additions and 189 deletions

View File

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

View File

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

View File

@ -1,19 +1,38 @@
{ {
'name': 'POS Open Workshop', 'name': 'POS Open Workshop',
'license': 'AGPL-3', 'license': 'AGPL-3',
'version': '16.0.1.0.0', 'version': '13.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','product','sale','contacts','point_of_sale'],
'author': 'matthias.lotz', 'author': 'matthias.lotz',
'category': 'Point of Sale', 'category': 'Point of Sale',
'data': [ 'data': [
'data/data.xml',
'security/ir.model.access.csv', 'security/ir.model.access.csv',
], 'views/machine_product_training_views.xml',
'views/menu_views.xml',
'views/machine_area_views.xml',
'views/machine_views.xml',
'views/res_partner_view.xml',
'views/assets.xml',
'data/data.xml',
],
'qweb': [
'static/src/xml/ows_briefing_details.xml',
'static/src/xml/ows_briefing_details_edit.xml',
'static/src/xml/ows_pos_order_selector.xml',
'static/src/xml/ows_machine_sidebar.xml',
'static/src/xml/ows_pos_machine_access_view.xml',
],
'installable': True, 'installable': True,
'assets': {
'point_of_sale.assets': [
'static/src/js/machine_access_sidebar.js',
'static/src/css/pos.css',
],
},
'post_init_hook': 'run_migration',
'description': """ '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 16.0 konzipiert. Die App ist für den Einsatz in der Odoo-Version 13.0 konzipiert.
""", """,
} }

3
controllers/__init__.py Normal file
View File

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

11
controllers/pos_access.py Normal file
View File

@ -0,0 +1,11 @@
# Datei: controllers/pos_access.py
from odoo import http
from odoo.http import request
class OpenWorkshopPOSController(http.Controller):
@http.route('/open_workshop/partner_access', type='json', auth='user')
def get_partner_machine_access(self, partner_id):
Machine = request.env['ows.machine'].sudo()
return Machine.get_access_list_grouped(partner_id)

51
demo/demo_partners.csv Normal file
View File

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

2
demo/export.py Normal file
View File

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

38
demo/export_partner.py Normal file
View File

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

31
fix_missing_partners.py Normal file
View File

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

View File

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

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,52 @@ 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 = "<div class='tab-content'><div class='tab-pane active' id='machine_access_tab'>"
html += "<div class='o_group'>"
for area in areas: for area in areas:
html += f"<div class='o_group' style='margin-bottom: 1em;'>" html += "<table class='o_group o_inner_group o_group_col_6'><tbody>"
html += f"<table class='o_group o_inner_group'>"
html += f"<thead><tr><th>{area.name}</th><th></th><th>Datum</th><th>Gültig bis</th></tr></thead><tbody>" # Bereichsüberschrift
html += f"""
<tr>
<td colspan="3" style="width: 100%;">
<div class="o_horizontal_separator">{area.name}</div>
</td>
</tr>
<tr>
<td class="o_td_label"><label class="o_form_label"></label></td>
<td class="o_td_label"><label class="o_form_label">Datum</label></td>
<td class="o_td_label"><label class="o_form_label">Gültig bis</label></td>
</tr>
"""
machines = self.env['ows.machine'].search([('area_id', '=', area.id)], order="name") 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='fa fa-check text-success'></span>" if access else "<span class='fa fa-times text-danger'></span>"
date_granted = access.date_granted.strftime('%Y-%m-%d') if access and access.date_granted else "" date_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 class="o_td_label"><label class="o_form_label">{icon} {machine.name}</label></td>
<td class='o_td_field'>{icon}</td> <td class="o_td_field">{date_granted}</td>
<td class='o_td_field'>{date_granted}</td> <td class="o_td_field">{date_expiry}</td>
<td class='o_td_field'>{date_expiry}</td>
</tr> </tr>
""" """
html += "</tbody></table></div>"
html += "</div>" html += "</tbody></table>"
html += "</div></div></div>"
partner.machine_access_html = html partner.machine_access_html = html
@api.model @api.model
def migrate_existing_partners(self): def migrate_existing_partners(self):
""" """
@ -283,6 +330,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'
@ -306,17 +378,56 @@ class OwsUser(models.Model):
('partner_unique', 'unique(partner_id)', 'Jeder Partner darf nur einen OWS-Datensatz haben.') ('partner_unique', 'unique(partner_id)', 'Jeder Partner darf nur einen OWS-Datensatz haben.')
] ]
AVAILABLE_COLORS = [
('#000000', 'schwarz'),
('#ff0000', 'Rot'),
('#E91E63', 'Pink'),
('#9C27B0', 'Lila'),
('#3F51B5', 'Indigo'),
('#0000ff', 'Blau'),
('#008000', 'Grün'),
('#ffff00', 'Gelb'),
('#FF9800', 'Orange'),
('#795548', 'Braun'),
('#1f1f1f', 'Grau'),
]
class OwsMachineArea(models.Model): 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):
_name = 'ows.machine' _name = 'ows.machine'
@ -325,13 +436,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'")
description = fields.Text() category = fields.Selection([
active = fields.Boolean(default=True) ('green', 'Kategorie 1: grün'),
area_id = fields.Many2one('ows.machine.area', string='Bereich') ('yellow', 'Kategorie 2: gelb'),
product_ids = fields.One2many('ows.machine.product', 'machine_id', string="Nutzungsprodukte") ('red', 'Kategorie 3: rot'),
product_names = fields.Char(string="Nutzungsprodukte Liste", compute="_compute_product_using_names", store=False,) ], string="Sicherheitskategorie", required=True, default='red', help="Sicherheitsrelevante Maschinenkategorie:\n"
"- grün: keine Einweisungspflicht\n"
"- gelb: empfohlene Einweisung\n"
"- rot: Einweisung zwingend erforderlich")
category_icon = fields.Char(string="Kategorie-Symbol", compute="_compute_category_icon", store=False)
@api.depends('category')
def _compute_category_icon(self):
for rec in self:
icon_map = {
'green': '🟢',
'yellow': '🟡',
'red': '🔴',
}
rec.category_icon = icon_map.get(rec.category, '')
description = fields.Text(string="Gerätebeschreibung", translate=True, help="Beschreibung der Maschine oder des Geräts.")
active = fields.Boolean(string="Aktive", default=True, help="Ist die Maschine oder das Gerät aktiv? Inaktive Maschinen werden nicht mehr in der POS-Ansicht angezeigt.")
area_id = fields.Many2one('ows.machine.area', string='Bereich', help="Bereich in der Werkstatt, in dem die Maschine oder das Gerät steht.")
product_ids = fields.One2many('ows.machine.product', 'machine_id', string="Nutzungsprodukte", help="Dies ist das zugehörige Produkt, falls die Maschine oder das Geräte eine zeitliche Nutzungsgebühr hat.")
product_names = fields.Char(string="Liste der Nutzungsprodukte", compute="_compute_product_using_names", store=False,)
training_ids = fields.One2many('ows.machine.training', 'machine_id', string="Einweisungsprodukte") training_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 +489,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("🔍 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 +527,21 @@ class OwsMachine(models.Model):
'name': machine.name, 'name': machine.name,
'has_access': has_access, 'has_access': has_access,
}) })
res.append({ if machine_list:
'area': area.name, access_by_area.append({
'color_hex': area.color_hex or '#000000', 'area': area.name,
'machines': machine_list 'color_hex': area.color_hex or '#000000',
}) '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):

49
models/pos_order.py Normal file
View File

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

View File

@ -29,6 +29,19 @@ def run_migration(cr, registry):
_logger.error(f"[OWS] Fehler bei automatischer Felder-Migration: {e}") _logger.error(f"[OWS] Fehler bei automatischer Felder-Migration: {e}")
# Testsystem-Anpassungen (Admin umbenennen + andere archivieren)
try:
env['hr.employee'].anonymize_for_testsystem()
_logger.info("[OWS] Testsystem-Anpassung der Mitarbeiter abgeschlossen.")
except Exception as e:
_logger.error(f"[OWS] Fehler bei Testsystem-Anpassung der Mitarbeiter: {e}")
# Archivierung aller Kontakte die keinen User Account haben
try:
env['res.partner'].archive_partners_without_users()
_logger.info("[OWS] Testsystem-Anpassung der Kontakte abgeschlossen.")
except Exception as e:
_logger.error(f"[OWS] Fehler bei Testsystem-Anpassung der Kontakte: {e}")
#import_machine_products.run_import(cr, registry) #import_machine_products.run_import(cr, registry)

View File

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

View File

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

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

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

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

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

59
scripts/uninstall_rpc.py Executable file
View File

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

View File

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

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

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,2 +1 @@
from . import test_res_partner from . import test_res_partner
from . import test_access_rights

View File

@ -1,74 +0,0 @@
'''
/odoo_env/src/odoo/odoo-bin \
-d hh16-test \
--data-dir=/env/filestore/ \
--addons-path=/odoo_env/src/odoo/addons,/odoo_env/src/odoo/odoo/addons,/odoo_env/src/openupgrade,/odoo_env/src/OCA/web,/odoo_env/src/OCA/server-tools,/odoo_env/src/vvow \
--test-enable \
--stop-after-init \
--test-tags open_workshop \
--log-level=debug \
--http-port=9070 \
--db_host=db \
--db_user=odoo \
--db_password=odoo
'''
from odoo.tests.common import TransactionCase
from odoo.tests import tagged
import logging
_logger = logging.getLogger(__name__)
@tagged('post_install', 'open_workshop', 'access_rights')
class TestOpenWorkshopAccessRights(TransactionCase):
def setUp(self):
super().setUp()
self.group_user = self.env.ref('base.group_user')
self.test_user = self.env['res.users'].create({
'name': 'Access Test User',
'login': 'access_test_user',
'groups_id': [(6, 0, [self.group_user.id])],
})
self.models_to_test = [
'ows.machine',
'ows.machine.access',
'ows.machine.product',
'ows.machine.training',
'ows.machine.area',
'ows.user',
]
def test_model_access_rights(self):
for model_name in self.models_to_test:
with self.subTest(model=model_name):
model = self.env[model_name].with_user(self.test_user)
records = model.search([], limit=1)
# Test Lesezugriff
self.assertTrue(records.exists(), f"Kein Zugriff auf {model_name} oder leer")
# Test Schreib-, Erstell- und Löschrechte nur wenn Eintrag existiert
if records:
# Schreibtest
write_fields = [f for f in records._fields if records._fields[f].type in ('char', 'text') and f != 'id']
if write_fields:
field = write_fields[0]
test_value = 'Test Write'
try:
records.write({field: test_value})
except Exception as e:
self.fail(f"❌ Schreibrechte auf {model_name}.{field} verweigert: {e}")
# Erstellung
try:
new = records.copy()
self.assertTrue(new.exists(), f"❌ Kein Erstellrecht auf {model_name}")
except Exception as e:
self.fail(f"❌ Erstellung in {model_name} verweigert: {e}")
# Löschung
try:
new.unlink()
except Exception as e:
self.fail(f"❌ Löschrechte auf {model_name} verweigert: {e}")

View File

@ -1,6 +1,8 @@
## Testausführung ## Testausführung
# /odoo_env/src/odoo/odoo-bin -d hh16 --test-enable --stop-after-init --test-tags open_workshop --log-level=debug --update open_workshop --stop-after-init --db_host=db --db_user=odoo --db_password=odoo # 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.common import TransactionCase
from odoo.tests import tagged from odoo.tests import tagged

76
todo.md
View File

@ -1,72 +1,4 @@
# TODO: Reaktivierung des Moduls "Open Workshop" in Odoo 16.0 [ ] Help System
[ ] Möglichkeit, Einweisungen manuell zu setzen?
## ✨ Ziel [ ] Möglichkeit, Einweisungen von Personen im Backend zurückzusetzen (geht im Moment nur über die Datenbank direkt)
Die schrittweise Wiederherstellung der Funktionalität des Moduls `open_workshop` in einer nach Odoo 16.0 migrierten Instanz, basierend auf einer zuvor deaktivierten Datenbank. [ ]
---
## 🔄 1. Grundlagen sicherstellen
- [x] Sicherstellen, dass `ows_models.py` korrekt geladen wird
- [x] Alle Modelle müssen sich fehlerfrei installieren lassen
- [x] Tabellen wie `ows_user`, `ows_machine`, etc. sind vorhanden und konsistent
- [x] Relation `res.partner.ows_user_id` vorhanden
---
## 🧹 2. Datenbasierte Komponenten reaktivieren
### 2.1 `data/demo_data.xml`
- [ ] Beispieldaten für `ows.machine.area` und `ows.machine` erstellen
- [ ] IDs müssen konfliktfrei mit bestehender Datenbank sein
- [ ] Module danach neu starten: `-u open_workshop`
### 2.2 `security/ir.model.access.csv`
- [x] Zugriff für alle verwendeten Modelle definieren
- `ows.user`, `ows.machine`, `ows.machine.access`
- Optional: `res.partner` (nur read)
- [x] Installieren
- [ ] testen ob Zugriff möglich ist -> tests/test_access_rights.py
---
## 🎨 3. Backend Views stufenweise aktivieren
### 3.1 `views/menu_views.xml`
- [ ] Menüs einbinden, ohne Abhängigkeiten
- [ ] Test: Odoo starten & Menüs sichtbar?
### 3.2 `views/machine_area_views.xml`
### 3.3 `views/machine_views.xml`
- [ ] Baumansicht (list) zuerst aktivieren
- [ ] Danach Form-View (form) hinzufügen
### 3.4 `views/res_partner_view.xml`
- [ ] Tab "Maschinenfreigaben" aktivieren
- [ ] Nur Felder mit klarer Modellbindung einbinden
---
## 💻 4. POS Assets & QWeb (optional)
### 4.1 `views/assets.xml`
- [ ] QWeb-Templates und POS JS nur aktivieren, wenn POS-Modul auch vorhanden ist
- [ ] Kompatibilität zu JS (ES5 / `odoo.define`) prüfen
---
## 🔬 5. Tools & Debugging
- [ ] `odoo-bin shell -d hh16` für gezielte Tests nutzen
- [ ] Überprüfen ob Einträge in `ir.model.data` korrekt vorhanden sind
- [ ] Logdateien auf Foreign Key oder View-Probleme prüfen
---
## 🔧 Optional
- [ ] `migrate_existing_partners()` über `res.partner` testen
- [ ] Migration der alten `vvow_*` Felder validieren
---
## ⚙ Nächste Schritte
- [ ] Schritt 2 (Demo- und Security-Dateien) zuerst
- [ ] Schrittweise View-Dateien aktivieren
- [ ] Modul vollständig über Backend installierbar machen
- [ ] POS-Integration zuletzt wiederherstellen

13
views/assets.xml Normal file
View File

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

View File

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

View File

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

69
views/machine_views.xml Normal file
View File

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

81
views/menu_views.xml Normal file
View File

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

111
views/res_partner_view.xml Normal file
View File

@ -0,0 +1,111 @@
<odoo>
<!-- Zentrale View für alle drei Tabs in garantierter Reihenfolge -->
<record id="view_partner_form_inherit_open_workshop_tabs" model="ir.ui.view">
<field name="name">res.partner.form.ows.tabs</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form"/>
<field name="priority" eval="10"/>
<field name="arch" type="xml">
<xpath expr="//page[@name='sales_purchases']" position="before">
<!-- Tab 1: HOBBYHIMMEL Basis -->
<page name="ows_basic" string="HOBBYHIMMEL Basis">
<group name="container_row_2">
<group string="Sicherheit">
<field name="security_briefing"/>
<field name="security_id"/>
</group>
<group string="Zugang">
<field name="rfid_card"/>
</group>
</group>
</page>
<!-- Tab 2: HOBBYHIMMEL Einweisungen (HTML) -->
<page name="ows_machine_access_html" string="HOBBYHIMMEL Einweisungen">
<field name="machine_access_html" readonly="1" widget="html"/>
</page>
<!-- Tab 3: Einweisungen (Liste) -->
<page name="ows_machine_access_list" string="Einweisungen (Liste)">
<field name="machine_access_ids">
<tree>
<field name="machine_id"/>
<field name="date_granted"/>
<field name="date_expiry"/>
<field name="granted_by_pos"/>
</tree>
</field>
</page>
</xpath>
</field>
</record>
<!-- Geburtstag direkt nach der USt-ID -->
<record id="view_partner_form_inherit_ows_birthday" model="ir.ui.view">
<field name="name">res.partner.form.ows.birthday</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form"/>
<field name="priority" eval="15"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='vat']" position="after">
<field name="birthday"/>
</xpath>
</field>
</record>
<!-- List View Anpassung -->
<record id="ows_userList_inherit" model="ir.ui.view">
<field name="name">res.partner.ows.tree</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_tree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='vat']" position="after">
<field name="create_date" optional="show"/>
<field name="security_briefing" optional="show"/>
<field name="security_id" optional="show"/>
<field name="rfid_card" optional="show"/>
<field name="category_id" widget="many2many_tags"/>
</xpath>
<xpath expr="//field[@name='vat']" position="replace">
<field name="vat" invisible="1"/>
</xpath>
<xpath expr="//field[@name='email']" position="replace">
<field name="email" invisible="1"/>
</xpath>
<xpath expr="//field[@name='phone']" position="replace">
<field name="phone" invisible="1"/>
</xpath>
<xpath expr="//field[@name='state_id']" position="replace">
<field name="state_id" invisible="1"/>
</xpath>
<xpath expr="//field[@name='country_id']" position="replace">
<field name="country_id" invisible="1"/>
</xpath>
</field>
</record>
<!-- Standardwerte setzen (company_type = person) -->
<record id="view_partner_form_inherit" model="ir.ui.view">
<field name="name">res.partner.form.inherit.default_person</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form"/>
<field name="arch" type="xml">
<field name="company_type" position="attributes">
<attribute name="default">person</attribute>
</field>
</field>
</record>
<!-- Optional: Kontakte-Action, falls gebraucht -->
<record id="contacts.action_contacts" model="ir.actions.act_window">
<field name="view_mode">tree,kanban,form,activity</field>
</record>
<record id="contacts.action_contacts_view_kanban" model="ir.actions.act_window.view">
<field name="sequence" eval="1"/>
</record>
<record id="contacts.action_contacts_view_tree" model="ir.actions.act_window.view">
<field name="sequence" eval="0"/>
</record>
</odoo>