Compare commits

..

1 Commits
main ... 19.0

47 changed files with 649 additions and 851 deletions

0
.babelrc Normal file → Executable file
View File

0
.gitignore vendored Normal file → Executable file
View File

0
CHROME_BLUETOOTH_TROUBLESHOOTING.md Normal file → Executable file
View File

0
MULTI_DEVICE_SUPPORT.md Normal file → Executable file
View File

0
QUICK_START_CHROME.md Normal file → Executable file
View File

0
README.md Normal file → Executable file
View File

0
SPEED_OPTIMIZATIONS.md Normal file → Executable file
View File

170
TESTING_SCENARIOS.md Executable file
View File

@ -0,0 +1,170 @@
# Testing Scenarios
## Current Status
The code is working! It correctly detects when no Bluetooth printer is configured and attempts to fall back to standard print.
## Enhanced Logging
Added comprehensive logging to track:
1. When `originalPrintHtml` is called
2. What it returns
3. Any errors that occur
4. Connection status checks
5. Reconnection attempts
## Test Scenarios
### Scenario 1: No Bluetooth Printer Configured ✅
**Current behavior:**
```
[BluetoothPrint] printHtml() called
[BluetoothPrint] Web Bluetooth API available
[BluetoothPrint] No Bluetooth printer configured, using standard print
[BluetoothPrint] Calling originalPrintHtml with: [element]
[BluetoothPrint] originalPrintHtml returned: [result]
```
**Expected:** Browser print dialog should open
**If not working:** Check the console for errors from `originalPrintHtml`
### Scenario 2: Bluetooth Printer Configured but Not Connected
**Steps:**
1. Connect to printer once (saves config)
2. Disconnect printer or turn it off
3. Try to print receipt
**Expected logs:**
```
[BluetoothPrint] Bluetooth printer configured: RPP02N
[BluetoothPrint] Current connection status: disconnected
[BluetoothPrint] Printer not connected, attempting to reconnect...
[BluetoothPrint] Reconnection failed: [error]
[BluetoothPrint] Falling back to standard print
```
**Expected:** Browser print dialog should open
### Scenario 3: Bluetooth Printer Connected ✅
**Steps:**
1. Click Bluetooth icon
2. Connect to RPP02N
3. Make a sale and print
**Expected logs:**
```
[BluetoothPrint] Bluetooth printer configured: RPP02N
[BluetoothPrint] Current connection status: connected
[BluetoothPrint] Attempting bluetooth print...
[BluetoothPrint] Starting bluetooth print from HTML...
[BluetoothPrint] Parsing receipt data from HTML...
[BluetoothPrint] Parsed X lines from HTML
[BluetoothPrint] Generating ESC/POS commands...
[BluetoothPrint] Generated XXX bytes of ESC/POS data
[BluetoothPrint] Sending to printer...
[BluetoothPrint] Print completed successfully
```
**Expected:** Receipt prints to Bluetooth printer
### Scenario 4: Bluetooth Print Fails Mid-Process
**What happens:** Connection drops during print
**Expected logs:**
```
[BluetoothPrint] Attempting bluetooth print...
[BluetoothPrint] Bluetooth print failed: [error]
[BluetoothPrint] Falling back to standard print after error
[BluetoothPrint] Fallback print returned: [result]
```
**Expected:** Browser print dialog opens as fallback
## What to Test Now
### Test 1: Connect Bluetooth Printer
1. Update module: `./odoo-bin -u pos_bluetooth_thermal_printer -d your_database`
2. Clear browser cache
3. Click Bluetooth icon in POS
4. Scan and connect to RPP02N
5. Make a sale and print receipt
6. **Check console logs**
7. **Verify receipt prints to Bluetooth printer**
### Test 2: Standard Print Fallback
1. Disconnect Bluetooth printer (turn off or unpair)
2. Make a sale and print receipt
3. **Check console logs** - should show reconnection attempt
4. **Verify browser print dialog opens**
## Debugging Standard Print Issue
If standard print (browser dialog) doesn't open, check:
### 1. Check Console for Errors
Look for:
```
[BluetoothPrint] Error calling originalPrintHtml: [error]
[BluetoothPrint] Standard print failed: [error]
[BluetoothPrint] Fallback print also failed: [error]
```
### 2. Check originalPrintHtml Return Value
Look for:
```
[BluetoothPrint] originalPrintHtml returned: [value]
```
If it returns `undefined` or `null`, the original method might not be opening the print dialog.
### 3. Possible Issues
**Issue A: Original method doesn't exist**
- `originalPrintHtml` might be `undefined`
- Check if `PosPrinterService.prototype.printHtml` exists before patching
**Issue B: Original method needs different context**
- Might need to pass additional parameters
- Might need different `this` binding
**Issue C: Browser blocks print dialog**
- Some browsers block print dialogs not triggered by user action
- Might need user interaction first
### 4. Alternative Fallback
If `originalPrintHtml` doesn't work, we can implement a direct print fallback:
```javascript
// Direct browser print as last resort
const printWindow = window.open('', '_blank');
printWindow.document.write(el.outerHTML);
printWindow.document.close();
printWindow.print();
```
## Expected Results After Update
### With Bluetooth Printer Connected:
✅ Receipt prints to Bluetooth printer automatically
✅ No browser print dialog
✅ Console shows successful print process
### Without Bluetooth Printer:
✅ Console shows "No Bluetooth printer configured"
✅ Falls back to standard print
✅ Browser print dialog opens (or should open)
### With Bluetooth Error:
✅ Console shows error details
✅ Falls back to standard print
✅ Sale completes successfully
## Next Steps
1. **Update module**
2. **Test with Bluetooth printer connected** - Should print!
3. **Test without Bluetooth printer** - Check if standard print works
4. **Share console logs** if any issues
The Bluetooth printing should work now when printer is connected! 🎉

0
__init__.py Normal file → Executable file
View File

