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"""
+
"
+
+ 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_base/static/src/css/pos.css b/open_workshop/static/src/css/pos.css
similarity index 100%
rename from open_workshop_base/static/src/css/pos.css
rename to open_workshop/static/src/css/pos.css
diff --git a/open_workshop_base/static/src/js/ows_machine_access_list.js b/open_workshop/static/src/js/ows_machine_access_list.js
similarity index 92%
rename from open_workshop_base/static/src/js/ows_machine_access_list.js
rename to open_workshop/static/src/js/ows_machine_access_list.js
index 6bcd0ee..f3ecc0f 100644
--- a/open_workshop_base/static/src/js/ows_machine_access_list.js
+++ b/open_workshop/static/src/js/ows_machine_access_list.js
@@ -7,7 +7,7 @@ import { rpc } from "@web/core/network/rpc";
import { registry } from "@web/core/registry";
export class OwsMachineAccessList extends Component {
- static template = 'open_workshop_base.OwsMachineAccessList';
+ static template = 'open_workshop.OwsMachineAccessList';
setup() {
this.pos = usePos();
@@ -68,5 +68,5 @@ export class OwsMachineAccessList extends Component {
-registry.category("templates").add("open_workshop_base.OwsMachineAccessList", OwsMachineAccessList);
+registry.category("templates").add("open_workshop.OwsMachineAccessList", OwsMachineAccessList);
diff --git a/open_workshop_base/static/src/js/ows_pos_customer_sidebar.js b/open_workshop/static/src/js/ows_pos_customer_sidebar.js
similarity index 96%
rename from open_workshop_base/static/src/js/ows_pos_customer_sidebar.js
rename to open_workshop/static/src/js/ows_pos_customer_sidebar.js
index 3c9e987..b14b7b6 100644
--- a/open_workshop_base/static/src/js/ows_pos_customer_sidebar.js
+++ b/open_workshop/static/src/js/ows_pos_customer_sidebar.js
@@ -7,7 +7,7 @@ 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_base.OwsPosCustomerSidebar";
+ static template = "open_workshop.OwsPosCustomerSidebar";
setup() {
this.pos = usePos(); // ✅ Holt dir Zugriff auf den zentralen POS-Store
diff --git a/open_workshop_base/static/src/js/ows_pos_sidebar.js b/open_workshop/static/src/js/ows_pos_sidebar.js
similarity index 85%
rename from open_workshop_base/static/src/js/ows_pos_sidebar.js
rename to open_workshop/static/src/js/ows_pos_sidebar.js
index 962fe7a..dfdc8e3 100644
--- a/open_workshop_base/static/src/js/ows_pos_sidebar.js
+++ b/open_workshop/static/src/js/ows_pos_sidebar.js
@@ -6,6 +6,6 @@ import { OwsPosCustomerSidebar } from "./ows_pos_customer_sidebar";
import { OwsMachineAccessList } from "./ows_machine_access_list";
export class OwsPosSidebar extends Component {
- static template = "open_workshop_base.OwsPosSidebar";
+ static template = "open_workshop.OwsPosSidebar";
static components = { OwsPosCustomerSidebar, OwsMachineAccessList };
}
\ No newline at end of file
diff --git a/open_workshop_base/static/src/js/ows_product_screen_template_patch.js b/open_workshop/static/src/js/ows_product_screen_template_patch.js
similarity index 100%
rename from open_workshop_base/static/src/js/ows_product_screen_template_patch.js
rename to open_workshop/static/src/js/ows_product_screen_template_patch.js
diff --git a/open_workshop_base/static/src/xml/ows_machine_access_list.xml b/open_workshop/static/src/xml/ows_machine_access_list.xml
similarity index 98%
rename from open_workshop_base/static/src/xml/ows_machine_access_list.xml
rename to open_workshop/static/src/xml/ows_machine_access_list.xml
index 0b2f64f..787441b 100644
--- a/open_workshop_base/static/src/xml/ows_machine_access_list.xml
+++ b/open_workshop/static/src/xml/ows_machine_access_list.xml
@@ -1,4 +1,4 @@
-
+
diff --git a/open_workshop_base/static/src/xml/ows_pos_customer_sidebar.xml b/open_workshop/static/src/xml/ows_pos_customer_sidebar.xml
similarity index 95%
rename from open_workshop_base/static/src/xml/ows_pos_customer_sidebar.xml
rename to open_workshop/static/src/xml/ows_pos_customer_sidebar.xml
index fc6a596..31f5e35 100644
--- a/open_workshop_base/static/src/xml/ows_pos_customer_sidebar.xml
+++ b/open_workshop/static/src/xml/ows_pos_customer_sidebar.xml
@@ -1,6 +1,6 @@
-
+