"
+ html = ""
for area in areas:
- html += f"
"
- html += f"
"
- html += f"| {area.name} | | Datum | Gültig bis |
"
+ html += f"""
+ "
- 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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
diff --git a/views/machine_area_views.xml b/views/machine_area_views.xml
new file mode 100644
index 0000000..e843133
--- /dev/null
+++ b/views/machine_area_views.xml
@@ -0,0 +1,40 @@
+
+
+
+
+ Maschinenbereiche
+ ows.machine.area
+ list,form
+
+
+
+
+
+
+
+ ows.machine.area.tree
+ ows.machine.area
+
+
+
+
+
+
+
+
+
+
+
+ ows.machine.area.form
+ ows.machine.area
+
+
+
+
+
diff --git a/views/machine_product_training_views.xml b/views/machine_product_training_views.xml
new file mode 100644
index 0000000..621c9fe
--- /dev/null
+++ b/views/machine_product_training_views.xml
@@ -0,0 +1,49 @@
+
+
+
+ ows.machine.product.tree
+ ows.machine.product
+
+
+
+
+
+
+
+
+
+
+ ows.machine.training.tree
+ ows.machine.training
+
+
+
+
+
+
+
+
+
+
+ Maschinen-Nutzungsprodukte
+ ows.machine.product
+ list
+
+
+ Verwalte die Zuordnung von Maschinen zu Nutzungsprodukten.
+
+
+
+
+
+ Maschinen-Einweisungsprodukte
+ ows.machine.training
+ list
+
+
+ Verwalte die Zuordnung von Maschinen zu Einweisungsprodukten.
+
+
+
+
+
diff --git a/views/machine_views.xml b/views/machine_views.xml
new file mode 100644
index 0000000..c883fce
--- /dev/null
+++ b/views/machine_views.xml
@@ -0,0 +1,71 @@
+
+
+
+
+ ows.machine.tree
+ ows.machine
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ows.machine.form
+ ows.machine
+
+
+
+
+
diff --git a/views/menu_views.xml b/views/menu_views.xml
new file mode 100644
index 0000000..23285b6
--- /dev/null
+++ b/views/menu_views.xml
@@ -0,0 +1,81 @@
+
+
+
+
+ Maschinen
+ ows.machine
+ list,form
+
+
+
+
+ Einweisungs-Produkte
+ ows.machine.product
+ list,form
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ows.machine.product.tree
+ ows.machine.product
+
+
+
+
+
+
+
+
+
+ ows.machine.product.form
+ ows.machine.product
+
+
+
+
+
diff --git a/views/res_partner_view.xml b/views/res_partner_view.xml
new file mode 100644
index 0000000..0f87640
--- /dev/null
+++ b/views/res_partner_view.xml
@@ -0,0 +1,104 @@
+
+
+
+ res.partner.form.ows.tabs
+ res.partner
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ res.partner.form.ows.birthday
+ res.partner
+
+
+
+
+
+
+
+
+
+
+
+ res.partner.ows.tree
+ res.partner
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ res.partner.form.inherit.default_person
+ res.partner
+
+
+
+ person
+
+
+
+
+
+
+ list,kanban,form,activity
+
+
+
+
+
+
+
+