2
__manifest__.py Normal file → Executable file
View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
{ {
'name': 'POS Bluetooth Thermal Printer', 'name': 'POS Bluetooth Thermal Printer',
'version': '18.0.1.0.0', 'version': '19.0.1.0.0',
'category': 'Point of Sale', 'category': 'Point of Sale',
'summary': 'Connect POS to bluetooth thermal printers using Web Bluetooth API', 'summary': 'Connect POS to bluetooth thermal printers using Web Bluetooth API',
'description': """ 'description': """

0
jest.config.js Normal file → Executable file
View File

0
models/__init__.py Normal file → Executable file
View File

0
models/pos_config.py Normal file → Executable file
View File

0
package.json Normal file → Executable file
View File

0
security/ir.model.access.csv Normal file → Executable file
View File

0
static/src/css/.gitkeep Normal file → Executable file
View File

0
static/src/css/bluetooth_printer.css Normal file → Executable file
View File

0
static/src/js/.gitkeep Normal file → Executable file
View File

12
static/src/js/bluetooth_printer_config.js Normal file → Executable file
View File

@ -39,7 +39,6 @@ export class BluetoothPrinterConfig extends Component {
characterSet: 'CP437', characterSet: 'CP437',
paperWidth: 48, paperWidth: 48,
paperWidthMm: 58, // Paper width in millimeters (58mm or 80mm) paperWidthMm: 58, // Paper width in millimeters (58mm or 80mm)
dpi: 203, // Printer DPI (203 or 304)
autoReconnect: true, autoReconnect: true,
timeout: 10000, timeout: 10000,
@ -84,7 +83,6 @@ export class BluetoothPrinterConfig extends Component {
this.state.characterSet = config.settings.characterSet || 'CP437'; this.state.characterSet = config.settings.characterSet || 'CP437';
this.state.paperWidth = config.settings.paperWidth || 48; this.state.paperWidth = config.settings.paperWidth || 48;
this.state.paperWidthMm = config.settings.paperWidthMm || 58; this.state.paperWidthMm = config.settings.paperWidthMm || 58;
this.state.dpi = config.settings.dpi || 203;
this.state.autoReconnect = config.settings.autoReconnect !== false; this.state.autoReconnect = config.settings.autoReconnect !== false;
this.state.timeout = config.settings.timeout || 10000; this.state.timeout = config.settings.timeout || 10000;
} }
@ -289,15 +287,6 @@ export class BluetoothPrinterConfig extends Component {
this._saveConfiguration(); this._saveConfiguration();
} }
/**
* Handle DPI change
* @param {Event} event - Change event
*/
onDpiChange(event) {
this.state.dpi = parseInt(event.target.value, 10);
this._saveConfiguration();
}
/** /**
* Handle auto-reconnect toggle * Handle auto-reconnect toggle
* @param {Event} event - Change event * @param {Event} event - Change event
@ -336,7 +325,6 @@ export class BluetoothPrinterConfig extends Component {
characterSet: this.state.characterSet, characterSet: this.state.characterSet,
paperWidth: this.state.paperWidth, paperWidth: this.state.paperWidth,
paperWidthMm: this.state.paperWidthMm, paperWidthMm: this.state.paperWidthMm,
dpi: this.state.dpi,
autoReconnect: this.state.autoReconnect, autoReconnect: this.state.autoReconnect,
timeout: this.state.timeout timeout: this.state.timeout
} }

24
static/src/js/bluetooth_printer_manager.js Normal file → Executable file
View File

@ -70,7 +70,7 @@ export class TimeoutError extends Error {
export class BluetoothPrinterManager { export class BluetoothPrinterManager {
constructor(errorNotificationService = null) { constructor(errorNotificationService = null) {
// Debug logging // Debug logging
this.debugMode = false; // Set to false to disable verbose logging this.debugMode = true; // Set to false to disable verbose logging
// Connection state // Connection state
this.device = null; this.device = null;
@ -309,14 +309,14 @@ export class BluetoothPrinterManager {
for (const serviceUUID of serviceUUIDs) { for (const serviceUUID of serviceUUIDs) {
try { try {
this._log(`Trying service UUID: ${serviceUUID}`); console.log(`Trying service UUID: ${serviceUUID}`);
this.service = await this.server.getPrimaryService(serviceUUID); this.service = await this.server.getPrimaryService(serviceUUID);
// Try to find a writable characteristic // Try to find a writable characteristic
for (const charUUID of characteristicUUIDs) { for (const charUUID of characteristicUUIDs) {
try { try {
this.characteristic = await this.service.getCharacteristic(charUUID); this.characteristic = await this.service.getCharacteristic(charUUID);
this._log(`Found characteristic: ${charUUID}`); console.log(`Found characteristic: ${charUUID}`);
serviceFound = true; serviceFound = true;
break; break;
} catch (charError) { } catch (charError) {
@ -346,7 +346,7 @@ export class BluetoothPrinterManager {
if (char.properties.write || char.properties.writeWithoutResponse) { if (char.properties.write || char.properties.writeWithoutResponse) {
this.service = service; this.service = service;
this.characteristic = char; this.characteristic = char;
this._log(`Using fallback characteristic: ${char.uuid}`); console.log(`Using fallback characteristic: ${char.uuid}`);
serviceFound = true; serviceFound = true;
break; break;
} }
@ -466,7 +466,7 @@ export class BluetoothPrinterManager {
chunks.push(escposData.slice(i, i + chunkSize)); chunks.push(escposData.slice(i, i + chunkSize));
} }
this._log(`Sending ${chunks.length} chunks (${escposData.length} bytes, ${chunkSize} bytes/chunk)`); console.log(`[Bluetooth] Sending ${chunks.length} chunks (${escposData.length} bytes, ${chunkSize} bytes/chunk)`);
// Determine write method based on characteristic properties // Determine write method based on characteristic properties
const useWriteWithoutResponse = this.characteristic.properties.writeWithoutResponse; const useWriteWithoutResponse = this.characteristic.properties.writeWithoutResponse;
@ -502,7 +502,7 @@ export class BluetoothPrinterManager {
// Progress logging every 20% // Progress logging every 20%
if (i % Math.ceil(chunks.length / 5) === 0) { if (i % Math.ceil(chunks.length / 5) === 0) {
const progress = Math.round((i / chunks.length) * 100); const progress = Math.round((i / chunks.length) * 100);
this._log(`Progress: ${progress}%`); console.log(`[Bluetooth] Progress: ${progress}%`);
} }
} catch (chunkError) { } catch (chunkError) {
console.error(`Failed to send chunk ${i + 1}/${chunks.length}:`, chunkError); console.error(`Failed to send chunk ${i + 1}/${chunks.length}:`, chunkError);
@ -514,7 +514,7 @@ export class BluetoothPrinterManager {
const duration = ((endTime - startTime) / 1000).toFixed(2); const duration = ((endTime - startTime) / 1000).toFixed(2);
const speed = (escposData.length / 1024 / (duration || 1)).toFixed(2); const speed = (escposData.length / 1024 / (duration || 1)).toFixed(2);
this._log(`Transmission complete in ${duration}s (${speed} KB/s)`); console.log(`[Bluetooth] Transmission complete in ${duration}s (${speed} KB/s)`);
this.isPrinting = false; this.isPrinting = false;
this._emit('print-completed', { success: true }); this._emit('print-completed', { success: true });
@ -594,7 +594,7 @@ export class BluetoothPrinterManager {
} }
try { try {
this._log(`Reconnection attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts}`); console.log(`Reconnection attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts}`);
// Try to reconnect // Try to reconnect
await this.connectToPrinter(this.device); await this.connectToPrinter(this.device);
@ -602,7 +602,7 @@ export class BluetoothPrinterManager {
this.isReconnecting = false; this.isReconnecting = false;
this.reconnectAttempts = 0; this.reconnectAttempts = 0;
this._log('Reconnection successful'); console.log('Reconnection successful');
// Emit reconnection success event // Emit reconnection success event
this._emit('reconnection-success', { this._emit('reconnection-success', {
@ -630,7 +630,7 @@ export class BluetoothPrinterManager {
// Wait before next attempt (exponential backoff) // Wait before next attempt (exponential backoff)
if (attempt < this.maxReconnectAttempts - 1) { if (attempt < this.maxReconnectAttempts - 1) {
const delay = this.reconnectDelays[attempt]; const delay = this.reconnectDelays[attempt];
this._log(`Waiting ${delay}ms before next attempt`); console.log(`Waiting ${delay}ms before next attempt`);
await this._sleep(delay); await this._sleep(delay);
} }
} }
@ -694,12 +694,12 @@ export class BluetoothPrinterManager {
* @private * @private
*/ */
_onDisconnected() { _onDisconnected() {
this._log('Bluetooth device disconnected'); console.log('Bluetooth device disconnected');
this._setConnectionStatus('disconnected'); this._setConnectionStatus('disconnected');
// Attempt auto-reconnection if enabled // Attempt auto-reconnection if enabled
if (this.autoReconnectEnabled) { if (this.autoReconnectEnabled) {
this._log('Starting auto-reconnection...'); console.log('Starting auto-reconnection...');
this.autoReconnect().catch(error => { this.autoReconnect().catch(error => {
console.error('Auto-reconnection failed:', error); console.error('Auto-reconnection failed:', error);
}); });

0
static/src/js/connection_status_widget.js Normal file → Executable file
View File

0
static/src/js/error_notification_service.js Normal file → Executable file
View File

0
static/src/js/escpos_generator.js Normal file → Executable file
View File

63
static/src/js/escpos_graphics.js Normal file → Executable file
View File

@ -35,38 +35,33 @@ export class EscPosGraphics {
* @returns {Uint8Array} Complete ESC/POS command sequence * @returns {Uint8Array} Complete ESC/POS command sequence
*/ */
generateBitmapCommands(bitmap) { generateBitmapCommands(bitmap) {
console.log('[EscPosGraphics] Generating bitmap commands (optimized)...');
console.log('[EscPosGraphics] Original dimensions:', bitmap.width, 'x', bitmap.height);
const startTime = performance.now(); const startTime = performance.now();
// OPTIMIZATION: Remove blank lines from top and bottom // OPTIMIZATION: Remove blank lines from top and bottom
const optimizedBitmap = this._removeBlankLines(bitmap); const optimizedBitmap = this._removeBlankLines(bitmap);
console.log('[EscPosGraphics] Optimized dimensions:', optimizedBitmap.width, 'x', optimizedBitmap.height);
console.log('[EscPosGraphics] Saved', bitmap.height - optimizedBitmap.height, 'blank lines');
const commands = [];
// Initialize printer // Initialize printer
const initCmd = this.initialize(); commands.push(...this.initialize());
// Print bitmap using raster graphics mode // Print bitmap using raster graphics mode
const rasterCommands = this._generateRasterGraphics(optimizedBitmap); const rasterCommands = this._generateRasterGraphics(optimizedBitmap);
commands.push(...rasterCommands);
// Feed paper and cut // Feed paper and cut
const feedCmd = this._feedAndCut(4); commands.push(...this._feedAndCut(4));
// Combine all commands safely without spreading large arrays const result = new Uint8Array(commands);
const totalLength = initCmd.length + rasterCommands.length + feedCmd.length;
const result = new Uint8Array(totalLength);
let offset = 0;
result.set(initCmd, offset);
offset += initCmd.length;
result.set(rasterCommands, offset);
offset += rasterCommands.length;
result.set(new Uint8Array(feedCmd), offset);
const endTime = performance.now(); const endTime = performance.now();
console.log('[EscPosGraphics] Command generation took:', (endTime - startTime).toFixed(2), 'ms');
console.log('[EscPosGraphics] Generated', result.length, 'bytes of commands');
return result; return result;
} }
@ -139,6 +134,7 @@ export class EscPosGraphics {
* @returns {Array} Command bytes * @returns {Array} Command bytes
*/ */
_generateRasterGraphics(bitmap) { _generateRasterGraphics(bitmap) {
const commands = [];
const { data, width, height, bytesPerLine } = bitmap; const { data, width, height, bytesPerLine } = bitmap;
// Calculate dimensions // Calculate dimensions
@ -148,29 +144,26 @@ export class EscPosGraphics {
const heightLow = height & 0xFF; const heightLow = height & 0xFF;
const heightHigh = (height >> 8) & 0xFF; const heightHigh = (height >> 8) & 0xFF;
console.log('[EscPosGraphics] Bitmap width:', width, 'pixels');
console.log('[EscPosGraphics] Bitmap height:', height, 'lines');
console.log('[EscPosGraphics] Bytes per line:', bytesPerLine);
console.log('[EscPosGraphics] Width bytes (xL xH):', widthLow, widthHigh, '=', widthBytes);
console.log('[EscPosGraphics] Height (yL yH):', heightLow, heightHigh, '=', height);
console.log('[EscPosGraphics] Total data size:', data.length, 'bytes');
console.log('[EscPosGraphics] Expected data size:', bytesPerLine * height, 'bytes');
// GS v 0 - Print raster bitmap // GS v 0 - Print raster bitmap
// Format: GS v 0 m xL xH yL yH d1...dk // Format: GS v 0 m xL xH yL yH d1...dk
// Header length: 8 bytes // m = mode (0 = normal, 1 = double width, 2 = double height, 3 = quadruple)
const header = [ commands.push(GS, 0x76, 0x30, 0x00); // GS v 0 m (m=0 for normal)
GS, 0x76, 0x30, 0x00, // GS v 0 m (m=0 for normal) commands.push(widthLow, widthHigh); // xL xH (width in bytes)
widthLow, widthHigh, // xL xH (width in bytes) commands.push(heightLow, heightHigh); // yL yH (height in dots)
heightLow, heightHigh // yL yH (height in dots)
];
// Create Uint8Array for the complete command // Add bitmap data
const totalLength = header.length + data.length + 1; // +1 for LF commands.push(...data);
const commands = new Uint8Array(totalLength);
// Set header // Add line feed after image
commands.set(header, 0); commands.push(LF);
// Set bitmap data
commands.set(data, header.length);
// Set LF
commands[totalLength - 1] = LF;
return commands; return commands;
} }
@ -247,7 +240,7 @@ export class EscPosGraphics {
chunks.push(chunk); chunks.push(chunk);
} }
console.log('[EscPosGraphics] Split into', chunks.length, 'chunks');
return chunks; return chunks;
} }

488
static/src/js/html_to_image.js Normal file → Executable file
View File

@ -5,11 +5,6 @@
* *
* Converts HTML receipt elements to images for thermal printer graphics mode. * Converts HTML receipt elements to images for thermal printer graphics mode.
* Uses canvas to render HTML and convert to bitmap format. * Uses canvas to render HTML and convert to bitmap format.
*
* FIXED: Improved rendering to preserve exact HTML layout, use 100% paper width,
* and properly render images, borders, backgrounds, and text alignment.
* The improved method uses getBoundingClientRect() to calculate actual element
* positions and renders them with full styling preservation.
*/ */
export class HtmlToImageConverter { export class HtmlToImageConverter {
@ -18,8 +13,8 @@ export class HtmlToImageConverter {
// 58mm paper: 384 pixels (48mm printable * 8 dots/mm) // 58mm paper: 384 pixels (48mm printable * 8 dots/mm)
// 80mm paper: 576 pixels (72mm printable * 8 dots/mm) // 80mm paper: 576 pixels (72mm printable * 8 dots/mm)
this.paperWidthMm = 58; this.paperWidthMm = 58;
this.paperWidth = 464; // Default for 58mm (full width) this.paperWidth = 384; // Default for 58mm
this.dpi = 203; // Default thermal printer DPI (can be 203 or 304) this.dpi = 203; // Typical thermal printer DPI
this.scale = 2; // Higher scale for better quality this.scale = 2; // Higher scale for better quality
} }
@ -31,7 +26,8 @@ export class HtmlToImageConverter {
* @returns {Promise<HTMLCanvasElement>} Canvas with rendered HTML * @returns {Promise<HTMLCanvasElement>} Canvas with rendered HTML
*/ */
async htmlToCanvas(element) { async htmlToCanvas(element) {
console.log('[HtmlToImage] Converting HTML to canvas...');
console.log('[HtmlToImage] Paper width:', this.paperWidth, 'pixels');
// Clone the element to avoid modifying the original // Clone the element to avoid modifying the original
const clone = element.cloneNode(true); const clone = element.cloneNode(true);
@ -41,113 +37,42 @@ export class HtmlToImageConverter {
clone.style.maxWidth = `${this.paperWidth}px`; clone.style.maxWidth = `${this.paperWidth}px`;
clone.style.minWidth = `${this.paperWidth}px`; clone.style.minWidth = `${this.paperWidth}px`;
clone.style.boxSizing = 'border-box'; clone.style.boxSizing = 'border-box';
clone.style.margin = '0'; clone.style.padding = '10px';
clone.style.padding = '0';
clone.style.backgroundColor = 'white'; clone.style.backgroundColor = 'white';
clone.style.color = 'black'; clone.style.color = 'black';
clone.style.overflow = 'visible'; clone.style.fontFamily = 'monospace, Courier, "Courier New"';
clone.style.fontSize = '12px';
// Scale all fonts by 150% (3x) to match test print readability clone.style.lineHeight = '1.4';
// Test print uses ESC/POS text mode which has larger fonts
const fontScale = 1.2;
// Create a temporary container to measure height // Create a temporary container to measure height
// Container must be large enough to hold the scaled content
const container = document.createElement('div'); const container = document.createElement('div');
container.style.position = 'fixed'; container.style.position = 'fixed';
container.style.left = '-9999px'; // Move off-screen instead of using opacity container.style.left = '0';
container.style.top = '0'; container.style.top = '0';
container.style.width = `${this.paperWidth}px`; container.style.width = `${this.paperWidth}px`;
container.style.maxWidth = `${this.paperWidth}px`; container.style.maxWidth = `${this.paperWidth}px`;
container.style.minWidth = `${this.paperWidth}px`;
container.style.backgroundColor = 'white'; container.style.backgroundColor = 'white';
container.style.overflow = 'visible'; container.style.overflow = 'visible';
container.style.zIndex = '-1000'; container.style.zIndex = '-1000';
container.style.boxSizing = 'border-box'; container.style.opacity = '0';
container.appendChild(clone);
document.body.appendChild(container);
const allElements = clone.querySelectorAll('*');
allElements.forEach(element => {
const style = window.getComputedStyle(element);
const currentFontSize = parseFloat(style.fontSize);
if (currentFontSize > 0) {
const newFontSize = currentFontSize * fontScale;
element.style.fontSize = `${newFontSize}px`;
}
// Also reduce padding to prevent overflow and cropping
const paddingLeft = parseFloat(style.paddingLeft) || 0;
const paddingRight = parseFloat(style.paddingRight) || 0;
// If padding is excessive, reduce it
if (paddingLeft > 8) {
element.style.paddingLeft = '4px';
}
if (paddingRight > 8) {
element.style.paddingRight = '4px';
}
});
// Also scale the root element font size
const rootFontSize = parseFloat(window.getComputedStyle(clone).fontSize);
if (rootFontSize > 0) {
clone.style.fontSize = `${rootFontSize * fontScale}px`;
}
// Reduce root padding to prevent cropping
clone.style.paddingLeft = '4px';
clone.style.paddingRight = '4px';
// Create a temporary container to measure height
// Container must be large enough to hold the scaled content
// Duplicate container declaration removed
container.style.position = 'fixed';
container.style.left = '-9999px'; // Move off-screen instead of using opacity
container.style.top = '0';
container.style.width = `${this.paperWidth}px`;
container.style.maxWidth = `${this.paperWidth}px`;
container.style.minWidth = `${this.paperWidth}px`;
container.style.backgroundColor = 'white';
container.style.overflow = 'visible';
container.style.zIndex = '-1000';
container.style.boxSizing = 'border-box';
container.appendChild(clone); container.appendChild(clone);
document.body.appendChild(container); document.body.appendChild(container);
try { try {
// Wait for layout to settle, fonts to load, and images to load // Wait for layout to settle and fonts to load
await new Promise(resolve => setTimeout(resolve, 200)); await new Promise(resolve => setTimeout(resolve, 100));
// Load all images in the clone
const images = clone.querySelectorAll('img');
await Promise.all(Array.from(images).map(img => {
return new Promise((resolve) => {
if (img.complete) {
resolve();
} else {
img.onload = resolve;
img.onerror = resolve; // Continue even if image fails
// Timeout after 2 seconds
setTimeout(resolve, 2000);
}
});
}));
// Get the actual rendered dimensions // Get the actual rendered dimensions
const rect = clone.getBoundingClientRect(); const rect = container.getBoundingClientRect();
const width = this.paperWidth; const width = this.paperWidth;
const height = Math.ceil(rect.height); const height = Math.max(Math.ceil(rect.height), 100);
console.log('[HtmlToImage] Measured dimensions:', width, 'x', height);
console.log('[HtmlToImage] Container rect:', rect);
console.log('[HtmlToImage] Receipt HTML preview (first 1000 chars):', clone.outerHTML.substring(0, 1000));
console.log('[HtmlToImage] Receipt text content:', clone.textContent.substring(0, 500));
// Create canvas
// Create canvas with exact dimensions
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
canvas.width = width; canvas.width = width;
canvas.height = height; canvas.height = height;
@ -157,12 +82,11 @@ export class HtmlToImageConverter {
ctx.fillStyle = 'white'; ctx.fillStyle = 'white';
ctx.fillRect(0, 0, width, height); ctx.fillRect(0, 0, width, height);
// Render the DOM to canvas using improved method // Render the DOM to canvas manually
// Content is already at the correct width, no scaling needed console.log('[HtmlToImage] Rendering DOM to canvas...');
await this._renderDomToCanvas(clone, ctx, width, height);
await this._renderDomToCanvasImproved(clone, ctx, 0, 0, width, height);
console.log('[HtmlToImage] Canvas rendering complete');
return canvas; return canvas;
} finally { } finally {
@ -172,322 +96,7 @@ export class HtmlToImageConverter {
} }
/** /**
* Render DOM element to canvas - Improved method that preserves layout * Render DOM element to canvas - Simple and reliable approach
* Recursively renders elements with proper positioning, styling, and images
*
* @private
* @param {HTMLElement} element - Element to render
* @param {CanvasRenderingContext2D} ctx - Canvas context
* @param {number} offsetX - X offset for rendering
* @param {number} offsetY - Y offset for rendering
* @param {number} canvasWidth - Canvas width
* @param {number} canvasHeight - Canvas height
* @returns {Promise<number>} Height used
*/
async _renderDomToCanvasImproved(element, ctx, offsetX, offsetY, canvasWidth, canvasHeight) {
// Background is already filled, no need to fill again
// Get the bounding rect of the root element to calculate offsets
const rootRect = element.getBoundingClientRect();
// Render element recursively with proper offset calculation
await this._renderElement(element, ctx, -rootRect.left, -rootRect.top, rootRect);
return canvasHeight;
}
/**
* Recursively render an element and its children
*
* @private
* @param {HTMLElement} element - Element to render
* @param {CanvasRenderingContext2D} ctx - Canvas context
* @param {number} offsetX - X offset
* @param {number} offsetY - Y offset
* @param {DOMRect} rootRect - Root element bounding rect for reference
*/
async _renderElement(element, ctx, offsetX, offsetY, rootRect) {
if (!element || element.nodeType !== Node.ELEMENT_NODE) {
return;
}
const style = window.getComputedStyle(element);
// Skip hidden elements
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
return;
}
const rect = element.getBoundingClientRect();
const x = rect.left + offsetX;
const y = rect.top + offsetY;
// Render background
const bgColor = style.backgroundColor;
if (bgColor && bgColor !== 'rgba(0, 0, 0, 0)' && bgColor !== 'transparent') {
ctx.fillStyle = bgColor;
ctx.fillRect(x, y, rect.width, rect.height);
}
// Render borders
this._renderBorders(ctx, x, y, rect.width, rect.height, style);
// Render images
if (element.tagName === 'IMG') {
await this._renderImage(element, ctx, x, y, rect.width, rect.height);
}
// Render children (text and elements)
if (element.childNodes.length > 0) {
// Check if this is a specialized text-only element (optimization + wrapping handling)
// We keep this optimization for simple blocks, but improve the check
// Actually, to fix mixed content, we should just iterate childNodes.
// But we need to handle wrapping for long text nodes.
// For now, let's assume standard recursion is safer for mixed content.
const hasOnlyText = Array.from(element.childNodes).every(
node => node.nodeType === Node.TEXT_NODE || (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'BR')
);
// Use the block text renderer mainly for simple text blocks which might need wrapping logic
// that _renderText provides (it handles word wrapping based on width).
// However, relying ONLY on this means mixed content fails.
if (hasOnlyText) {
this._renderText(element, ctx, x, y, rect.width, rect.height, style);
} else {
// Mixed content or complex structure
for (const node of element.childNodes) {
if (node.nodeType === Node.ELEMENT_NODE) {
await this._renderElement(node, ctx, offsetX, offsetY, rootRect);
} else if (node.nodeType === Node.TEXT_NODE) {
const text = node.textContent.trim();
if (text) {
// Text node in mixed content
// We need its exact position
const range = document.createRange();
range.selectNode(node);
const textRect = range.getBoundingClientRect();
// Calculate relative position
const textX = textRect.left + offsetX;
const textY = textRect.top + offsetY;
// Draw the text node using parent's style
// Note: Text nodes don't have padding/margin themselves effectively (handled by layout)
this._drawTextNode(ctx, text, textX, textY, textRect.width, style);
}
}
}
}
}
}
/**
* Draw a single text node
*
* @private
* @param {CanvasRenderingContext2D} ctx - Canvas context
* @param {string} text - Text to draw
* @param {number} x - X position
* @param {number} y - Y position
* @param {number} width - Node width
* @param {CSSStyleDeclaration} style - Computed style of parent
*/
_drawTextNode(ctx, text, x, y, width, style) {
// Set font
const fontSize = parseFloat(style.fontSize) || 12;
const fontWeight = style.fontWeight;
const fontFamily = style.fontFamily.split(',')[0].replace(/['"]/g, '') || 'monospace';
const isBold = fontWeight === 'bold' || parseInt(fontWeight) >= 600;
ctx.font = `${isBold ? 'bold' : 'normal'} ${fontSize}px ${fontFamily}`;
ctx.fillStyle = style.color || 'black';
ctx.textBaseline = 'top'; // Align to top of rect
// Handle alignment - though for a text node range rect, the rect SHOULD already be aligned by layout
// So we can arguably just draw at x.
// However, range rects can be tricky.
// Simple draw for now - rely on DOM layout for positioning
ctx.textAlign = 'left';
// Text nodes usually don't need wrapping if they are part of a laid-out line
// (the browser already wrapped them into lines, and if it spans lines, we might get a big rect)
// If it spans lines, this simple fillText might be issue, but for missing words like "Total", it's fine.
ctx.fillText(text, x, y);
}
/**
* Render text content of an element
*
* @private
* @param {HTMLElement} element - Element containing text
* @param {CanvasRenderingContext2D} ctx - Canvas context
* @param {number} x - X position
* @param {number} y - Y position
* @param {number} width - Element width
* @param {number} height - Element height
* @param {CSSStyleDeclaration} style - Computed style
*/
_renderText(element, ctx, x, y, width, height, style) {
const text = element.textContent.trim();
if (!text) return;
// Set font
const fontSize = parseFloat(style.fontSize) || 12;
const fontWeight = style.fontWeight;
const fontFamily = style.fontFamily.split(',')[0].replace(/['"]/g, '') || 'monospace';
const isBold = fontWeight === 'bold' || parseInt(fontWeight) >= 600;
ctx.font = `${isBold ? 'bold' : 'normal'} ${fontSize}px ${fontFamily}`;
ctx.fillStyle = style.color || 'black';
ctx.textBaseline = 'top';
// Handle text alignment
const textAlign = style.textAlign || 'left';
const paddingLeft = parseFloat(style.paddingLeft || 0);
const paddingRight = parseFloat(style.paddingRight || 0);
const paddingTop = parseFloat(style.paddingTop || 0);
let textX = x + paddingLeft;
if (textAlign === 'center') {
ctx.textAlign = 'center';
textX = x + width / 2;
} else if (textAlign === 'right') {
ctx.textAlign = 'right';
textX = x + width - paddingRight;
} else {
ctx.textAlign = 'left';
}
const textY = y + paddingTop;
// Word wrap if needed
const maxWidth = width - paddingLeft - paddingRight;
const lineHeight = parseFloat(style.lineHeight) || fontSize * 1.4;
this._wrapAndDrawText(ctx, text, textX, textY, maxWidth, lineHeight);
// Reset text align
ctx.textAlign = 'left';
}
/**
* Wrap and draw text with proper line breaks
*
* @private
* @param {CanvasRenderingContext2D} ctx - Canvas context
* @param {string} text - Text to draw
* @param {number} x - X position
* @param {number} y - Y position
* @param {number} maxWidth - Maximum width
* @param {number} lineHeight - Line height
*/
_wrapAndDrawText(ctx, text, x, y, maxWidth, lineHeight) {
const words = text.split(' ');
let line = '';
let currentY = y;
for (let i = 0; i < words.length; i++) {
const testLine = line + (line ? ' ' : '') + words[i];
const metrics = ctx.measureText(testLine);
if (metrics.width > maxWidth && line) {
ctx.fillText(line, x, currentY);
line = words[i];
currentY += lineHeight;
} else {
line = testLine;
}
}
if (line) {
ctx.fillText(line, x, currentY);
}
}
/**
* Render borders of an element
*
* @private
* @param {CanvasRenderingContext2D} ctx - Canvas context
* @param {number} x - X position
* @param {number} y - Y position
* @param {number} width - Element width
* @param {number} height - Element height
* @param {CSSStyleDeclaration} style - Computed style
*/
_renderBorders(ctx, x, y, width, height, style) {
const borderTop = parseFloat(style.borderTopWidth) || 0;
const borderRight = parseFloat(style.borderRightWidth) || 0;
const borderBottom = parseFloat(style.borderBottomWidth) || 0;
const borderLeft = parseFloat(style.borderLeftWidth) || 0;
if (borderTop > 0) {
ctx.strokeStyle = style.borderTopColor || 'black';
ctx.lineWidth = borderTop;
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + width, y);
ctx.stroke();
}
if (borderRight > 0) {
ctx.strokeStyle = style.borderRightColor || 'black';
ctx.lineWidth = borderRight;
ctx.beginPath();
ctx.moveTo(x + width, y);
ctx.lineTo(x + width, y + height);
ctx.stroke();
}
if (borderBottom > 0) {
ctx.strokeStyle = style.borderBottomColor || 'black';
ctx.lineWidth = borderBottom;
ctx.beginPath();
ctx.moveTo(x, y + height);
ctx.lineTo(x + width, y + height);
ctx.stroke();
}
if (borderLeft > 0) {
ctx.strokeStyle = style.borderLeftColor || 'black';
ctx.lineWidth = borderLeft;
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x, y + height);
ctx.stroke();
}
}
/**
* Render an image element
*
* @private
* @param {HTMLImageElement} img - Image element
* @param {CanvasRenderingContext2D} ctx - Canvas context
* @param {number} x - X position
* @param {number} y - Y position
* @param {number} width - Display width
* @param {number} height - Display height
*/
async _renderImage(img, ctx, x, y, width, height) {
try {
if (img.complete && img.naturalWidth > 0) {
ctx.drawImage(img, x, y, width, height);
}
} catch (error) {
console.warn('[HtmlToImage] Failed to render image:', error);
}
}
/**
* Render DOM element to canvas - Simple fallback method
* Extracts text content and renders it line by line with proper formatting * Extracts text content and renders it line by line with proper formatting
* *
* @private * @private
@ -497,7 +106,7 @@ export class HtmlToImageConverter {
* @param {number} height - Canvas height * @param {number} height - Canvas height
*/ */
async _renderDomToCanvas(element, ctx, width, height) { async _renderDomToCanvas(element, ctx, width, height) {
console.log('[HtmlToImage] Rendering DOM to canvas (simple method)...');
// Set default styles // Set default styles
ctx.fillStyle = 'white'; ctx.fillStyle = 'white';
@ -630,7 +239,7 @@ export class HtmlToImageConverter {
// Start processing // Start processing
try { try {
processElement(element); processElement(element);
console.log('[HtmlToImage] Rendering complete, height used:', y);
} catch (error) { } catch (error) {
console.error('[HtmlToImage] Error during rendering:', error); console.error('[HtmlToImage] Error during rendering:', error);
// Ultimate fallback - just print all text // Ultimate fallback - just print all text
@ -650,7 +259,7 @@ export class HtmlToImageConverter {
* @returns {Uint8Array} Bitmap data * @returns {Uint8Array} Bitmap data
*/ */
canvasToBitmap(canvas) { canvasToBitmap(canvas) {
console.log('[HtmlToImage] Converting canvas to bitmap (optimized)...');
const startTime = performance.now(); const startTime = performance.now();
const ctx = canvas.getContext('2d', { willReadFrequently: true }); const ctx = canvas.getContext('2d', { willReadFrequently: true });
@ -708,7 +317,11 @@ export class HtmlToImageConverter {
} }
const endTime = performance.now(); const endTime = performance.now();
console.log('[HtmlToImage] Bitmap conversion took:', (endTime - startTime).toFixed(2), 'ms');
console.log('[HtmlToImage] Bitmap dimensions:', width, 'pixels x', height, 'pixels');
console.log('[HtmlToImage] Bytes per line:', bytesPerLine, 'bytes');
console.log('[HtmlToImage] Total bitmap size:', bitmapData.length, 'bytes');
console.log('[HtmlToImage] Expected size:', bytesPerLine * height, 'bytes');
return { return {
data: bitmapData, data: bitmapData,
@ -725,7 +338,7 @@ export class HtmlToImageConverter {
* @returns {Promise<Object>} Bitmap data with dimensions * @returns {Promise<Object>} Bitmap data with dimensions
*/ */
async htmlToBitmap(element) { async htmlToBitmap(element) {
console.log('[HtmlToImage] Converting HTML to bitmap...');
try { try {
// Convert HTML to canvas // Convert HTML to canvas
@ -734,7 +347,7 @@ export class HtmlToImageConverter {
// Convert canvas to bitmap // Convert canvas to bitmap
const bitmap = this.canvasToBitmap(canvas); const bitmap = this.canvasToBitmap(canvas);
console.log('[HtmlToImage] Conversion complete');
return bitmap; return bitmap;
} catch (error) { } catch (error) {
console.error('[HtmlToImage] Conversion failed:', error); console.error('[HtmlToImage] Conversion failed:', error);
@ -743,37 +356,28 @@ export class HtmlToImageConverter {
} }
/** /**
* Set paper width in millimeters and DPI * Set paper width in millimeters
* *
* @param {number} widthMm - Paper width in millimeters (58 or 80) * @param {number} widthMm - Paper width in millimeters (58 or 80)
* @param {number} dpi - Printer DPI (203 or 304), defaults to 203
*/ */
setPaperWidth(widthMm, dpi = 203) { setPaperWidth(widthMm) {
this.paperWidthMm = widthMm; this.paperWidthMm = widthMm;
this.dpi = dpi;
// Calculate dots per mm based on DPI
// 203 DPI = 8 dots/mm (203 / 25.4)
// 304 DPI = 12 dots/mm (304 / 25.4)
const dotsPerMm = Math.round(dpi / 25.4);
// Calculate pixel width based on PRINTABLE width (essential for thermal printers)
// 58mm Paper -> ~48mm Printable
// 80mm Paper -> ~72mm Printable
let printableWidthMm;
// Calculate pixel width based on paper size
// Thermal printers: 8 dots per mm
// Account for margins (5mm on each side)
if (widthMm === 58) { if (widthMm === 58) {
printableWidthMm = 48; // Standard printable width for 58mm // 58mm paper: 48mm printable width = 384 pixels
this.paperWidth = 384;
} else if (widthMm === 80) { } else if (widthMm === 80) {
printableWidthMm = 72; // Standard printable width for 80mm // 80mm paper: 72mm printable width = 576 pixels
this.paperWidth = 576;
} else { } else {
// Custom width: subtract ~10mm margin // Custom width: use full width minus 10mm margins
printableWidthMm = Math.max(widthMm - 10, widthMm * 0.8); this.paperWidth = (widthMm - 10) * 8;
} }
this.paperWidth = Math.floor(printableWidthMm * dotsPerMm); console.log('[HtmlToImage] Setting paper width to', widthMm, 'mm (', this.paperWidth, 'pixels)');
} }
} }

0
static/src/js/pos_navbar_extension.js Normal file → Executable file
View File

146
static/src/js/pos_receipt_printer.js Normal file → Executable file
View File

@ -60,15 +60,18 @@ patch(PosPrinterService.prototype, {
async printHtml(el) { async printHtml(el) {
// Wrap entire method in try-catch to catch any errors // Wrap entire method in try-catch to catch any errors
try { try {
console.log('[BluetoothPrint] printHtml() called');
console.log('[BluetoothPrint] Element type:', el?.constructor?.name);
console.log('[BluetoothPrint] Element tag:', el?.tagName);
console.log('[BluetoothPrint] Element classes:', el?.className);
// Check if Web Bluetooth API is available // Check if Web Bluetooth API is available
if (!navigator.bluetooth) { if (!navigator.bluetooth) {
console.log('[BluetoothPrint] Web Bluetooth API not available, using browser print dialog');
await this._printViaBrowserDialog(el); await this._printViaBrowserDialog(el);
return true; return true;
} }
console.log('[BluetoothPrint] Web Bluetooth API available');
// Check if a Bluetooth printer is configured (from localStorage) // Check if a Bluetooth printer is configured (from localStorage)
// We don't need POS config - we check if user has configured a printer // We don't need POS config - we check if user has configured a printer
@ -76,15 +79,16 @@ patch(PosPrinterService.prototype, {
const config = storage.loadConfiguration(1); // Default POS config ID const config = storage.loadConfiguration(1); // Default POS config ID
if (!config || !config.deviceId) { if (!config || !config.deviceId) {
console.log('[BluetoothPrint] No Bluetooth printer configured, using standard print');
console.log('[BluetoothPrint] Calling originalPrintHtml with:', el);
try { try {
const result = await originalPrintHtml.call(this, el); const result = await originalPrintHtml.call(this, el);
console.log('[BluetoothPrint] originalPrintHtml returned:', result);
// If original method returned false, it didn't handle the print // If original method returned false, it didn't handle the print
// So we need to handle it ourselves // So we need to handle it ourselves
if (result === false) { if (result === false) {
console.log('[BluetoothPrint] originalPrintHtml returned false, opening browser print dialog directly');
await this._printViaBrowserDialog(el); await this._printViaBrowserDialog(el);
return true; return true;
} }
@ -93,30 +97,32 @@ patch(PosPrinterService.prototype, {
} catch (error) { } catch (error) {
console.error('[BluetoothPrint] Error calling originalPrintHtml:', error); console.error('[BluetoothPrint] Error calling originalPrintHtml:', error);
// Fallback to browser print dialog // Fallback to browser print dialog
console.log('[BluetoothPrint] Falling back to browser print dialog');
await this._printViaBrowserDialog(el); await this._printViaBrowserDialog(el);
return true; return true;
} }
} }
console.log('[BluetoothPrint] Bluetooth printer configured:', config.deviceName);
// Initialize bluetooth services // Initialize bluetooth services
const services = initializeBluetoothPrinting(null); // Restored console.log('[BluetoothPrint] Initializing bluetooth services...');
const services = initializeBluetoothPrinting(null);
console.log('[BluetoothPrint] Bluetooth services initialized');
// Check if printer is actually connected // Check if printer is actually connected
const connectionStatus = services.bluetoothManager.getConnectionStatus(); const connectionStatus = services.bluetoothManager.getConnectionStatus();
console.log('[BluetoothPrint] Current connection status:', connectionStatus);
if (connectionStatus !== 'connected') { if (connectionStatus !== 'connected') {
console.log('[BluetoothPrint] Printer not connected, attempting to reconnect...');
// Try to reconnect // Try to reconnect
try { try {
await services.bluetoothManager.autoReconnect(); await services.bluetoothManager.autoReconnect();
console.log('[BluetoothPrint] Reconnection successful');
} catch (reconnectError) { } catch (reconnectError) {
console.error('[BluetoothPrint] Reconnection failed:', reconnectError); console.error('[BluetoothPrint] Reconnection failed:', reconnectError);
console.log('[BluetoothPrint] Falling back to browser print dialog');
await this._printViaBrowserDialog(el); await this._printViaBrowserDialog(el);
return true; return true;
} }
@ -124,17 +130,17 @@ patch(PosPrinterService.prototype, {
try { try {
// Attempt bluetooth printing // Attempt bluetooth printing
console.log('[BluetoothPrint] Attempting bluetooth print...');
await this._printViaBluetoothFromHtml(el, services, config); await this._printViaBluetoothFromHtml(el, services, config);
console.log('[BluetoothPrint] Print completed successfully');
return true; return true;
} catch (printError) { } catch (printError) {
console.error('[BluetoothPrint] Bluetooth print failed:', printError); console.error('[BluetoothPrint] Bluetooth print failed:', printError);
console.error('[BluetoothPrint] Error stack:', printError.stack); console.error('[BluetoothPrint] Error stack:', printError.stack);
// Fallback to browser print dialog // Fallback to browser print dialog
console.log('[BluetoothPrint] Falling back to browser print dialog after error');
await this._printViaBrowserDialog(el); await this._printViaBrowserDialog(el);
return true; return true;
} }
@ -143,7 +149,7 @@ patch(PosPrinterService.prototype, {
console.error('[BluetoothPrint] Error name:', error.name); console.error('[BluetoothPrint] Error name:', error.name);
console.error('[BluetoothPrint] Error message:', error.message); console.error('[BluetoothPrint] Error message:', error.message);
console.error('[BluetoothPrint] Error stack:', error.stack); console.error('[BluetoothPrint] Error stack:', error.stack);
console.log('[BluetoothPrint] Falling back to browser print dialog due to critical error');
try { try {
await this._printViaBrowserDialog(el); await this._printViaBrowserDialog(el);
return true; return true;
@ -163,7 +169,7 @@ patch(PosPrinterService.prototype, {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async _printViaBrowserDialog(el) { async _printViaBrowserDialog(el) {
console.log('[BluetoothPrint] Opening browser print dialog...');
return new Promise((resolve) => { return new Promise((resolve) => {
// Create a hidden iframe for printing // Create a hidden iframe for printing
@ -185,7 +191,7 @@ patch(PosPrinterService.prototype, {
} }
if (printFrame.parentNode) { if (printFrame.parentNode) {
document.body.removeChild(printFrame); document.body.removeChild(printFrame);
console.log('[BluetoothPrint] Iframe cleaned up');
} }
resolve(); resolve();
}; };
@ -199,7 +205,7 @@ patch(PosPrinterService.prototype, {
try { try {
printFrame.contentWindow.focus(); printFrame.contentWindow.focus();
printFrame.contentWindow.print(); printFrame.contentWindow.print();
console.log('[BluetoothPrint] Print dialog opened');
// Clean up after a delay // Clean up after a delay
setTimeout(cleanup, 1000); setTimeout(cleanup, 1000);
@ -260,11 +266,11 @@ patch(PosPrinterService.prototype, {
frameDoc.write(printHtml); frameDoc.write(printHtml);
frameDoc.close(); frameDoc.close();
console.log('[BluetoothPrint] Receipt content written to iframe');
// Wait for content to load // Wait for content to load
printFrame.contentWindow.addEventListener('load', () => { printFrame.contentWindow.addEventListener('load', () => {
console.log('[BluetoothPrint] Iframe loaded, triggering print...');
// Small delay to ensure rendering is complete // Small delay to ensure rendering is complete
setTimeout(triggerPrint, 100); setTimeout(triggerPrint, 100);
}); });
@ -272,7 +278,7 @@ patch(PosPrinterService.prototype, {
// Fallback if load event doesn't fire within 2 seconds // Fallback if load event doesn't fire within 2 seconds
timeoutId = setTimeout(() => { timeoutId = setTimeout(() => {
if (!printTriggered) { if (!printTriggered) {
console.log('[BluetoothPrint] Load timeout, attempting print anyway...');
triggerPrint(); triggerPrint();
} }
}, 2000); }, 2000);
@ -298,11 +304,14 @@ patch(PosPrinterService.prototype, {
async _printViaBluetoothFromHtml(el, services, config) { async _printViaBluetoothFromHtml(el, services, config) {
const { bluetoothManager } = services; const { bluetoothManager } = services;
console.log('[BluetoothPrint] Starting bluetooth print from HTML...');
console.log('[BluetoothPrint] Using GRAPHICS MODE for exact HTML layout');
console.log('[BluetoothPrint] Element:', el);
console.log('[BluetoothPrint] Config:', config);
// Check connection status // Check connection status
const status = bluetoothManager.getConnectionStatus(); const status = bluetoothManager.getConnectionStatus();
console.log('[BluetoothPrint] Connection status:', status);
if (status !== 'connected') { if (status !== 'connected') {
throw new PrinterNotConnectedError('Bluetooth printer is not connected'); throw new PrinterNotConnectedError('Bluetooth printer is not connected');
@ -310,32 +319,46 @@ patch(PosPrinterService.prototype, {
try { try {
// Convert HTML to bitmap image // Convert HTML to bitmap image
console.log('[BluetoothPrint] Converting HTML to bitmap...');
console.log('[BluetoothPrint] Creating HtmlToImageConverter...');
const converter = new HtmlToImageConverter(); const converter = new HtmlToImageConverter();
console.log('[BluetoothPrint] HtmlToImageConverter created successfully');
// Get paper width and DPI from configuration // Get paper width from configuration
// Get paper width and DPI from configuration
const paperWidthMm = config?.settings?.paperWidthMm || 58; const paperWidthMm = config?.settings?.paperWidthMm || 58;
const dpi = config?.settings?.dpi || 203; console.log('[BluetoothPrint] Using paper width:', paperWidthMm, 'mm');
converter.setPaperWidth(paperWidthMm, dpi); console.log('[BluetoothPrint] Setting paper width on converter...');
converter.setPaperWidth(paperWidthMm);
console.log('[BluetoothPrint] Paper width set successfully');
console.log('[BluetoothPrint] Converting HTML to bitmap...');
const bitmap = await converter.htmlToBitmap(el); const bitmap = await converter.htmlToBitmap(el);
console.log('[BluetoothPrint] Bitmap created:', bitmap.width, 'x', bitmap.height);
console.log('[BluetoothPrint] Bitmap data length:', bitmap.data.length);
// Generate ESC/POS graphics commands // Generate ESC/POS graphics commands
console.log('[BluetoothPrint] Generating ESC/POS graphics commands...');
console.log('[BluetoothPrint] Creating EscPosGraphics...');
const graphicsGenerator = new EscPosGraphics(); const graphicsGenerator = new EscPosGraphics();
console.log('[BluetoothPrint] EscPosGraphics created successfully');
console.log('[BluetoothPrint] Generating bitmap commands...');
const escposData = graphicsGenerator.generateBitmapCommands(bitmap); const escposData = graphicsGenerator.generateBitmapCommands(bitmap);
console.log('[BluetoothPrint] Generated', escposData.length, 'bytes of graphics data');
// Send data to printer // Send data to printer
console.log('[BluetoothPrint] Sending graphics to printer...');
const startTime = performance.now();
await bluetoothManager.sendData(escposData); await bluetoothManager.sendData(escposData);
const endTime = performance.now();
console.log('[BluetoothPrint] Graphics print completed successfully in', (endTime - startTime).toFixed(0), 'ms');
} catch (error) { } catch (error) {
console.error('[BluetoothPrint] Graphics mode failed:', error); console.error('[BluetoothPrint] Graphics mode failed:', error);
console.error('[BluetoothPrint] Error name:', error.name); console.error('[BluetoothPrint] Error name:', error.name);
console.error('[BluetoothPrint] Error message:', error.message); console.error('[BluetoothPrint] Error message:', error.message);
console.error('[BluetoothPrint] Error stack:', error.stack); console.error('[BluetoothPrint] Error stack:', error.stack);
console.log('[BluetoothPrint] Falling back to text mode...');
// Fallback to text mode if graphics fails // Fallback to text mode if graphics fails
await this._printViaBluetoothTextMode(el, services); await this._printViaBluetoothTextMode(el, services);
@ -354,16 +377,22 @@ patch(PosPrinterService.prototype, {
async _printViaBluetoothTextMode(el, services) { async _printViaBluetoothTextMode(el, services) {
const { bluetoothManager, escposGenerator } = services; const { bluetoothManager, escposGenerator } = services;
console.log('[BluetoothPrint] Using TEXT MODE (fallback)');
// Parse receipt data from HTML element // Parse receipt data from HTML element
console.log('[BluetoothPrint] Parsing receipt data from HTML...');
const receiptData = this._parseReceiptDataFromHtml(el); const receiptData = this._parseReceiptDataFromHtml(el);
console.log('[BluetoothPrint] Parsed receipt data:', JSON.stringify(receiptData, null, 2));
// Generate ESC/POS commands // Generate ESC/POS commands
console.log('[BluetoothPrint] Generating ESC/POS text commands...');
const escposData = escposGenerator.generateReceipt(receiptData); const escposData = escposGenerator.generateReceipt(receiptData);
console.log('[BluetoothPrint] Generated', escposData.length, 'bytes of text data');
// Send data to printer // Send data to printer
console.log('[BluetoothPrint] Sending text to printer...');
await bluetoothManager.sendData(escposData); await bluetoothManager.sendData(escposData);
console.log('[BluetoothPrint] Text print completed successfully');
}, },
/** /**
@ -376,25 +405,34 @@ patch(PosPrinterService.prototype, {
* @throws {Error} If printing fails * @throws {Error} If printing fails
*/ */
async _printViaBluetooth(el, pos) { async _printViaBluetooth(el, pos) {
const services = initializeBluetoothPrinting(); const services = initializeBluetoothPrinting();
const { bluetoothManager, escposGenerator } = services; const { bluetoothManager, escposGenerator } = services;
console.log('[BluetoothPrint] Starting bluetooth print...');
console.log('[BluetoothPrint] Element:', el);
// Check connection status // Check connection status
const status = bluetoothManager.getConnectionStatus(); const status = bluetoothManager.getConnectionStatus();
console.log('[BluetoothPrint] Connection status:', status);
if (status !== 'connected') { if (status !== 'connected') {
throw new PrinterNotConnectedError('Bluetooth printer is not connected'); throw new PrinterNotConnectedError('Bluetooth printer is not connected');
} }
// Parse receipt data from POS order // Parse receipt data from POS order
console.log('[BluetoothPrint] Parsing receipt data...');
const receiptData = this._parseReceiptDataFromPos(pos); const receiptData = this._parseReceiptDataFromPos(pos);
console.log('[BluetoothPrint] Receipt data:', receiptData);
// Generate ESC/POS commands // Generate ESC/POS commands
console.log('[BluetoothPrint] Generating ESC/POS commands...');
const escposData = escposGenerator.generateReceipt(receiptData); const escposData = escposGenerator.generateReceipt(receiptData);
console.log('[BluetoothPrint] Generated', escposData.length, 'bytes of ESC/POS data');
// Send data to printer // Send data to printer
console.log('[BluetoothPrint] Sending to printer...');
await bluetoothManager.sendData(escposData); await bluetoothManager.sendData(escposData);
console.log('[BluetoothPrint] Print completed successfully');
}, },
/** /**
@ -505,16 +543,22 @@ patch(PosPrinterService.prototype, {
* @returns {Object} Structured receipt data * @returns {Object} Structured receipt data
*/ */
_parseReceiptDataFromHtml(el) { _parseReceiptDataFromHtml(el) {
console.log('[BluetoothPrint] _parseReceiptDataFromHtml called');
console.log('[BluetoothPrint] Receipt HTML structure:', el.outerHTML.substring(0, 500));
console.log('[BluetoothPrint] Receipt classes:', el.className);
console.log('[BluetoothPrint] Receipt children count:', el.children.length);
// Extract text content from HTML // Extract text content from HTML
const getText = (selector) => { const getText = (selector) => {
const element = el.querySelector(selector); const element = el.querySelector(selector);
const text = element ? element.textContent.trim() : ''; const text = element ? element.textContent.trim() : '';
console.log(`[BluetoothPrint] getText('${selector}'):`, text || '(empty)');
return text; return text;
}; };
const getAll = (selector) => { const getAll = (selector) => {
const elements = Array.from(el.querySelectorAll(selector)); const elements = Array.from(el.querySelectorAll(selector));
console.log(`[BluetoothPrint] getAll('${selector}'):`, elements.length, 'elements found');
return elements; return elements;
}; };
@ -535,6 +579,7 @@ patch(PosPrinterService.prototype, {
}; };
// Parse order lines - try multiple selectors // Parse order lines - try multiple selectors
console.log('[BluetoothPrint] Searching for order lines...');
let lineElements = getAll('.orderline'); let lineElements = getAll('.orderline');
if (lineElements.length === 0) { if (lineElements.length === 0) {
lineElements = getAll('.pos-receipt-orderline'); lineElements = getAll('.pos-receipt-orderline');
@ -550,14 +595,17 @@ patch(PosPrinterService.prototype, {
lineElements = getAll('tbody tr'); lineElements = getAll('tbody tr');
} }
console.log('[BluetoothPrint] Found', lineElements.length, 'line elements');
const lines = lineElements.map((line, index) => { const lines = lineElements.map((line, index) => {
console.log(`[BluetoothPrint] Processing line ${index}:`, line.outerHTML.substring(0, 200));
const productName = line.querySelector('.product-name, td:first-child, .pos-receipt-left-align')?.textContent.trim() || ''; const productName = line.querySelector('.product-name, td:first-child, .pos-receipt-left-align')?.textContent.trim() || '';
const qtyText = line.querySelector('.qty, .quantity')?.textContent.trim() || '1'; const qtyText = line.querySelector('.qty, .quantity')?.textContent.trim() || '1';
const priceText = line.querySelector('.price, .price-unit')?.textContent.trim() || '0'; const priceText = line.querySelector('.price, .price-unit')?.textContent.trim() || '0';
const totalText = line.querySelector('.price-total, .total, td:last-child, .pos-receipt-right-align')?.textContent.trim() || '0'; const totalText = line.querySelector('.price-total, .total, td:last-child, .pos-receipt-right-align')?.textContent.trim() || '0';
console.log(`[BluetoothPrint] Line ${index} raw data:`, { productName, qtyText, priceText, totalText });
// Parse numbers (remove currency symbols and commas) // Parse numbers (remove currency symbols and commas)
const parseNumber = (str) => { const parseNumber = (str) => {
@ -572,15 +620,16 @@ patch(PosPrinterService.prototype, {
total: parseNumber(totalText) total: parseNumber(totalText)
}; };
console.log(`[BluetoothPrint] Line ${index} parsed:`, parsedLine);
return parsedLine; return parsedLine;
}).filter(line => line.productName); // Filter out empty lines }).filter(line => line.productName); // Filter out empty lines
console.log('[BluetoothPrint] Parsed', lines.length, 'lines from HTML');
console.log('[BluetoothPrint] All lines:', JSON.stringify(lines, null, 2));
// Parse totals // Parse totals
console.log('[BluetoothPrint] Parsing totals...');
const totals = { const totals = {
subtotal: this._parseAmount(getText('.pos-receipt-subtotal, .subtotal')), subtotal: this._parseAmount(getText('.pos-receipt-subtotal, .subtotal')),
tax: this._parseAmount(getText('.pos-receipt-tax, .tax')), tax: this._parseAmount(getText('.pos-receipt-tax, .tax')),
@ -588,12 +637,14 @@ patch(PosPrinterService.prototype, {
total: this._parseAmount(getText('.pos-receipt-total, .total, .amount-total')) total: this._parseAmount(getText('.pos-receipt-total, .total, .amount-total'))
}; };
console.log('[BluetoothPrint] Parsed totals:', totals);
// If totals not found in specific elements, calculate from lines // If totals not found in specific elements, calculate from lines
if (totals.total === 0 && lines.length > 0) { if (totals.total === 0 && lines.length > 0) {
console.log('[BluetoothPrint] Total is 0, calculating from lines...');
totals.total = lines.reduce((sum, line) => sum + line.total, 0); totals.total = lines.reduce((sum, line) => sum + line.total, 0);
totals.subtotal = totals.total; totals.subtotal = totals.total;
console.log('[BluetoothPrint] Calculated totals:', totals);
} }
// Parse payment info // Parse payment info
@ -618,7 +669,7 @@ patch(PosPrinterService.prototype, {
footerData footerData
}; };
console.log('[BluetoothPrint] Parsed receipt data from HTML:', receiptData);
return receiptData; return receiptData;
}, },
@ -644,6 +695,8 @@ patch(PosPrinterService.prototype, {
* @returns {Object} Structured receipt data * @returns {Object} Structured receipt data
*/ */
_parseReceiptDataFromPos(pos) { _parseReceiptDataFromPos(pos) {
console.log('[BluetoothPrint] _parseReceiptDataFromPos called');
console.log('[BluetoothPrint] POS object keys:', Object.keys(pos));
// In Odoo 18, get current order from POS // In Odoo 18, get current order from POS
// Try multiple ways to access the order // Try multiple ways to access the order
@ -651,13 +704,17 @@ patch(PosPrinterService.prototype, {
if (typeof pos.get_order === 'function') { if (typeof pos.get_order === 'function') {
order = pos.get_order(); order = pos.get_order();
console.log('[BluetoothPrint] Got order via get_order()');
} else if (pos.selectedOrder) { } else if (pos.selectedOrder) {
order = pos.selectedOrder; order = pos.selectedOrder;
console.log('[BluetoothPrint] Got order via selectedOrder');
} else if (pos.orders && pos.orders.length > 0) { } else if (pos.orders && pos.orders.length > 0) {
order = pos.orders[pos.orders.length - 1]; order = pos.orders[pos.orders.length - 1];
console.log('[BluetoothPrint] Got order via orders array');
} }
console.log('[BluetoothPrint] Order:', order);
console.log('[BluetoothPrint] Order keys:', order ? Object.keys(order) : 'null');
if (!order) { if (!order) {
throw new Error('No active order found'); throw new Error('No active order found');
@ -665,6 +722,7 @@ patch(PosPrinterService.prototype, {
// Get company info // Get company info
const company = pos.company || {}; const company = pos.company || {};
console.log('[BluetoothPrint] Company:', company);
// Get cashier info - try multiple ways // Get cashier info - try multiple ways
let cashierName = ''; let cashierName = '';
@ -675,7 +733,7 @@ patch(PosPrinterService.prototype, {
} else if (pos.user) { } else if (pos.user) {
cashierName = pos.user.name || ''; cashierName = pos.user.name || '';
} }
console.log('[BluetoothPrint] Cashier name:', cashierName);
// Get customer info - try multiple ways // Get customer info - try multiple ways
let customerName = null; let customerName = null;
@ -686,7 +744,7 @@ patch(PosPrinterService.prototype, {
} else if (order.partner_id) { } else if (order.partner_id) {
customerName = order.partner_id[1] || null; customerName = order.partner_id[1] || null;
} }
console.log('[BluetoothPrint] Customer name:', customerName);
// Build receipt data structure // Build receipt data structure
const receiptData = { const receiptData = {
@ -711,7 +769,7 @@ patch(PosPrinterService.prototype, {
} }
}; };
console.log('[BluetoothPrint] Parsed receipt data:', receiptData);
return receiptData; return receiptData;
}, },

18
static/src/js/pos_session_integration.js Normal file → Executable file
View File

@ -36,7 +36,7 @@ patch(PosStore.prototype, {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async _initializeBluetoothPrinter() { async _initializeBluetoothPrinter() {
console.log('Initializing bluetooth printer for POS session...');
try { try {
const notificationService = this.env?.services?.notification || null; const notificationService = this.env?.services?.notification || null;
@ -59,12 +59,12 @@ patch(PosStore.prototype, {
if (!config) { if (!config) {
// No printer configured for this device // No printer configured for this device
// console.log('No bluetooth printer configured for this device'); console.log('No bluetooth printer configured for this device');
this._promptPrinterConfiguration(); this._promptPrinterConfiguration();
return; return;
} }
// console.log('Found printer configuration:', config.deviceName); console.log('Found printer configuration:', config.deviceName);
// Enable auto-reconnect based on saved settings // Enable auto-reconnect based on saved settings
const autoReconnect = config.settings?.autoReconnect !== false; const autoReconnect = config.settings?.autoReconnect !== false;
@ -94,7 +94,7 @@ patch(PosStore.prototype, {
const { bluetoothManager } = services; const { bluetoothManager } = services;
try { try {
// console.log('Attempting to connect to printer:', config.deviceName); console.log('Attempting to connect to printer:', config.deviceName);
// Try to get the previously paired device // Try to get the previously paired device
const devices = await navigator.bluetooth.getDevices(); const devices = await navigator.bluetooth.getDevices();
@ -116,7 +116,7 @@ patch(PosStore.prototype, {
// Connect to the printer // Connect to the printer
await bluetoothManager.connectToPrinter(device); await bluetoothManager.connectToPrinter(device);
// console.log('Successfully connected to bluetooth printer'); console.log('Successfully connected to bluetooth printer');
// Success notification is now handled by error service in bluetooth manager // Success notification is now handled by error service in bluetooth manager
} catch (error) { } catch (error) {
@ -172,7 +172,7 @@ patch(PosStore.prototype, {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async _cleanupBluetoothPrinter() { async _cleanupBluetoothPrinter() {
// console.log('Cleaning up bluetooth printer connection...'); console.log('Cleaning up bluetooth printer connection...');
try { try {
const services = getBluetoothPrintingServices(); const services = getBluetoothPrintingServices();
@ -182,9 +182,9 @@ patch(PosStore.prototype, {
const status = bluetoothManager.getConnectionStatus(); const status = bluetoothManager.getConnectionStatus();
if (status === 'connected' || status === 'connecting') { if (status === 'connected' || status === 'connecting') {
// console.log('Disconnecting bluetooth printer...'); console.log('Disconnecting bluetooth printer...');
await bluetoothManager.disconnect(); await bluetoothManager.disconnect();
// console.log('Bluetooth printer disconnected'); console.log('Bluetooth printer disconnected');
} }
} catch (error) { } catch (error) {
console.error('Error cleaning up bluetooth printer:', error); console.error('Error cleaning up bluetooth printer:', error);
@ -258,7 +258,7 @@ patch(PosStore.prototype, {
sticky: false sticky: false
}); });
} else { } else {
// console.log(`[${type.toUpperCase()}] ${message}`); console.log(`[${type.toUpperCase()}] ${message}`);
} }
} }
}); });

0
static/src/js/storage_manager.js Normal file → Executable file
View File

0
static/src/tests/__mocks__/odoo-module.js Normal file → Executable file
View File

0
static/src/tests/configuration_properties.test.js Normal file → Executable file
View File

0
static/src/tests/connection_properties.test.js Normal file → Executable file
View File

0
static/src/tests/error_handling.test.js Normal file → Executable file
View File

0
static/src/tests/error_notification_service.test.js Normal file → Executable file
View File

0
static/src/tests/escpos_properties.test.js Normal file → Executable file
View File

0
static/src/tests/multi_device_demo.js Normal file → Executable file
View File

0
static/src/tests/multi_device_support.test.js Normal file → Executable file
View File

0
static/src/tests/printing_properties.test.js Normal file → Executable file
View File

0
static/src/tests/retry_logic.test.js Normal file → Executable file
View File

0
static/src/tests/setup.js Normal file → Executable file
View File

0
static/src/tests/status_indicator_properties.test.js Normal file → Executable file
View File

0
static/src/xml/.gitkeep Normal file → Executable file
View File

15
static/src/xml/bluetooth_printer_config.xml Normal file → Executable file
View File

@ -127,21 +127,6 @@
</small> </small>
</div> </div>
<!-- Printer DPI -->
<div class="form-group">
<label for="dpi">Printer Resolution (DPI)</label>
<select id="dpi"
class="form-control"
t-model="state.dpi"
t-on-change="onDpiChange">
<option value="203">203 DPI (Standard)</option>
<option value="304">304 DPI (High Resolution)</option>
</select>
<small class="form-text text-muted">
Select your printer's resolution. Most thermal printers use 203 DPI. Use 304 DPI for higher quality printers.
</small>
</div>
<!-- Auto Reconnect --> <!-- Auto Reconnect -->
<div class="form-group form-check"> <div class="form-group form-check">
<input type="checkbox" <input type="checkbox"

0
static/src/xml/connection_status.xml Normal file → Executable file
View File

0
static/src/xml/pos_navbar_extension.xml Normal file → Executable file
View File

0
static/test_receipt_print.js Normal file → Executable file
View File

0
views/pos_config_views.xml Normal file → Executable file
View File