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:
parent
daddb51327
commit
1abeb97afa
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from . import models
|
||||
# Reine Frontend-Lösung für POS Customer Display Anpassung
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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") {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user