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
27 changed files with 3297 additions and 303 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,3 +1,5 @@
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,9 +1,9 @@
{
'name': 'POS Open Workshop',
'license': 'AGPL-3',
'version': '17.0.1.0.0',
'version': '13.0.1.0.0',
'summary': 'Erstellt Maschinenfreigaben basierend auf POS-Einweisungsprodukten',
'depends': ['base','product','hr','sale','contacts','point_of_sale'],
'depends': ['base','product','sale','contacts','point_of_sale'],
'author': 'matthias.lotz',
'category': 'Point of Sale',
'data': [
@ -13,6 +13,7 @@
'views/machine_area_views.xml',
'views/machine_views.xml',
'views/res_partner_view.xml',
'views/assets.xml',
'data/data.xml',
],
'qweb': [
@ -25,15 +26,13 @@
'installable': True,
'assets': {
'point_of_sale.assets': [
'open_workshop/static/src/js/product_screen_template_patch.js',
'open_workshop/static/src/css/pos.css',
'open_workshop/static/src/js/ows_pos_customer_sidebar.js',
'open_workshop/static/src/xml/ows_pos_customer_sidebar.xml',
'open_workshop/static/src/xml/ows_product_screen.xml',
'static/src/js/machine_access_sidebar.js',
'static/src/css/pos.css',
],
},
'post_init_hook': 'run_migration',
'description': """
Diese App erstellt Maschinenfreigaben basierend auf POS-Einweisungsprodukten.
Die App ist für den Einsatz in der Odoo-Version 17.0 konzipiert.
Die App ist für den Einsatz in der Odoo-Version 13.0 konzipiert.
""",
}

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

@ -168,21 +168,24 @@ class ResPartner(models.Model):
def _compute_machine_access_html(self):
areas = self.env['ows.machine.area'].search([], order="name")
for partner in self:
html = ""
html = "<div class='tab-content'><div class='tab-pane active' id='machine_access_tab'>"
html += "<div class='o_group'>"
for area in areas:
html += "<table class='o_group o_inner_group o_group_col_6'><tbody>"
# Bereichsüberschrift
html += f"""
<div class="o_form_sheet">
<h3 class="o_form_label">{area.name}</h3>
<table class="table table-sm table-bordered o_form_table">
<thead>
<tr>
<th>Maschine</th>
<th>Status</th>
<th>Datum</th>
<th>Gültig bis</th>
<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>
</thead>
<tbody>
"""
machines = self.env['ows.machine'].search([('area_id', '=', area.id)], order="name")
@ -192,21 +195,21 @@ class ResPartner(models.Model):
('partner_id', '=', partner.id),
('machine_id', '=', machine.id),
], limit=1)
icon = '<span class="text-success fa fa-check"/>' if access else '<span class="text-danger fa fa-times"/>'
icon = "<span class='fa fa-check text-success'></span>" if access else "<span class='fa fa-times text-danger'></span>"
date_granted = access.date_granted.strftime('%Y-%m-%d') if access and access.date_granted else "-"
date_expiry = access.date_expiry.strftime('%Y-%m-%d') if access and access.date_expiry else "-"
html += f"""
<tr>
<td>{machine.name}</td>
<td>{icon}</td>
<td>{date_granted}</td>
<td>{date_expiry}</td>
</tr>
<tr>
<td class="o_td_label"><label class="o_form_label">{icon} {machine.name}</label></td>
<td class="o_td_field">{date_granted}</td>
<td class="o_td_field">{date_expiry}</td>
</tr>
"""
html += "</tbody></table></div>"
html += "</tbody></table>"
html += "</div></div></div>"
partner.machine_access_html = html
@ -375,17 +378,56 @@ class OwsUser(models.Model):
('partner_unique', 'unique(partner_id)', 'Jeder Partner darf nur einen OWS-Datensatz haben.')
]
AVAILABLE_COLORS = [
('#000000', 'schwarz'),
('#ff0000', 'Rot'),
('#E91E63', 'Pink'),
('#9C27B0', 'Lila'),
('#3F51B5', 'Indigo'),
('#0000ff', 'Blau'),
('#008000', 'Grün'),
('#ffff00', 'Gelb'),
('#FF9800', 'Orange'),
('#795548', 'Braun'),
('#1f1f1f', 'Grau'),
]
class OwsMachineArea(models.Model):
_name = 'ows.machine.area'
_table = "ows_machine_area"
_table = 'ows_machine_area'
_description = 'OWS: Maschinenbereich'
_order = 'name'
name = fields.Char(required=True, translate=True)
#color = fields.Integer(string="Farbe")
color_hex = fields.Char(string="Farbe (Hex)", help="Hex-Farbcode wie #FF0000 für Rot")
name = fields.Char(string="Name", required=True, translate=True)
color_hex = fields.Selection(
selection=AVAILABLE_COLORS,
string="Farbe (Hex)",
required=True,
)
color_hex_value = fields.Char(
string="Farbcode",
compute='_compute_color_hex_value',
store=False
)
color_name = fields.Char(
string="Farbname",
compute='_compute_color_name',
store=False
)
@api.depends('color_hex')
def _compute_color_hex_value(self):
for rec in self:
rec.color_hex_value = rec.color_hex or ''
@api.depends('color_hex')
def _compute_color_name(self):
label_dict = dict(AVAILABLE_COLORS)
for rec in self:
rec.color_name = label_dict.get(rec.color_hex, 'Unbekannt')
class OwsMachine(models.Model):
_name = 'ows.machine'
@ -394,13 +436,37 @@ class OwsMachine(models.Model):
name = fields.Char(required=True, translate=True)
code = fields.Char(required=True, help="Eindeutiger Kurzcode, z.B. 'lasercutter'")
description = fields.Text()
active = fields.Boolean(default=True)
area_id = fields.Many2one('ows.machine.area', string='Bereich')
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,)
category = fields.Selection([
('green', 'Kategorie 1: grün'),
('yellow', 'Kategorie 2: gelb'),
('red', 'Kategorie 3: rot'),
], string="Sicherheitskategorie", required=True, default='red', help="Sicherheitsrelevante Maschinenkategorie:\n"
"- grün: keine Einweisungspflicht\n"
"- gelb: empfohlene Einweisung\n"
"- rot: Einweisung zwingend erforderlich")
category_icon = fields.Char(string="Kategorie-Symbol", compute="_compute_category_icon", store=False)
@api.depends('category')
def _compute_category_icon(self):
for rec in self:
icon_map = {
'green': '🟢',
'yellow': '🟡',
'red': '🔴',
}
rec.category_icon = icon_map.get(rec.category, '')
description = fields.Text(string="Gerätebeschreibung", translate=True, help="Beschreibung der Maschine oder des Geräts.")
active = fields.Boolean(string="Aktive", default=True, help="Ist die Maschine oder das Gerät aktiv? Inaktive Maschinen werden nicht mehr in der POS-Ansicht angezeigt.")
area_id = fields.Many2one('ows.machine.area', string='Bereich', help="Bereich in der Werkstatt, in dem die Maschine oder das Gerät steht.")
product_ids = fields.One2many('ows.machine.product', 'machine_id', string="Nutzungsprodukte", help="Dies ist das zugehörige Produkt, falls die Maschine oder das Geräte eine zeitliche Nutzungsgebühr hat.")
product_names = fields.Char(string="Liste der Nutzungsprodukte", compute="_compute_product_using_names", store=False,)
training_ids = fields.One2many('ows.machine.training', 'machine_id', string="Einweisungsprodukte")
training_names = fields.Char(string="Einweisungsprodukte Liste", compute="_compute_product_training_names", store=False,)
training_names = fields.Char(string="Liste der Einweisungsprodukte", compute="_compute_product_training_names", store=False,)
storage_location = fields.Char(string="Lagerort", help="Lagerort der Maschine oder des Geräts.")
purchase_price = fields.Float(string="Kaufpreis", help="Kaufpreis der Maschine oder des Geräts.")
purchase_date = fields.Date(string="Kaufdatum", help="Kaufdatum der Maschine oder des Geräts.")
@api.depends('product_ids.product_id.name')
def _compute_product_using_names(self):
@ -423,12 +489,34 @@ class OwsMachine(models.Model):
@api.model
def get_access_list_grouped(self, partner_id):
"""
Gibt eine gruppierte Liste von Maschinenzugängen für einen bestimmten Partner zurück. Diese Funktion wird in
Odoo POS Frontend verwendet um die Ansicht zu erzeugen auf Welche Maschinen der Partner Zugriff hat.
Für einen gegebenen Partner (über die partner_id) werden alle Maschinenbereiche (areas) abgefragt.
Für jeden Bereich wird geprüft, auf welche Maschinen der Partner Zugriff hat. Das Ergebnis wird
als Liste von Bereichen mit jeweils zugehörigen Maschinen und Zugriffsstatus zurückgegeben.
Zusätzlich werden sicherheitsrelevante Informationen des Partners (wie Sicherheitsunterweisung,
Sicherheits-ID, RFID-Karte und Geburtstag) aus dem zugehörigen ows_user ermittelt und mitgeliefert.
Args:
partner_id (int): Die ID des Partners, für den die Zugriffsübersicht erstellt werden soll.
Returns:
dict: Ein Dictionary mit folgenden Schlüsseln:
- 'access_by_area': Liste von Bereichen mit Maschinen und Zugriffsstatus.
- 'security_briefing': Sicherheitsunterweisung des Nutzers (bool oder False).
- 'security_id': Sicherheits-ID des Nutzers (str oder '').
- 'rfid_card': RFID-Kartennummer des Nutzers (str oder '').
- 'birthday': Geburtstag des Nutzers (str oder '').
"""
partner = self.env['res.partner'].browse(partner_id)
areas = self.env['ows.machine.area'].search([], order="name")
_logger.info("🔍 Maschinenbereiche: %s", areas.mapped('name'))
_logger.info("🔍 Partner_id: %s", partner_id)
res = []
access_by_area = []
for area in areas:
machines = self.search([('area_id', '=', area.id)], order="name")
machines = self.search([('area_id', '=', area.id), ('category', '=', 'red')], order="name")
machine_list = []
for machine in machines:
has_access = bool(self.env['ows.machine.access'].search([
@ -439,12 +527,21 @@ class OwsMachine(models.Model):
'name': machine.name,
'has_access': has_access,
})
res.append({
'area': area.name,
'color_hex': area.color_hex or '#000000',
'machines': machine_list
})
return res
if machine_list:
access_by_area.append({
'area': area.name,
'color_hex': area.color_hex or '#000000',
'machines': machine_list
})
user = partner.ows_user_id[:1]
return {
'access_by_area': access_by_area,
'security_briefing': user.security_briefing if user else False,
'security_id': user.security_id if user else '',
'rfid_card': user.rfid_card if user else '',
'birthday': user.birthday if user else '',
}
class OwsMachineAccess(models.Model):

View File

@ -8,9 +8,9 @@ _logger = logging.getLogger(__name__)
_logger.info("✅ pos_order.py geladen")
# debugpy.listen(("0.0.0.0", 5678))
print("✅ debugpy wartet auf Verbindung (Port 5678) ...")
# print("✅ debugpy wartet auf Verbindung (Port 5678) ...")
# Optional: Starte erst, wenn VS Code verbunden ist
#debugpy.wait_for_client()
# debugpy.wait_for_client()
class PosOrder(models.Model):
_inherit = 'pos.order'

View File

@ -29,6 +29,19 @@ def run_migration(cr, registry):
_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)

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

