diff --git a/README.md b/README.md index 87195e0..8157e1e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Open Workshop (open_workshop ows) -Dieses Odoo v13.0 Modul erweitert das POS- und Kontakt-Modul um Funktionen für offene Werkstätten (FabLabs, Makerspaces etc.) und dient der Verwaltung von Maschinen, Naschinen Einweisungen Produkten, Maschinen Nutzungsprodukten und Zugangsberechtigungen zu den Maschinen. +Dieses Odoo v18.0 Modul erweitert das POS- und Kontakt-Modul um Funktionen für offene Werkstätten (FabLabs, Makerspaces etc.) und dient der Verwaltung von Maschinen, Naschinen Einweisungen Produkten, Maschinen Nutzungsprodukten und Zugangsberechtigungen zu den Maschinen. ## Funktionen @@ -28,40 +28,10 @@ Dieses Odoo v13.0 Modul erweitert das POS- und Kontakt-Modul um Funktionen für ## Installation 1. Dieses Modul in den Custom-Addons-Ordner kopieren -2. Vor der Installation von open_worshop muss vvow_pos deinstalliert werden. Die Funktionalität von vvow_pos wird durch open_workshop ersetzt und erweitert. -3. ggf. muss die alte Datenbank manuell migiriert werden, es gibt ca 9 gelöscht res.partner auf die Verweise aus POS bestehen. Diese res.parnter müssen wieder hergestellt werden. Dazu gibt es ein Skript unter - ```folder - scripts/fix_missing_pos_partner.py - ``` - ```bash - opt/odoo/odoo/odoo-bin shell -d hobbyhimmel < scrpts/fix_missing_pos_partner.py - ``` -4. Odoo starten mit: - ```bash - odoo-bin -d deine_datenbank -u open_workshop - ``` -5. Alternativ im Backend unter Apps installieren - -## Automatische Migrationen - -Beim ersten Laden des Moduls werden folgende Migrationen durchgeführt: -- Bestehende `res.partner` erhalten automatisch `ows.user`-Eintrag (inkl. Übernahme alter Felder wie vvow_birthday, vvow_security, vvow_security_id, vvow_rfid. -- Alte Felder mit Maschinenfreigaben (`vvow_holz_*`, `vvow_metall_*`, `vvow_fablab_*`) werden in `ows.machine.access` übertragen - - inkl. Übernahme des Änderungsdatum aus `mail.message` wann der Nutzer die Einweisung erhalten hat (ist noch fehlerhaft) +2. Im Odoo Backend unter Apps installieren ## Entwicklerhinweise -### post_init_hook -Die Datei `post_init_hook.py` ruft automatisch nach der Installation folgende Methoden auf: -```python -res.partner.migrate_existing_partners() -res.partner.migrate_machine_access_from_old_fields() -``` - -### Datenimport -- Maschinenbereiche, Maschinen werden über `.xml`-Dateien in `data/` geladen -- Die Zuordnung von Maschine zu Einweisungsprodukten und Nutzungsprodukten muss derzeit noch manuell erstellt werden. Ein skript dafür folgt. - ## ToDos - Bearbeitung der Maschinenfreigaben im Backend - Automatische Erstellung von `mail.message` bei manueller Freigabe diff --git a/__init__.py b/__init__.py index 0650744..d7af4f2 100644 --- a/__init__.py +++ b/__init__.py @@ -1 +1,3 @@ from . import models +from . import controllers + diff --git a/__manifest__.py b/__manifest__.py index 234b4fe..721ee43 100644 --- a/__manifest__.py +++ b/__manifest__.py @@ -1,19 +1,39 @@ { 'name': 'POS Open Workshop', 'license': 'AGPL-3', - 'version': '16.0.1.0.0', + 'version': '18.0.1.0.0', 'summary': 'Erstellt Maschinenfreigaben basierend auf POS-Einweisungsprodukten', 'depends': ['base','product','sale','contacts','point_of_sale'], 'author': 'matthias.lotz', 'category': 'Point of Sale', 'data': [ - 'data/data.xml', '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', + 'data/data.xml', + ], 'installable': True, + 'assets': { + 'web.assets_backend': [ + 'open_workshop/static/src/css/category_color.css', + ], + 'point_of_sale._assets_pos': [ + 'open_workshop/static/src/css/pos.css', + 'open_workshop/static/src/js/ows_machine_access_list.js', + 'open_workshop/static/src/js/ows_pos_customer_sidebar.js', + 'open_workshop/static/src/js/ows_pos_sidebar.js', + 'open_workshop/static/src/js/ows_product_screen_template_patch.js', + 'open_workshop/static/src/xml/ows_machine_access_list.xml', + 'open_workshop/static/src/xml/ows_pos_customer_sidebar.xml', + 'open_workshop/static/src/xml/ows_pos_sidebar.xml', + 'open_workshop/static/src/xml/ows_product_screen_template_patch.xml', + ], + }, 'description': """ 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 18.0 konzipiert. """, } diff --git a/controllers/__init__.py b/controllers/__init__.py new file mode 100644 index 0000000..4dc11d1 --- /dev/null +++ b/controllers/__init__.py @@ -0,0 +1,3 @@ +# Datei: open_workshop/controllers/__init__.py + +from . import pos_access diff --git a/controllers/pos_access.py b/controllers/pos_access.py new file mode 100644 index 0000000..edd56c4 --- /dev/null +++ b/controllers/pos_access.py @@ -0,0 +1,14 @@ +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, **kwargs): + partner_id = kwargs.get('params', {}).get('partner_id') + if not partner_id: + return {"error": "Missing partner_id"} + + Machine = request.env['ows.machine'].sudo() + return Machine.get_access_list_grouped(partner_id) + diff --git a/helper/data_product_and_categories.xml b/helper/data_product_and_categories.xml deleted file mode 100644 index dd3566c..0000000 --- a/helper/data_product_and_categories.xml +++ /dev/null @@ -1,177 +0,0 @@ - - - - Einweisungen - - - Maschinennutzung - - - 3D Druck (30 Minuten) - - 0.25 - True - - - - Bandschleifer (1 Minute) - - 0.1 - True - - - - Bandsäge (1 Minute) - - 0.1 - True - - - - CNC Fräse (1 Minute) - - 0.1 - True - - - - CNC Sicherheitseinweisung - - 25.0 - True - - - - Drehbank (1 Minute) - - 0.1 - True - - - - Einweisung 3D Drucker Delta - - 15.0 - True - - - - Einweisung 3D Drucker Prusa - - 20.0 - True - - - - Einweisung Bandsäge - - 15.0 - True - - - - Einweisung Drehbank - - 20.0 - True - - - - Einweisung FKS - - 20.0 - True - - - - Einweisung Hobel - - 15.0 - True - - - - Einweisung Laser - - 15.0 - True - - - - Einweisung Metallfräse - - 20.0 - True - - - - Einweisung Schweißgerät - - 10.0 - True - - - - Einweisung in maschinelle Holzverbindungen - - 15.0 - True - - - - Formatkreissäge (1 Minute) - - 0.1 - True - - - - Fräse - Deckel (1 Minute) - - 0.1 - True - - - - Hobel (1 Minute) - - 0.1 - True - - - - Laser (Aktivminute) - - 0.7000000000000001 - True - - - - Sandstrahlbox (1 Minute) - - 0.2 - True - - - - Schweißgerät (1 Minute) - - 0.2 - True - - - - Schweißkabine (eigenes Schweißgerät - 1 Minute) - - 0.1 - True - - - - Sonstige Dienstleistungen/Nutzung - - 1.0 - True - - - \ No newline at end of file diff --git a/helper/export.sh b/helper/export.sh deleted file mode 100755 index 24f8fbd..0000000 --- a/helper/export.sh +++ /dev/null @@ -1,2 +0,0 @@ -/opt/odoo/odoo/odoo-bin shell -d hobbyhimmel < /home/odoo/custom_addons/open_workshop/data/export_products_and_categories.py - diff --git a/helper/export_categories.py b/helper/export_categories.py deleted file mode 100644 index 1c1dec8..0000000 --- a/helper/export_categories.py +++ /dev/null @@ -1,20 +0,0 @@ -# /opt/odoo/odoo/odoo-bin shell -d < export_categories.py -import csv -from odoo import api, SUPERUSER_ID -import os - - -categories = env['product.category'].search([('name', 'in', ['Einweisungen', 'Maschinennutzung'])]) -file_path = os.path.join(os.getcwd(), 'product_category.csv') - -with open(file_path, 'w', newline='') as csvfile: - writer = csv.writer(csvfile) - writer.writerow(['id', 'name', 'parent_id/id']) - for cat in categories: - xml_id = f"open_workshop.cat_{cat.name.lower().replace(' ', '_')}" - parent_id = cat.parent_id and f"base.{cat.parent_id.xml_id}" or '' - writer.writerow([xml_id, cat.name, parent_id]) - -# Aufruf in odoo shell z. B.: -# env = odoo.api.Environment(cr, SUPERUSER_ID, {}) -# export_categories(env) diff --git a/helper/export_products.py b/helper/export_products.py deleted file mode 100644 index f3aa4b2..0000000 --- a/helper/export_products.py +++ /dev/null @@ -1,26 +0,0 @@ -# /opt/odoo/odoo/odoo-bin shell -d < export_products.py -import csv -from odoo import api, SUPERUSER_ID -import os - -# Kategorien suchen -category_names = ['Einweisungen', 'Maschinennutzung'] -categories = env['product.category'].search([('name', 'in', category_names)]) -products = env['product.product'].search([('categ_id', 'in', categories.ids)]) - -file_path = os.path.join(os.getcwd(), 'product_product.csv') - -with open(file_path, 'w', newline='') as csvfile: - writer = csv.writer(csvfile) - writer.writerow(['id', 'name', 'default_code', 'list_price', 'categ_id/id', 'available_in_pos']) - for prod in products: - cat_xml_id = f"open_workshop.cat_{prod.categ_id.name.lower().replace(' ', '_')}" - xml_id = f"open_workshop.prod_{prod.default_code or prod.name.lower().replace(' ', '_')}" - writer.writerow([ - xml_id, - prod.name, - prod.default_code or '', - prod.list_price, - cat_xml_id, - '1' if prod.available_in_pos else '0' - ]) diff --git a/helper/export_products_and_categories.py b/helper/export_products_and_categories.py deleted file mode 100644 index c583527..0000000 --- a/helper/export_products_and_categories.py +++ /dev/null @@ -1,47 +0,0 @@ -# -*- coding: utf-8 -*- -# /opt/odoo/odoo/odoo-bin shell -d < export_products_and_categories.py - -import xml.etree.ElementTree as ET - -def xml_safe_id(name): - return name.lower().replace(' ', '_').replace('/', '_').replace('(', '').replace(')', '').replace('-', '_') - -def export_categories_and_products(env): - root = ET.Element("odoo") - - # Export Kategorien - categories = env['product.category'].search([ - ('name', 'in', ['Einweisungen', 'Maschinennutzung']) - ]) - for cat in categories: - record = ET.SubElement(root, "record", { - "id": f"cat_{xml_safe_id(cat.name)}", - "model": "product.category" - }) - ET.SubElement(record, "field", name="name").text = cat.name - if cat.parent_id: - ET.SubElement(record, "field", name="parent_id", attrib={"ref": f"cat_{xml_safe_id(cat.parent_id.name)}"}) - - # Export Produkte - products = env['product.product'].search([ - ('categ_id.name', 'in', ['Einweisungen', 'Maschinennutzung']) - ]) - for product in products: - record = ET.SubElement(root, "record", { - "id": f"prod_{xml_safe_id(product.name)}", - "model": "product.product" - }) - ET.SubElement(record, "field", name="name").text = product.name or '' - ET.SubElement(record, "field", name="default_code").text = product.default_code or '' - ET.SubElement(record, "field", name="list_price").text = str(product.list_price or 0.0) - ET.SubElement(record, "field", name="available_in_pos").text = "True" - if product.categ_id: - ET.SubElement(record, "field", name="categ_id", attrib={"ref": f"cat_{xml_safe_id(product.categ_id.name)}"}) - - tree = ET.ElementTree(root) - tree.write("data_product_and_categories.xml", encoding="utf-8", xml_declaration=True) - print("✅ XML export saved to data_product_and_categories.xml") - -# Automatischer Start in odoo-bin shell -if 'env' in globals(): - export_categories_and_products(env) diff --git a/helper/install.sh b/helper/install.sh deleted file mode 100755 index c67d878..0000000 --- a/helper/install.sh +++ /dev/null @@ -1 +0,0 @@ -/opt/odoo/odoo/odoo-bin -d hobbyhimmel13_dev -i open_workshop --stop-after-init diff --git a/helper/machine_product_links.xml b/helper/machine_product_links.xml deleted file mode 100644 index be99f67..0000000 --- a/helper/machine_product_links.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/helper/update.sh b/helper/update.sh deleted file mode 100755 index bb99953..0000000 --- a/helper/update.sh +++ /dev/null @@ -1 +0,0 @@ -/opt/odoo/odoo/odoo-bin -d hobbyhimmel --update=open_workshop --dev=all --stop-after-init diff --git a/log/.gitignore b/log/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/log/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/models/__init__.py b/models/__init__.py index 6c9f9d9..9f91cbd 100644 --- a/models/__init__.py +++ b/models/__init__.py @@ -1,5 +1,5 @@ from . import ows_models - +from . import pos_order diff --git a/models/ows_models.py b/models/ows_models.py index 93613a3..192b465 100644 --- a/models/ows_models.py +++ b/models/ows_models.py @@ -8,6 +8,33 @@ import logging _logger = logging.getLogger(__name__) _logger.info("✅ ows_models.py geladen") +class HREmployee(models.Model): + _inherit = 'hr.employee' + + @api.model + def anonymize_for_testsystem(self): + """Benennt Admin-Angestellten um und archiviert alle anderen für das Testsystem.""" + admin_user = self.env['res.users'].search([('name', '=', 'Administrator')], limit=1) + _logger.info(f"[OWS] Administrator-Benutzer gefunden: {admin_user.name} (ID: {admin_user.id})") + admin_employee = self.search([('user_id', '=', admin_user.id)], limit=1) + + if admin_employee: + admin_employee.write({ + 'name': 'TESTSYSTEM', + 'job_title': 'Testumgebung', + 'work_email': False, + 'work_phone': False, + }) + _logger.info("[OWS] Admin-Angestellter wurde umbenannt.") + else: + _logger.warning("[OWS] Kein Angestellter für user_admin gefunden.") + + # Alle anderen Angestellten archivieren + other_employees = self.search([('id', '!=', admin_employee.id)]) + other_employees.write({'active': False}) + _logger.info("[OWS] %d Angestellte archiviert.", len(other_employees)) + + class ResPartner(models.Model): _inherit = 'res.partner' _logger.info("✅ ows ResPartner geladen") @@ -141,32 +168,50 @@ 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 = "" for area in areas: - html += f"
" - html += f"" - html += f"" + html += f""" +
+

