Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions addons/pos_sunmi/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import models
24 changes: 24 additions & 0 deletions addons/pos_sunmi/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'POS Sunmi',
'category': 'Sales/Point of Sale',
'summary': 'Sunmi ePOS Printers in PoS',
'description': """
Use Sunmi ePOS Printers without the IoT Box in the Point of Sale
""",
'depends': ['point_of_sale'],
'data': [
'views/pos_printer.xml',
],
'assets': {
'point_of_sale._assets_pos': [
'pos_sunmi/static/lib/**/*',
'pos_sunmi/static/src/**/*',
],
'web.assets_backend': [
'pos_sunmi/static/lib/**/*',
],
},
'author': 'Odoo S.A.',
'license': 'LGPL-3',
}
1 change: 1 addition & 0 deletions addons/pos_sunmi/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import pos_printer
18 changes: 18 additions & 0 deletions addons/pos_sunmi/models/pos_printer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError


class PosPrinter(models.Model):
_inherit = 'pos.printer'

printer_type = fields.Selection(
selection_add=[('sunmi', 'Sunmi')],
ondelete={'sunmi': 'set default'}
)

@api.constrains('printer_type', 'use_type')
def _constrains_sunmi_printer(self):
for record in self:
if record.printer_type == 'sunmi' and record.use_type != 'receipt':
raise ValidationError(_("Sunmi printers can only be used as Receipt printers."))
1,060 changes: 1,060 additions & 0 deletions addons/pos_sunmi/static/lib/sunmi-printer/sunmi-printer.js

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { patch } from "@web/core/utils/patch";
import { PosTicketPrinterService } from "@point_of_sale/app/services/pos_ticket_printer_service";
import { SunmiPrinterAdapter } from "@pos_sunmi/app/utils/sunmi_printer";
import { logPosMessage } from "@point_of_sale/app/utils/pretty_console_log";

const CONSOLE_COLOR = "#28ffeb";

patch(PosTicketPrinterService.prototype, {
async createPrinterInstance(printer) {
if (printer.printer_type === "sunmi") {
try {
const sunmiPrinterAdapter = new SunmiPrinterAdapter({ printer });
await sunmiPrinterAdapter.launchPrintService();
return sunmiPrinterAdapter;
} catch (error) {
console.error("Error cannot detect printer: " + error);
logPosMessage(
"PosTicketPrinterService",
"detectSunmiPrinter",
"Unable to detect Sunmi printer: " + error.message,
CONSOLE_COLOR,
[error]
);
return null;
}
}
return await super.createPrinterInstance(...arguments);
},
});
134 changes: 134 additions & 0 deletions addons/pos_sunmi/static/src/app/utils/sunmi_printer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { BasePrinter } from "@point_of_sale/app/utils/printer/base_printer";
import { logPosMessage } from "@point_of_sale/app/utils/pretty_console_log";
import { _t } from "@web/core/l10n/translation";

const CONSOLE_COLOR = "#28ffeb";

/* global SUNMI */
export class SunmiPrinterAdapter extends BasePrinter {
setup({ printer } = {}) {
if (typeof SUNMI === "undefined") {
throw new Error("SUNMI global object is not available. Ensure the Sunmi printer SDK is loaded.");
}
super.setup(...arguments);
this.sunmiPrinter = new SUNMI();
this.isConnected = false;
this.printer = printer;
const {
TextStyle,
BarcodeStyle,
QrStyle,
BitmapStyle,
AreaStyle,
LabelStyle,
} = this.sunmiPrinter.class;
this.BitmapStyle = BitmapStyle;
}

isConnected() {
return this.sunmiPrinter.connected;
}

async printerStatus() {
try {
return await this.getStatus();
} catch (error) {
logPosMessage(
"SunmiPrinterAdapter",
"printerStatus",
"Failed to get printer status: " + error.message,
CONSOLE_COLOR,
[error]
);
return { value: -1 };
}
}

async launchPrintService() {
let retries = 3;
while (retries > 0) {
try {
await this.sunmiPrinter.launchPrinterService();
await this.sunmiPrinter.init();
this.isConnected = true;
return;
} catch (error) {
retries -= 1;
console.warn(`Retrying Sunmi printer initialization (${3 - retries}/3)...`);
if (retries === 0) {
throw new Error("Failed to initialize Sunmi printer after 3 attempts: " + error.message);
}
await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second before retrying
}
}
}

async getStatus() {
try {
const res = await this.sunmiPrinter.printer.queryApi.getStatus();
return res.status.includes("READY") ? { value: 0 } : { value: -1 };
} catch (error) {
console.log("Error:" + error)
return false;
}
}

getInfo() {
this.sunmiPrinter.printer.queryApi.getInfo(sunmiPrinter.ENUM.PrinterInfo.ID)
.then((res) => {
const data = JSON.stringify(res);
});
}

async renderBitmap(bitmap) {
var bitmap = bitmap.replace(/^data:.+;base64,/, "");
try {
const res = await this.sunmiPrinter.printer.canvasApi.renderBitmap(bitmap);
// const res = await this.sunmiPrinter.printer.canvasApi.renderBitmap(bitmap, this.BitmapStyle.getStyle().setPosY(110));
} catch (error) {
console.log("error: " + error);
return false;
}
}

async printBitmap(bitmap) {
var bitmap = bitmap.replace(/^data:.+;base64,/, "");
await this.sunmiPrinter.printer.lineApi.printBitmap(bitmap, this.BitmapStyle.getStyle().setAlign("CENTER"));
}


/**
* @override
*/
// convert canvas into jpeg image to print
processCanvas(canvas) {
return canvas.toDataURL("image/jpeg");
}

/**
* @override
*/
async sendPrintingJob(img) {
try {
const status = await this.printerStatus();
if (status.value !== 0) {
return { result: false, errorCode: status.value, canRetry: true };
}
const processedImg = this.processCanvas(img);
await this.printBitmap(processedImg);
this.sunmiPrinter.printer.lineApi.autoOut();
return { result: true };
// potential improvement: add retries for failed attempts
} catch (error) {
console.error("Error print job failed: " + error);
logPosMessage(
"SunmiPrinterAdapter",
"sendPrintingJob",
"Printing job failed: " + error.message,
CONSOLE_COLOR,
[error]
);
return { result: false, errorCode: error.message, canRetry: true };
}
}
}
13 changes: 13 additions & 0 deletions addons/pos_sunmi/views/pos_printer.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="pos_printer_view_form_inherit_pos_sunmi" model="ir.ui.view">
<field name="name">pos.printer.view.form.inherit.pos.sunmi</field>
<field name="model">pos.printer</field>
<field name="inherit_id" ref="point_of_sale.view_pos_printer_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='use_type']" position="after">
<field name="printer_type"/>
</xpath>
</field>
</record>
</odoo>