File diff suppressed because it is too large Load Diff

View File

@ -53,7 +53,7 @@ odoo.define('open_workshop.machine_access_sidebar', function (require) {
}).then(function (result) {
partner.create_date = partner.create_date && partner.create_date.substring(0, 10);
var html = QWeb.render('PartnerMachineAccessList', {
areas: result || [],
areas: result.access_by_area || [],
partner: partner,
});
self.$('.access-content').html(html);

View File

@ -1,44 +0,0 @@
// @odoo-module
import { Component } from "@odoo/owl";
import { usePos } from "@point_of_sale/app/store/pos_hook";
export class OwsPosCustomerSidebar extends Component {
static template = "open_workshop.OwsPosCustomerSidebar";
setup() {
this.pos = usePos();
}
addOrder() {
this.pos.add_new_order();
}
removeCurrentOrder() {
const order = this.pos.get_order();
if (order) {
this.pos.removeOrder(order);
}
}
openTicketScreen() {
this.pos.showScreen("TicketScreen");
}
getFilteredOrderList() {
return this.pos.get_order_list();
}
getDate(order) {
const date = new Date(order.creationDate || order.creation_date || Date.now());
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}
getPartner(order) {
return order.get_partner()?.name || "Kein Kunde";
}
selectOrder(order) {
this.pos.set_order(order);
}
}

View File

@ -1,16 +0,0 @@
// @odoo-module
import { ProductScreen } from "@point_of_sale/app/screens/product_screen/product_screen";
import { registry } from "@web/core/registry";
import { OwsPosCustomerSidebar } from "@open_workshop/js/ows_pos_customer_sidebar";
export class OwsProductScreen extends ProductScreen {
static template = "open_workshop.ProductScreen";
static components = {
...ProductScreen.components,
OwsPosCustomerSidebar,
};
}
registry.category("pos_screens").remove("ProductScreen");
registry.category("pos_screens").add("ProductScreen", OwsProductScreen);

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="MachineAccessSidebar">
<t t-name="MachineAccessSidebar" owl="1">
<div class="machine-access-sidebar">
<div class="access-content">
</div>

View File

@ -1,23 +0,0 @@
<?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 t-esc="getDate(order)"/> <t t-esc="getPartner(order)"/></div>
</div>
</t>
</div>
</div>
</t>
</templates>

View File

@ -1,45 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="open_workshop.ProductScreen" owl="1">
<div class="product-screen d-flex h-100 bg-100" t-att-class="{ 'd-none': !props.isShown }">
<div class="custompane d-flex flex-column border-end bg-200">
<OwsPosCustomerSidebar />
</div>
<div t-if="!ui.isSmall || pos.mobile_pane === 'left'"
t-att-class="{'flex-grow-1': ui.isSmall}"
class="leftpane d-flex flex-column border-end bg-200" >
<OrderWidget lines="currentOrder.orderlines" t-slot-scope="scope"
total="env.utils.formatCurrency(currentOrder.get_total_with_tax())"
tax="!env.utils.floatIsZero(currentOrder.get_total_tax()) and env.utils.formatCurrency(currentOrder.get_total_tax()) or ''">
<t t-set="line" t-value="scope.line" />
<Orderline line="line.getDisplayData()"
t-on-click="() => this.selectLine(line)"
class="{ ...line.getDisplayClasses(), 'selected' : line.selected }"/>
<t t-set-slot="details" />
</OrderWidget>
<div class="pads border-top">
<div class="control-buttons d-flex flex-wrap border-bottom overflow-hidden bg-300" t-if="!ui.isSmall">
<t t-foreach="controlButtons" t-as="cb" t-key="cb.name">
<t t-component="cb.component" t-key="cb.name"/>
</t>
</div>
<div class="subpads d-flex">
<ActionpadWidget partner="partner" actionName="constructor.numpadActionName" actionType="'payment'" onClickMore.bind="displayAllControlPopup" />
<Numpad buttons="getNumpadButtons()" onClick.bind="onNumpadClick" class="'w-100'"/>
</div>
</div>
</div>
<div class="rightpane overflow-auto d-flex flex-grow-1 flex-column bg-300 w-60" t-if="!ui.isSmall || pos.mobile_pane === 'right'">
<ProductsWidget />
<t t-if="ui.isSmall">
<div class="product-reminder d-flex justify-content-center align-items-center py-1 text-bg-warning bg-opacity-50 fw-bolder"
t-if="currentOrder.get_selected_orderline() and currentOrder.hasJustAddedProduct" t-key="animationKey" >
<span><t t-esc="selectedOrderlineQuantity"/> <t t-esc="selectedOrderlineDisplayName"/> <t t-esc="selectedOrderlineTotal"/></span>
</div>
</t>
</div>
</div>
</t>
</templates>

76
todo.md
View File

@ -1,72 +1,4 @@
# TODO: Reaktivierung des Moduls "Open Workshop" in Odoo 16.0
## ✨ Ziel
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
[ ] Help System
[ ] Möglichkeit, Einweisungen manuell zu setzen?
[ ] Möglichkeit, Einweisungen von Personen im Backend zurückzusetzen (geht im Moment nur über die Datenbank direkt)
[ ]

View File

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

@ -18,7 +18,7 @@
<field name="arch" type="xml">
<tree editable="bottom">
<field name="machine_id"/>
<field name="training_id" domain="[('categ_id.name', 'in', ['Einweisungen', 'Kurse'])]"/>
<field name="training_id" domain="[('categ_id.name', '=', 'Einweisungen')]"/>
</tree>
</field>
</record>

View File

@ -6,30 +6,44 @@
<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="code"/>
<field name="area_id"/>
<field name="description"/>
<field name="active"/>
</group>
<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>

View File

@ -1,22 +1,34 @@
<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">
<page name="ows_machine_access" string="Offene Werkstatt (Hobbyhimmel)">
<!-- EINWEISUNG: Zwei Felder nebeneinander -->
<group name="container_row_2" string="Sicherheitseinweisung" col="2">
<field name="security_briefing"/>
<field name="security_id"/>
</group>
<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">
<!-- MASCHINENFREIGABEN: Volle Breite -->
<group string="Maschinenfreigaben" col="2">
<field name="machine_access_ids" colspan="2" context="{'default_partner_id': id}" nolabel="1">
<!-- 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"/>
@ -24,16 +36,11 @@
<field name="granted_by_pos"/>
</tree>
</field>
</group>
</page>
<!-- ÜBERSICHT: Volle Breite -->
<group string="Maschinenfreigaben Übersicht" >
<field name="machine_access_html" colspan="2" readonly="1" widget="html" nolabel="1"/>
</group>
</page>
</xpath>
</field>
</record>
</xpath>
</field>
</record>
<!-- Geburtstag direkt nach der USt-ID -->
<record id="view_partner_form_inherit_ows_birthday" model="ir.ui.view">