{area.name}

+
{area.name}DatumGültig bis
+ + + + + + + + + + """ + machines = self.env['ows.machine'].search([('area_id', '=', area.id)], order="name") + for machine in machines: access = self.env['ows.machine.access'].search([ ('partner_id', '=', partner.id), ('machine_id', '=', machine.id), ], limit=1) - icon = "" if access 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 "" + icon = '' if access 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 "-" + html += f""" - - - - - - + + + + + + """ + html += "
MaschineStatusDatumGültig bis
{icon}{date_granted}{date_expiry}
{machine.name}{icon}{date_granted}{date_expiry}
" - html += "
" + partner.machine_access_html = html + + + @api.model def migrate_existing_partners(self): """ @@ -283,6 +328,31 @@ class ResPartner(models.Model): _logger.info(f"[OWS Migration] ✅ Maschinenfreigaben erstellt: {count_created}") self.env.cr.commit() + + @api.model + def archive_partners_without_users(self): + """ + Archiviert alle Partner (res.partner), die keine Benutzer (res.users) sind. + """ + Partner = self.env['res.partner'] + User = self.env['res.users'] + + # IDs aller Partner, die ein Benutzerkonto haben + user_partner_ids = User.search([]).mapped('partner_id').ids + + # Alle Partner ohne Benutzerkonto + partners_to_archive = Partner.search([ + ('id', 'not in', user_partner_ids), + ('active', '=', True), + ]) + + count = len(partners_to_archive) + partners_to_archive.write({'active': False}) + for p in partners_to_archive: + _logger.debug(f"[OWS] Archiviert Partner: {p.name} (ID {p.id})") + _logger.info(f"[OWS] Archiviert {count} Partner ohne Benutzerkonto.") + self.env.cr.commit() + class OwsUser(models.Model): _name = 'ows.user' @@ -307,15 +377,57 @@ class OwsUser(models.Model): ] +AVAILABLE_COLORS = [ + ('#000000', 'schwarz'), + ('#ff0000', 'Rot'), + ('#E91E63', 'Pink'), + ('#9C27B0', 'Lila'), + ('#3F51B5', 'Indigo'), + ('#0000ff', 'Blau'), + ('#008000', 'Grün'), + ('#ffff00', 'Gelb'), + ('#FF9800', 'Orange'), + ('#795548', 'Braun'), + ('#ffffff', 'Weiss'), +] + class OwsMachineArea(models.Model): _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): @@ -325,13 +437,37 @@ class OwsMachine(models.Model): name = fields.Char(required=True, translate=True) code = fields.Char(required=True, help="Eindeutiger Kurzcode, z.B. 'lasercutter'") + category = fields.Selection([ + ('green', 'Kategorie 1: grün'), + ('yellow', 'Kategorie 2: gelb'), + ('red', 'Kategorie 3: rot'), + ], string="Sicherheitskategorie", required=True, default='red', help="Sicherheitsrelevante Maschinenkategorie:\n" + "- grün: keine Einweisungspflicht\n" + "- gelb: empfohlene Einweisung\n" + "- rot: Einweisung zwingend erforderlich") + + category_icon = fields.Char(string="Kategorie-Symbol", compute="_compute_category_icon", store=False) + + @api.depends('category') + def _compute_category_icon(self): + for rec in self: + icon_map = { + 'green': '🟢', + 'yellow': '🟡', + 'red': '🔴', + } + rec.category_icon = icon_map.get(rec.category, '⚪') + description = fields.Text() active = fields.Boolean(default=True) - area_id = fields.Many2one('ows.machine.area', string='Bereich') + area_id = fields.Many2one('ows.machine.area', string='Bereich', help="Bereich, in dem die Maschine oder das Gerät steht.") product_ids = fields.One2many('ows.machine.product', 'machine_id', string="Nutzungsprodukte") - product_names = fields.Char(string="Nutzungsprodukte Liste", compute="_compute_product_using_names", store=False,) + product_names = fields.Char(string="Liste der Nutzungsprodukte", compute="_compute_product_using_names", store=False,) training_ids = fields.One2many('ows.machine.training', 'machine_id', string="Einweisungsprodukte") - training_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): @@ -354,12 +490,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 = [] + _logger.info("Access RPC called with partner_id=%s", partner_id) + 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([ @@ -370,12 +528,22 @@ 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): @@ -398,7 +566,7 @@ class OwsMachineAccess(models.Model): class OwsMachineProduct(models.Model): _name = 'ows.machine.product' _table = 'ows_machine_product' - _description = 'OWS: Zurordnung Produkt der Nutzung zur die Maschine' + _description = 'OWS: Zuordnung Produkt der Nutzung zu der Maschine' product_id = fields.Many2one('product.product', required=True, domain=[('available_in_pos', '=', True)], ondelete='cascade') machine_id = fields.Many2one('ows.machine', required=True, ondelete='cascade') @@ -406,7 +574,7 @@ class OwsMachineProduct(models.Model): class OwsMachineTraining(models.Model): _name = 'ows.machine.training' _table = 'ows_machine_training' - _description = 'OWS: Zurordnung Produkt der Einweisung zur die Maschine' + _description = 'OWS: Zuordnung Produkt der Einweisung zu der Maschine' training_id = fields.Many2one('product.product', required=True, domain=[('available_in_pos', '=', True)], ondelete='cascade') machine_id = fields.Many2one('ows.machine', required=True, ondelete='cascade') diff --git a/models/pos_order.py b/models/pos_order.py new file mode 100644 index 0000000..71ae7c2 --- /dev/null +++ b/models/pos_order.py @@ -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 diff --git a/post_init_hook.py b/post_init_hook.py deleted file mode 100644 index 708a000..0000000 --- a/post_init_hook.py +++ /dev/null @@ -1,45 +0,0 @@ -# -*- coding: utf-8 -*- -from odoo import SUPERUSER_ID -from odoo.api import Environment -#from . import import_machine_products -import logging - -_logger = logging.getLogger(__name__) - -def run_migration(cr, registry): - """ - Wird nach der Modulinstallation automatisch ausgeführt. - Migriert vorhandene res.partner-Einträge zu ows.user. - """ - env = Environment(cr, SUPERUSER_ID, {}) - - _logger.info("[OWS] Starte automatische Partner-Migration bei Modulinstallation...") - try: - env['res.partner'].migrate_existing_partners() - _logger.info("[OWS] Automatische Partner-Migration abgeschlossen.") - except Exception as e: - _logger.error(f"[OWS] Fehler bei automatischer Partner-Migration: {e}") - - - try: - env['res.partner'].migrate_machine_access_from_old_fields() - _logger.info("[OWS] Automatische Felder-Migration für Maschinenfreigaben in res.partner.") - - except Exception as e: - _logger.error(f"[OWS] Fehler bei automatischer Felder-Migration: {e}") - - - - - #import_machine_products.run_import(cr, registry) - - -''' Funktioniert nicht: - try: - module = env['ir.module.module'].search([('name', '=', 'vvow_einweisungen')], limit=1) - if module and module.state != 'uninstalled': - _logger.info("[OWS] Deinstalliere altes Modul vvow_einweisungen...") - module.button_immediate_uninstall() - except Exception as e: - _logger.error(f"[OWS] Fehler bei deinstallieren von vvow_einweisungen: {e}") -''' \ No newline at end of file diff --git a/security/ir.model.access.csv b/security/ir.model.access.csv index d011792..022703c 100644 --- a/security/ir.model.access.csv +++ b/security/ir.model.access.csv @@ -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_user,ows.machine,model_ows_machine,base.group_user,1,1,1,1 access_ows_machine_product_user,ows.machine.product,model_ows_machine_product,base.group_user,1,1,1,1 +access_ows_machine_training_user,access_ows_machine_training_user,model_ows_machine_training,base.group_user,1,1,1,1 access_ows_machine_area,ows.machine.area,model_ows_machine_area,base.group_user,1,1,1,1 access_ows_user,ows.user,model_ows_user,base.group_user,1,1,1,1 -access_ows_machine_training_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 diff --git a/static/src/css/category_color.css b/static/src/css/category_color.css new file mode 100644 index 0000000..154419d --- /dev/null +++ b/static/src/css/category_color.css @@ -0,0 +1,9 @@ +.category-color-circle { + display: inline-block; + width: 14px; + height: 14px; + border-radius: 50%; + margin-left: 8px; + vertical-align: middle; + border: 1px solid #444; +} diff --git a/static/src/css/pos.css b/static/src/css/pos.css new file mode 100644 index 0000000..cfdfc4d --- /dev/null +++ b/static/src/css/pos.css @@ -0,0 +1,57 @@ +.custompane { + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; +} + +.ows-sidebar { + flex: 1 1 auto; + width: 220px; + display: flex; + flex-direction: column; + overflow: hidden; +} + + +.order-entry:hover { cursor: pointer; } +.order-entry.selected { background-color: #007bff; color: white; } + +.ows-customer-list { + flex: 1 1 auto; + overflow-y: auto; + min-height: 0; /* notwendig für Scrollbar */ +} + +.client-details-grid { + flex-shrink: 0; + max-height: 60%; + overflow-y: auto; + background: #fff; +} + +.pos *::-webkit-scrollbar { + width: 8px; + height:8px; +} + +.sidebar-line { + display: flex; + justify-content: space-between; + gap: 0.5em; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + padding: 0.2em 0; +} + +.sidebar-date { + flex-shrink: 0; + } + +.sidebar-name { + flex-shrink: 1; + text-align: right; + overflow: hidden; + text-overflow: ellipsis; +} diff --git a/static/src/js/ows_machine_access_list.js b/static/src/js/ows_machine_access_list.js new file mode 100644 index 0000000..f3ecc0f --- /dev/null +++ b/static/src/js/ows_machine_access_list.js @@ -0,0 +1,72 @@ +// @odoo-module ows_machine_access_list.js + +import { Component, useState } from "@odoo/owl"; +import { useBus } from "@web/core/utils/hooks"; +import { usePos } from "@point_of_sale/app/store/pos_hook"; +import { rpc } from "@web/core/network/rpc"; +import { registry } from "@web/core/registry"; + +export class OwsMachineAccessList extends Component { + static template = 'open_workshop.OwsMachineAccessList'; + + setup() { + this.pos = usePos(); + + this.state = useState({ + client: null, + grouped_accesses: [], + security_briefing: false, + security_id: '', + rfid_card: '', + birthday: '', + }); + + // 🔁 Reagiere auf Partnerwechsel über den Odoo-Bus + useBus(this.env.bus, 'partner-changed', () => { + this.updateAccessList(); + }); + + // 🔃 Beim Mounten initiale Daten laden + this.updateAccessList(); + } + + async updateAccessList() { + const order = this.pos.get_order(); + const partner = order?.get_partner?.(); + this.state.client = partner || null; + if (!partner) { + this.state.grouped_accesses = []; + this.state.security_briefing = false; + this.state.security_id = ''; + this.state.rfid_card = ''; + this.state.birthday = ''; + return; + } + + try { + const data = await rpc("/open_workshop/partner_access", { + params: { partner_id: partner.id }, + }); + + this.state.grouped_accesses = data.access_by_area || []; + this.state.security_briefing = data.security_briefing; + this.state.security_id = data.security_id; + this.state.rfid_card = data.rfid_card; + this.state.birthday = data.birthday; + } catch (error) { + console.error("Fehler beim Laden der Einweisungen:", error); + this.state.grouped_accesses = []; + this.state.security_briefing = false; + this.state.security_id = ''; + this.state.rfid_card = ''; + this.state.birthday = ''; + } + + } +} + + + + +registry.category("templates").add("open_workshop.OwsMachineAccessList", OwsMachineAccessList); + diff --git a/static/src/js/ows_pos_customer_sidebar.js b/static/src/js/ows_pos_customer_sidebar.js new file mode 100644 index 0000000..e8539f8 --- /dev/null +++ b/static/src/js/ows_pos_customer_sidebar.js @@ -0,0 +1,79 @@ +// @odoo-module ows_pos_customer_sidebar.js + +import { Component } from "@odoo/owl"; +import { useService } from "@web/core/utils/hooks"; +import { usePos } from "@point_of_sale/app/store/pos_hook"; +import { _t } from "@web/core/l10n/translation"; +import { ask } from "@web/core/confirmation_dialog/confirmation_dialog"; + +export class OwsPosCustomerSidebar extends Component { + static template = "open_workshop.OwsPosCustomerSidebar"; + + setup() { + this.pos = usePos(); // ✅ Holt dir Zugriff auf den zentralen POS-Store + this.dialog = useService("dialog"); + } + + addOrder() { + this.pos.add_new_order(); // ✅ Neue Order wird aktive Order + this.pos.showScreen("ProductScreen"); + this.pos.selectPartner(); + this.env.bus.trigger('partner-changed'); // ✅ Event manuell auslösen + } + + async removeCurrentOrder() { + const order = this.pos.get_order(); + if (!order) return; + + // 🛑 Sicherheitsabfrage: Order enthält bereits Positionen? + if (order.get_orderlines().length > 0) { + const confirmed = await ask(this.dialog, { + title: _t("Order enthält Positionen"), + body: _t("Möchtest du diese Order wirklich löschen?"), + confirmText: _t("Löschen"), + cancelText: _t("Abbrechen"), + }); + if (!confirmed) return; + } + + // 📋 Alle verbleibenden Orders (nach der aktuellen) + const remainingOrders = this.pos.get_order_list().filter(o => o !== order); + + // 🗑 Order entfernen + this.pos.removeOrder(order); + + // ✅ Wenn noch andere Orders existieren, eine davon aktivieren + if (remainingOrders.length > 0) { + this.pos.set_order(remainingOrders[remainingOrders.length - 1]); + } + + this.env.bus.trigger('partner-changed'); + } + + openTicketScreen() { + this.pos.showScreen("TicketScreen"); + } + + // 🔧 FIXED: Zugriff auf Order-Liste korrigiert + getFilteredOrderList() { + return this.pos.get_open_orders(); + } + + getDate(order) { + const date = new Date(order.date_order); + const dd = String(date.getDate()).padStart(2, '0'); + const mm = String(date.getMonth() + 1).padStart(2, '0'); + const hh = String(date.getHours()).padStart(2, '0'); + const mi = String(date.getMinutes()).padStart(2, '0'); + return `${dd}.${mm}. ${hh}:${mi}`; + } + + getPartner(order) { + return order.get_partner()?.name || "Kein Kunde"; + } + + selectOrder(order) { + this.pos.set_order(order); + this.env.bus.trigger('partner-changed'); + } +} diff --git a/static/src/js/ows_pos_sidebar.js b/static/src/js/ows_pos_sidebar.js new file mode 100644 index 0000000..dfdc8e3 --- /dev/null +++ b/static/src/js/ows_pos_sidebar.js @@ -0,0 +1,11 @@ +// ows_pos_sidebar.js +// @odoo-module + +import { Component } from "@odoo/owl"; +import { OwsPosCustomerSidebar } from "./ows_pos_customer_sidebar"; +import { OwsMachineAccessList } from "./ows_machine_access_list"; + +export class OwsPosSidebar extends Component { + static template = "open_workshop.OwsPosSidebar"; + static components = { OwsPosCustomerSidebar, OwsMachineAccessList }; +} \ No newline at end of file diff --git a/static/src/js/ows_product_screen_template_patch.js b/static/src/js/ows_product_screen_template_patch.js new file mode 100644 index 0000000..29690f3 --- /dev/null +++ b/static/src/js/ows_product_screen_template_patch.js @@ -0,0 +1,15 @@ +// product_screen_template_patch.js +// @odoo-module + +import { registry } from "@web/core/registry"; +import { ProductScreen } from "@point_of_sale/app/screens/product_screen/product_screen"; +import { OwsPosSidebar } from "./ows_pos_sidebar"; + +class OwsProductScreen extends ProductScreen { + static components = Object.assign({}, ProductScreen.components, { + OwsPosSidebar, + }); +} + +registry.category("pos_screens").remove("ProductScreen"); +registry.category("pos_screens").add("ProductScreen", OwsProductScreen); diff --git a/static/src/xml/ows_machine_access_list.xml b/static/src/xml/ows_machine_access_list.xml new file mode 100644 index 0000000..787441b --- /dev/null +++ b/static/src/xml/ows_machine_access_list.xml @@ -0,0 +1,76 @@ + +
+ + + +
+
    +
  • Einweisungen
  • +
