Cleanup: Remove debug logs, unused backend files, add comprehensive comments

- Removed all console.log debug statements from JavaScript files
- Deleted unnecessary backend models (hr_employee.py, res_users.py, pos_session.py, pos_config.py)
- Added comprehensive German comments explaining the BroadcastChannel architecture
- Updated README with detailed technical documentation about frontend-only approach
- Removed debug info box from customer display template
- Simplified __init__.py files (pure frontend solution)

The module now uses a clean, maintainable frontend-only approach that:
- Patches Chrome component with effect(batched()) for reactive updates
- Sends cashier data via BroadcastChannel (same pattern as order updates)
- Uses hr.employee.public URL format (identical to POS navbar)
- Falls back to company logo when no employee image available
This commit is contained in:
Matthias Lotz 2026-01-10 15:10:51 +01:00
parent daddb51327
commit 1abeb97afa
10 changed files with 86 additions and 178 deletions

View File

@ -1,46 +1,61 @@
# 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.
Dieses Modul passt das POS Customer Display in Odoo 18 an und zeigt das Bild des aktuellen Kassierers 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
- **Responsive Design**: Das Bild wird mit korrektem Seitenverhältnis skaliert (`object-fit: contain`)
- **Fallback**: Bei fehlendem Bild wird das Firmen-Logo 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
1. Modul installieren über Apps
2. **Wichtig**: Stelle sicher, dass in Personal/Mitarbeiter ein Bild für die Kassierer hinterlegt ist
3. POS öffnen und Customer Display starten - das Kassiererbild erscheint automatisch
## Technische Details
### Architektur - Reine Frontend-Lösung
Das Modul verwendet **keine Backend-Anpassungen**, sondern arbeitet komplett im Frontend:
**POS-Seite** (`pos_store.js`):
- Patched die `Chrome` Komponente aus `@point_of_sale/app/pos_app`
- Verwendet `effect(batched())` für reaktive Updates (gleiche Architektur wie Odoo's Order-Updates)
- Sendet Kassierer-Daten via `BroadcastChannel("UPDATE_CUSTOMER_DISPLAY")`
- Nutzt URL-Format: `/web/image/hr.employee.public/{id}/avatar_1920` (identisch zum POS Navbar)
**Customer Display** (`customer_display.js`):
- Lauscht auf `BroadcastChannel` Messages
- Zeigt Mitarbeiterbild mit automatischem Fallback zum Logo
**Template** (`customer_display.xml`):
- Überschreibt den `o_customer_display_sidebar`
- Verwendet `<img>` Tag mit `object-fit: contain` für saubere Skalierung
- Fallback zum Firmen-Logo via `onerror` Handler
### Warum Frontend-only?
Die ursprüngliche Implementierung verwendete Backend-Modelle (`pos_config.py`, `pos_session.py`, etc.), aber:
- Die Daten wurden nicht zuverlässig ins Frontend geladen
- Das Frontend kann direkt auf `pos.get_cashier()` (res.users) zugreifen
- Das `hr.employee.public` Model ist bereits verfügbar
- Eine Frontend-Lösung ist wartungsärmer, performanter und zuverlässiger
### Dateien
- `__manifest__.py` - Modul-Definition mit Abhängigkeiten
- `static/src/js/customer_display.js` - JavaScript-Komponente für Employee-Daten
- `__manifest__.py` - Modul-Definition mit Asset-Bundles
- `static/src/js/pos_store.js` - POS Chrome Patch für Kassierer-Updates
- `static/src/js/customer_display.js` - Customer Display Component
- `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)
- `point_of_sale` (POS System)
- `hr` (Personal/Human Resources für Mitarbeiterbilder)
## Version
18.0.1.0.0
## Lizenz
LGPL-3

View File

@ -1,2 +1,3 @@
# -*- coding: utf-8 -*-
from . import models
# Reine Frontend-Lösung für POS Customer Display Anpassung

View File

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
from . import pos_session
from . import pos_config
from . import hr_employee
from . import res_users
# Keine Backend-Modelle mehr nötig - reine Frontend-Lösung

View File

@ -1,10 +0,0 @@
# -*- 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']

View File

@ -1,41 +0,0 @@
# -*- 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

View File

@ -1,35 +0,0 @@
# -*- 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

View File

@ -1,14 +0,0 @@
# -*- 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

View File

@ -3,49 +3,52 @@
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
/**
* Erweitert das Customer Display um die Anzeige des aktuellen Kassierers.
* Lauscht auf BroadcastChannel Messages von der POS und aktualisiert
* das angezeigte Mitarbeiterbild dynamisch.
*/
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
// Initialisiere Employee-Daten
this.employeeData = null;
// Lausche auf BroadcastChannel für lokale Customer Displays
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
// Trigger re-render um das neue Bild anzuzeigen
this.render();
}
};
}
},
/**
* Gibt die URL des Mitarbeiterbilds zurück.
* Verwendet nur BroadcastChannel-Daten, kein Session-Fallback.
* Wenn keine Daten vorhanden sind, wird das Firmenlogo angezeigt.
*/
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;
return this.employeeData?.employee_image_url ||
this.data?.employeeData?.employee_image_url ||
'';
},
/**
* Gibt den Namen des aktuellen Mitarbeiters zurück.
*/
get currentEmployeeName() {
// Only use BroadcastChannel data, no session fallback
return this.employeeData?.employee_name || this.data?.employeeData?.employee_name || '';
},
/**
* Prüft ob ein Mitarbeiterbild vorhanden ist.
*/
get hasEmployeeImage() {
return Boolean(this.currentEmployeeImage);
}

View File

@ -5,24 +5,23 @@ 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');
/**
* Erweitert die POS Chrome Komponente um die Anzeige des aktuellen Kassierers
* im Customer Display. Verwendet dieselbe Architektur wie Odoo's Order-Updates:
* - effect() mit batched() für reaktive Updates
* - BroadcastChannel für Kommunikation zwischen POS und Customer Display
*/
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)
// Reagiere auf Kassierer-Änderungen (z.B. bei Kassierer-Wechsel)
effect(
batched(({ cashier }) => {
console.log('[Employee Display] Effect triggered! cashier:', cashier);
if (cashier) {
this.sendCashierToCustomerDisplay(cashier);
}
@ -30,12 +29,12 @@ patch(Chrome.prototype, {
[this.pos]
);
// Also send when any order changes (piggyback on order updates)
// This ensures customer display gets cashier data even if opened late
// Sende Kassierer-Daten auch bei jeder Order-Änderung
// Dies stellt sicher, dass das Customer Display die Daten bekommt,
// auch wenn es später geöffnet wird
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);
@ -43,36 +42,33 @@ patch(Chrome.prototype, {
};
},
/**
* Sendet die Kassierer-Informationen an das Customer Display.
* Verwendet die gleiche URL-Struktur wie der POS Navbar für Employee-Bilder.
*
* @param {Object} cashier - Der aktuelle Kassierer (res.users)
*/
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
// Verwende hr.employee.public für das Avatar-Bild
// (gleiche URL wie im POS Navbar verwendet wird)
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
// Füge employeeData zur Order-Display-Data hinzu
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
// Sende via BroadcastChannel (lokal) oder Remote-API
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") {

View File

@ -36,11 +36,6 @@
<t t-esc="currentEmployeeName"/>
</div>
</div>
<!-- Debug Info -->
<div class="position-absolute top-0 start-0 p-2 text-white bg-dark small" style="z-index: 1000;">
Debug: <t t-esc="session.employee_image_url or 'NO IMAGE URL'"/>
</div>
</div>
</xpath>