Umstellung: Employee Image Generator speichert nur Foto, Customer Display baut Badge dynamisch
Employee Image Generator: - Speichert nur zugeschnittenes 369×492px Foto (nicht mehr A4-Badge) - Speichert job_focus zusätzlich zu image_1920 - html2canvas Dependency entfernt - Vorschau bleibt zur Visualisierung Customer Display: - Baut Badge dynamisch aus Employee-Daten (Foto, Name, job_focus) - Responsive Design basierend auf thekenheld print.html - Neues employee_badge.css für Screen-Optimierung - Footer bleibt immer am unteren Bildschirmrand - job_focus mit max-height und overflow für lange Texte - Lädt job_focus über hr.employee ID (nicht user_id) Vorteile: Flexibler, aktuelle Daten, kleinere Dateien, wiederverwendbares Foto
This commit is contained in:
parent
98c96018d6
commit
00df958985
|
|
@ -3,28 +3,29 @@
|
|||
'name': 'Open Workshop - Employee Image Generator',
|
||||
'version': '18.0.1.0.0',
|
||||
'category': 'Human Resources',
|
||||
'summary': 'Generate professional employee name badges with photo cropping',
|
||||
'summary': 'Upload and crop employee photos (369x492) with job focus',
|
||||
'description': """
|
||||
Employee Image Generator
|
||||
=========================
|
||||
|
||||
Creates professional employee name badges (Thekenschilder) directly in Odoo.
|
||||
Upload and crop employee photos directly in Odoo.
|
||||
|
||||
Features:
|
||||
---------
|
||||
* Upload and crop employee photos with fixed aspect ratio (369x492)
|
||||
* Add employee name and job description
|
||||
* Upload and crop employee photos to fixed 369x492 pixels
|
||||
* Add job focus areas (Schwerpunkte)
|
||||
* Integrated Cropper.js for professional image editing
|
||||
* Fixed crop frame - only image moves and zooms
|
||||
* Direct integration in Employee form
|
||||
* Replaces external thekenheld webapp
|
||||
* Saved photo can be used in POS Customer Display
|
||||
|
||||
Usage:
|
||||
------
|
||||
1. Go to Employee form
|
||||
2. Click "Namensschild erstellen" button
|
||||
3. Upload photo and crop it
|
||||
4. Enter name and job focus areas
|
||||
5. Save - the generated image becomes the employee avatar
|
||||
3. Upload photo, zoom and position it in fixed crop frame
|
||||
4. Enter job focus areas (optional)
|
||||
5. Save - photo and job_focus are saved to employee
|
||||
""",
|
||||
'author': 'Open Workshop',
|
||||
'website': 'https://www.open-workshop.de',
|
||||
|
|
@ -39,9 +40,8 @@
|
|||
'open_workshop_employee_imagegenerator/static/lib/cropperjs/cropper.css',
|
||||
'open_workshop_employee_imagegenerator/static/src/css/employee_image_widget.css',
|
||||
'open_workshop_employee_imagegenerator/static/src/css/badge_template.css',
|
||||
# Libraries
|
||||
# Cropper.js Library
|
||||
'open_workshop_employee_imagegenerator/static/lib/cropperjs/cropper.js',
|
||||
'open_workshop_employee_imagegenerator/static/lib/html2canvas.min.js',
|
||||
# Our custom widget
|
||||
'open_workshop_employee_imagegenerator/static/src/js/employee_image_widget.js',
|
||||
'open_workshop_employee_imagegenerator/static/src/xml/employee_image_widget.xml',
|
||||
|
|
|
|||
|
|
@ -166,46 +166,25 @@ class EmployeeImageGenerator extends Component {
|
|||
// Keine zusätzliche Logik nötig - OWL reactive state macht das automatisch!
|
||||
}
|
||||
|
||||
// Schritt 4: Finale Speicherung - Konvertiere HTML-Template zu Bild mit html2canvas
|
||||
// Schritt 4: Finale Speicherung - Speichere nur das zugeschnittene Foto + job_focus
|
||||
async saveFinalImage() {
|
||||
try {
|
||||
const badgeElement = this.badgeTemplate.el;
|
||||
if (!badgeElement) {
|
||||
this.notification.add('Badge-Template nicht gefunden', { type: 'danger' });
|
||||
// Verwende das bereits zugeschnittene Foto (369×492px)
|
||||
if (!this.state.croppedImageDataUrl) {
|
||||
this.notification.add('Kein zugeschnittenes Foto vorhanden', { type: 'danger' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Entferne temporär die Skalierung vom Wrapper (nicht vom Badge selbst)
|
||||
const wrapper = badgeElement.parentElement;
|
||||
const originalTransform = wrapper ? wrapper.style.transform : '';
|
||||
if (wrapper) {
|
||||
wrapper.style.transform = 'none';
|
||||
}
|
||||
|
||||
// Konvertiere HTML-Element zu Canvas mit html2canvas in voller A4-Größe
|
||||
const canvas = await window.html2canvas(badgeElement, {
|
||||
width: this.badgeWidth,
|
||||
height: this.badgeHeight,
|
||||
scale: 1, // Wichtig: keine zusätzliche Skalierung
|
||||
backgroundColor: '#ffffff',
|
||||
});
|
||||
|
||||
// Stelle die Skalierung für die Vorschau wieder her
|
||||
if (wrapper) {
|
||||
wrapper.style.transform = originalTransform;
|
||||
}
|
||||
|
||||
// Konvertiere Canvas zu Base64 PNG
|
||||
const finalImageBase64 = canvas.toDataURL('image/png');
|
||||
|
||||
// Speichere in Odoo (entferne data:image/png;base64, prefix)
|
||||
const imageData = finalImageBase64.split(',')[1];
|
||||
// Extrahiere Base64-Daten (entferne data:image/jpeg;base64, prefix)
|
||||
const imageData = this.state.croppedImageDataUrl.split(',')[1];
|
||||
|
||||
// Speichere Foto + job_focus in Odoo
|
||||
await this.orm.write('hr.employee', [this.state.employeeId], {
|
||||
image_1920: imageData,
|
||||
job_focus: this.state.jobFocus || '',
|
||||
});
|
||||
|
||||
this.notification.add('Namensschild erfolgreich gespeichert!', {
|
||||
this.notification.add('Foto erfolgreich gespeichert!', {
|
||||
type: 'success',
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -3,11 +3,21 @@
|
|||
'name': 'Open Workshop - POS Customer Display Customization',
|
||||
'version': '18.0.1.0.0',
|
||||
'category': 'Point of Sale',
|
||||
'summary': 'Anpassungen am POS Customer Display Sidebar',
|
||||
'summary': 'Show employee badge (Thekenschild) in POS Customer Display',
|
||||
'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
|
||||
POS Customer Display - Employee Badge
|
||||
======================================
|
||||
|
||||
Zeigt ein dynamisches Mitarbeiter-Namensschild im POS Customer Display.
|
||||
|
||||
Features:
|
||||
---------
|
||||
* Baut Namensschild dynamisch aus Employee-Daten auf
|
||||
* Zeigt Mitarbeiterfoto (von open_workshop_employee_imagegenerator)
|
||||
* Zeigt Name und Schwerpunkte (job_focus)
|
||||
* Responsive Design basierend auf thekenheld print.html
|
||||
* Aktualisiert automatisch bei Kassiererwechsel
|
||||
* Fallback auf Company-Logo wenn kein Foto vorhanden
|
||||
""",
|
||||
'author': 'Open Workshop',
|
||||
'depends': [
|
||||
|
|
@ -20,6 +30,7 @@
|
|||
'open_workshop_pos_customer_display/static/src/js/pos_store.js',
|
||||
],
|
||||
'point_of_sale.customer_display_assets': [
|
||||
'open_workshop_pos_customer_display/static/src/css/employee_badge.css',
|
||||
'open_workshop_pos_customer_display/static/src/js/customer_display.js',
|
||||
'open_workshop_pos_customer_display/static/src/xml/customer_display.xml',
|
||||
],
|
||||
|
|
|
|||
|
|
@ -0,0 +1,109 @@
|
|||
/* Employee Badge Design - Basierend auf thekenheld print.html */
|
||||
/* Responsive Version für Customer Display - Screen-optimiert */
|
||||
|
||||
.employee-badge-container {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
background: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.employee-badge-header {
|
||||
background-color: rgb(255, 253, 253);
|
||||
padding: 1.5vh 4vw;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.employee-badge-logo {
|
||||
max-width: 15vw;
|
||||
max-height: 7vh;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* Content - wächst und scrollt bei Bedarf */
|
||||
.employee-badge-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: 2vh 3vw;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.employee-badge-headline1 {
|
||||
font-size: clamp(1.2rem, 3.5vw, 2.5rem);
|
||||
font-weight: bold;
|
||||
margin-bottom: 1vh;
|
||||
}
|
||||
|
||||
.employee-badge-headline2 {
|
||||
font-size: clamp(0.8rem, 2vw, 1.5rem);
|
||||
margin-bottom: 2vh;
|
||||
}
|
||||
|
||||
.employee-badge-photo {
|
||||
max-width: 40%;
|
||||
max-height: 45%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
object-fit: cover;
|
||||
margin: 2vh 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.employee-badge-name {
|
||||
font-size: clamp(1.5rem, 5vw, 4rem);
|
||||
font-weight: bold;
|
||||
margin: 2vh 0 1vh 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.employee-badge-divider {
|
||||
width: 80%;
|
||||
border: 1px solid #333;
|
||||
margin: 1vh 0;
|
||||
}
|
||||
|
||||
.employee-badge-headline3 {
|
||||
font-size: clamp(1rem, 2.5vw, 1.8rem);
|
||||
font-weight: bold;
|
||||
margin: 1vh 0;
|
||||
}
|
||||
|
||||
.employee-badge-topics {
|
||||
font-size: clamp(0.9rem, 2.2vw, 1.6rem);
|
||||
margin: 1vh 0;
|
||||
max-width: 80%;
|
||||
max-height: 15vh;
|
||||
overflow-y: auto;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
hyphens: auto;
|
||||
}
|
||||
|
||||
/* Footer - bleibt immer am unteren Bildschirmrand */
|
||||
.employee-badge-footer {
|
||||
background-color: rgb(255, 255, 255);
|
||||
color: red;
|
||||
padding: 1.5vh 3vw;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.employee-badge-warning {
|
||||
font-size: clamp(0.9rem, 2.5vw, 2rem);
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
|
@ -4,9 +4,9 @@ import { patch } from "@web/core/utils/patch";
|
|||
import { CustomerDisplay } from "@point_of_sale/customer_display/customer_display";
|
||||
|
||||
/**
|
||||
* Erweitert das Customer Display um die Anzeige des aktuellen Kassierers.
|
||||
* Erweitert das Customer Display um die Anzeige des aktuellen Kassierers als Badge.
|
||||
* Lauscht auf BroadcastChannel Messages von der POS und aktualisiert
|
||||
* das angezeigte Mitarbeiterbild dynamisch.
|
||||
* das angezeigte Mitarbeiterbild + job_focus dynamisch.
|
||||
*/
|
||||
patch(CustomerDisplay.prototype, {
|
||||
setup() {
|
||||
|
|
@ -30,8 +30,6 @@ patch(CustomerDisplay.prototype, {
|
|||
|
||||
/**
|
||||
* 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() {
|
||||
return this.employeeData?.employee_image_url ||
|
||||
|
|
@ -46,6 +44,20 @@ patch(CustomerDisplay.prototype, {
|
|||
return this.employeeData?.employee_name || this.data?.employeeData?.employee_name || '';
|
||||
},
|
||||
|
||||
/**
|
||||
* Gibt die Schwerpunkte des aktuellen Mitarbeiters zurück.
|
||||
*/
|
||||
get currentEmployeeJobFocus() {
|
||||
return this.employeeData?.employee_job_focus || this.data?.employeeData?.employee_job_focus || '';
|
||||
},
|
||||
|
||||
/**
|
||||
* Gibt die URL des Company-Logos zurück.
|
||||
*/
|
||||
get companyLogo() {
|
||||
return `/logo?company=${this.session.company_id}`;
|
||||
},
|
||||
|
||||
/**
|
||||
* Prüft ob ein Mitarbeiterbild vorhanden ist.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -45,21 +45,37 @@ patch(Chrome.prototype, {
|
|||
/**
|
||||
* Sendet die Kassierer-Informationen an das Customer Display.
|
||||
* Verwendet die gleiche URL-Struktur wie der POS Navbar für Employee-Bilder.
|
||||
* Lädt auch job_focus aus hr.employee.
|
||||
*
|
||||
* @param {Object} cashier - Der aktuelle Kassierer (res.users)
|
||||
*/
|
||||
sendCashierToCustomerDisplay(cashier) {
|
||||
async sendCashierToCustomerDisplay(cashier) {
|
||||
if (!cashier) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Verwende hr.employee.public für das Avatar-Bild
|
||||
// (gleiche URL wie im POS Navbar verwendet wird)
|
||||
// Hole job_focus direkt mit der cashier.id (ist bereits hr.employee ID)
|
||||
let jobFocus = '';
|
||||
try {
|
||||
const employees = await this.pos.data.searchRead('hr.employee', [['id', '=', cashier.id]], ['job_focus']);
|
||||
console.log('[POS] Employee search for employee_id', cashier.id, ':', employees);
|
||||
if (employees && employees.length > 0) {
|
||||
jobFocus = employees[0].job_focus || '';
|
||||
console.log('[POS] job_focus loaded:', jobFocus);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[POS] Fehler beim Laden von job_focus:', error);
|
||||
}
|
||||
|
||||
// Verwende hr.employee.public mit cashier.id
|
||||
const employeeData = {
|
||||
employee_id: cashier.id,
|
||||
employee_name: cashier.name,
|
||||
employee_image_url: `/web/image/hr.employee.public/${cashier.id}/avatar_1920`
|
||||
employee_image_url: `/web/image/hr.employee.public/${cashier.id}/avatar_1920`,
|
||||
employee_job_focus: jobFocus,
|
||||
};
|
||||
|
||||
console.log('[POS] Sending to Customer Display:', employeeData);
|
||||
|
||||
// Füge employeeData zur Order-Display-Data hinzu
|
||||
const selectedOrder = this.pos.get_order();
|
||||
|
|
|
|||
|
|
@ -8,34 +8,45 @@
|
|||
<div class="o_customer_display_sidebar d-flex flex-column align-items-center justify-content-center p-0 bg-view position-relative"
|
||||
style="min-width: 30%; min-height: 100%; overflow: hidden;">
|
||||
|
||||
<!-- Kassiererbild als IMG (volle Größe, Seitenverhältnis beibehalten) -->
|
||||
<img t-if="currentEmployeeImage"
|
||||
t-att-src="currentEmployeeImage"
|
||||
t-att-alt="currentEmployeeName"
|
||||
class="position-absolute top-0 start-0 w-100 h-100 employee-image"
|
||||
style="object-fit: contain; object-position: center;"
|
||||
t-on-error="(ev) => ev.target.style.display = 'none'"/>
|
||||
<!-- Employee Badge - dynamisch aufgebaut -->
|
||||
<div t-if="currentEmployeeImage" class="employee-badge-container">
|
||||
<!-- Header -->
|
||||
<div class="employee-badge-header">
|
||||
<img t-if="companyLogo"
|
||||
t-att-src="companyLogo"
|
||||
alt="Logo"
|
||||
class="employee-badge-logo"/>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="employee-badge-content">
|
||||
<div class="employee-badge-headline1">An der Theke für Dich</div>
|
||||
<div class="employee-badge-headline2">Werkstattaufsicht und Werkzeugausgabe</div>
|
||||
|
||||
<img t-att-src="currentEmployeeImage"
|
||||
t-att-alt="currentEmployeeName"
|
||||
class="employee-badge-photo"
|
||||
t-on-error="(ev) => ev.target.style.display = 'none'"/>
|
||||
|
||||
<div class="employee-badge-name" t-esc="currentEmployeeName || 'Dein Name'"/>
|
||||
|
||||
<hr class="employee-badge-divider"/>
|
||||
|
||||
<div class="employee-badge-headline3">Themenschwerpunkte:</div>
|
||||
<div class="employee-badge-topics" t-esc="currentEmployeeJobFocus || 'Deine Schwerpunkte'"/>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="employee-badge-footer">
|
||||
<div class="employee-badge-warning">! Zugang zur Theke nur für Berechtigte !</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fallback: Logo wenn kein Kassiererbild vorhanden oder Ladefehler -->
|
||||
<!-- Fallback: Logo wenn kein Kassiererbild vorhanden -->
|
||||
<div t-if="!currentEmployeeImage"
|
||||
class="o_customer_display_logo d-flex align-items-center justify-content-center w-100 h-100 p-4">
|
||||
<img class="img-fluid logo-image" t-attf-src="/logo?company={{session.company_id}}" style="max-width: 80%; max-height: 80%;"/>
|
||||
</div>
|
||||
|
||||
<!-- Logo als Fallback auch wenn Employee-Image existiert aber nicht lädt -->
|
||||
<div t-if="currentEmployeeImage"
|
||||
class="o_customer_display_logo d-flex align-items-center justify-content-center w-100 h-100 p-4 position-absolute top-0 start-0"
|
||||
style="pointer-events: none; z-index: -1;">
|
||||
<img class="img-fluid" t-attf-src="/logo?company={{session.company_id}}" style="max-width: 80%; max-height: 80%;"/>
|
||||
</div>
|
||||
|
||||
<!-- Optional: Kassierer-Name als Overlay unten -->
|
||||
<div t-if="currentEmployeeImage and currentEmployeeName"
|
||||
class="position-absolute bottom-0 w-100 mb-2 d-flex justify-content-center">
|
||||
<div class="px-3 py-2 rounded-3 text-bg-dark bg-opacity-75 small text-center">
|
||||
<t t-esc="currentEmployeeName"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user