+ +
+
    +
  • + + Werkstatt +
  • + +
  • + + + + + + + Haftungsausschluss +
  • + + +
  • +
      + ‼️Bitte Prüfen‼️ +
    +
  • +
    + + +
      +
    • + Id: + + + +
    • +
    • + Geburtstag: + + + +
    • +
    +
    +
+
+
+
+ + + + +
+
    + +
  • + + + + +
  • +
    +
+
+
+
+ +
+
diff --git a/static/src/xml/ows_pos_customer_sidebar.xml b/static/src/xml/ows_pos_customer_sidebar.xml new file mode 100644 index 0000000..31f5e35 --- /dev/null +++ b/static/src/xml/ows_pos_customer_sidebar.xml @@ -0,0 +1,27 @@ + + + +
+
+ +
+ + +
+
+
+ + +
+ +
+
+
+
+
+
diff --git a/static/src/xml/ows_pos_sidebar.xml b/static/src/xml/ows_pos_sidebar.xml new file mode 100644 index 0000000..1777b19 --- /dev/null +++ b/static/src/xml/ows_pos_sidebar.xml @@ -0,0 +1,9 @@ + + + +
+ + +
+
+
\ No newline at end of file diff --git a/static/src/xml/ows_product_screen_template_patch.xml b/static/src/xml/ows_product_screen_template_patch.xml new file mode 100644 index 0000000..e243e82 --- /dev/null +++ b/static/src/xml/ows_product_screen_template_patch.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index ea197db..0000000 --- a/tests/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from . import test_res_partner -from . import test_access_rights diff --git a/tests/test_access_rights.py b/tests/test_access_rights.py deleted file mode 100644 index 7f91d26..0000000 --- a/tests/test_access_rights.py +++ /dev/null @@ -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}") diff --git a/tests/test_res_partner.py b/tests/test_res_partner.py deleted file mode 100644 index 967fe1e..0000000 --- a/tests/test_res_partner.py +++ /dev/null @@ -1,136 +0,0 @@ -## 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 - - -from odoo.tests.common import TransactionCase -from odoo.tests import tagged -import logging -_logger = logging.getLogger(__name__) - -@tagged('post_install', 'open_workshop', 'vvow') -class TestResPartnerVvowFields(TransactionCase): - - def setUp(self): - super().setUp() - self.partner_model = self.env['res.partner'] - self.user_model = self.env['ows.user'] - - def test_create_with_vvow_fields_creates_user(self): - partner = self.partner_model.create({ - 'name': 'Max Test', - 'vvow_birthday': '1990-01-01', - 'vvow_rfid_card': 'ABC12345', - 'vvow_security_briefing': True, - 'vvow_security_id': 'HB-001', - }) - - user = partner.ows_user_id - self.assertEqual(len(user), 1) - self.assertEqual(user.birthday.isoformat(), '1990-01-01') - self.assertEqual(user.rfid_card, 'ABC12345') - self.assertTrue(user.security_briefing) - self.assertEqual(user.security_id, 'HB-001') - - # Check that Fassade korrekt ausgerechnet ist - self.assertEqual(partner.birthday.isoformat(), '1990-01-01') - self.assertEqual(partner.rfid_card, 'ABC12345') - self.assertTrue(partner.security_briefing) - self.assertEqual(partner.security_id, 'HB-001') - - def test_write_vvow_fields_updates_user_and_fassade(self): - partner = self.partner_model.create({'name': 'Update Tester'}) - user = partner.ows_user_id - - partner.write({ - 'vvow_birthday': '1985-12-31', - 'vvow_rfid_card': 'NEW123', - 'vvow_security_briefing': False, - 'vvow_security_id': 'NEU-SEC', - }) - - self.assertEqual(user.birthday.isoformat(), '1985-12-31') - self.assertEqual(user.rfid_card, 'NEW123') - self.assertFalse(user.security_briefing) - self.assertEqual(user.security_id, 'NEU-SEC') - - # Sicherstellen, dass die berechneten Felder auch aktualisiert wurden - self.assertEqual(partner.birthday.isoformat(), '1985-12-31') - self.assertEqual(partner.rfid_card, 'NEW123') - self.assertFalse(partner.security_briefing) - self.assertEqual(partner.security_id, 'NEU-SEC') - - def test_change_fassade_field_updates_user(self): - partner = self.partner_model.create({ - 'name': 'Change Fassade', - 'vvow_security_briefing': True, - }) - user = partner.ows_user_id - - # Ändere security_briefing (die Fassade) - partner.write({'security_briefing': False}) - - # Muss sich im ows.user widerspiegeln - self.assertFalse(user.security_briefing) - - # vvow-Sync: vvow bleibt wie gehabt (wird nicht überschrieben) - self.assertTrue(partner.vvow_security_briefing) - - def test_change_and_read_consistency(self): - partner = self.partner_model.create({ - 'name': 'Read-Change-Test', - 'vvow_security_briefing': True, - }) - user = partner.ows_user_id - - # Ändere security_briefing über Fassade - partner.write({'security_briefing': False}) - self.assertFalse(user.security_briefing) - self.assertFalse(partner.security_briefing) - - # Lies vvow_* separat aus – sollte weiterhin den ursprünglichen vvow-Wert zeigen - self.assertTrue(partner.vvow_security_briefing) - - # Ändere vvow_* erneut – jetzt sollte alles wieder gesetzt werden - partner.write({'vvow_security_briefing': True}) - self.assertTrue(partner.security_briefing) - self.assertTrue(user.security_briefing) - - - def test_inverse_does_not_clear_unrelated_fields(self): - _logger.info("🔍 Starte Test für Feldsynchronisation") - # Schritt 1: Partner mit vvow_* Feldern erzeugen → soll automatisch ows.user erzeugen - partner = self.partner_model.create({ - 'name': 'Unabhängigkeits-Test', - 'vvow_birthday': '1995-05-05', - 'vvow_rfid_card': 'ABC999', - 'vvow_security_briefing': True, - 'vvow_security_id': 'SEC-XYZ', - }) - _logger.debug("✅ Partner angelegt: %s", partner.name) - - # Validierung: es wurde ein ows.user erzeugt - self.assertEqual(len(partner.ows_user_id), 1, "Kein ows.user erzeugt") - user = partner.ows_user_id - _logger.debug("👤 ows.user: %s", user.read(['birthday', 'security_id'])) - - # Sicherstellen, dass alle Felder korrekt gesetzt wurden - self.assertEqual(user.birthday.isoformat(), '1995-05-05') - self.assertEqual(user.rfid_card, 'ABC999') - self.assertTrue(user.security_briefing) - self.assertEqual(user.security_id, 'SEC-XYZ') - - # Schritt 2: Nur ein Feld über die Fassade ändern (inverse wird getriggert) - partner.write({'security_briefing': False}) - _logger.info("✏️ security_briefing auf False gesetzt") - - # Schritt 3: DB-Cache leeren - user.flush() - user.invalidate_cache() - - _logger.debug("📦 user nach write: %s", user.read(['birthday', 'security_id'])) - - # Schritt 4: Sicherstellen, dass die anderen Felder NICHT gelöscht wurden - self.assertEqual(user.birthday.isoformat(), '1995-05-05') - self.assertEqual(user.rfid_card, 'ABC999') - self.assertEqual(user.security_id, 'SEC-XYZ') - self.assertFalse(user.security_briefing) diff --git a/todo.md b/todo.md index ac15d99..ecd8091 100644 --- a/todo.md +++ b/todo.md @@ -1,72 +1,101 @@ -# TODO: Reaktivierung des Moduls "Open Workshop" in Odoo 16.0 +# 🎯 Ziel des Moduls „Open Workshop“ +Das Modul Open Workshop ist für den Einsatz in einer offenen Werkstatt / einem FabLab gedacht, in dem Kund:innen selbstständig Maschinen nutzen können, aber nur, wenn bestimmte Voraussetzungen erfüllt sind (z. B. Einweisung). Das Ziel ist es, Sicherheit, Zugriffssteuerung, Selbstverwaltung und Abrechnung in einer Odoo-gestützten Umgebung zu ermöglichen – insbesondere im POS (Point-of-Sale)-Modul. -## ✨ 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. +#🛠️ Funktionaler Rahmen & Umgebung +## Betriebsumgebung +Odoo in mehreren Versionen (aktuell Fokus auf Odoo 18.0) ---- +Docker-basierte Installation mit Entwicklungs-, Test- und Produktivinstanzen -## 🔄 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 +PostgreSQL-Datenbank ---- +Gitea zur Versionierung und CI (z. B. act_runner) -## 🧹 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` +POS läuft in einem speziellen Frontend mit festen Ansichten und Touch-Bedienung -### 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 +Nutzung durch Personal & Werkstattnutzer:innen auf verschiedenen Geräten ---- +Kiosksysteme für Anmeldung, Einweisung, ggf. auch Selbstbedienung -## 🎨 3. Backend Views stufenweise aktivieren -### 3.1 `views/menu_views.xml` -- [ ] Menüs einbinden, ohne Abhängigkeiten -- [ ] Test: Odoo starten & Menüs sichtbar? +Interne Systeme z. T. über *.lan.hobbyhimmel.de zugänglich -### 3.2 `views/machine_area_views.xml` -### 3.3 `views/machine_views.xml` -- [ ] Baumansicht (list) zuerst aktivieren -- [ ] Danach Form-View (form) hinzufügen +# 🔐 Kernfunktionen des Moduls „Open Workshop“ +## Maschinenverwaltung +Maschinenmodell mit Zuordnung zu Bereichen (machine areas) -### 3.4 `views/res_partner_view.xml` -- [ ] Tab "Maschinenfreigaben" aktivieren -- [ ] Nur Felder mit klarer Modellbindung einbinden +Darstellung aller Maschinen gruppiert nach Bereich im POS ---- +Maschinenbereiche enthalten u. a. Farbe (hex), Beschreibung etc. -## 💻 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 +Upload von Dokumenten und Bildern möglich ---- +## Zugriffsverwaltung / Einweisungen +Modell ows.machine.access steuert, welche:r Partner:in auf welche Maschine zugreifen darf -## 🔬 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 +Einweisungen sind Produkte in Odoo, deren Kauf automatisch eine machine.access-Eintragung erzeugt ---- +Kunden können mehrfach eingewiesen werden (z. B. bei mehreren Maschinen in einem Kurs) -## 🔧 Optional -- [ ] `migrate_existing_partners()` über `res.partner` testen -- [ ] Migration der alten `vvow_*` Felder validieren +Darstellung im POS über grünes Häkchen / rotes Kreuz ---- +## Sicherheitsinformationen +Modell ows.user verknüpft 1:1 mit res.partner, enthält: -## ⚙ 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 +Geburtstag + +RFID-Card-ID + +Sicherheitsunterweisung (bool) + +Sicherheits-ID + +Synchronisation zwischen res.partner-Fassadenfeldern und ows.user + +## POS-Integration +Erweiterung der POS-Ansicht: + +Rechte Spalte zeigt dauerhaft Maschinenfreigaben an + +Dynamische Darstellung pro Partner + +Gruppiert nach Maschinenbereichen + +Farben und Styles aus der DB + +Buttons zur Partnerauswahl und Order-Wechsel + +Maschinennutzungsprodukte nur sichtbar, wenn Kunde eingewiesen ist + +Einweisungsvideos können verlinkt sein + +## Datenpflege und Tests +Produkte aus Kategorien „Einweisungen“ und „Maschinennutzung“ werden im Modul als fest installierte Daten gepflegt + +Strukturierte Tests für res.partner und ows.user (Erstellen, Lesen, Schreiben, Sync) + +# 🧩 Mögliche Erweiterungsbereiche (aus deinen bisherigen Ideen) +Dokumentation & Tutorials per DokuWiki-Integration + +Selbstregistrierung von Nutzer:innen (Kioskmodus) + +Zeiterfassung an Maschinen per RFID/IoT (z. B. IoT-Box oder AMC3301) + +Verwaltung von Maschinen-Verbrauchsmaterialien + +Kalenderbuchung & Eventintegration (für Einweisungstermine) + +Automatisiertes Backup & Restore von POS-Umgebungen via CI + +Nutzung von OWL-Komponenten (ab v17) zur modernen POS-Darstellung + +# 📦 Modulstruktur und Philosophie +Das Modul ist modular und gut in Odoo integriert + +Wo möglich, nutzt du bestehende Odoo-Modelle (res.partner, product.template, pos.order) + +Zusätzliche Modelle (ows.machine, ows.machine.access, ows.user, ows.machine.area) kapseln Werkstatt-spezifische Logik + +Fokus auf Datensicherheit, Nachvollziehbarkeit und einfache Erweiterbarkeit + +Frontend-Anpassungen (POS) sind tief integriert, ohne das Standardverhalten zu sehr zu stören diff --git a/views/assets.xml b/views/assets.xml new file mode 100644 index 0000000..2f88e74 --- /dev/null +++ b/views/assets.xml @@ -0,0 +1,8 @@ + +