diff --git a/open_workshop/__init__.py b/open_workshop/__init__.py new file mode 100644 index 0000000..d7af4f2 --- /dev/null +++ b/open_workshop/__init__.py @@ -0,0 +1,3 @@ +from . import models +from . import controllers + diff --git a/open_workshop/__manifest__.py b/open_workshop/__manifest__.py new file mode 100644 index 0000000..79cff82 --- /dev/null +++ b/open_workshop/__manifest__.py @@ -0,0 +1,39 @@ +{ + 'name': 'POS Open Workshop', + 'license': 'AGPL-3', + 'version': '18.0.1.0.1', + 'summary': 'Erstellt Maschinenfreigaben basierend auf POS-Einweisungsprodukten', + 'depends': ['base', 'account', 'hr','product','sale','contacts','point_of_sale'], + 'author': 'matthias.lotz', + 'category': 'Point of Sale', + 'data': [ + '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': False, # Wird durch open_workshop_base ersetzt + '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 18.0 konzipiert. + """, +} diff --git a/open_workshop/controllers/__init__.py b/open_workshop/controllers/__init__.py new file mode 100644 index 0000000..4dc11d1 --- /dev/null +++ b/open_workshop/controllers/__init__.py @@ -0,0 +1,3 @@ +# Datei: open_workshop/controllers/__init__.py + +from . import pos_access diff --git a/open_workshop/controllers/pos_access.py b/open_workshop/controllers/pos_access.py new file mode 100644 index 0000000..edd56c4 --- /dev/null +++ b/open_workshop/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/open_workshop/data/data.xml b/open_workshop/data/data.xml new file mode 100644 index 0000000..7fa434d --- /dev/null +++ b/open_workshop/data/data.xml @@ -0,0 +1,153 @@ + + + + Fablab + #008000 + + + + Holzbereich + #ff0000 + + + + Metallbereich + #0000ff + + + + Elektronikbereich + #ffff00 + + + + + Sabako Laser + sabako_laser + + + + + Prusa + prusa + + + + + Prusa MMU + prusa_mmu + + + + + 3D Delta + 3d_delta + + + + + CNC Beamicon + cnc_beamicon + + + + + + Formatkreissäge + formatkreissaege + + + + + Bandsäge + bandsaege_holz + + + + + Abricht Dickenhobel + dickenhobel + + + + + Drechselbank + drechselbank + + + + + Festool Domino Fräse + festool_domino + + + + + Maffel Duo Dübler + maffel_duo + + + + + Lamello Zeta P2 + lamello_zeta_p2 + + + + + + Kreissäge + kreissaege_metall + + + + + Bandsäge + bandsaege_metall + + + + + MIG/MAG Schweißgeräte + mig_mag + + + + + WIG Schweißgerät + wig + + + + + Schweißen allgemein + schweissen_allgemein + + + + + Drehbank + drehbank + + + + + Fräse + fraese + + + + + Abkantbank + abkantbank + + + + + + Lötkolben + loetkolben + + + + diff --git a/open_workshop/models/__init__.py b/open_workshop/models/__init__.py new file mode 100644 index 0000000..9f91cbd --- /dev/null +++ b/open_workshop/models/__init__.py @@ -0,0 +1,5 @@ +from . import ows_models +from . import pos_order + + + diff --git a/open_workshop/models/ows_models.py b/open_workshop/models/ows_models.py new file mode 100644 index 0000000..7be4120 --- /dev/null +++ b/open_workshop/models/ows_models.py @@ -0,0 +1,589 @@ +# -*- coding: utf-8 -*- +# ows_models.py +# Part of Odoo Open Workshop + +from odoo import models, fields, api +from markupsafe import escape as html_escape +import logging +_logger = logging.getLogger(__name__) +_logger.info("✅ ows_models.py geladen") + +class HREmployee(models.Model): + _inherit = 'hr.employee' + + @api.model + def anonymize_for_testsystem(self): + """Benennt Admin-Angestellten um und archiviert alle anderen für das Testsystem.""" + admin_user = self.env['res.users'].search([('name', '=', 'Administrator')], limit=1) + if not admin_user: + _logger.error("[OWS] Administrator-Benutzer nicht gefunden!") + return + + _logger.info(f"[OWS] Administrator-Benutzer gefunden: {admin_user.name} (ID: {admin_user.id})") + + # Suche auch archivierte Employees + admin_employee = self.with_context(active_test=False).search([('user_id', '=', admin_user.id)], limit=1) + + if admin_employee: + # Administrator-Employee reaktivieren und umbenennen + admin_employee.write({ + 'name': 'TESTSYSTEM', + 'job_title': 'Testumgebung', + 'work_email': 'office@hobbyhimmel.de', + 'work_phone': False, + 'active': True, # Reaktivieren falls archiviert + }) + _logger.info(f"[OWS] Admin-Angestellter reaktiviert und umbenannt: {admin_employee.name} (ID: {admin_employee.id})") + else: + _logger.warning("[OWS] Kein Angestellter für Administrator gefunden.") + return + + # Alle anderen Angestellten archivieren (auch bereits archivierte berücksichtigen) + other_employees = self.with_context(active_test=False).search([('id', '!=', admin_employee.id)]) + other_employees.write({'active': False}) + _logger.info("[OWS] %d Angestellte archiviert.", len(other_employees)) + + +class ResPartner(models.Model): + _inherit = 'res.partner' + _logger.info("✅ ows ResPartner geladen") + + ows_user_id = fields.One2many('ows.user', 'partner_id', string="OWS Benutzerdaten") + + # Alte Felder (weiterhin sichtbar für externe Programme) + birthday = fields.Date( + string="Geburtstag", + compute='_compute_ows_user_fields', + inverse='_inverse_birthday', + store=False + ) + rfid_card = fields.Text( + string="RFID Card ID", + compute='_compute_ows_user_fields', + inverse='_inverse_rfid_card', + store=False + ) + security_briefing = fields.Boolean( + string="Haftungsausschluss", + compute='_compute_ows_user_fields', + inverse='_inverse_security_briefing', + store=False + ) + security_id = fields.Text( + string="Haftungsausschluss ID", + compute='_compute_ows_user_fields', + inverse='_inverse_security_id', + store=False + ) + # Neue direkte vvow_* Felder + vvow_birthday = fields.Date(string="Geburtstag (vvow)") + vvow_rfid_card = fields.Text(string="RFID Card ID (vvow)") + vvow_security_briefing = fields.Boolean(string="Haftungsausschluss (vvow)") + vvow_security_id = fields.Text(string="Haftungsausschluss ID (vvow)") + + @api.depends('ows_user_id') + def _compute_ows_user_fields(self): + for partner in self: + user = partner.ows_user_id[0] if partner.ows_user_id else False + partner.birthday = user.birthday if user else False + partner.rfid_card = user.rfid_card if user else False + partner.security_briefing = user.security_briefing if user else False + partner.security_id = user.security_id if user else False + + def _inverse_birthday(self): + for partner in self: + user = partner.ows_user_id[0] if partner.ows_user_id else False + if user: + user.birthday = partner.birthday + + def _inverse_rfid_card(self): + for partner in self: + user = partner.ows_user_id[0] if partner.ows_user_id else False + if user: + user.rfid_card = partner.rfid_card + + def _inverse_security_briefing(self): + for partner in self: + user = partner.ows_user_id[0] if partner.ows_user_id else False + if user: + user.security_briefing = partner.security_briefing + + def _inverse_security_id(self): + for partner in self: + user = partner.ows_user_id[0] if partner.ows_user_id else False + if user: + user.security_id = partner.security_id + + + @api.model_create_multi + def create(self, vals_list): + partners = super().create(vals_list) + for vals, partner in zip(vals_list, partners): + self.env['ows.user'].create({ + 'partner_id': partner.id, + 'birthday': vals.get('birthday') or vals.get('vvow_birthday'), + 'rfid_card': vals.get('rfid_card') or vals.get('vvow_rfid_card'), + 'security_briefing': vals.get('security_briefing') or vals.get('vvow_security_briefing'), + 'security_id': vals.get('security_id') or vals.get('vvow_security_id'), + }) + return partners + + def write(self, vals): + res = super().write(vals) + for partner in self: + user = partner.ows_user_id[0] if partner.ows_user_id else False + if not user: + continue + + # Synchronisation alt -> user + if 'birthday' in vals: + user.birthday = vals['birthday'] + if 'rfid_card' in vals: + user.rfid_card = vals['rfid_card'] + if 'security_briefing' in vals: + user.security_briefing = vals['security_briefing'] + if 'security_id' in vals: + user.security_id = vals['security_id'] + + # Synchronisation vvow_* -> user + alt + if 'vvow_birthday' in vals: + user.birthday = vals['vvow_birthday'] + partner.birthday = vals['vvow_birthday'] + if 'vvow_rfid_card' in vals: + user.rfid_card = vals['vvow_rfid_card'] + partner.rfid_card = vals['vvow_rfid_card'] + if 'vvow_security_briefing' in vals: + user.security_briefing = vals['vvow_security_briefing'] + partner.security_briefing = vals['vvow_security_briefing'] + if 'vvow_security_id' in vals: + user.security_id = vals['vvow_security_id'] + partner.security_id = vals['vvow_security_id'] + return res + + machine_access_ids = fields.One2many( + 'ows.machine.access', + 'partner_id', + string='Maschinenfreigaben' + ) + + machine_access_html = fields.Html( + string="Maschinenfreigabe", + compute="_compute_machine_access_html", + sanitize=False + ) + + + @api.depends('machine_access_ids') + def _compute_machine_access_html(self): + areas = self.env['ows.machine.area'].search([], order="name") + for partner in self: + html = "" + for area in areas: + html += f""" +
+

{area.name}

+ + + + + + + + + + + """ + + 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 "-" + + html += f""" + + + + + + + """ + + html += "
MaschineStatusDatumGültig bis
{machine.name}{icon}{date_granted}{date_expiry}
" + + partner.machine_access_html = html + + + + + @api.model + def migrate_existing_partners(self): + """ + Erstellt für alle vorhandenen res.partner einen ows.user, + wenn noch keiner existiert, und übernimmt alte vvow_* Felder. + Führt am Ende automatisch einen commit durch. + + Verwendung in der odoo shell: + env['res.partner'].migrate_existing_partners() + env['ows.user'].search_count([]) + env['ows.user'].search([]).mapped('partner_id.name') + """ + migrated = 0 + skipped = 0 + + partners = self.env['res.partner'].search([]) + for partner in partners: + if partner.ows_user_id: + skipped += 1 + continue + + # Werte lesen (werden evtl. durch _inherit hinzugefügt) + vals = { + 'partner_id': partner.id, + 'birthday': getattr(partner, 'vvow_birthday', False), + 'rfid_card': getattr(partner, 'vvow_rfid_card', False), + 'security_briefing': getattr(partner, 'vvow_security_briefing', False), + 'security_id': getattr(partner, 'vvow_security_id', False), + } + self.env['ows.user'].create(vals) + migrated += 1 + _logger.info(f"Erzeuge ows.user für Partner {partner.id} ({partner.name}) mit Werten: {vals}") + + _logger.info(f"[OWS Migration] ✅ Migriert: {migrated}, ❌ Übersprungen: {skipped}") + + # 🔐 Commit am Ende + self.env.cr.commit() + + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': "Migration abgeschlossen", + 'message': f"{migrated} Partner migriert, {skipped} übersprungen.", + 'sticky': False, + } + } + + @api.model + def migrate_machine_access_from_old_fields(self): + """ + Migriert alte vvow_* Boolean-Felder in strukturierte ows.machine.access Einträge. + Das Freigabe-Datum wird aus dem Änderungsverlauf (mail.message.tracking_value_ids) extrahiert. + """ + mapping = [ + ('vvow_holz_sander', 'bandschleifer'), + ('vvow_holz_felder_bandsaw', 'bandsaege_holz'), + ('vvow_holz_felder_jointer', 'dickenhobel'), + ('vvow_holz_felder_mill', 'felder_fraese'), + ('vvow_holz_felder_tablesaw', 'formatkreissaege'), + ('vvow_holz_mill', 'tischfraese'), + ('vvow_holz_lathe', 'drechselbank'), + ('vvow_holz_domino', 'festool_domino'), + ('vvow_holz_duoduebler', 'maffel_duo'), + ('vvow_holz_lamello', 'lamello_zeta_p2'), + ('vvow_fablab_laser_sabko', 'sabako_laser'), + ('vvow_fablab_3dprint_delta', '3d_delta'), + ('vvow_fablab_3dprint_prusa', 'prusa'), + ('vvow_fablab_3dprint_prusa_mmu', 'prusa_mmu'), + ('vvow_fablab_cnc_beamicon', 'cnc_beamicon'), + ('vvow_metall_welding_mig', 'mig_mag'), + ('vvow_metall_welding_wig', 'wig'), + ('vvow_metall_welding_misc', 'schweissen_allgemein'), + ('vvow_metall_lathe', 'drehbank'), + ('vvow_metall_bandsaw', 'bandsaege_metall'), + ('vvow_metall_mill_fp2', 'fraese'), + ('vvow_metall_bende', 'abkantbank'), + ('vvow_metall_chop_saw', 'kreissaege_metall'), + ] + + MachineAccess = self.env['ows.machine.access'] + MailMessage = self.env['mail.message'] + TrackingValue = self.env['mail.tracking.value'] + + count_created = 0 + + for partner in self.search([]): + for field_name, machine_code in mapping: + if not getattr(partner, field_name, False): + continue + + machine = self.env['ows.machine'].search([('code', '=', machine_code)], limit=1) + if not machine: + continue + + # Änderungsverlauf durchsuchen: Wann wurde das Feld auf True gesetzt? + tracking = TrackingValue.search([ + ('field', '=', field_name), + ('field_type', '=', 'boolean'), + ('new_value_integer', '=', 1), + ('mail_message_id.model', '=', 'res.partner'), + ('mail_message_id.res_id', '=', partner.id), + ], order='id ASC', limit=1) + + date_granted = tracking.mail_message_id.date if tracking else fields.Datetime.now() + + if not MachineAccess.search([('partner_id', '=', partner.id), ('machine_id', '=', machine.id)]): + MachineAccess.create({ + 'partner_id': partner.id, + 'machine_id': machine.id, + 'date_granted': date_granted, + }) + count_created += 1 + + _logger.info(f"[OWS Migration] ✅ Maschinenfreigaben erstellt: {count_created}") + self.env.cr.commit() + + @api.model + def archive_partners_without_users(self): + """ + Archiviert alle Partner (res.partner), die keine Benutzer (res.users) sind. + """ + Partner = self.env['res.partner'] + User = self.env['res.users'] + + # IDs aller Partner, die ein Benutzerkonto haben + user_partner_ids = User.search([]).mapped('partner_id').ids + + # Alle Partner ohne Benutzerkonto + partners_to_archive = Partner.search([ + ('id', 'not in', user_partner_ids), + ('active', '=', True), + ]) + + count = len(partners_to_archive) + partners_to_archive.write({'active': False}) + for p in partners_to_archive: + _logger.debug(f"[OWS] Archiviert Partner: {p.name} (ID {p.id})") + _logger.info(f"[OWS] Archiviert {count} Partner ohne Benutzerkonto.") + self.env.cr.commit() + + +class OwsUser(models.Model): + _name = 'ows.user' + _description = 'OWS: Benutzerdaten' + _table = 'ows_user' + _logger.info("✅ ows_user geladen") + + partner_id = fields.Many2one( + 'res.partner', + required=True, + ondelete='cascade', + string='Kontakt' + ) + + birthday = fields.Date('Geburtstag') + rfid_card = fields.Text('RFID Card ID') + security_briefing = fields.Boolean('Haftungsausschluss', default=False) + security_id = fields.Text('Haftungsausschluss ID') + + _sql_constraints = [ + ('partner_unique', 'unique(partner_id)', 'Jeder Partner darf nur einen OWS-Datensatz haben.') + ] + + +AVAILABLE_COLORS = [ + ('#000000', 'schwarz'), + ('#ff0000', 'Rot'), + ('#E91E63', 'Pink'), + ('#9C27B0', 'Lila'), + ('#3F51B5', 'Indigo'), + ('#0000ff', 'Blau'), + ('#008000', 'Grün'), + ('#ffff00', 'Gelb'), + ('#FF9800', 'Orange'), + ('#795548', 'Braun'), + ('#ffffff', 'Weiss'), +] + +class OwsMachineArea(models.Model): + _name = 'ows.machine.area' + _table = 'ows_machine_area' + _description = 'OWS: Maschinenbereich' + _order = 'name' + + name = fields.Char(string="Name", required=True, translate=True) + + color_hex = fields.Selection( + selection=AVAILABLE_COLORS, + string="Farbe (Hex)", + required=True, + ) + + color_hex_value = fields.Char( + string="Farbcode", + compute='_compute_color_hex_value', + store=False + ) + + color_name = fields.Char( + string="Farbname", + compute='_compute_color_name', + store=False + ) + + @api.depends('color_hex') + def _compute_color_hex_value(self): + for rec in self: + rec.color_hex_value = rec.color_hex or '' + + @api.depends('color_hex') + def _compute_color_name(self): + label_dict = dict(AVAILABLE_COLORS) + for rec in self: + rec.color_name = label_dict.get(rec.color_hex, 'Unbekannt') + + + +class OwsMachine(models.Model): + _name = 'ows.machine' + _table = 'ows_machine' + _description = 'OWS: Maschine' + + name = fields.Char(required=True, translate=True) + code = fields.Char(required=True, help="Eindeutiger Kurzcode, z.B. 'lasercutter'") + category = fields.Selection([ + ('green', 'Kategorie 1: grün'), + ('yellow', 'Kategorie 2: gelb'), + ('red', 'Kategorie 3: rot'), + ], string="Sicherheitskategorie", required=True, default='red', help="Sicherheitsrelevante Maschinenkategorie:\n" + "- grün: keine Einweisungspflicht\n" + "- gelb: empfohlene Einweisung\n" + "- rot: Einweisung zwingend erforderlich") + + category_icon = fields.Char(string="Kategorie-Symbol", compute="_compute_category_icon", store=False) + + @api.depends('category') + def _compute_category_icon(self): + for rec in self: + icon_map = { + 'green': '🟢', + 'yellow': '🟡', + 'red': '🔴', + } + rec.category_icon = icon_map.get(rec.category, '⚪') + + description = fields.Text() + active = fields.Boolean(default=True) + 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="Liste der Nutzungsprodukte", compute="_compute_product_using_names", store=False,) + training_ids = fields.One2many('ows.machine.training', 'machine_id', string="Einweisungsprodukte") + training_names = fields.Char(string="Liste der Einweisungsprodukte", compute="_compute_product_training_names", store=False,) + storage_location = fields.Char(string="Lagerort", help="Lagerort der Maschine oder des Geräts.") + purchase_price = fields.Float(string="Kaufpreis", help="Kaufpreis der Maschine oder des Geräts.") + purchase_date = fields.Date(string="Kaufdatum", help="Kaufdatum der Maschine oder des Geräts.") + + @api.depends('product_ids.product_id.name') + def _compute_product_using_names(self): + for machine in self: + names = machine.product_ids.mapped('product_id.name') + machine.product_names = ", ".join(names) + + @api.depends('training_ids.training_id.name') + def _compute_product_training_names(self): + for machine in self: + names = machine.training_ids.mapped('training_id.name') + machine.training_names = ", ".join(names) + + _sql_constraints = [ + ('code_unique', 'unique(code)', 'Maschinencode muss eindeutig sein.') + ] + + def name_get(self): + return [(rec.id, f"{rec.name} ({rec.code})") for rec in self] + + @api.model + def get_access_list_grouped(self, partner_id): + """ + Gibt eine gruppierte Liste von Maschinenzugängen für einen bestimmten Partner zurück. Diese Funktion wird in + Odoo POS Frontend verwendet um die Ansicht zu erzeugen auf Welche Maschinen der Partner Zugriff hat. + + Für einen gegebenen Partner (über die partner_id) werden alle Maschinenbereiche (areas) abgefragt. + Für jeden Bereich wird geprüft, auf welche Maschinen der Partner Zugriff hat. Das Ergebnis wird + als Liste von Bereichen mit jeweils zugehörigen Maschinen und Zugriffsstatus zurückgegeben. + + Zusätzlich werden sicherheitsrelevante Informationen des Partners (wie Sicherheitsunterweisung, + Sicherheits-ID, RFID-Karte und Geburtstag) aus dem zugehörigen ows_user ermittelt und mitgeliefert. + + Args: + partner_id (int): Die ID des Partners, für den die Zugriffsübersicht erstellt werden soll. + + Returns: + dict: Ein Dictionary mit folgenden Schlüsseln: + - 'access_by_area': Liste von Bereichen mit Maschinen und Zugriffsstatus. + - 'security_briefing': Sicherheitsunterweisung des Nutzers (bool oder False). + - 'security_id': Sicherheits-ID des Nutzers (str oder ''). + - 'rfid_card': RFID-Kartennummer des Nutzers (str oder ''). + - 'birthday': Geburtstag des Nutzers (str oder ''). + """ + partner = self.env['res.partner'].browse(partner_id) + areas = self.env['ows.machine.area'].search([], order="name") + _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), ('category', '=', 'red')], order="name") + machine_list = [] + for machine in machines: + has_access = bool(self.env['ows.machine.access'].search([ + ('partner_id', '=', partner_id), + ('machine_id', '=', machine.id), + ], limit=1)) + machine_list.append({ + 'name': machine.name, + 'has_access': has_access, + }) + if machine_list: + access_by_area.append({ + 'area': area.name, + 'color_hex': area.color_hex or '#000000', + 'machines': machine_list + }) + + user = partner.ows_user_id[:1] + return { + 'access_by_area': access_by_area, + 'security_briefing': user.security_briefing if user else False, + 'security_id': user.security_id if user else '', + 'rfid_card': user.rfid_card if user else '', + 'birthday': user.birthday if user else '', + } + + + +class OwsMachineAccess(models.Model): + _name = 'ows.machine.access' + _table = 'ows_machine_access' + _description = 'OWS: Maschinenfreigabe' + _order = 'partner_id, machine_id' + + partner_id = fields.Many2one('res.partner', required=True, ondelete='cascade') + machine_id = fields.Many2one('ows.machine', required=True) + date_granted = fields.Date(default=fields.Date.today) + date_expiry = fields.Date(string="Ablaufdatum") + granted_by_pos = fields.Boolean(default=True) + + _sql_constraints = [ + ('partner_machine_unique', 'unique(partner_id, machine_id)', 'Der Kunde hat diese Freigabe bereits.') + ] + + +class OwsMachineProduct(models.Model): + _name = 'ows.machine.product' + _table = 'ows_machine_product' + _description = 'OWS: 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') + +class OwsMachineTraining(models.Model): + _name = 'ows.machine.training' + _table = 'ows_machine_training' + _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/open_workshop/models/pos_order.py b/open_workshop/models/pos_order.py new file mode 100644 index 0000000..326b719 --- /dev/null +++ b/open_workshop/models/pos_order.py @@ -0,0 +1,50 @@ +from odoo import models, fields, api +from collections import defaultdict + +#import debugpy +import logging + +_logger = logging.getLogger(__name__) +_logger.info("✅ pos_order.py geladen Test 2") + +#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, existing_order): + _logger.info("🚨 DEBUG: _process_order wurde aufgerufen mit order: %s", order.get('name', 'unbekannt')) + pos_order_id = super(PosOrder, self)._process_order(order, 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/open_workshop/security/ir.model.access.csv b/open_workshop/security/ir.model.access.csv new file mode 100644 index 0000000..022703c --- /dev/null +++ b/open_workshop/security/ir.model.access.csv @@ -0,0 +1,8 @@ +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,ows.machine.training,model_ows_machine_training,base.group_user,1,1,1,1 diff --git a/open_workshop/static/src/css/category_color.css b/open_workshop/static/src/css/category_color.css new file mode 100644 index 0000000..154419d --- /dev/null +++ b/open_workshop/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/open_workshop/static/src/css/pos.css b/open_workshop/static/src/css/pos.css new file mode 100644 index 0000000..cfdfc4d --- /dev/null +++ b/open_workshop/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/open_workshop/static/src/js/ows_machine_access_list.js b/open_workshop/static/src/js/ows_machine_access_list.js new file mode 100644 index 0000000..f3ecc0f --- /dev/null +++ b/open_workshop/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/open_workshop/static/src/js/ows_pos_customer_sidebar.js b/open_workshop/static/src/js/ows_pos_customer_sidebar.js new file mode 100644 index 0000000..b14b7b6 --- /dev/null +++ b/open_workshop/static/src/js/ows_pos_customer_sidebar.js @@ -0,0 +1,54 @@ +// @odoo-module ows_pos_customer_sidebar.js + +import { Component } from "@odoo/owl"; +import { useService } from "@web/core/utils/hooks"; +import { usePos } from "@point_of_sale/app/store/pos_hook"; +import { _t } from "@web/core/l10n/translation"; +import { ask } from "@web/core/confirmation_dialog/confirmation_dialog"; + +export class OwsPosCustomerSidebar extends Component { + static template = "open_workshop.OwsPosCustomerSidebar"; + + setup() { + this.pos = usePos(); // ✅ Holt dir Zugriff auf den zentralen POS-Store + this.dialog = useService("dialog"); + } + + addOrder() { + this.pos.add_new_order(); // ✅ Neue Order wird aktive Order + this.pos.showScreen("ProductScreen"); + this.pos.selectPartner(); + this.env.bus.trigger('partner-changed'); // ✅ Event manuell auslösen + } + + async removeCurrentOrder() { + this.pos.onDeleteOrder(this.pos.get_order()) + } + + openTicketScreen() { + this.pos.showScreen("TicketScreen"); + } + + // 🔧 FIXED: Zugriff auf Order-Liste korrigiert + getFilteredOrderList() { + return this.pos.get_open_orders(); + } + + getDate(order) { + const date = new Date(order.date_order); + const dd = String(date.getDate()).padStart(2, '0'); + const mm = String(date.getMonth() + 1).padStart(2, '0'); + const hh = String(date.getHours()).padStart(2, '0'); + const mi = String(date.getMinutes()).padStart(2, '0'); + return `${dd}.${mm}. ${hh}:${mi}`; + } + + getPartner(order) { + return order.get_partner()?.name || "Kein Kunde"; + } + + selectOrder(order) { + this.pos.set_order(order); + this.env.bus.trigger('partner-changed'); + } +} diff --git a/open_workshop/static/src/js/ows_pos_sidebar.js b/open_workshop/static/src/js/ows_pos_sidebar.js new file mode 100644 index 0000000..dfdc8e3 --- /dev/null +++ b/open_workshop/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/open_workshop/static/src/js/ows_product_screen_template_patch.js b/open_workshop/static/src/js/ows_product_screen_template_patch.js new file mode 100644 index 0000000..29690f3 --- /dev/null +++ b/open_workshop/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/open_workshop/static/src/xml/ows_machine_access_list.xml b/open_workshop/static/src/xml/ows_machine_access_list.xml new file mode 100644 index 0000000..787441b --- /dev/null +++ b/open_workshop/static/src/xml/ows_machine_access_list.xml @@ -0,0 +1,76 @@ + +
+ + + +
+
    +
  • Einweisungen
  • +
+ +
+
    +
  • + + Werkstatt +
  • + +
  • + + + + + + + Haftungsausschluss +
  • + + +
  • +
      + ‼️Bitte Prüfen‼️ +
    +
  • +
    + + +
      +
    • + Id: + + + +
    • +
    • + Geburtstag: + + + +
    • +
    +
    +
+
+
+
+ + + + +
+
    + +
  • + + + + +
  • +
    +
+
+
+
+ +
+
diff --git a/open_workshop/static/src/xml/ows_pos_customer_sidebar.xml b/open_workshop/static/src/xml/ows_pos_customer_sidebar.xml new file mode 100644 index 0000000..31f5e35 --- /dev/null +++ b/open_workshop/static/src/xml/ows_pos_customer_sidebar.xml @@ -0,0 +1,27 @@ + + + +
+
+ +
+ + +
+
+
+ + +
+ +
+
+
+
+
+
diff --git a/open_workshop/static/src/xml/ows_pos_sidebar.xml b/open_workshop/static/src/xml/ows_pos_sidebar.xml new file mode 100644 index 0000000..1777b19 --- /dev/null +++ b/open_workshop/static/src/xml/ows_pos_sidebar.xml @@ -0,0 +1,9 @@ + + + +
+ + +
+
+
\ No newline at end of file diff --git a/open_workshop/static/src/xml/ows_product_screen_template_patch.xml b/open_workshop/static/src/xml/ows_product_screen_template_patch.xml new file mode 100644 index 0000000..e243e82 --- /dev/null +++ b/open_workshop/static/src/xml/ows_product_screen_template_patch.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/open_workshop/views/assets.xml b/open_workshop/views/assets.xml new file mode 100644 index 0000000..2f88e74 --- /dev/null +++ b/open_workshop/views/assets.xml @@ -0,0 +1,8 @@ + +