From daddb51327b46a5cc112d93a28dddc4f2843b6de Mon Sep 17 00:00:00 2001 From: "matthias.lotz" Date: Sat, 10 Jan 2026 14:49:50 +0100 Subject: [PATCH] Add POS customer display customization module - Show current cashier's employee image in customer display sidebar - Image updates automatically when cashier changes - Fallback to company logo when no image available or before first order - Uses BroadcastChannel to sync between POS and customer display - Full-size image display with correct aspect ratio (object-fit: contain) --- open_workshop_pos_customer_display/README.md | 46 ++++++++++ .../__init__.py | 2 + .../__manifest__.py | 31 +++++++ .../models/__init__.py | 5 ++ .../models/hr_employee.py | 10 +++ .../models/pos_config.py | 41 +++++++++ .../models/pos_session.py | 35 ++++++++ .../models/res_users.py | 14 +++ .../static/src/js/customer_display.js | 52 +++++++++++ .../static/src/js/pos_store.js | 87 +++++++++++++++++++ .../static/src/xml/customer_display.xml | 48 ++++++++++ 11 files changed, 371 insertions(+) create mode 100644 open_workshop_pos_customer_display/README.md create mode 100644 open_workshop_pos_customer_display/__init__.py create mode 100644 open_workshop_pos_customer_display/__manifest__.py create mode 100644 open_workshop_pos_customer_display/models/__init__.py create mode 100644 open_workshop_pos_customer_display/models/hr_employee.py create mode 100644 open_workshop_pos_customer_display/models/pos_config.py create mode 100644 open_workshop_pos_customer_display/models/pos_session.py create mode 100644 open_workshop_pos_customer_display/models/res_users.py create mode 100644 open_workshop_pos_customer_display/static/src/js/customer_display.js create mode 100644 open_workshop_pos_customer_display/static/src/js/pos_store.js create mode 100644 open_workshop_pos_customer_display/static/src/xml/customer_display.xml diff --git a/open_workshop_pos_customer_display/README.md b/open_workshop_pos_customer_display/README.md new file mode 100644 index 0000000..af3cbe1 --- /dev/null +++ b/open_workshop_pos_customer_display/README.md @@ -0,0 +1,46 @@ +# Open Workshop - POS Customer Display Customization + +## Beschreibung +Dieses Modul passt das POS Customer Display in Odoo 18 an und zeigt Informationen über den aktuellen Kassierer in der Sidebar an. + +## Features +- **Kassiererbild**: Zeigt das Profilbild des aktuellen Kassierers (aus Personal/HR) in voller Sidebar-Größe +- **Automatische Aktualisierung**: Die Anzeige wird automatisch aktualisiert, wenn der Kassierer wechselt +- **Responsive Design**: Das Bild wird mit korrektem Seitenverhältnis skaliert (cover-Modus) +- **Fallback**: Bei fehlendem Bild wird das User-Avatar oder Company-Logo angezeigt +- **Kassierer-Name**: Optional wird der Name des Kassierers als Overlay am unteren Rand angezeigt + +## Installation +1. Modul in den Addon-Pfad kopieren +2. Apps-Liste aktualisieren +3. Modul installieren +4. **Wichtig**: Stelle sicher, dass in Personal/Mitarbeiter ein Bild für die Kassierer hinterlegt ist + +## Technische Details + +### Dateien +- `__manifest__.py` - Modul-Definition mit Abhängigkeiten +- `static/src/js/customer_display.js` - JavaScript-Komponente für Employee-Daten +- `static/src/xml/customer_display.xml` - Template-Override für die Sidebar + +### Funktionsweise +1. Beim Laden des Customer Displays wird der aktuelle User ermittelt +2. Der zugehörige Employee-Datensatz wird aus `hr.employee` geladen +3. Das Bild wird über `/web/image/hr.employee/{id}/image_1920` angezeigt +4. Bei Kassiererwechsel wird die Komponente neu gerendert + +### Anpassungen +Die Sidebar verwendet folgende CSS-Eigenschaften für das Bild: +- `background-size: cover` - Bild füllt den gesamten Bereich +- `background-position: center` - Bild wird zentriert +- Das Seitenverhältnis wird automatisch beibehalten + +## Abhängigkeiten +- point_of_sale +- hr (Personal/Human Resources) + +## Version +18.0.1.0.0 + +## Lizenz +LGPL-3 diff --git a/open_workshop_pos_customer_display/__init__.py b/open_workshop_pos_customer_display/__init__.py new file mode 100644 index 0000000..a0fdc10 --- /dev/null +++ b/open_workshop_pos_customer_display/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import models diff --git a/open_workshop_pos_customer_display/__manifest__.py b/open_workshop_pos_customer_display/__manifest__.py new file mode 100644 index 0000000..011fc9b --- /dev/null +++ b/open_workshop_pos_customer_display/__manifest__.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +{ + 'name': 'Open Workshop - POS Customer Display Customization', + 'version': '18.0.1.0.0', + 'category': 'Point of Sale', + 'summary': 'Anpassungen am POS Customer Display Sidebar', + 'description': """ + Dieses Modul erweitert das POS Customer Display und passt die Sidebar an. + - Zeigt das Bild des aktuellen Kassierers in der Sidebar an + - Aktualisiert die Anzeige bei Kassiererwechsel + """, + 'author': 'Open Workshop', + 'depends': [ + 'point_of_sale', + 'hr', + ], + 'data': [], + 'assets': { + 'point_of_sale.assets_prod': [ + 'open_workshop_pos_customer_display/static/src/js/pos_store.js', + ], + 'point_of_sale.customer_display_assets': [ + 'open_workshop_pos_customer_display/static/src/js/customer_display.js', + 'open_workshop_pos_customer_display/static/src/xml/customer_display.xml', + ], + }, + 'installable': True, + 'application': False, + 'auto_install': False, + 'license': 'LGPL-3', +} diff --git a/open_workshop_pos_customer_display/models/__init__.py b/open_workshop_pos_customer_display/models/__init__.py new file mode 100644 index 0000000..d003f7d --- /dev/null +++ b/open_workshop_pos_customer_display/models/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +from . import pos_session +from . import pos_config +from . import hr_employee +from . import res_users diff --git a/open_workshop_pos_customer_display/models/hr_employee.py b/open_workshop_pos_customer_display/models/hr_employee.py new file mode 100644 index 0000000..2570d07 --- /dev/null +++ b/open_workshop_pos_customer_display/models/hr_employee.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +from odoo import models + + +class HrEmployee(models.Model): + _inherit = 'hr.employee' + + def _load_pos_data_fields(self, config_id): + """Define which fields to load for hr.employee in POS""" + return ['id', 'name', 'user_id', 'image_1920'] diff --git a/open_workshop_pos_customer_display/models/pos_config.py b/open_workshop_pos_customer_display/models/pos_config.py new file mode 100644 index 0000000..76137c3 --- /dev/null +++ b/open_workshop_pos_customer_display/models/pos_config.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +from odoo import models + + +class PosConfig(models.Model): + _inherit = 'pos.config' + + def _get_customer_display_data(self): + """Erweitere Customer Display Daten um Employee-Informationen""" + result = super()._get_customer_display_data() + + # Hole die aktuelle offene Session für diese POS Config + session = self.env['pos.session'].search([ + ('config_id', '=', self.id), + ('state', '=', 'opened') + ], limit=1) + + if not session: + return result + + # Hole den aktuellen Kassierer aus der Session + # Der aktuelle Kassierer wird in pos.session nicht gespeichert, + # sondern nur im Frontend. Daher verwenden wir den Session-User als Fallback. + user_id = session.user_id.id + + # Hole Employee-Daten für den User + employee = self.env['hr.employee'].search([ + ('user_id', '=', user_id) + ], limit=1) + + if employee and employee.image_1920: + result['employee_id'] = employee.id + result['employee_name'] = employee.name + result['employee_image_url'] = f'/web/image/hr.employee/{employee.id}/image_1920' + else: + # Fallback: User-Avatar + result['employee_id'] = user_id + result['employee_name'] = self.env['res.users'].browse(user_id).name + result['employee_image_url'] = f'/web/image/res.users/{user_id}/avatar_1920' + + return result diff --git a/open_workshop_pos_customer_display/models/pos_session.py b/open_workshop_pos_customer_display/models/pos_session.py new file mode 100644 index 0000000..01c387d --- /dev/null +++ b/open_workshop_pos_customer_display/models/pos_session.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +from odoo import models, api + + +class PosSession(models.Model): + _inherit = 'pos.session' + + @api.model + def _load_pos_data_models(self, config_id): + """Füge hr.employee zu den zu ladenden Modellen hinzu""" + models = super()._load_pos_data_models(config_id) + if 'hr.employee' not in models: + models.append('hr.employee') + return models + + def _load_pos_data(self, data): + """Erweitere Session-Daten um Employee-Informationen""" + result = super()._load_pos_data(data) + + # Hole Employee-Daten des aktuellen Users + employee = self.env['hr.employee'].search([ + ('user_id', '=', self.env.uid) + ], limit=1) + + if employee and employee.image_1920: + result['data'][0]['employee_id'] = employee.id + result['data'][0]['employee_name'] = employee.name + result['data'][0]['employee_image_url'] = f'/web/image/hr.employee/{employee.id}/image_1920' + else: + # Fallback: User-Avatar + result['data'][0]['employee_id'] = self.env.uid + result['data'][0]['employee_name'] = self.env.user.name + result['data'][0]['employee_image_url'] = f'/web/image/res.users/{self.env.uid}/avatar_1920' + + return result diff --git a/open_workshop_pos_customer_display/models/res_users.py b/open_workshop_pos_customer_display/models/res_users.py new file mode 100644 index 0000000..1c0b8c8 --- /dev/null +++ b/open_workshop_pos_customer_display/models/res_users.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +from odoo import models + + +class ResUsers(models.Model): + _inherit = 'res.users' + + def _load_pos_data_fields(self, config_id): + """Define which fields to load for res.users in POS""" + fields = super()._load_pos_data_fields(config_id) + # Add employee_ids to load the related employee + if 'employee_ids' not in fields: + fields.append('employee_ids') + return fields diff --git a/open_workshop_pos_customer_display/static/src/js/customer_display.js b/open_workshop_pos_customer_display/static/src/js/customer_display.js new file mode 100644 index 0000000..ad7f5da --- /dev/null +++ b/open_workshop_pos_customer_display/static/src/js/customer_display.js @@ -0,0 +1,52 @@ +/** @odoo-module */ + +import { patch } from "@web/core/utils/patch"; +import { CustomerDisplay } from "@point_of_sale/customer_display/customer_display"; + +console.log('[Employee Display] customer_display.js loaded'); + +// Patch CustomerDisplay to show employee data +patch(CustomerDisplay.prototype, { + setup() { + super.setup(...arguments); + console.log('[Employee Display] CustomerDisplay.setup(), initial this.data:', this.data); + console.log('[Employee Display] Session:', this.session); + + // Listen to BroadcastChannel to receive employee updates + this.employeeData = null; + if (this.session.type === "local") { + const channel = new BroadcastChannel("UPDATE_CUSTOMER_DISPLAY"); + channel.onmessage = (event) => { + console.log('[Employee Display] ✅ BroadcastChannel message received:', event.data); + if (event.data.employeeData) { + console.log('[Employee Display] Employee data:', event.data.employeeData); + console.log('[Employee Display] Image URL:', event.data.employeeData.employee_image_url); + this.employeeData = event.data.employeeData; + // Trigger re-render + this.render(); + } + }; + } + }, + + get currentEmployeeImage() { + // Only use BroadcastChannel data, no session fallback + // This ensures we show the logo until we get the real cashier data + const image = this.employeeData?.employee_image_url || + this.data?.employeeData?.employee_image_url || + ''; + console.log('[Employee Display] currentEmployeeImage returning:', image); + console.log('[Employee Display] Source: this.employeeData:', this.employeeData?.employee_image_url, + 'this.data:', this.data?.employeeData?.employee_image_url); + return image; + }, + + get currentEmployeeName() { + // Only use BroadcastChannel data, no session fallback + return this.employeeData?.employee_name || this.data?.employeeData?.employee_name || ''; + }, + + get hasEmployeeImage() { + return Boolean(this.currentEmployeeImage); + } +}); diff --git a/open_workshop_pos_customer_display/static/src/js/pos_store.js b/open_workshop_pos_customer_display/static/src/js/pos_store.js new file mode 100644 index 0000000..02e7800 --- /dev/null +++ b/open_workshop_pos_customer_display/static/src/js/pos_store.js @@ -0,0 +1,87 @@ +/** @odoo-module */ + +import { patch } from "@web/core/utils/patch"; +import { Chrome } from "@point_of_sale/app/pos_app"; +import { effect } from "@web/core/utils/reactive"; +import { batched } from "@web/core/utils/timing"; + +console.log('[Employee Display] pos_store.js loaded'); + +patch(Chrome.prototype, { + setup() { + console.log('[Employee Display] Chrome.setup() patched - starting'); + super.setup(...arguments); + + console.log('[Employee Display] customer_display_type:', this.pos.config.customer_display_type); + if (this.pos.config.customer_display_type === "none") { + console.log('[Employee Display] Customer display disabled, skipping'); + return; + } + + console.log('[Employee Display] Setting up cashier effect'); + // Add reactive effect for cashier changes (same pattern as order effect in Chrome) + effect( + batched(({ cashier }) => { + console.log('[Employee Display] Effect triggered! cashier:', cashier); + if (cashier) { + this.sendCashierToCustomerDisplay(cashier); + } + }), + [this.pos] + ); + + // Also send when any order changes (piggyback on order updates) + // This ensures customer display gets cashier data even if opened late + const originalSendOrder = this.sendOrderToCustomerDisplay.bind(this); + this.sendOrderToCustomerDisplay = function(selectedOrder, scaleData) { + originalSendOrder(selectedOrder, scaleData); + // Also send cashier data with every order update + const cashier = this.pos.get_cashier(); + if (cashier) { + this.sendCashierToCustomerDisplay(cashier); + } + }; + }, + + sendCashierToCustomerDisplay(cashier) { + console.log('[Employee Display] sendCashierToCustomerDisplay called with:', cashier); + + if (!cashier) { + console.log('[Employee Display] No cashier, skipping'); + return; + } + + // Use cashier directly - same as POS navbar uses hr.employee.public + const employeeData = { + employee_id: cashier.id, + employee_name: cashier.name, + employee_image_url: `/web/image/hr.employee.public/${cashier.id}/avatar_1920` + }; + + console.log('[Employee Display] Created employeeData:', employeeData); + + // Get current order data and add employee data + const selectedOrder = this.pos.get_order(); + console.log('[Employee Display] Current order:', selectedOrder); + if (selectedOrder) { + const customerDisplayData = selectedOrder.getCustomerDisplayData(); + customerDisplayData.employeeData = employeeData; + console.log('[Employee Display] Sending to customer display:', customerDisplayData); + console.log('[Employee Display] employeeData in object:', customerDisplayData.employeeData); + console.log('[Employee Display] Full object keys:', Object.keys(customerDisplayData)); + + // Send using the same channels as order updates + if (this.pos.config.customer_display_type === "local") { + console.log('[Employee Display] Posting to BroadcastChannel'); + this.customerDisplayChannel.postMessage(customerDisplayData); + } + if (this.pos.config.customer_display_type === "remote") { + this.pos.data.call("pos.config", "update_customer_display", [ + [this.pos.config.id], + customerDisplayData, + this.pos.config.access_token, + ]); + } + } + }, +}); diff --git a/open_workshop_pos_customer_display/static/src/xml/customer_display.xml b/open_workshop_pos_customer_display/static/src/xml/customer_display.xml new file mode 100644 index 0000000..c15c30f --- /dev/null +++ b/open_workshop_pos_customer_display/static/src/xml/customer_display.xml @@ -0,0 +1,48 @@ + + + + + + + +
+ + + + + + + + + + + +
+
+ +
+
+ + +
+ Debug: +
+
+
+ +
+