From b19252ce0f09ca3be5246aac2a250472dd22fe2f Mon Sep 17 00:00:00 2001 From: Sylvain Garancher Date: Tue, 4 Apr 2017 19:23:07 +0200 Subject: [PATCH 01/54] [ADD] Add printer_zpl2 module (#66) * [FIX] printer_tray: Allow to call print_option with no report * [ADD] Add printer_zpl2 module --- printer_zpl2/README.rst | 99 ++ printer_zpl2/__init__.py | 6 + printer_zpl2/__openerp__.py | 24 + printer_zpl2/models/__init__.py | 6 + printer_zpl2/models/printing_label_zpl2.py | 188 ++++ .../models/printing_label_zpl2_component.py | 144 +++ printer_zpl2/security/ir.model.access.csv | 5 + printer_zpl2/tests/__init__.py | 6 + .../tests/test_printing_label_zpl2.py | 941 ++++++++++++++++++ .../tests/test_wizard_print_record_label.py | 92 ++ printer_zpl2/views/printing_label_zpl2.xml | 144 +++ printer_zpl2/wizard/__init__.py | 5 + printer_zpl2/wizard/print_record_label.py | 44 + printer_zpl2/wizard/print_record_label.xml | 30 + 14 files changed, 1734 insertions(+) create mode 100644 printer_zpl2/README.rst create mode 100644 printer_zpl2/__init__.py create mode 100644 printer_zpl2/__openerp__.py create mode 100644 printer_zpl2/models/__init__.py create mode 100644 printer_zpl2/models/printing_label_zpl2.py create mode 100644 printer_zpl2/models/printing_label_zpl2_component.py create mode 100644 printer_zpl2/security/ir.model.access.csv create mode 100644 printer_zpl2/tests/__init__.py create mode 100644 printer_zpl2/tests/test_printing_label_zpl2.py create mode 100644 printer_zpl2/tests/test_wizard_print_record_label.py create mode 100644 printer_zpl2/views/printing_label_zpl2.xml create mode 100644 printer_zpl2/wizard/__init__.py create mode 100644 printer_zpl2/wizard/print_record_label.py create mode 100644 printer_zpl2/wizard/print_record_label.xml diff --git a/printer_zpl2/README.rst b/printer_zpl2/README.rst new file mode 100644 index 0000000..6336639 --- /dev/null +++ b/printer_zpl2/README.rst @@ -0,0 +1,99 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 + +===================== +ZPL II Label printing +===================== + +This module extends the **Report to printer** (``base_report_to_printer``) +module to add a ZPL II label printing feature. + +This module is meant to be used as a base for module development, and does not provide a GUI on its own. +See below for more details. + +Installation +============ + +Nothing special, just install the module. + +Configuration +============= + +To configure this module, you need to: + +#. Go to *Settings > Printing > Labels > ZPL II* +#. Create new labels + +It's also possible to add a label printing wizard on any model by creating a new *ir.values* record. +For example, to add the printing wizard on the *product.product* model : + +.. code-block:: xml + + + Print Product Label + action + client_action_multi + product.product + + + +Usage +===== + +To print a label, you need to call use the label printing method from anywhere (other modules, server actions, etc.). + +.. code-block:: python + + # Example : Print the label of a product + self.env['printing.label.zpl2'].browse(label_id).print_label( + self.env['printing.printer'].browse(printer_id), + self.env['product.product'].browse(product_id)) + +You can also use the generic label printing wizard, if added on some models. + +.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas + :alt: Try me on Runbot + :target: https://runbot.odoo-community.org/runbot/144/9.0 + +Known issues / Roadmap +====================== + +* Add a button to generate the ir.values for a model +* Develop a "Designer" view in a separate module, to allow drawing labels with simple mouse clicks/drags + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues +`_. In case of trouble, please +check there if your issue has already been reported. If you spotted it first, +help us smashing it by providing a detailed and welcomed feedback. + +Credits +======= + +Images +------ + +* Odoo Community Association: `Icon `_. + +Contributors +------------ + +* Sylvain Garancher + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +This module is maintained by the OCA. + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +To contribute to this module, please visit https://odoo-community.org. diff --git a/printer_zpl2/__init__.py b/printer_zpl2/__init__.py new file mode 100644 index 0000000..6b40cb0 --- /dev/null +++ b/printer_zpl2/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2016 SYLEAM () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import models +from . import wizard diff --git a/printer_zpl2/__openerp__.py b/printer_zpl2/__openerp__.py new file mode 100644 index 0000000..1e1f614 --- /dev/null +++ b/printer_zpl2/__openerp__.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2016 SYLEAM () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + 'name': 'Printer ZPL II', + 'version': '9.0.1.0.0', + 'category': 'Printer', + 'author': 'SYLEAM, Odoo Community Association (OCA)', + 'website': 'http://www.syleam.fr/', + 'license': 'AGPL-3', + 'external_dependancies': { + 'python': ['zpl2'], + }, + 'depends': [ + 'base_report_to_printer', + ], + 'data': [ + 'security/ir.model.access.csv', + 'views/printing_label_zpl2.xml', + 'wizard/print_record_label.xml', + ], + 'installable': True, +} diff --git a/printer_zpl2/models/__init__.py b/printer_zpl2/models/__init__.py new file mode 100644 index 0000000..048ad6e --- /dev/null +++ b/printer_zpl2/models/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2016 SYLEAM () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import printing_label_zpl2_component +from . import printing_label_zpl2 diff --git a/printer_zpl2/models/printing_label_zpl2.py b/printer_zpl2/models/printing_label_zpl2.py new file mode 100644 index 0000000..b93e7c1 --- /dev/null +++ b/printer_zpl2/models/printing_label_zpl2.py @@ -0,0 +1,188 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2016 SYLEAM () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import time +import datetime +import logging +from openerp import api, exceptions, fields, models +from openerp.tools.translate import _ +from openerp.tools.safe_eval import safe_eval + +_logger = logging.getLogger(__name__) + +try: + import zpl2 +except ImportError: + _logger.debug('Cannot `import zpl2`.') + + +class PrintingLabelZpl2(models.Model): + _name = 'printing.label.zpl2' + _description = 'ZPL II Label' + + name = fields.Char(required=True, help='Label Name.') + description = fields.Char(help='Long description for this label.') + model_id = fields.Many2one( + comodel_name='ir.model', string='Model', required=True, + help='Model used to print this label.') + origin_x = fields.Integer( + required=True, default=10, + help='Origin point of the contents in the label, X coordinate.') + origin_y = fields.Integer( + required=True, default=10, + help='Origin point of the contents in the label, Y coordinate.') + width = fields.Integer( + required=True, default=480, + help='With of the label, will be set on the printer before printing.') + component_ids = fields.One2many( + comodel_name='printing.label.zpl2.component', inverse_name='label_id', + string='Label Components', + help='Components which will be printed on the label.') + + @api.multi + def _generate_zpl2_components_data( + self, label_data, record, page_number=1, page_count=1, + label_offset_x=0, label_offset_y=0, **extra): + self.ensure_one() + + # Add all elements to print in a list of tuples : + # [(component, data, offset_x, offset_y)] + to_print = [] + for component in self.component_ids: + eval_args = extra + eval_args.update({ + 'object': record, + 'page_number': str(page_number + 1), + 'page_count': str(page_count), + 'time': time, + 'datetime': datetime, + }) + data = safe_eval(component.data, eval_args) or '' + + # Generate a list of elements if the component is repeatable + for idx in range( + component.repeat_offset, + component.repeat_offset + component.repeat_count): + printed_data = data + # Pick the right value if data is a collection + if isinstance(data, (list, tuple, set, models.BaseModel)): + # If we reached the end of data, quit the loop + if idx >= len(data): + break + + # Set the real data to display + printed_data = data[idx] + + position = idx - component.repeat_offset + to_print.append(( + component, printed_data, + label_offset_x + component.repeat_offset_x * position, + label_offset_y + component.repeat_offset_y * position, + )) + + for (component, data, offset_x, offset_y) in to_print: + component_offset_x = component.origin_x + offset_x + component_offset_y = component.origin_y + offset_y + if component.component_type == 'text': + barcode_arguments = dict([ + (field_name, component[field_name]) + for field_name in [ + zpl2.ARG_FONT, + zpl2.ARG_ORIENTATION, + zpl2.ARG_HEIGHT, + zpl2.ARG_WIDTH, + zpl2.ARG_REVERSE_PRINT, + zpl2.ARG_IN_BLOCK, + zpl2.ARG_BLOCK_WIDTH, + zpl2.ARG_BLOCK_LINES, + zpl2.ARG_BLOCK_SPACES, + zpl2.ARG_BLOCK_JUSTIFY, + zpl2.ARG_BLOCK_LEFT_MARGIN, + ] + ]) + label_data.font_data( + component_offset_x, component_offset_y, + barcode_arguments, data) + elif component.component_type == 'rectangle': + label_data.graphic_box( + component_offset_x, component_offset_y, { + zpl2.ARG_WIDTH: component.width, + zpl2.ARG_HEIGHT: component.height, + zpl2.ARG_THICKNESS: component.thickness, + zpl2.ARG_COLOR: component.color, + zpl2.ARG_ROUNDING: component.rounding, + }) + elif component.component_type == 'circle': + label_data.graphic_circle( + component_offset_x, component_offset_y, { + zpl2.ARG_DIAMETER: component.width, + zpl2.ARG_THICKNESS: component.thickness, + zpl2.ARG_COLOR: component.color, + }) + elif component.component_type == 'sublabel': + component_offset_x += component.sublabel_id.origin_x + component_offset_y += component.sublabel_id.origin_y + component.sublabel_id._generate_zpl2_components_data( + label_data, data, + label_offset_x=component_offset_x, + label_offset_y=component_offset_y) + else: + barcode_arguments = dict([ + (field_name, component[field_name]) + for field_name in [ + zpl2.ARG_ORIENTATION, + zpl2.ARG_CHECK_DIGITS, + zpl2.ARG_HEIGHT, + zpl2.ARG_INTERPRETATION_LINE, + zpl2.ARG_INTERPRETATION_LINE_ABOVE, + zpl2.ARG_SECURITY_LEVEL, + zpl2.ARG_COLUMNS_COUNT, + zpl2.ARG_ROWS_COUNT, + zpl2.ARG_TRUNCATE, + zpl2.ARG_MODULE_WIDTH, + zpl2.ARG_BAR_WIDTH_RATIO, + ] + ]) + label_data.barcode_data( + component.origin_x + offset_x, + component.origin_y + offset_y, + component.component_type, barcode_arguments, data) + + @api.multi + def _generate_zpl2_data(self, record, page_count=1, **extra): + self.ensure_one() + label_data = zpl2.Zpl2() + + for page_number in range(page_count): + # Initialize printer's configuration + label_data.label_start() + label_data.print_width(self.width) + label_data.label_encoding() + + label_data.label_home(self.origin_x, self.origin_y) + + self._generate_zpl2_components_data( + label_data, record, page_number=page_number, + page_count=page_count) + + # Restore printer's configuration and end the label + label_data.configuration_update(zpl2.CONF_RECALL_LAST_SAVED) + label_data.label_end() + + return label_data.output() + + @api.multi + def print_label(self, printer, record, page_count=1, **extra): + for label in self: + if record._name != label.model_id.model: + raise exceptions.UserError( + _('This label cannot be used on {model}').format( + model=record._name)) + + # Send the label to printer + label_contents = label._generate_zpl2_data( + record, page_count=page_count, **extra) + printer.print_document(None, label_contents, 'raw') + + return True diff --git a/printer_zpl2/models/printing_label_zpl2_component.py b/printer_zpl2/models/printing_label_zpl2_component.py new file mode 100644 index 0000000..c61fd41 --- /dev/null +++ b/printer_zpl2/models/printing_label_zpl2_component.py @@ -0,0 +1,144 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2016 SYLEAM () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging +from openerp import fields, models + +_logger = logging.getLogger(__name__) + +try: + import zpl2 +except ImportError: + _logger.debug('Cannot `import zpl2`.') + + +class PrintingLabelZpl2Component(models.Model): + _name = 'printing.label.zpl2.component' + _description = 'ZPL II Label Component' + _order = 'sequence' + + label_id = fields.Many2one( + comodel_name='printing.label.zpl2', string='Label', + required=True, ondelete='cascade', help='Label using this component.') + sequence = fields.Integer(help='Order used to print the elements.') + name = fields.Char(required=True, help='Name of the component.') + origin_x = fields.Integer( + required=True, default=10, + help='Origin point of the component in the label, X coordinate.') + origin_y = fields.Integer( + required=True, default=10, + help='Origin point of the component in the label, Y coordinate.') + component_type = fields.Selection( + selection=[ + ('text', 'Text'), + ('rectangle', 'Rectangle / Line'), + ('circle', 'Circle'), + (zpl2.BARCODE_CODE_11, 'Code 11'), + (zpl2.BARCODE_INTERLEAVED_2_OF_5, 'Interleaved 2 of 5'), + (zpl2.BARCODE_CODE_39, 'Code 39'), + (zpl2.BARCODE_CODE_49, 'Code 49'), + (zpl2.BARCODE_PDF417, 'PDF417'), + (zpl2.BARCODE_EAN_8, 'EAN-8'), + (zpl2.BARCODE_UPC_E, 'UPC-E'), + (zpl2.BARCODE_CODE_128, 'Code 128'), + (zpl2.BARCODE_EAN_13, 'EAN-13'), + ('sublabel', 'Sublabel'), + ], string='Type', required=True, default='text', oldname='type', + help='Type of content, simple text or barcode.') + font = fields.Selection( + selection=[ + (zpl2.FONT_DEFAULT, 'Default'), + (zpl2.FONT_9X5, '9x5'), + (zpl2.FONT_11X7, '11x7'), + (zpl2.FONT_18X10, '18x10'), + (zpl2.FONT_28X15, '28x15'), + (zpl2.FONT_26X13, '26x13'), + (zpl2.FONT_60X40, '60x40'), + (zpl2.FONT_21X13, '21x13'), + ], required=True, default=zpl2.FONT_DEFAULT, + help='Font to use, for text only.') + thickness = fields.Integer(help='Thickness of the line to draw.') + color = fields.Selection( + selection=[ + (zpl2.COLOR_BLACK, 'Black'), + (zpl2.COLOR_WHITE, 'White'), + ], default=zpl2.COLOR_BLACK, + help='Color of the line to draw.') + orientation = fields.Selection( + selection=[ + (zpl2.ORIENTATION_NORMAL, 'Normal'), + (zpl2.ORIENTATION_ROTATED, 'Rotated'), + (zpl2.ORIENTATION_INVERTED, 'Inverted'), + (zpl2.ORIENTATION_BOTTOM_UP, 'Read from Bottom up'), + ], required=True, default=zpl2.ORIENTATION_NORMAL, + help='Orientation of the barcode.') + check_digits = fields.Boolean( + help='Check if you want to compute and print the check digit.') + height = fields.Integer( + help='Height of the printed component. For a text component, height ' + 'of a single character.') + width = fields.Integer( + help='Width of the printed component. For a text component, width of ' + 'a single character.') + rounding = fields.Integer( + help='Rounding of the printed rectangle corners.') + interpretation_line = fields.Boolean( + help='Check if you want the interpretation line to be printed.') + interpretation_line_above = fields.Boolean( + help='Check if you want the interpretation line to be printed above ' + 'the barcode.') + module_width = fields.Integer( + default=2, help='Module width for the barcode.') + bar_width_ratio = fields.Float( + default=3.0, help='Ratio between wide bar and narrow bar.') + security_level = fields.Integer(help='Security level for error detection.') + columns_count = fields.Integer(help='Number of data columns to encode.') + rows_count = fields.Integer(help='Number of rows to encode.') + truncate = fields.Boolean( + help='Check if you want to truncate the barcode.') + data = fields.Char( + size=256, default='""', required=True, + help='Data to print on this component. Resource values can be ' + 'inserted with %(object.field_name)s.') + sublabel_id = fields.Many2one( + comodel_name='printing.label.zpl2', string='Sublabel', + help='Another label to include into this one as a component. ' + 'This allows to define reusable labels parts.') + repeat = fields.Boolean( + string='Repeatable', + help='Check this box to repeat this component on the label.') + repeat_offset = fields.Integer( + default=0, + help='Number of elements to skip when reading a list of elements.') + repeat_count = fields.Integer( + default=1, + help='Maximum count of repeats of the component.') + repeat_offset_x = fields.Integer( + help='X coordinate offset between each occurence of this component on ' + 'the label.') + repeat_offset_y = fields.Integer( + help='Y coordinate offset between each occurence of this component on ' + 'the label.') + reverse_print = fields.Boolean( + help='If checked, the data will be printed in the inverse color of ' + 'the background.') + in_block = fields.Boolean( + help='If checked, the data will be restrected in a ' + 'defined block on the label.') + block_width = fields.Integer(help='Width of the block.') + block_lines = fields.Integer( + default=1, help='Maximum number of lines to print in the block.') + block_spaces = fields.Integer( + help='Number of spaces added between lines in the block.') + block_justify = fields.Selection( + selection=[ + (zpl2.JUSTIFY_LEFT, 'Left'), + (zpl2.JUSTIFY_CENTER, 'Center'), + (zpl2.JUSTIFY_JUSTIFIED, 'Justified'), + (zpl2.JUSTIFY_RIGHT, 'Right'), + ], string='Justify', required=True, default='L', + help='Choose how the text will be justified in the block.') + block_left_margin = fields.Integer( + string='Left Margin', + help='Left margin for the second and other lines in the block.') diff --git a/printer_zpl2/security/ir.model.access.csv b/printer_zpl2/security/ir.model.access.csv new file mode 100644 index 0000000..acd4271 --- /dev/null +++ b/printer_zpl2/security/ir.model.access.csv @@ -0,0 +1,5 @@ +"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" +"printing_label_zpl2_user","Printing Label ZPL2 User","model_printing_label_zpl2","base_report_to_printer.printing_group_user",1,0,0,0 +"printing_label_zpl2_manager","Printing Label ZPL2 Manager","model_printing_label_zpl2","base_report_to_printer.printing_group_manager",1,1,1,1 +"printing_label_zpl2_component_user","Printing Label ZPL2 Component User","model_printing_label_zpl2_component","base_report_to_printer.printing_group_user",1,0,0,0 +"printing_label_zpl2_component_manager","Printing Label ZPL2 Component Manager","model_printing_label_zpl2_component","base_report_to_printer.printing_group_manager",1,1,1,1 diff --git a/printer_zpl2/tests/__init__.py b/printer_zpl2/tests/__init__.py new file mode 100644 index 0000000..4483773 --- /dev/null +++ b/printer_zpl2/tests/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2016 SYLEAM () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import test_printing_label_zpl2 +from . import test_wizard_print_record_label diff --git a/printer_zpl2/tests/test_printing_label_zpl2.py b/printer_zpl2/tests/test_printing_label_zpl2.py new file mode 100644 index 0000000..e90d5fe --- /dev/null +++ b/printer_zpl2/tests/test_printing_label_zpl2.py @@ -0,0 +1,941 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +import mock + +from openerp import exceptions +from openerp.tests.common import TransactionCase + + +model = 'openerp.addons.base_report_to_printer.models.printing_server' + + +class TestPrintingLabelZpl2(TransactionCase): + def setUp(self): + super(TestPrintingLabelZpl2, self).setUp() + self.Model = self.env['printing.label.zpl2'] + self.ComponentModel = self.env['printing.label.zpl2.component'] + self.server = self.env['printing.server'].create({}) + self.printer = self.env['printing.printer'].create({ + 'name': 'Printer', + 'server_id': self.server.id, + 'system_name': 'Sys Name', + 'default': True, + 'status': 'unknown', + 'status_message': 'Msg', + 'model': 'res.users', + 'location': 'Location', + 'uri': 'URI', + }) + self.label_vals = { + 'name': 'ZPL II Label', + 'model_id': self.env.ref( + 'base_report_to_printer.model_printing_printer').id, + } + self.component_vals = { + 'name': 'ZPL II Label Component', + } + + def new_label(self, vals=None): + values = self.label_vals.copy() + if vals is not None: + values.update(vals) + return self.Model.create(values) + + def new_component(self, vals=None): + values = self.component_vals.copy() + if vals is not None: + values.update(vals) + return self.ComponentModel.create(values) + + def test_print_on_bad_model(self): + """ Check that printing on the bad model raises an exception """ + label = self.new_label() + with self.assertRaises(exceptions.UserError): + label.print_label(self.printer, label) + + @mock.patch('%s.cups' % model) + def test_print_empty_label(self, cups): + """ Check that printing an empty label works """ + label = self.new_label() + label.print_label(self.printer, self.printer) + cups.Connection().printFile.assert_called_once() + + def test_empty_label_contents(self): + """ Check contents of an empty label """ + label = self.new_label() + contents = label._generate_zpl2_data(self.printer) + self.assertEqual( + contents, + # Label start + '^XA\n' + # Print width + '^PW480\n' + # UTF-8 encoding + '^CI28\n' + # Label position + '^LH10,10\n' + # Recall last saved parameters + '^JUR\n' + # Label end + '^XZ') + + def test_sublabel_label_contents(self): + """ Check contents of a sublabel label component """ + sublabel = self.new_label({ + 'name': 'Sublabel', + }) + data = 'Some text' + self.new_component({ + 'label_id': sublabel.id, + 'data': '"' + data + '"', + }) + label = self.new_label() + self.new_component({ + 'label_id': label.id, + 'name': 'Sublabel contents', + 'component_type': 'sublabel', + 'sublabel_id': sublabel.id, + }) + contents = label._generate_zpl2_data(self.printer) + self.assertEqual( + contents, + # Label start + '^XA\n' + # Print width + '^PW480\n' + # UTF-8 encoding + '^CI28\n' + # Label position + '^LH10,10\n' + # Sublabel component position + # Position 30x30 because the default values are : + # - 10x10 for the sublabel component in the main label + # - 10x10 for the sublabel in the sublabel component + # - 10x10 for the component in the sublabel + '^FO30,30' + # Sublabel component format + '^A0N,10,10' + # Sublabel component contents + '^FD{contents}' + # Sublabel component end + '^FS\n' + # Recall last saved parameters + '^JUR\n' + # Label end + '^XZ'.format(contents=data)) + + def test_repeatable_component_label_fixed_contents(self): + """ Check contents of a repeatable label component + + Check that a fixed value is repeated each time + """ + label = self.new_label({ + 'model_id': self.env.ref( + 'printer_zpl2.model_printing_label_zpl2').id, + }) + data = 'Some text' + self.new_component({ + 'label_id': label.id, + 'data': '"' + data + '"', + 'repeat': True, + 'repeat_count': 3, + 'repeat_offset_y': 15, + }) + contents = label._generate_zpl2_data(label) + self.assertEqual( + contents, + # Label start + '^XA\n' + # Print width + '^PW480\n' + # UTF-8 encoding + '^CI28\n' + # Label position + '^LH10,10\n' + # First component position + '^FO10,10' + # First component format + '^A0N,10,10' + # First component contents + '^FD{contents}' + # First component end + '^FS\n' + # Second component position + '^FO10,25' + # Second component format + '^A0N,10,10' + # Second component contents + '^FD{contents}' + # Second component end + '^FS\n' + # Third component position + '^FO10,40' + # Third component format + '^A0N,10,10' + # Third component contents + '^FD{contents}' + # Third component end + '^FS\n' + # Recall last saved parameters + '^JUR\n' + # Label end + '^XZ'.format(contents=data)) + + def test_repeatable_component_label_iterable_contents(self): + """ Check contents of a repeatable label component + + Check that an iterable contents (list, tuple, etc.) is browsed + If the repeat_count is higher than the value length, all values are + displayed + """ + label = self.new_label({ + 'model_id': self.env.ref( + 'printer_zpl2.model_printing_label_zpl2').id, + }) + data = ['First text', 'Second text', 'Third text'] + self.new_component({ + 'label_id': label.id, + 'data': str(data), + 'repeat': True, + 'repeat_offset': 1, + 'repeat_count': 3, + 'repeat_offset_y': 15, + }) + contents = label._generate_zpl2_data(label) + self.assertEqual( + contents, + # Label start + '^XA\n' + # Print width + '^PW480\n' + # UTF-8 encoding + '^CI28\n' + # Label position + '^LH10,10\n' + # First component position + '^FO10,10' + # First component format + '^A0N,10,10' + # First component contents + '^FD{contents[1]}' + # First component end + '^FS\n' + # Second component position + '^FO10,25' + # Second component format + '^A0N,10,10' + # Second component contents + '^FD{contents[2]}' + # Second component end + '^FS\n' + # Recall last saved parameters + '^JUR\n' + # Label end + '^XZ'.format(contents=data)) + + def test_repeatable_component_label_iterable_offset(self): + """ Check contents of a repeatable label component with an offset + + Check that an iterable contents (list, tuple, etc.) is browsed + If the repeat_count is higher than the value length, all values are + displayed + """ + label = self.new_label({ + 'model_id': self.env.ref( + 'printer_zpl2.model_printing_label_zpl2').id, + }) + data = ['Text {value}'.format(value=ind) for ind in range(20)] + self.new_component({ + 'label_id': label.id, + 'data': str(data), + 'repeat': True, + 'repeat_offset': 10, + 'repeat_count': 3, + 'repeat_offset_y': 15, + }) + contents = label._generate_zpl2_data(label) + self.assertEqual( + contents, + # Label start + '^XA\n' + # Print width + '^PW480\n' + # UTF-8 encoding + '^CI28\n' + # Label position + '^LH10,10\n' + # First component position + '^FO10,10' + # First component format + '^A0N,10,10' + # First component contents + '^FD{contents[10]}' + # First component end + '^FS\n' + # Second component position + '^FO10,25' + # Second component format + '^A0N,10,10' + # Second component contents + '^FD{contents[11]}' + # Second component end + '^FS\n' + # Third component position + '^FO10,40' + # Third component format + '^A0N,10,10' + # Third component contents + '^FD{contents[12]}' + # Third component end + '^FS\n' + # Recall last saved parameters + '^JUR\n' + # Label end + '^XZ'.format(contents=data)) + + def test_repeatable_sublabel_contents(self): + """ Check contents of a repeatable sublabel label component """ + sublabel = self.new_label({ + 'name': 'Sublabel', + 'model_id': self.env.ref( + 'printer_zpl2.model_printing_label_zpl2_component').id, + }) + self.new_component({ + 'label_id': sublabel.id, + 'name': 'Components name', + 'data': 'object.name', + }) + self.new_component({ + 'label_id': sublabel.id, + 'name': 'Components data', + 'data': 'object.data', + 'origin_x': 50, + }) + label = self.new_label({ + 'model_id': self.env.ref( + 'printer_zpl2.model_printing_label_zpl2').id, + }) + self.new_component({ + 'label_id': label.id, + 'name': 'Label name', + 'data': 'object.name', + }) + self.new_component({ + 'label_id': label.id, + 'name': 'Label components', + 'component_type': 'sublabel', + 'origin_x': 15, + 'origin_y': 30, + 'data': 'object.component_ids', + 'sublabel_id': sublabel.id, + 'repeat': True, + 'repeat_count': 3, + 'repeat_offset_y': 15, + }) + contents = label._generate_zpl2_data(label) + self.assertEqual( + contents, + # Label start + '^XA\n' + # Print width + '^PW480\n' + # UTF-8 encoding + '^CI28\n' + # Label position + '^LH10,10\n' + # Label name component position + '^FO10,10' + # Label name component format + '^A0N,10,10' + # Label name component contents + '^FD{label.name}' + # Label name component end + '^FS\n' + # First component name component position + '^FO35,50' + # First component name component format + '^A0N,10,10' + # First component name component contents + '^FD{label.component_ids[0].name}' + # First component name component end + '^FS\n' + # First component data component position + '^FO75,50' + # First component data component format + '^A0N,10,10' + # First component data component contents + '^FD{label.component_ids[0].data}' + # First component data component end + '^FS\n' + # Second component name component position + '^FO35,65' + # Second component name component format + '^A0N,10,10' + # Second component name component contents + '^FD{label.component_ids[1].name}' + # Second component name component end + '^FS\n' + # Second component data component position + '^FO75,65' + # Second component data component format + '^A0N,10,10' + # Second component data component contents + '^FD{label.component_ids[1].data}' + # Second component data component end + '^FS\n' + # Recall last saved parameters + '^JUR\n' + # Label end + '^XZ'.format(label=label)) + + def test_text_label_contents(self): + """ Check contents of a text label """ + label = self.new_label() + data = 'Some text' + self.new_component({ + 'label_id': label.id, + 'data': '"' + data + '"', + }) + contents = label._generate_zpl2_data(self.printer) + self.assertEqual( + contents, + # Label start + '^XA\n' + # Print width + '^PW480\n' + # UTF-8 encoding + '^CI28\n' + # Label position + '^LH10,10\n' + # Component position + '^FO10,10' + # Component format + '^A0N,10,10' + # Component contents + '^FD{contents}' + # Component end + '^FS\n' + # Recall last saved parameters + '^JUR\n' + # Label end + '^XZ'.format(contents=data)) + + def test_reversed_text_label_contents(self): + """ Check contents of a text label """ + label = self.new_label() + data = 'Some text' + self.new_component({ + 'label_id': label.id, + 'data': '"' + data + '"', + 'reverse_print': True, + }) + contents = label._generate_zpl2_data(self.printer) + self.assertEqual( + contents, + # Label start + '^XA\n' + # Print width + '^PW480\n' + # UTF-8 encoding + '^CI28\n' + # Label position + '^LH10,10\n' + # Component position + '^FO10,10' + # Component format + '^A0N,10,10' + # Reverse print argument + '^FR' + # Component contents + '^FD{contents}' + # Component end + '^FS\n' + # Recall last saved parameters + '^JUR\n' + # Label end + '^XZ'.format(contents=data)) + + def test_block_text_label_contents(self): + """ Check contents of a text label """ + label = self.new_label() + data = 'Some text' + self.new_component({ + 'label_id': label.id, + 'data': '"' + data + '"', + 'in_block': True, + }) + contents = label._generate_zpl2_data(self.printer) + self.assertEqual( + contents, + # Label start + '^XA\n' + # Print width + '^PW480\n' + # UTF-8 encoding + '^CI28\n' + # Label position + '^LH10,10\n' + # Component position + '^FO10,10' + # Component format + '^A0N,10,10' + # Block definition + '^FB0,1,0,L,0' + # Component contents + '^FD{contents}' + # Component end + '^FS\n' + # Recall last saved parameters + '^JUR\n' + # Label end + '^XZ'.format(contents=data)) + + def test_rectangle_label_contents(self): + """ Check contents of a rectangle label """ + label = self.new_label() + self.new_component({ + 'label_id': label.id, + 'component_type': 'rectangle', + }) + contents = label._generate_zpl2_data(self.printer) + self.assertEqual( + contents, + # Label start + '^XA\n' + # Print width + '^PW480\n' + # UTF-8 encoding + '^CI28\n' + # Label position + '^LH10,10\n' + # Component position + '^FO10,10' + # Component format + '^GB1,1,1,B,0' + # Component end + '^FS\n' + # Recall last saved parameters + '^JUR\n' + # Label end + '^XZ') + + def test_circle_label_contents(self): + """ Check contents of a circle label """ + label = self.new_label() + self.new_component({ + 'label_id': label.id, + 'component_type': 'circle', + }) + contents = label._generate_zpl2_data(self.printer) + self.assertEqual( + contents, + # Label start + '^XA\n' + # Print width + '^PW480\n' + # UTF-8 encoding + '^CI28\n' + # Label position + '^LH10,10\n' + # Component position + '^FO10,10' + # Component format + '^GC3,2,B' + # Component end + '^FS\n' + # Recall last saved parameters + '^JUR\n' + # Label end + '^XZ') + + def test_code11_barcode_label_contents(self): + """ Check contents of a code 11 barcode label """ + label = self.new_label() + data = 'Some text' + self.new_component({ + 'label_id': label.id, + 'component_type': 'code_11', + 'data': '"' + data + '"', + }) + contents = label._generate_zpl2_data(self.printer) + self.assertEqual( + contents, + # Label start + '^XA\n' + # Print width + '^PW480\n' + # UTF-8 encoding + '^CI28\n' + # Label position + '^LH10,10\n' + # Barcode default format + '^BY2,3.0' + # Component position + '^FO10,10' + # Component format + '^B1N,N,0,N,N' + # Component contents + '^FD{contents}' + # Component end + '^FS\n' + # Recall last saved parameters + '^JUR\n' + # Label end + '^XZ'.format(contents=data)) + + def test_2of5_barcode_label_contents(self): + """ Check contents of a interleaved 2 of 5 barcode label """ + label = self.new_label() + data = 'Some text' + self.new_component({ + 'label_id': label.id, + 'component_type': 'interleaved_2_of_5', + 'data': '"' + data + '"', + }) + contents = label._generate_zpl2_data(self.printer) + self.assertEqual( + contents, + # Label start + '^XA\n' + # Print width + '^PW480\n' + # UTF-8 encoding + '^CI28\n' + # Label position + '^LH10,10\n' + # Barcode default format + '^BY2,3.0' + # Component position + '^FO10,10' + # Component format + '^B2N,0,N,N,N' + # Component contents + '^FD{contents}' + # Component end + '^FS\n' + # Recall last saved parameters + '^JUR\n' + # Label end + '^XZ'.format(contents=data)) + + def test_code39_barcode_label_contents(self): + """ Check contents of a code 39 barcode label """ + label = self.new_label() + data = 'Some text' + self.new_component({ + 'label_id': label.id, + 'component_type': 'code_39', + 'data': '"' + data + '"', + }) + contents = label._generate_zpl2_data(self.printer) + self.assertEqual( + contents, + # Label start + '^XA\n' + # Print width + '^PW480\n' + # UTF-8 encoding + '^CI28\n' + # Label position + '^LH10,10\n' + # Barcode default format + '^BY2,3.0' + # Component position + '^FO10,10' + # Component format + '^B3N,N,0,N,N' + # Component contents + '^FD{contents}' + # Component end + '^FS\n' + # Recall last saved parameters + '^JUR\n' + # Label end + '^XZ'.format(contents=data)) + + def test_code49_barcode_label_contents(self): + """ Check contents of a code 49 barcode label """ + label = self.new_label() + data = 'Some text' + self.new_component({ + 'label_id': label.id, + 'component_type': 'code_49', + 'data': '"' + data + '"', + }) + contents = label._generate_zpl2_data(self.printer) + self.assertEqual( + contents, + # Label start + '^XA\n' + # Print width + '^PW480\n' + # UTF-8 encoding + '^CI28\n' + # Label position + '^LH10,10\n' + # Barcode default format + '^BY2,3.0' + # Component position + '^FO10,10' + # Component format + '^B4N,0,N' + # Component contents + '^FD{contents}' + # Component end + '^FS\n' + # Recall last saved parameters + '^JUR\n' + # Label end + '^XZ'.format(contents=data)) + + def test_code49_barcode_label_contents_line(self): + """ Check contents of a code 49 barcode label """ + label = self.new_label() + data = 'Some text' + self.new_component({ + 'label_id': label.id, + 'component_type': 'code_49', + 'data': '"' + data + '"', + 'interpretation_line': True, + }) + contents = label._generate_zpl2_data(self.printer) + self.assertEqual( + contents, + # Label start + '^XA\n' + # Print width + '^PW480\n' + # UTF-8 encoding + '^CI28\n' + # Label position + '^LH10,10\n' + # Barcode default format + '^BY2,3.0' + # Component position + '^FO10,10' + # Component format + '^B4N,0,B' + # Component contents + '^FD{contents}' + # Component end + '^FS\n' + # Recall last saved parameters + '^JUR\n' + # Label end + '^XZ'.format(contents=data)) + + def test_code49_barcode_label_contents_with_above(self): + """ Check contents of a code 49 barconde label + with interpretation line above + """ + label = self.new_label() + data = 'Some text' + self.new_component({ + 'label_id': label.id, + 'component_type': 'code_49', + 'data': '"' + data + '"', + 'interpretation_line': True, + 'interpretation_line_above': True, + }) + contents = label._generate_zpl2_data(self.printer) + self.assertEqual( + contents, + # Label start + '^XA\n' + # Print width + '^PW480\n' + # UTF-8 encoding + '^CI28\n' + # Label position + '^LH10,10\n' + # Barcode default format + '^BY2,3.0' + # Component position + '^FO10,10' + # Component format + '^B4N,0,A' + # Component contents + '^FD{contents}' + # Component end + '^FS\n' + # Recall last saved parameters + '^JUR\n' + # Label end + '^XZ'.format(contents=data)) + + def test_pdf417_barcode_label_contents(self): + """ Check contents of a pdf417 barcode label """ + label = self.new_label() + data = 'Some text' + self.new_component({ + 'label_id': label.id, + 'component_type': 'pdf417', + 'data': '"' + data + '"', + }) + contents = label._generate_zpl2_data(self.printer) + self.assertEqual( + contents, + # Label start + '^XA\n' + # Print width + '^PW480\n' + # UTF-8 encoding + '^CI28\n' + # Label position + '^LH10,10\n' + # Barcode default format + '^BY2,3.0' + # Component position + '^FO10,10' + # Component format + '^B7N,0,0,0,0,N' + # Component contents + '^FD{contents}' + # Component end + '^FS\n' + # Recall last saved parameters + '^JUR\n' + # Label end + '^XZ'.format(contents=data)) + + def test_ean8_barcode_label_contents(self): + """ Check contents of a ean-8 barcode label """ + label = self.new_label() + data = 'Some text' + self.new_component({ + 'label_id': label.id, + 'component_type': 'ean-8', + 'data': '"' + data + '"', + }) + contents = label._generate_zpl2_data(self.printer) + self.assertEqual( + contents, + # Label start + '^XA\n' + # Print width + '^PW480\n' + # UTF-8 encoding + '^CI28\n' + # Label position + '^LH10,10\n' + # Barcode default format + '^BY2,3.0' + # Component position + '^FO10,10' + # Component format + '^B8N,0,N,N' + # Component contents + '^FD{contents}' + # Component end + '^FS\n' + # Recall last saved parameters + '^JUR\n' + # Label end + '^XZ'.format(contents=data)) + + def test_upce_barcode_label_contents(self): + """ Check contents of a upc-e barcode label """ + label = self.new_label() + data = 'Some text' + self.new_component({ + 'label_id': label.id, + 'component_type': 'upc-e', + 'data': '"' + data + '"', + }) + contents = label._generate_zpl2_data(self.printer) + self.assertEqual( + contents, + # Label start + '^XA\n' + # Print width + '^PW480\n' + # UTF-8 encoding + '^CI28\n' + # Label position + '^LH10,10\n' + # Barcode default format + '^BY2,3.0' + # Component position + '^FO10,10' + # Component format + '^B9N,0,N,N,N' + # Component contents + '^FD{contents}' + # Component end + '^FS\n' + # Recall last saved parameters + '^JUR\n' + # Label end + '^XZ'.format(contents=data)) + + def test_code128_barcode_label_contents(self): + """ Check contents of a code 128 barcode label """ + label = self.new_label() + data = 'Some text' + self.new_component({ + 'label_id': label.id, + 'component_type': 'code_128', + 'data': '"' + data + '"', + }) + contents = label._generate_zpl2_data(self.printer) + self.assertEqual( + contents, + # Label start + '^XA\n' + # Print width + '^PW480\n' + # UTF-8 encoding + '^CI28\n' + # Label position + '^LH10,10\n' + # Barcode default format + '^BY2,3.0' + # Component position + '^FO10,10' + # Component format + '^BCN,0,N,N,N' + # Component contents + '^FD{contents}' + # Component end + '^FS\n' + # Recall last saved parameters + '^JUR\n' + # Label end + '^XZ'.format(contents=data)) + + def test_ean13_barcode_label_contents(self): + """ Check contents of a ean-13 barcode label """ + label = self.new_label() + data = 'Some text' + self.new_component({ + 'label_id': label.id, + 'component_type': 'ean-13', + 'data': '"' + data + '"', + }) + contents = label._generate_zpl2_data(self.printer) + self.assertEqual( + contents, + # Label start + '^XA\n' + # Print width + '^PW480\n' + # UTF-8 encoding + '^CI28\n' + # Label position + '^LH10,10\n' + # Barcode default format + '^BY2,3.0' + # Component position + '^FO10,10' + # Component format + '^BEN,0,N,N' + # Component contents + '^FD{contents}' + # Component end + '^FS\n' + # Recall last saved parameters + '^JUR\n' + # Label end + '^XZ'.format(contents=data)) diff --git a/printer_zpl2/tests/test_wizard_print_record_label.py b/printer_zpl2/tests/test_wizard_print_record_label.py new file mode 100644 index 0000000..1293a40 --- /dev/null +++ b/printer_zpl2/tests/test_wizard_print_record_label.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +import mock + +from openerp.tests.common import TransactionCase + + +model = 'openerp.addons.base_report_to_printer.models.printing_server' + + +class TestWizardPrintRecordLabel(TransactionCase): + def setUp(self): + super(TestWizardPrintRecordLabel, self).setUp() + self.Model = self.env['wizard.print.record.label'] + self.server = self.env['printing.server'].create({}) + self.printer = self.env['printing.printer'].create({ + 'name': 'Printer', + 'server_id': self.server.id, + 'system_name': 'Sys Name', + 'default': True, + 'status': 'unknown', + 'status_message': 'Msg', + 'model': 'res.users', + 'location': 'Location', + 'uri': 'URI', + }) + self.label = self.env['printing.label.zpl2'].create({ + 'name': 'ZPL II Label', + 'model_id': self.env.ref( + 'base_report_to_printer.model_printing_printer').id, + }) + + @mock.patch('%s.cups' % model) + def test_print_record_label(self, cups): + """ Check that printing a label using the generic wizard works """ + wizard_obj = self.Model.with_context( + active_model='printing.printer', + active_id=self.printer.id, + active_ids=[self.printer.id], + ) + wizard = wizard_obj.create({}) + self.assertEqual(wizard.printer_id, self.printer) + self.assertEqual(wizard.label_id, self.label) + wizard.print_label() + cups.Connection().printFile.assert_called_once() + + def test_wizard_multiple_printers_and_labels(self): + """ Check that printer_id and label_id are not automatically filled + when there are multiple possible values + """ + self.env['printing.printer'].create({ + 'name': 'Other_Printer', + 'server_id': self.server.id, + 'system_name': 'Sys Name', + 'default': True, + 'status': 'unknown', + 'status_message': 'Msg', + 'model': 'res.users', + 'location': 'Location', + 'uri': 'URI', + }) + self.env['printing.label.zpl2'].create({ + 'name': 'Other ZPL II Label', + 'model_id': self.env.ref( + 'base_report_to_printer.model_printing_printer').id, + }) + wizard_obj = self.Model.with_context( + active_model='printing.printer', + active_id=self.printer.id, + active_ids=[self.printer.id], + ) + values = wizard_obj.default_get(['printer_id', 'label_id']) + self.assertEqual(values.get('printer_id', False), False) + self.assertEqual(values.get('label_id', False), False) + + def test_wizard_multiple_labels_but_on_different_models(self): + """ Check that label_id is automatically filled when there are multiple + labels, but only one on the right model + """ + self.env['printing.label.zpl2'].create({ + 'name': 'Other ZPL II Label', + 'model_id': self.env.ref('base.model_res_users').id, + }) + wizard_obj = self.Model.with_context( + active_model='printing.printer', + active_id=self.printer.id, + active_ids=[self.printer.id], + ) + wizard = wizard_obj.create({}) + self.assertEqual(wizard.label_id, self.label) diff --git a/printer_zpl2/views/printing_label_zpl2.xml b/printer_zpl2/views/printing_label_zpl2.xml new file mode 100644 index 0000000..1043b8b --- /dev/null +++ b/printer_zpl2/views/printing_label_zpl2.xml @@ -0,0 +1,144 @@ + + + + + + + printing.label.zpl2.tree + printing.label.zpl2 + + + + + + + + + printing.label.zpl2.form + printing.label.zpl2 + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + printing.label.zpl2.search + printing.label.zpl2 + + + + + + + + + ZPL II + ir.actions.act_window + printing.label.zpl2 + form + tree,form + + [] + {} + + + + + form + + + + + + tree + + + +
diff --git a/printer_zpl2/wizard/__init__.py b/printer_zpl2/wizard/__init__.py new file mode 100644 index 0000000..5c68984 --- /dev/null +++ b/printer_zpl2/wizard/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2016 SYLEAM () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import print_record_label diff --git a/printer_zpl2/wizard/print_record_label.py b/printer_zpl2/wizard/print_record_label.py new file mode 100644 index 0000000..121ded0 --- /dev/null +++ b/printer_zpl2/wizard/print_record_label.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2016 SYLEAM () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from openerp import models, api, fields + + +class PrintRecordLabel(models.TransientModel): + _name = 'wizard.print.record.label' + _description = 'Print Record Label' + + printer_id = fields.Many2one( + comodel_name='printing.printer', string='Printer', required=True, + help='Printer used to print the labels.') + label_id = fields.Many2one( + comodel_name='printing.label.zpl2', string='Label', required=True, + domain=lambda self: [ + ('model_id.model', '=', self.env.context.get('active_model'))], + help='Label to print.') + + @api.model + def default_get(self, fields_list): + values = super(PrintRecordLabel, self).default_get(fields_list) + + # Automatically select the printer and label, if only one is available + printers = self.env['printing.printer'].search([]) + if len(printers) == 1: + values['printer_id'] = printers.id + + labels = self.env['printing.label.zpl2'].search([ + ('model_id.model', '=', self.env.context.get('active_model')), + ]) + if len(labels) == 1: + values['label_id'] = labels.id + + return values + + @api.multi + def print_label(self): + """ Prints a label per selected record """ + record_model = self.env.context['active_model'] + for record_id in self.env.context['active_ids']: + record = self.env[record_model].browse(record_id) + self.label_id.print_label(self.printer_id, record) diff --git a/printer_zpl2/wizard/print_record_label.xml b/printer_zpl2/wizard/print_record_label.xml new file mode 100644 index 0000000..ed64c90 --- /dev/null +++ b/printer_zpl2/wizard/print_record_label.xml @@ -0,0 +1,30 @@ + + + + + wizard.print.record.label.form + wizard.print.record.label + +
+ + + + +
+ + - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
- printing.label.zpl2.search printing.label.zpl2 + diff --git a/printer_zpl2/wizard/__init__.py b/printer_zpl2/wizard/__init__.py index 5c68984..c9a79f0 100644 --- a/printer_zpl2/wizard/__init__.py +++ b/printer_zpl2/wizard/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (C) 2016 SYLEAM () # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). diff --git a/printer_zpl2/wizard/print_record_label.py b/printer_zpl2/wizard/print_record_label.py index bf39b0f..1944960 100644 --- a/printer_zpl2/wizard/print_record_label.py +++ b/printer_zpl2/wizard/print_record_label.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (C) 2016 SYLEAM () # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). @@ -38,7 +37,6 @@ class PrintRecordLabel(models.TransientModel): return values - @api.multi def print_label(self): """ Prints a label per selected record """ record_model = self.env.context['active_model'] diff --git a/printer_zpl2/wizard/print_record_label.xml b/printer_zpl2/wizard/print_record_label.xml index ed64c90..4f19936 100644 --- a/printer_zpl2/wizard/print_record_label.xml +++ b/printer_zpl2/wizard/print_record_label.xml @@ -13,7 +13,10 @@ - From 5467d9d384e5a4f075ed65e319e2708bb33ec9c5 Mon Sep 17 00:00:00 2001 From: Sylvain GARANCHER Date: Tue, 13 Feb 2018 16:57:56 +0100 Subject: [PATCH 16/54] [IMP] Add diagonal lines management --- printer_zpl2/models/printing_label_zpl2.py | 10 +++++++ .../models/printing_label_zpl2_component.py | 7 +++++ .../tests/test_printing_label_zpl2.py | 29 +++++++++++++++++++ printer_zpl2/views/printing_label_zpl2.xml | 13 +++++---- 4 files changed, 53 insertions(+), 6 deletions(-) diff --git a/printer_zpl2/models/printing_label_zpl2.py b/printer_zpl2/models/printing_label_zpl2.py index 9552d8d..e8bf697 100644 --- a/printer_zpl2/models/printing_label_zpl2.py +++ b/printer_zpl2/models/printing_label_zpl2.py @@ -123,6 +123,16 @@ class PrintingLabelZpl2(models.Model): zpl2.ARG_COLOR: component.color, zpl2.ARG_ROUNDING: component.rounding, }) + elif component.component_type == 'diagonal': + label_data.graphic_diagonal_line( + component_offset_x, component_offset_y, { + zpl2.ARG_WIDTH: component.width, + zpl2.ARG_HEIGHT: component.height, + zpl2.ARG_THICKNESS: component.thickness, + zpl2.ARG_COLOR: component.color, + zpl2.ARG_DIAGONAL_ORIENTATION: + component.diagonal_orientation, + }) elif component.component_type == 'graphic': image = component.graphic_image or data pil_image = Image.open(io.BytesIO( diff --git a/printer_zpl2/models/printing_label_zpl2_component.py b/printer_zpl2/models/printing_label_zpl2_component.py index 903169e..efdd593 100644 --- a/printer_zpl2/models/printing_label_zpl2_component.py +++ b/printer_zpl2/models/printing_label_zpl2_component.py @@ -42,6 +42,7 @@ class PrintingLabelZpl2Component(models.Model): selection=[ ('text', 'Text'), ('rectangle', 'Rectangle / Line'), + ('diagonal', 'Diagonal Line'), ('circle', 'Circle'), ('graphic', 'Graphic'), (zpl2.BARCODE_CODE_11, 'Code 11'), @@ -85,6 +86,12 @@ class PrintingLabelZpl2Component(models.Model): (zpl2.ORIENTATION_BOTTOM_UP, 'Read from Bottom up'), ], required=True, default=zpl2.ORIENTATION_NORMAL, help='Orientation of the barcode.') + diagonal_orientation = fields.Selection( + selection=[ + (zpl2.DIAGONAL_ORIENTATION_LEFT, 'Left (\\)'), + (zpl2.DIAGONAL_ORIENTATION_RIGHT, 'Right (/)'), + ], default=zpl2.DIAGONAL_ORIENTATION_LEFT, + help='Orientation of the diagonal line.') check_digits = fields.Boolean( help='Check if you want to compute and print the check digit.') height = fields.Integer( diff --git a/printer_zpl2/tests/test_printing_label_zpl2.py b/printer_zpl2/tests/test_printing_label_zpl2.py index ec610fe..7418382 100644 --- a/printer_zpl2/tests/test_printing_label_zpl2.py +++ b/printer_zpl2/tests/test_printing_label_zpl2.py @@ -527,6 +527,35 @@ class TestPrintingLabelZpl2(TransactionCase): # Label end '^XZ') + def test_diagonal_line_label_contents(self): + """ Check contents of a diagonal line label """ + label = self.new_label() + self.new_component({ + 'label_id': label.id, + 'component_type': 'diagonal', + }) + contents = label._generate_zpl2_data(self.printer).decode("utf-8") + self.assertEqual( + contents, + # Label start + '^XA\n' + # Print width + '^PW480\n' + # UTF-8 encoding + '^CI28\n' + # Label position + '^LH10,10\n' + # Component position + '^FO10,10' + # Component format + '^GD3,3,1,B,L' + # Component end + '^FS\n' + # Recall last saved parameters + '^JUR\n' + # Label end + '^XZ') + def test_circle_label_contents(self): """ Check contents of a circle label """ label = self.new_label() diff --git a/printer_zpl2/views/printing_label_zpl2.xml b/printer_zpl2/views/printing_label_zpl2.xml index fd7d04c..9dd679e 100644 --- a/printer_zpl2/views/printing_label_zpl2.xml +++ b/printer_zpl2/views/printing_label_zpl2.xml @@ -64,24 +64,25 @@ - + - + - + - - + + + - + From 76e719e09b4820e72aa56771532bc16f2de13d63 Mon Sep 17 00:00:00 2001 From: Florent de Labarre Date: Tue, 23 Jan 2018 00:28:50 +0100 Subject: [PATCH 17/54] [IMP] Add a preview on the label using labelary.com --- printer_zpl2/models/printing_label_zpl2.py | 82 ++++++++++++++++++++- printer_zpl2/tests/__init__.py | 1 + printer_zpl2/tests/test_test_mode.py | 85 ++++++++++++++++++++++ printer_zpl2/views/printing_label_zpl2.xml | 24 ++++++ 4 files changed, 190 insertions(+), 2 deletions(-) create mode 100644 printer_zpl2/tests/test_test_mode.py diff --git a/printer_zpl2/models/printing_label_zpl2.py b/printer_zpl2/models/printing_label_zpl2.py index e8bf697..a12cd17 100644 --- a/printer_zpl2/models/printing_label_zpl2.py +++ b/printer_zpl2/models/printing_label_zpl2.py @@ -6,8 +6,9 @@ import base64 import datetime import io import logging +import requests from PIL import Image, ImageOps -from odoo import exceptions, fields, models, _ +from odoo import api, exceptions, fields, models, _ from odoo.tools.safe_eval import safe_eval _logger = logging.getLogger(__name__) @@ -48,6 +49,19 @@ class PrintingLabelZpl2(models.Model): default=True) action_window_id = fields.Many2one( comodel_name='ir.actions.act_window', string='Action', readonly=True) + test_labelary_mode = fields.Boolean(string='Mode Labelary') + record_id = fields.Integer(string='Record ID', default=1) + extra = fields.Text(string="Extra", default='{}') + labelary_image = fields.Binary(string='Image from Labelary', readonly=True) + labelary_dpmm = fields.Selection( + selection=[ + ('6dpmm', '6dpmm (152 pdi)'), + ('8dpmm', '8dpmm (203 dpi)'), + ('12dpmm', '12dpmm (300 pdi)'), + ('24dpmm', '24dpmm (600 dpi)'), + ], string='Print density', required=True, default='8dpmm') + labelary_width = fields.Float(string='Width in mm', default=140) + labelary_height = fields.Float(string='Height in mm', default=70) def _generate_zpl2_components_data( self, label_data, record, page_number=1, page_count=1, @@ -205,10 +219,12 @@ class PrintingLabelZpl2(models.Model): self.ensure_one() label_data = zpl2.Zpl2() + labelary_emul = extra.get('labelary_emul', False) for page_number in range(page_count): # Initialize printer's configuration label_data.label_start() - label_data.print_width(self.width) + if not labelary_emul: + label_data.print_width(self.width) label_data.label_encoding() label_data.label_home(self.origin_x, self.origin_y) @@ -255,3 +271,65 @@ class PrintingLabelZpl2(models.Model): def unlink_action(self): self.mapped('action_window_id').unlink() + + def _get_record(self): + self.ensure_one() + Obj = self.env[self.model_id.model] + record = Obj.search([('id', '=', self.record_id)], limit=1) + if not record: + record = Obj.search([], limit=1, order='id desc') + self.record_id = record.id + + return record + + @api.onchange( + 'record_id', 'labelary_dpmm', 'labelary_width', 'labelary_height', + 'component_ids', 'origin_x', 'origin_y') + def _on_change_labelary(self): + self.ensure_one() + if not(self.test_labelary_mode and self.record_id and + self.labelary_width and self.labelary_height and + self.labelary_dpmm and self.component_ids): + return + record = self._get_record() + if record: + # If case there an error (in the data field with the safe_eval + # for exemple) the new component or the update is not lost. + try: + url = 'http://api.labelary.com/v1/printers/' \ + '{dpmm}/labels/{width}x{height}/0/' + width = round(self.labelary_width / 25.4, 2) + height = round(self.labelary_height / 25.4, 2) + url = url.format( + dpmm=self.labelary_dpmm, width=width, height=height) + extra = safe_eval(self.extra, {'env': self.env}) + zpl_file = self._generate_zpl2_data( + record, labelary_emul=True, **extra) + files = {'file': zpl_file} + headers = {'Accept': 'image/png'} + response = requests.post( + url, headers=headers, files=files, stream=True) + if response.status_code == 200: + # Add a padd + im = Image.open(io.BytesIO(response.content)) + im_size = im.size + new_im = Image.new( + 'RGB', (im_size[0] + 2, im_size[1] + 2), + (164, 164, 164)) + new_im.paste(im, (1, 1)) + imgByteArr = io.BytesIO() + new_im.save(imgByteArr, format='PNG') + self.labelary_image = base64.b64encode( + imgByteArr.getvalue()) + else: + return {'warning': { + 'title': _('Error with Labelary API.'), + 'message': response.status_code, + }} + + except Exception as e: + self.labelary_image = False + return {'warning': { + 'title': _('Some thing is wrong.'), + 'message': e, + }} diff --git a/printer_zpl2/tests/__init__.py b/printer_zpl2/tests/__init__.py index cc4e18f..03aa9ce 100644 --- a/printer_zpl2/tests/__init__.py +++ b/printer_zpl2/tests/__init__.py @@ -4,3 +4,4 @@ from . import test_printing_label_zpl2 from . import test_wizard_print_record_label from . import test_generate_action +from . import test_test_mode diff --git a/printer_zpl2/tests/test_test_mode.py b/printer_zpl2/tests/test_test_mode.py new file mode 100644 index 0000000..20abdb5 --- /dev/null +++ b/printer_zpl2/tests/test_test_mode.py @@ -0,0 +1,85 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo.tests.common import TransactionCase + +model = 'odoo.addons.base_report_to_printer.models.printing_server' + + +class TestWizardPrintRecordLabel(TransactionCase): + def setUp(self): + super(TestWizardPrintRecordLabel, self).setUp() + self.Model = self.env['wizard.print.record.label'] + self.server = self.env['printing.server'].create({}) + self.printer = self.env['printing.printer'].create({ + 'name': 'Printer', + 'server_id': self.server.id, + 'system_name': 'Sys Name', + 'default': True, + 'status': 'unknown', + 'status_message': 'Msg', + 'model': 'res.users', + 'location': 'Location', + 'uri': 'URI', + }) + self.label = self.env['printing.label.zpl2'].create({ + 'name': 'ZPL II Label', + 'model_id': self.env.ref( + 'base_report_to_printer.model_printing_printer').id, + }) + + def test_get_record(self): + """ Check if return a record """ + self.label.record_id = 10 + res = self.label._get_record() + + Obj = self.env[self.label.model_id.model] + record = Obj.search([('id', '=', self.label.record_id)], limit=1) + if not record: + record = Obj.search([], limit=1, order='id desc') + self.assertEqual(res, record) + + def test_emulation_without_params(self): + """ Check if not execute next if not in this mode """ + self.label.test_labelary_mode = False + self.label._on_change_labelary() + self.assertIs(self.label.labelary_image, None) + + def test_emulation_with_bad_header(self): + """ Check if bad header """ + self.label.test_labelary_mode = True + self.label.labelary_width = 80 + self.label.labelary_dpmm = '8dpmm' + self.label.labelary_height = 10000000 + self.env['printing.label.zpl2.component'].create({ + 'name': 'ZPL II Label', + 'label_id': self.label.id, + 'data': '"Test"'}) + self.label._on_change_labelary() + self.assertFalse(self.label.labelary_image) + + def test_emulation_with_bad_data_compute(self): + """ Check if bad data compute """ + self.label.test_labelary_mode = True + self.label.labelary_width = 80 + self.label.labelary_height = 30 + self.label.labelary_dpmm = '8dpmm' + component = self.env['printing.label.zpl2.component'].create({ + 'name': 'ZPL II Label', + 'label_id': self.label.id, + 'data': 'wrong_data'}) + self.label._on_change_labelary() + component.unlink() + self.assertIs(self.label.labelary_image, None) + + def test_emulation_with_good_data(self): + """ Check if ok """ + self.label.test_labelary_mode = True + self.label.labelary_width = 80 + self.label.labelary_height = 30 + self.label.labelary_dpmm = '8dpmm' + self.env['printing.label.zpl2.component'].create({ + 'name': 'ZPL II Label', + 'label_id': self.label.id, + 'data': '"good_data"', }) + self.label._on_change_labelary() + self.assertTrue(self.label.labelary_image) diff --git a/printer_zpl2/views/printing_label_zpl2.xml b/printer_zpl2/views/printing_label_zpl2.xml index 9dd679e..2263e06 100644 --- a/printer_zpl2/views/printing_label_zpl2.xml +++ b/printer_zpl2/views/printing_label_zpl2.xml @@ -126,6 +126,30 @@ + + +

+ Note : It is an emulation from http://labelary.com/, the result on printer can be diffrent. +

+
+ + + + + + + + + + + + + + + + + + From 92511a05335a7ce11926a4de10ef103abca7daec Mon Sep 17 00:00:00 2001 From: Florent de Labarre Date: Sat, 20 Jan 2018 23:20:11 +0100 Subject: [PATCH 18/54] [IMP] Add wizard to import ZPL2 --- printer_zpl2/README.rst | 1 + printer_zpl2/__manifest__.py | 1 + printer_zpl2/models/printing_label_zpl2.py | 11 + printer_zpl2/tests/__init__.py | 1 + printer_zpl2/tests/test_wizard_import_zpl2.py | 97 ++++ printer_zpl2/views/printing_label_zpl2.xml | 3 + printer_zpl2/wizard/__init__.py | 1 + printer_zpl2/wizard/wizard_import_zpl2.py | 437 ++++++++++++++++++ printer_zpl2/wizard/wizard_import_zpl2.xml | 24 + 9 files changed, 576 insertions(+) create mode 100644 printer_zpl2/tests/test_wizard_import_zpl2.py create mode 100644 printer_zpl2/wizard/wizard_import_zpl2.py create mode 100644 printer_zpl2/wizard/wizard_import_zpl2.xml diff --git a/printer_zpl2/README.rst b/printer_zpl2/README.rst index da103cc..e488011 100644 --- a/printer_zpl2/README.rst +++ b/printer_zpl2/README.rst @@ -24,6 +24,7 @@ To configure this module, you need to: #. Go to *Settings > Printing > Labels > ZPL II* #. Create new labels +#. Import ZPL2 code It's also possible to add a label printing wizard on any model by creating a new *ir.actions.act_window* record. For example, to add the printing wizard on the *product.product* model : diff --git a/printer_zpl2/__manifest__.py b/printer_zpl2/__manifest__.py index 3fe04b4..44e7b51 100644 --- a/printer_zpl2/__manifest__.py +++ b/printer_zpl2/__manifest__.py @@ -18,6 +18,7 @@ 'security/ir.model.access.csv', 'views/printing_label_zpl2.xml', 'wizard/print_record_label.xml', + 'wizard/wizard_import_zpl2.xml', ], 'installable': True, } diff --git a/printer_zpl2/models/printing_label_zpl2.py b/printer_zpl2/models/printing_label_zpl2.py index a12cd17..d175154 100644 --- a/printer_zpl2/models/printing_label_zpl2.py +++ b/printer_zpl2/models/printing_label_zpl2.py @@ -272,6 +272,17 @@ class PrintingLabelZpl2(models.Model): def unlink_action(self): self.mapped('action_window_id').unlink() + def import_zpl2(self): + self.ensure_one() + return { + 'view_type': 'form', + 'view_mode': 'form', + 'res_model': 'wizard.import.zpl2', + 'type': 'ir.actions.act_window', + 'target': 'new', + 'context': {'default_label_id': self.id}, + } + def _get_record(self): self.ensure_one() Obj = self.env[self.model_id.model] diff --git a/printer_zpl2/tests/__init__.py b/printer_zpl2/tests/__init__.py index 03aa9ce..78b9bd8 100644 --- a/printer_zpl2/tests/__init__.py +++ b/printer_zpl2/tests/__init__.py @@ -5,3 +5,4 @@ from . import test_printing_label_zpl2 from . import test_wizard_print_record_label from . import test_generate_action from . import test_test_mode +from . import test_wizard_import_zpl2 diff --git a/printer_zpl2/tests/test_wizard_import_zpl2.py b/printer_zpl2/tests/test_wizard_import_zpl2.py new file mode 100644 index 0000000..ce1a8e3 --- /dev/null +++ b/printer_zpl2/tests/test_wizard_import_zpl2.py @@ -0,0 +1,97 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo.tests.common import TransactionCase + + +class TestWizardImportZpl2(TransactionCase): + def setUp(self): + super(TestWizardImportZpl2, self).setUp() + self.Model = self.env['wizard.print.record.label'] + self.server = self.env['printing.server'].create({}) + self.printer = self.env['printing.printer'].create({ + 'name': 'Printer', + 'server_id': self.server.id, + 'system_name': 'Sys Name', + 'default': True, + 'status': 'unknown', + 'status_message': 'Msg', + 'model': 'res.users', + 'location': 'Location', + 'uri': 'URI', + }) + self.label = self.env['printing.label.zpl2'].create({ + 'name': 'ZPL II Label', + 'model_id': self.env.ref( + 'base_report_to_printer.model_printing_printer').id, + }) + + def test_open_wizard(self): + """ open wizard from label""" + res = self.label.import_zpl2() + self.assertEqual( + res.get('context').get('default_label_id'), + self.label.id) + + def test_wizard_import_zpl2(self): + """ Import ZPL2 from wizard """ + zpl_data = ("^XA\n" + "^CI28\n" + "^LH0,0\n" + "^CF0\n" + "^CFA,10\n" + "^CFB,10,10\n" + "^FO10,10^A0N,30,30^FDTEXT^FS\n" + "^BY2,3.0^FO600,60^BCN,30,N,N,N" + "^FDAJFGJAGJVJVHK^FS\n" + "^FO10,40^A0N,20,40^FB150,2,1,J,0^FDTEXT BLOCK^FS\n" + "^FO300,10^GC100,3,B^FS\n" + "^FO10,200^GB200,200,100,B,0^FS\n" + "^FO10,60^GFA,16.0,16.0,2.0," + "b'FFC0FFC0FFC0FFC0FFC0FFC0FFC0FFC0'^FS\n" + "^FO10,200^GB300,100,6,W,0^FS\n" + "^BY2,3.0^FO300,10^B1N,N,30,N,N^FD678987656789^FS\n" + "^BY2,3.0^FO300,70^B2N,30,Y,Y,N^FD567890987768^FS\n" + "^BY2,3.0^FO300,120^B3N,N,30,N,N^FD98765456787656^FS\n" + "^BY2,3.0^FO300,200^BQN,2,5,Q,7" + "^FDMM,A876567897656787658654645678^FS\n" + "^BY2,3.0^FO400,250^BER,40,Y,Y^FD9876789987654567^FS\n" + "^BY2,3.0^FO350,250^B7N,20,0,0,0,N^FD8765678987656789^FS\n" + "^BY2,3.0^FO700,10^B9N,20,N,N,N^FD87657890987654^FS\n" + "^BY2,3.0^FO600,200^B4N,50,N^FD7654567898765678^FS\n" + "^BY2,3.0^FO600,300^BEN,50,Y,Y^FD987654567890876567^FS\n" + "^FO300,300^AGI,50,50^FR^FDINVERTED^FS\n" + "^BY2,3.0^FO700,200^B8,50,N,N^FD987609876567^FS\n" + "^JUR\n" + "^XZ") + + vals = {'label_id': self.label.id, + 'delete_component': True, + 'data': zpl_data} + wizard = self.env['wizard.import.zpl2'].create(vals) + wizard.import_zpl2() + self.assertEqual( + 18, + len(self.label.component_ids)) + + def test_wizard_import_zpl2_add(self): + """ Import ZPL2 from wizard ADD""" + self.env['printing.label.zpl2.component'].create({ + 'name': 'ZPL II Label', + 'label_id': self.label.id, + 'data': '"data"', + 'sequence': 10}) + zpl_data = ("^XA\n" + "^CI28\n" + "^LH0,0\n" + "^FO10,10^A0N,30,30^FDTEXT^FS\n" + "^JUR\n" + "^XZ") + + vals = {'label_id': self.label.id, + 'delete_component': False, + 'data': zpl_data} + wizard = self.env['wizard.import.zpl2'].create(vals) + wizard.import_zpl2() + self.assertEqual( + 2, + len(self.label.component_ids)) diff --git a/printer_zpl2/views/printing_label_zpl2.xml b/printer_zpl2/views/printing_label_zpl2.xml index 2263e06..0272173 100644 --- a/printer_zpl2/views/printing_label_zpl2.xml +++ b/printer_zpl2/views/printing_label_zpl2.xml @@ -17,6 +17,9 @@ printing.label.zpl2
+
+
diff --git a/printer_zpl2/wizard/__init__.py b/printer_zpl2/wizard/__init__.py index c9a79f0..0389ab1 100644 --- a/printer_zpl2/wizard/__init__.py +++ b/printer_zpl2/wizard/__init__.py @@ -2,3 +2,4 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from . import print_record_label +from . import wizard_import_zpl2 diff --git a/printer_zpl2/wizard/wizard_import_zpl2.py b/printer_zpl2/wizard/wizard_import_zpl2.py new file mode 100644 index 0000000..8e5b8b0 --- /dev/null +++ b/printer_zpl2/wizard/wizard_import_zpl2.py @@ -0,0 +1,437 @@ +import logging +import re +import base64 +import binascii +import io + + +from PIL import Image, ImageOps +from odoo import models, api, fields, _ + +_logger = logging.getLogger(__name__) + +try: + import zpl2 +except ImportError: + _logger.debug('Cannot `import zpl2`.') + + +def _compute_arg(data, arg): + vals = {} + for i, d in enumerate(data.split(',')): + vals[arg[i]] = d + return vals + + +def _field_origin(data): + if data[:2] == 'FO': + position = data[2:] + vals = _compute_arg(position, ['origin_x', 'origin_y']) + return vals + return {} + + +def _font_format(data): + if data[:1] == 'A': + data = data.split(',') + vals = {} + if len(data[0]) > 1: + vals[zpl2.ARG_FONT] = data[0][1] + if len(data[0]) > 2: + vals[zpl2.ARG_ORIENTATION] = data[0][2] + + if len(data) > 1: + vals[zpl2.ARG_HEIGHT] = data[1] + if len(data) > 2: + vals[zpl2.ARG_WIDTH] = data[2] + return vals + return {} + + +def _default_font_format(data): + if data[:2] == 'CF': + args = [ + zpl2.ARG_FONT, + zpl2.ARG_HEIGHT, + zpl2.ARG_WIDTH, + ] + vals = _compute_arg(data[2:], args) + if vals.get(zpl2.ARG_HEIGHT, False) \ + and not vals.get(zpl2.ARG_WIDTH, False): + vals.update({zpl2.ARG_WIDTH: vals.get(zpl2.ARG_HEIGHT)}) + else: + vals.update({zpl2.ARG_HEIGHT: 10, zpl2.ARG_HEIGHT: 10}) + return vals + return {} + + +def _field_block(data): + if data[:2] == 'FB': + vals = {zpl2.ARG_IN_BLOCK: True} + args = [ + zpl2.ARG_BLOCK_WIDTH, + zpl2.ARG_BLOCK_LINES, + zpl2.ARG_BLOCK_SPACES, + zpl2.ARG_BLOCK_JUSTIFY, + zpl2.ARG_BLOCK_LEFT_MARGIN, + ] + vals.update(_compute_arg(data[2:], args)) + return vals + return {} + + +def _code11(data): + if data[:2] == 'B1': + vals = {'component_type': zpl2.BARCODE_CODE_11} + args = [ + zpl2.ARG_ORIENTATION, + zpl2.ARG_CHECK_DIGITS, + zpl2.ARG_HEIGHT, + zpl2.ARG_INTERPRETATION_LINE, + zpl2.ARG_INTERPRETATION_LINE_ABOVE, + ] + vals.update(_compute_arg(data[2:], args)) + return vals + return {} + + +def _interleaved2of5(data): + if data[:2] == 'B2': + vals = {'component_type': zpl2.BARCODE_INTERLEAVED_2_OF_5} + args = [ + zpl2.ARG_ORIENTATION, + zpl2.ARG_HEIGHT, + zpl2.ARG_INTERPRETATION_LINE, + zpl2.ARG_INTERPRETATION_LINE_ABOVE, + zpl2.ARG_CHECK_DIGITS, + ] + vals.update(_compute_arg(data[2:], args)) + return vals + return {} + + +def _code39(data): + if data[:2] == 'B3': + vals = {'component_type': zpl2.BARCODE_CODE_39} + args = [ + zpl2.ARG_ORIENTATION, + zpl2.ARG_CHECK_DIGITS, + zpl2.ARG_HEIGHT, + zpl2.ARG_INTERPRETATION_LINE, + zpl2.ARG_INTERPRETATION_LINE_ABOVE, + ] + vals.update(_compute_arg(data[2:], args)) + return vals + return {} + + +def _code49(data): + if data[:2] == 'B4': + vals = {'component_type': zpl2.BARCODE_CODE_49} + args = [ + zpl2.ARG_ORIENTATION, + zpl2.ARG_HEIGHT, + zpl2.ARG_INTERPRETATION_LINE, + zpl2.ARG_STARTING_MODE, + ] + vals.update(_compute_arg(data[2:], args)) + return vals + return {} + + +def _pdf417(data): + if data[:2] == 'B7': + vals = {'component_type': zpl2.BARCODE_PDF417} + args = [ + zpl2.ARG_ORIENTATION, + zpl2.ARG_HEIGHT, + zpl2.ARG_SECURITY_LEVEL, + zpl2.ARG_COLUMNS_COUNT, + zpl2.ARG_ROWS_COUNT, + zpl2.ARG_TRUNCATE, + ] + vals.update(_compute_arg(data[2:], args)) + return vals + return {} + + +def _ean8(data): + if data[:2] == 'B8': + vals = {'component_type': zpl2.BARCODE_EAN_8} + args = [ + zpl2.ARG_ORIENTATION, + zpl2.ARG_HEIGHT, + zpl2.ARG_INTERPRETATION_LINE, + zpl2.ARG_INTERPRETATION_LINE_ABOVE, + ] + vals.update(_compute_arg(data[2:], args)) + return vals + return {} + + +def _upce(data): + if data[:2] == 'B9': + vals = {'component_type': zpl2.BARCODE_UPC_E} + args = [ + zpl2.ARG_ORIENTATION, + zpl2.ARG_HEIGHT, + zpl2.ARG_INTERPRETATION_LINE, + zpl2.ARG_INTERPRETATION_LINE_ABOVE, + zpl2.ARG_CHECK_DIGITS, + ] + vals.update(_compute_arg(data[2:], args)) + return vals + return {} + + +def _code128(data): + if data[:2] == 'BC': + vals = {'component_type': zpl2.BARCODE_CODE_128} + args = [ + zpl2.ARG_ORIENTATION, + zpl2.ARG_HEIGHT, + zpl2.ARG_INTERPRETATION_LINE, + zpl2.ARG_INTERPRETATION_LINE_ABOVE, + zpl2.ARG_CHECK_DIGITS, + zpl2.ARG_MODE, + ] + vals.update(_compute_arg(data[2:], args)) + return vals + return {} + + +def _ean13(data): + if data[:2] == 'BE': + vals = {'component_type': zpl2.BARCODE_EAN_13} + args = [ + zpl2.ARG_ORIENTATION, + zpl2.ARG_HEIGHT, + zpl2.ARG_INTERPRETATION_LINE, + zpl2.ARG_INTERPRETATION_LINE_ABOVE, + ] + vals.update(_compute_arg(data[2:], args)) + return vals + return {} + + +def _qrcode(data): + if data[:2] == 'BQ': + vals = {'component_type': zpl2.BARCODE_QR_CODE} + args = [ + zpl2.ARG_ORIENTATION, + zpl2.ARG_MODEL, + zpl2.ARG_MAGNIFICATION_FACTOR, + zpl2.ARG_ERROR_CORRECTION, + zpl2.ARG_MASK_VALUE, + ] + vals.update(_compute_arg(data[2:], args)) + return vals + return {} + + +def _default_barcode_field(data): + if data[:2] == 'BY': + args = [ + zpl2.ARG_MODULE_WIDTH, + zpl2.ARG_BAR_WIDTH_RATIO, + zpl2.ARG_HEIGHT, + ] + return _compute_arg(data[2:], args) + return {} + + +def _field_reverse_print(data): + if data[:2] == 'FR': + return {zpl2.ARG_REVERSE_PRINT: True} + return {} + + +def _graphic_box(data): + if data[:2] == 'GB': + vals = {'component_type': 'rectangle'} + args = [ + zpl2.ARG_WIDTH, + zpl2.ARG_HEIGHT, + zpl2.ARG_THICKNESS, + zpl2.ARG_COLOR, + zpl2.ARG_ROUNDING, + ] + vals.update(_compute_arg(data[2:], args)) + return vals + return {} + + +def _graphic_circle(data): + if data[:2] == 'GC': + vals = {'component_type': 'circle'} + args = [ + zpl2.ARG_WIDTH, + zpl2.ARG_THICKNESS, + zpl2.ARG_COLOR, + ] + vals.update(_compute_arg(data[2:], args)) + return vals + return {} + + +def _graphic_field(data): + if data[:3] == 'GFA': + vals = {} + args = [ + 'compression', + 'total_bytes', + 'total_bytes', + 'bytes_per_row', + 'ascii_data', + ] + vals.update(_compute_arg(data[3:], args)) + + # Image + rawData = re.sub('[^A-F0-9]+', '', vals['ascii_data']) + rawData = binascii.unhexlify(rawData) + + width = int(float(vals['bytes_per_row']) * 8) + height = int(float(vals['total_bytes']) / width) * 8 + + img = Image.frombytes( + '1', (width, height), rawData, 'raw').convert('L') + img = ImageOps.invert(img) + + imgByteArr = io.BytesIO() + img.save(imgByteArr, format='PNG') + image = base64.b64encode(imgByteArr.getvalue()) + + return { + 'component_type': 'graphic', + 'graphic_image': image, + zpl2.ARG_WIDTH: width, + zpl2.ARG_HEIGHT: height, + } + return {} + + +def _get_data(data): + if data[:2] == 'FD': + return {'data': '"%s"' % data[2:]} + return {} + + +SUPPORTED_CODE = { + 'FO': {'method': _field_origin}, + 'FD': {'method': _get_data}, + 'A': {'method': _font_format}, + 'FB': {'method': _field_block}, + 'B1': {'method': _code11}, + 'B2': {'method': _interleaved2of5}, + 'B3': {'method': _code39}, + 'B4': {'method': _code49}, + 'B7': {'method': _pdf417}, + 'B8': {'method': _ean8}, + 'B9': {'method': _upce}, + 'BC': {'method': _code128}, + 'BE': {'method': _ean13}, + 'BQ': {'method': _qrcode}, + 'BY': { + 'method': _default_barcode_field, + 'default': [ + zpl2.BARCODE_CODE_11, + zpl2.BARCODE_INTERLEAVED_2_OF_5, + zpl2.BARCODE_CODE_39, + zpl2.BARCODE_CODE_49, + zpl2.BARCODE_PDF417, + zpl2.BARCODE_EAN_8, + zpl2.BARCODE_UPC_E, + zpl2.BARCODE_CODE_128, + zpl2.BARCODE_EAN_13, + zpl2.BARCODE_QR_CODE, + ], + }, + 'CF': {'method': _default_font_format, 'default': ['text']}, + 'FR': {'method': _field_reverse_print}, + 'GB': {'method': _graphic_box}, + 'GC': {'method': _graphic_circle}, + 'GFA': {'method': _graphic_field}, +} + + +class WizardImportZPl2(models.TransientModel): + _name = 'wizard.import.zpl2' + _description = 'Import ZPL2' + + label_id = fields.Many2one( + comodel_name='printing.label.zpl2', string='Label', + required=True, readonly=True,) + data = fields.Text( + required=True, help='Printer used to print the labels.') + delete_component = fields.Boolean( + string='Delete existing components', default=False) + + def _start_sequence(self): + sequences = self.mapped('label_id.component_ids.sequence') + if sequences: + return max(sequences) + 1 + return 0 + + def import_zpl2(self): + Zpl2Component = self.env['printing.label.zpl2.component'] + + if self.delete_component: + self.mapped('label_id.component_ids').unlink() + + Model = self.env['printing.label.zpl2.component'] + self.model_fields = Model.fields_get() + sequence = self._start_sequence() + default = {} + + for i, line in enumerate(self.data.split('\n')): + vals = {} + + args = line.split('^') + for arg in args: + for key, code in SUPPORTED_CODE.items(): + component_arg = code['method'](arg) + if component_arg: + if code.get('default', False): + for deft in code.get('default'): + default.update({deft: component_arg}) + else: + vals.update(component_arg) + break + + if vals: + if 'component_type' not in vals.keys(): + vals.update({'component_type': 'text'}) + + if vals['component_type'] in default.keys(): + vals.update(default[vals['component_type']]) + + vals = self._update_vals(vals) + + seq = sequence + i * 10 + vals.update({ + 'name': _('Import %s') % seq, + 'sequence': seq, + 'label_id': self.label_id.id, + }) + Zpl2Component.create(vals) + + def _update_vals(self, vals): + if 'orientation' in vals.keys() and vals['orientation'] == '': + vals['orientation'] = 'N' + + # Field + component = {} + for field, value in vals.items(): + if field in self.model_fields.keys(): + field_type = self.model_fields[field].get('type', False) + if field_type == 'boolean': + if value == '' or value == zpl2.BOOL_NO: + value = False + else: + value = True + if field_type in ('integer', 'float'): + value = float(value) + if field == 'model': + value = int(float(value)) + component.update({field: value}) + return component diff --git a/printer_zpl2/wizard/wizard_import_zpl2.xml b/printer_zpl2/wizard/wizard_import_zpl2.xml new file mode 100644 index 0000000..b718053 --- /dev/null +++ b/printer_zpl2/wizard/wizard_import_zpl2.xml @@ -0,0 +1,24 @@ + + + + wizard.import.zpl2.form + wizard.import.zpl2 + + + + + + + + + + + +
+
+ +
+
+
From d2f9e5961588540046a6d0d6b7bd0797cc72af8a Mon Sep 17 00:00:00 2001 From: Florent de Labarre Date: Tue, 23 Jan 2018 00:41:35 +0100 Subject: [PATCH 19/54] [IMP] Add a test mode to print a label on write --- printer_zpl2/README.rst | 1 + printer_zpl2/models/printing_label_zpl2.py | 11 +++++++++++ printer_zpl2/tests/test_test_mode.py | 10 ++++++++++ printer_zpl2/views/printing_label_zpl2.xml | 9 +++++++-- 4 files changed, 29 insertions(+), 2 deletions(-) diff --git a/printer_zpl2/README.rst b/printer_zpl2/README.rst index e488011..5400be5 100644 --- a/printer_zpl2/README.rst +++ b/printer_zpl2/README.rst @@ -25,6 +25,7 @@ To configure this module, you need to: #. Go to *Settings > Printing > Labels > ZPL II* #. Create new labels #. Import ZPL2 code +#. Use the Test Mode tab during the creation It's also possible to add a label printing wizard on any model by creating a new *ir.actions.act_window* record. For example, to add the printing wizard on the *product.product* model : diff --git a/printer_zpl2/models/printing_label_zpl2.py b/printer_zpl2/models/printing_label_zpl2.py index d175154..e6b9609 100644 --- a/printer_zpl2/models/printing_label_zpl2.py +++ b/printer_zpl2/models/printing_label_zpl2.py @@ -49,9 +49,12 @@ class PrintingLabelZpl2(models.Model): default=True) action_window_id = fields.Many2one( comodel_name='ir.actions.act_window', string='Action', readonly=True) + test_print_mode = fields.Boolean(string='Mode Print') test_labelary_mode = fields.Boolean(string='Mode Labelary') record_id = fields.Integer(string='Record ID', default=1) extra = fields.Text(string="Extra", default='{}') + printer_id = fields.Many2one( + comodel_name='printing.printer', string='Printer') labelary_image = fields.Binary(string='Image from Labelary', readonly=True) labelary_dpmm = fields.Selection( selection=[ @@ -293,6 +296,14 @@ class PrintingLabelZpl2(models.Model): return record + def print_test_label(self): + for label in self: + if label.test_print_mode and label.record_id and label.printer_id: + record = label._get_record() + extra = safe_eval(label.extra, {'env': self.env}) + if record: + label.print_label(label.printer_id, record, **extra) + @api.onchange( 'record_id', 'labelary_dpmm', 'labelary_width', 'labelary_height', 'component_ids', 'origin_x', 'origin_y') diff --git a/printer_zpl2/tests/test_test_mode.py b/printer_zpl2/tests/test_test_mode.py index 20abdb5..870a837 100644 --- a/printer_zpl2/tests/test_test_mode.py +++ b/printer_zpl2/tests/test_test_mode.py @@ -1,4 +1,5 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +import mock from odoo.tests.common import TransactionCase @@ -38,6 +39,15 @@ class TestWizardPrintRecordLabel(TransactionCase): record = Obj.search([], limit=1, order='id desc') self.assertEqual(res, record) + @mock.patch('%s.cups' % model) + def test_print_label_test(self, cups): + """ Check if print test """ + self.label.test_print_mode = True + self.label.printer_id = self.printer + self.label.record_id = 10 + self.label.print_test_label() + cups.Connection().printFile.assert_called_once() + def test_emulation_without_params(self): """ Check if not execute next if not in this mode """ self.label.test_labelary_mode = False diff --git a/printer_zpl2/views/printing_label_zpl2.xml b/printer_zpl2/views/printing_label_zpl2.xml index 0272173..d22cea1 100644 --- a/printer_zpl2/views/printing_label_zpl2.xml +++ b/printer_zpl2/views/printing_label_zpl2.xml @@ -38,6 +38,9 @@ + +
@@ -36,6 +40,7 @@ + @@ -44,7 +49,7 @@ - + @@ -77,6 +82,7 @@ + diff --git a/printer_zpl2/wizard/wizard_import_zpl2.py b/printer_zpl2/wizard/wizard_import_zpl2.py index 7c647dd..6493564 100644 --- a/printer_zpl2/wizard/wizard_import_zpl2.py +++ b/printer_zpl2/wizard/wizard_import_zpl2.py @@ -9,7 +9,7 @@ import re from PIL import Image, ImageOps -from odoo import _ as translate, fields, models +from odoo import _, fields, models _logger = logging.getLogger(__name__) @@ -376,7 +376,7 @@ class WizardImportZPl2(models.TransientModel): args = line.split("^") for arg in args: - for _, code in SUPPORTED_CODE.items(): + for _key, code in SUPPORTED_CODE.items(): component_arg = code["method"](arg) if component_arg: if code.get("default", False): @@ -398,8 +398,9 @@ class WizardImportZPl2(models.TransientModel): seq = sequence + i * 10 vals.update( { - "name": translate("Import %s") % seq, + "name": _("Import %s") % seq, "sequence": seq, + "model": str(zpl2.MODEL_ENHANCED), "label_id": self.label_id.id, } ) @@ -423,5 +424,7 @@ class WizardImportZPl2(models.TransientModel): value = True if field_type in ("integer", "float"): value = float(value) + if field == "model": + value = int(float(value)) component.update({field: value}) return component From f2eab9785faea1b05772cf6e3895cff110e44e31 Mon Sep 17 00:00:00 2001 From: Florent de Labarre Date: Thu, 5 Mar 2020 20:41:33 +0100 Subject: [PATCH 32/54] [IMP] print_zpl2 : quick move --- printer_zpl2/models/printing_label_zpl2.py | 38 +- .../models/printing_label_zpl2_component.py | 16 + .../tests/test_printing_label_zpl2.py | 25 +- printer_zpl2/tests/test_test_mode.py | 10 +- .../tests/test_wizard_print_record_label.py | 4 +- printer_zpl2/views/printing_label_zpl2.xml | 330 +++++++++++++----- printer_zpl2/wizard/print_record_label.xml | 15 +- printer_zpl2/wizard/wizard_import_zpl2.xml | 17 +- 8 files changed, 320 insertions(+), 135 deletions(-) diff --git a/printer_zpl2/models/printing_label_zpl2.py b/printer_zpl2/models/printing_label_zpl2.py index 3c347e7..8f33ee9 100644 --- a/printer_zpl2/models/printing_label_zpl2.py +++ b/printer_zpl2/models/printing_label_zpl2.py @@ -70,7 +70,8 @@ class PrintingLabelZpl2(models.Model): record_id = fields.Integer(string="Record ID", default=1) extra = fields.Text(string="Extra", default="{}") printer_id = fields.Many2one(comodel_name="printing.printer", string="Printer") - labelary_image = fields.Binary(string="Image from Labelary", readonly=True) + labelary_image = fields.Binary(string='Image from Labelary', + compute='_compute_labelary_image') labelary_dpmm = fields.Selection( selection=[ ("6dpmm", "6dpmm (152 pdi)"), @@ -407,16 +408,14 @@ class PrintingLabelZpl2(models.Model): if record: label.print_label(label.printer_id, record, **extra) - @api.onchange( - "record_id", - "labelary_dpmm", - "labelary_width", - "labelary_height", - "component_ids", - "origin_x", - "origin_y", - ) - def _on_change_labelary(self): + @api.depends( + 'record_id', 'labelary_dpmm', 'labelary_width', 'labelary_height', + 'component_ids', 'origin_x', 'origin_y', 'test_labelary_mode') + def _compute_labelary_image(self): + for label in self: + label.labelary_image = label._generate_labelary_image() + + def _generate_labelary_image(self): self.ensure_one() if not ( self.test_labelary_mode @@ -426,7 +425,7 @@ class PrintingLabelZpl2(models.Model): and self.labelary_dpmm and self.component_ids ): - return + return False record = self._get_record() if record: # If case there an error (in the data field with the safe_eval @@ -454,15 +453,12 @@ class PrintingLabelZpl2(models.Model): new_im.paste(im, (1, 1)) imgByteArr = io.BytesIO() new_im.save(imgByteArr, format="PNG") - self.labelary_image = base64.b64encode(imgByteArr.getvalue()) + return base64.b64encode(imgByteArr.getvalue()) else: - return { - "warning": { - "title": _("Error with Labelary API."), - "message": response.status_code, - } - } + _logger.warning( + _( + "Error with Labelary API. %s") % response.status_code) except Exception as e: - self.labelary_image = False - return {"warning": {"title": _("Some thing is wrong."), "message": e}} + _logger.warning(_("Error with Labelary API. %s") % e) + return False diff --git a/printer_zpl2/models/printing_label_zpl2_component.py b/printer_zpl2/models/printing_label_zpl2_component.py index 9b566fa..9ad4660 100644 --- a/printer_zpl2/models/printing_label_zpl2_component.py +++ b/printer_zpl2/models/printing_label_zpl2_component.py @@ -232,3 +232,19 @@ class PrintingLabelZpl2Component(models.Model): help="This field holds a static image to print. " "If not set, the data field is evaluated.", ) + + def action_plus_origin_x(self): + self.ensure_one() + self.origin_x += 10 + + def action_minus_origin_x(self): + self.ensure_one() + self.origin_x -= 10 + + def action_plus_origin_y(self): + self.ensure_one() + self.origin_y += 10 + + def action_minus_origin_y(self): + self.ensure_one() + self.origin_y -= 10 diff --git a/printer_zpl2/tests/test_printing_label_zpl2.py b/printer_zpl2/tests/test_printing_label_zpl2.py index 28c853f..1d67e8a 100644 --- a/printer_zpl2/tests/test_printing_label_zpl2.py +++ b/printer_zpl2/tests/test_printing_label_zpl2.py @@ -67,10 +67,10 @@ class TestPrintingLabelZpl2(TransactionCase): def test_print_empty_label(self, cups): """ Check that printing an empty label works """ label = self.new_label() - file_name = 'test.zpl' + file_name = "test.zpl" label.print_label(self.printer, self.printer) cups.Connection().printFile.assert_called_once_with( - printer.system_name, file_name, file_name, options={} + self.printer.system_name, file_name, file_name, options={} ) def test_empty_label_contents(self): @@ -1170,3 +1170,24 @@ class TestPrintingLabelZpl2(TransactionCase): self.assertEqual( contents, "^XA\n" "^PW480\n" "^CI28\n" "^LH10,10\n" "^JUR\n" "^XZ" ) + + def test_zpl2_component_quick_move(self): + """ Check component quick move """ + label = self.new_label() + component = self.new_component( + { + "label_id": label.id, + "component_type": "zpl2_raw", + "data": '""', + "origin_x": 20, + "origin_y": 30, + } + ) + component.action_plus_origin_x() + self.assertEqual(30, component.origin_x) + component.action_minus_origin_x() + self.assertEqual(20, component.origin_x) + component.action_plus_origin_y() + self.assertEqual(40, component.origin_y) + component.action_minus_origin_y() + self.assertEqual(30, component.origin_y) diff --git a/printer_zpl2/tests/test_test_mode.py b/printer_zpl2/tests/test_test_mode.py index b5dc60a..d6d7c4e 100644 --- a/printer_zpl2/tests/test_test_mode.py +++ b/printer_zpl2/tests/test_test_mode.py @@ -51,16 +51,15 @@ class TestWizardPrintRecordLabel(TransactionCase): self.label.test_print_mode = True self.label.printer_id = self.printer self.label.record_id = 10 - file_name = 'test.zpl' - self.label.print_test_label() + file_name = "test.zpl" + self.label.print_test_label() cups.Connection().printFile.assert_called_once_with( - self.printer.system_name, file_name, file_name, options={} + self.printer.system_name, file_name, file_name, options={} ) def test_emulation_without_params(self): """ Check if not execute next if not in this mode """ self.label.test_labelary_mode = False - self.label._on_change_labelary() self.assertIs(self.label.labelary_image, False) def test_emulation_with_bad_header(self): @@ -72,7 +71,6 @@ class TestWizardPrintRecordLabel(TransactionCase): self.env["printing.label.zpl2.component"].create( {"name": "ZPL II Label", "label_id": self.label.id, "data": '"Test"'} ) - self.label._on_change_labelary() self.assertFalse(self.label.labelary_image) def test_emulation_with_bad_data_compute(self): @@ -84,7 +82,6 @@ class TestWizardPrintRecordLabel(TransactionCase): component = self.env["printing.label.zpl2.component"].create( {"name": "ZPL II Label", "label_id": self.label.id, "data": "wrong_data"} ) - self.label._on_change_labelary() component.unlink() self.assertIs(self.label.labelary_image, False) @@ -97,5 +94,4 @@ class TestWizardPrintRecordLabel(TransactionCase): self.env["printing.label.zpl2.component"].create( {"name": "ZPL II Label", "label_id": self.label.id, "data": '"good_data"'} ) - self.label._on_change_labelary() self.assertTrue(self.label.labelary_image) diff --git a/printer_zpl2/tests/test_wizard_print_record_label.py b/printer_zpl2/tests/test_wizard_print_record_label.py index 19ce42c..2c2308d 100644 --- a/printer_zpl2/tests/test_wizard_print_record_label.py +++ b/printer_zpl2/tests/test_wizard_print_record_label.py @@ -48,9 +48,9 @@ class TestWizardPrintRecordLabel(TransactionCase): self.assertEqual(wizard.printer_id, self.printer) self.assertEqual(wizard.label_id, self.label) wizard.print_label() - file_name = 'test.zpl' + file_name = "test.zpl" cups.Connection().printFile.assert_called_once_with( - self.printer.system_name, file_name, file_name, options={} + self.printer.system_name, file_name, file_name, options={} ) def test_wizard_multiple_printers_and_labels(self): diff --git a/printer_zpl2/views/printing_label_zpl2.xml b/printer_zpl2/views/printing_label_zpl2.xml index 71f7e60..d99917d 100644 --- a/printer_zpl2/views/printing_label_zpl2.xml +++ b/printer_zpl2/views/printing_label_zpl2.xml @@ -1,4 +1,4 @@ - + - + - - - - - - - - - + + + + + + + + + - + - - - - + + + + - + - - - - - + + + + + - + - - - - + + + +
- - + +

Note : It is an emulation from http://labelary.com/, the result on printer can be different.

@@ -148,21 +258,42 @@ - - + + - - - - - + + + + + - + @@ -174,9 +305,13 @@ printing.label.zpl2 - - - + + + @@ -185,21 +320,32 @@ ir.actions.act_window printing.label.zpl2 tree,form - + [] {} - - - + + + form - + - - - + + + tree - + - +
diff --git a/printer_zpl2/wizard/print_record_label.xml b/printer_zpl2/wizard/print_record_label.xml index f111f2c..c4fafaf 100644 --- a/printer_zpl2/wizard/print_record_label.xml +++ b/printer_zpl2/wizard/print_record_label.xml @@ -1,4 +1,4 @@ - + -

Beta License: AGPL-3 OCA/report-print-send Translate me on Weblate Try me on Runbot

+

Beta License: AGPL-3 OCA/report-print-send Translate me on Weblate Try me on Runbot

This module extends the Report to printer (base_report_to_printer) module to add a ZPL II label printing feature.

This module is meant to be used as a base for module development, and does not provide a GUI on its own. @@ -446,7 +446,7 @@ change tests to reflect this

Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us smashing it by providing a detailed and welcomed -feedback.

+feedback.

Do not contact contributors directly about support or help with technical issues.

@@ -467,6 +467,7 @@ If you spotted it first, help us smashing it by providing a detailed and welcome
  • Jos De Graeve <Jos.DeGraeve@apertoso.be>
  • Rod Schouteden <rod.schouteden@dynapps.be>
  • Miquel Raïch <miquel.raich@forgeflow.com>
  • +
  • Lois Rilo <lois.rilo@forgeflow.com>
  • @@ -476,7 +477,7 @@ If you spotted it first, help us smashing it by providing a detailed and welcome

    OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use.

    -

    This module is part of the OCA/report-print-send project on GitHub.

    +

    This module is part of the OCA/report-print-send project on GitHub.

    You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

    From b0816532e8a75f9faf3264fdbb5bd2c3ea0a8c7e Mon Sep 17 00:00:00 2001 From: oca-git-bot Date: Thu, 31 Mar 2022 17:22:14 +0200 Subject: [PATCH 48/54] [IMP] update dotfiles [ci skip] --- printer_zpl2/tests/test_generate_action.py | 4 +- .../tests/test_printing_label_zpl2.py | 58 +++++++++---------- printer_zpl2/tests/test_test_mode.py | 12 ++-- printer_zpl2/tests/test_wizard_import_zpl2.py | 6 +- .../tests/test_wizard_print_record_label.py | 2 +- printer_zpl2/wizard/print_record_label.py | 2 +- 6 files changed, 42 insertions(+), 42 deletions(-) diff --git a/printer_zpl2/tests/test_generate_action.py b/printer_zpl2/tests/test_generate_action.py index 6b0769a..5e829dd 100644 --- a/printer_zpl2/tests/test_generate_action.py +++ b/printer_zpl2/tests/test_generate_action.py @@ -34,11 +34,11 @@ class TestWizardPrintRecordLabel(TransactionCase): ) def test_create_action(self): - """ Check the creation of action """ + """Check the creation of action""" self.label.create_action() self.assertTrue(self.label.action_window_id) def test_unlink_action(self): - """ Check the unlink of action """ + """Check the unlink of action""" self.label.unlink_action() self.assertFalse(self.label.action_window_id) diff --git a/printer_zpl2/tests/test_printing_label_zpl2.py b/printer_zpl2/tests/test_printing_label_zpl2.py index 3a9dcd9..f78322d 100644 --- a/printer_zpl2/tests/test_printing_label_zpl2.py +++ b/printer_zpl2/tests/test_printing_label_zpl2.py @@ -58,20 +58,20 @@ class TestPrintingLabelZpl2(TransactionCase): return self.ComponentModel.create(values) def test_print_on_bad_model(self): - """ Check that printing on the bad model raises an exception """ + """Check that printing on the bad model raises an exception""" label = self.new_label() with self.assertRaises(exceptions.UserError): label.print_label(self.printer, label) @mock.patch("%s.cups" % model) def test_print_empty_label(self, cups): - """ Check that printing an empty label works """ + """Check that printing an empty label works""" label = self.new_label() label.print_label(self.printer, self.printer) cups.Connection().printFile.assert_called_once() def test_empty_label_contents(self): - """ Check contents of an empty label """ + """Check contents of an empty label""" label = self.new_label() contents = label._generate_zpl2_data(self.printer).decode("utf-8") self.assertEqual( @@ -91,7 +91,7 @@ class TestPrintingLabelZpl2(TransactionCase): ) def test_sublabel_label_contents(self): - """ Check contents of a sublabel label component """ + """Check contents of a sublabel label component""" sublabel = self.new_label({"name": "Sublabel"}) data = "Some text" self.new_component({"label_id": sublabel.id, "data": '"' + data + '"'}) @@ -309,7 +309,7 @@ class TestPrintingLabelZpl2(TransactionCase): ) def test_repeatable_sublabel_contents(self): - """ Check contents of a repeatable sublabel label component """ + """Check contents of a repeatable sublabel label component""" sublabel = self.new_label( { "name": "Sublabel", @@ -407,7 +407,7 @@ class TestPrintingLabelZpl2(TransactionCase): ) def test_text_label_contents(self): - """ Check contents of a text label """ + """Check contents of a text label""" label = self.new_label() data = "Some text" self.new_component({"label_id": label.id, "data": '"%s"' % data}) @@ -437,7 +437,7 @@ class TestPrintingLabelZpl2(TransactionCase): ) def test_reversed_text_label_contents(self): - """ Check contents of a text label """ + """Check contents of a text label""" label = self.new_label() data = "Some text" self.new_component( @@ -471,7 +471,7 @@ class TestPrintingLabelZpl2(TransactionCase): ) def test_block_text_label_contents(self): - """ Check contents of a text label """ + """Check contents of a text label""" label = self.new_label() data = "Some text" self.new_component( @@ -505,7 +505,7 @@ class TestPrintingLabelZpl2(TransactionCase): ) def test_rectangle_label_contents(self): - """ Check contents of a rectangle label """ + """Check contents of a rectangle label""" label = self.new_label() self.new_component({"label_id": label.id, "component_type": "rectangle"}) contents = label._generate_zpl2_data(self.printer).decode("utf-8") @@ -532,7 +532,7 @@ class TestPrintingLabelZpl2(TransactionCase): ) def test_diagonal_line_label_contents(self): - """ Check contents of a diagonal line label """ + """Check contents of a diagonal line label""" label = self.new_label() self.new_component({"label_id": label.id, "component_type": "diagonal"}) contents = label._generate_zpl2_data(self.printer).decode("utf-8") @@ -559,7 +559,7 @@ class TestPrintingLabelZpl2(TransactionCase): ) def test_circle_label_contents(self): - """ Check contents of a circle label """ + """Check contents of a circle label""" label = self.new_label() self.new_component({"label_id": label.id, "component_type": "circle"}) contents = label._generate_zpl2_data(self.printer).decode("utf-8") @@ -586,7 +586,7 @@ class TestPrintingLabelZpl2(TransactionCase): ) def test_code11_barcode_label_contents(self): - """ Check contents of a code 11 barcode label """ + """Check contents of a code 11 barcode label""" label = self.new_label() data = "Some text" self.new_component( @@ -624,7 +624,7 @@ class TestPrintingLabelZpl2(TransactionCase): ) def test_2of5_barcode_label_contents(self): - """ Check contents of a interleaved 2 of 5 barcode label """ + """Check contents of a interleaved 2 of 5 barcode label""" label = self.new_label() data = "Some text" self.new_component( @@ -662,7 +662,7 @@ class TestPrintingLabelZpl2(TransactionCase): ) def test_code39_barcode_label_contents(self): - """ Check contents of a code 39 barcode label """ + """Check contents of a code 39 barcode label""" label = self.new_label() data = "Some text" self.new_component( @@ -700,7 +700,7 @@ class TestPrintingLabelZpl2(TransactionCase): ) def test_code49_barcode_label_contents(self): - """ Check contents of a code 49 barcode label """ + """Check contents of a code 49 barcode label""" label = self.new_label() data = "Some text" self.new_component( @@ -738,7 +738,7 @@ class TestPrintingLabelZpl2(TransactionCase): ) def test_code49_barcode_label_contents_line(self): - """ Check contents of a code 49 barcode label """ + """Check contents of a code 49 barcode label""" label = self.new_label() data = "Some text" self.new_component( @@ -819,7 +819,7 @@ class TestPrintingLabelZpl2(TransactionCase): ) def test_pdf417_barcode_label_contents(self): - """ Check contents of a pdf417 barcode label """ + """Check contents of a pdf417 barcode label""" label = self.new_label() data = "Some text" self.new_component( @@ -853,7 +853,7 @@ class TestPrintingLabelZpl2(TransactionCase): ) def test_ean8_barcode_label_contents(self): - """ Check contents of a ean-8 barcode label """ + """Check contents of a ean-8 barcode label""" label = self.new_label() data = "Some text" self.new_component( @@ -887,7 +887,7 @@ class TestPrintingLabelZpl2(TransactionCase): ) def test_upce_barcode_label_contents(self): - """ Check contents of a upc-e barcode label """ + """Check contents of a upc-e barcode label""" label = self.new_label() data = "Some text" self.new_component( @@ -921,7 +921,7 @@ class TestPrintingLabelZpl2(TransactionCase): ) def test_code128_barcode_label_contents(self): - """ Check contents of a code 128 barcode label """ + """Check contents of a code 128 barcode label""" label = self.new_label() data = "Some text" self.new_component( @@ -959,7 +959,7 @@ class TestPrintingLabelZpl2(TransactionCase): ) def test_ean13_barcode_label_contents(self): - """ Check contents of a ean-13 barcode label """ + """Check contents of a ean-13 barcode label""" label = self.new_label() data = "Some text" self.new_component( @@ -993,7 +993,7 @@ class TestPrintingLabelZpl2(TransactionCase): ) def test_qrcode_barcode_label_contents(self): - """ Check contents of a qr code barcode label """ + """Check contents of a qr code barcode label""" label = self.new_label() data = "Some text" self.new_component( @@ -1031,7 +1031,7 @@ class TestPrintingLabelZpl2(TransactionCase): ) def test_graphic_label_contents_blank(self): - """ Check contents of a image label """ + """Check contents of a image label""" label = self.new_label() data = "R0lGODlhAQABAIAAAP7//wAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==" self.new_component( @@ -1054,7 +1054,7 @@ class TestPrintingLabelZpl2(TransactionCase): ) def test_graphic_label_contents_blank_rotated(self): - """ Check contents of image rotated label """ + """Check contents of image rotated label""" label = self.new_label() data = "R0lGODlhAQABAIAAAP7//wAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==" self.new_component( @@ -1082,7 +1082,7 @@ class TestPrintingLabelZpl2(TransactionCase): ) def test_graphic_label_contents_blank_inverted(self): - """ Check contents of a image inverted label """ + """Check contents of a image inverted label""" label = self.new_label() data = "R0lGODlhAQABAIAAAP7//wAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==" self.new_component( @@ -1106,7 +1106,7 @@ class TestPrintingLabelZpl2(TransactionCase): ) def test_graphic_label_contents_blank_bottom(self): - """ Check contents of a image bottom label """ + """Check contents of a image bottom label""" label = self.new_label() data = "R0lGODlhAQABAIAAAP7//wAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==" self.new_component( @@ -1130,7 +1130,7 @@ class TestPrintingLabelZpl2(TransactionCase): ) def test_zpl2_raw_contents_blank(self): - """ Check contents of a image label """ + """Check contents of a image label""" label = self.new_label() data = "^FO50,50^GB100,100,100^FS" self.new_component( @@ -1153,7 +1153,7 @@ class TestPrintingLabelZpl2(TransactionCase): ) def test_zpl2_component_not_show(self): - """ Check to don't show no things """ + """Check to don't show no things""" label = self.new_label() data = "component_not_show" self.new_component( @@ -1169,7 +1169,7 @@ class TestPrintingLabelZpl2(TransactionCase): ) def test_zpl2_component_quick_move(self): - """ Check component quick move """ + """Check component quick move""" label = self.new_label() component = self.new_component( { diff --git a/printer_zpl2/tests/test_test_mode.py b/printer_zpl2/tests/test_test_mode.py index db7ea22..de3a8fe 100644 --- a/printer_zpl2/tests/test_test_mode.py +++ b/printer_zpl2/tests/test_test_mode.py @@ -35,7 +35,7 @@ class TestWizardPrintRecordLabel(TransactionCase): ) def test_get_record(self): - """ Check if return a record """ + """Check if return a record""" self.label.record_id = 10 res = self.label._get_record() @@ -47,7 +47,7 @@ class TestWizardPrintRecordLabel(TransactionCase): @mock.patch("%s.cups" % model) def test_print_label_test(self, cups): - """ Check if print test """ + """Check if print test""" self.label.test_print_mode = True self.label.printer_id = self.printer self.label.record_id = 10 @@ -55,12 +55,12 @@ class TestWizardPrintRecordLabel(TransactionCase): cups.Connection().printFile.assert_called_once() def test_emulation_without_params(self): - """ Check if not execute next if not in this mode """ + """Check if not execute next if not in this mode""" self.label.test_labelary_mode = False self.assertIs(self.label.labelary_image, False) def test_emulation_with_bad_header(self): - """ Check if bad header """ + """Check if bad header""" self.label.test_labelary_mode = True self.label.labelary_width = 80 self.label.labelary_dpmm = "8dpmm" @@ -71,7 +71,7 @@ class TestWizardPrintRecordLabel(TransactionCase): self.assertFalse(self.label.labelary_image) def test_emulation_with_bad_data_compute(self): - """ Check if bad data compute """ + """Check if bad data compute""" self.label.test_labelary_mode = True self.label.labelary_width = 80 self.label.labelary_height = 30 @@ -83,7 +83,7 @@ class TestWizardPrintRecordLabel(TransactionCase): self.assertIs(self.label.labelary_image, False) def test_emulation_with_good_data(self): - """ Check if ok """ + """Check if ok""" self.label.test_labelary_mode = True self.label.labelary_width = 80 self.label.labelary_height = 30 diff --git a/printer_zpl2/tests/test_wizard_import_zpl2.py b/printer_zpl2/tests/test_wizard_import_zpl2.py index 41f2d5e..111efa3 100644 --- a/printer_zpl2/tests/test_wizard_import_zpl2.py +++ b/printer_zpl2/tests/test_wizard_import_zpl2.py @@ -32,12 +32,12 @@ class TestWizardImportZpl2(TransactionCase): ) def test_open_wizard(self): - """ open wizard from label""" + """open wizard from label""" res = self.label.import_zpl2() self.assertEqual(res.get("context").get("default_label_id"), self.label.id) def test_wizard_import_zpl2(self): - """ Import ZPL2 from wizard """ + """Import ZPL2 from wizard""" zpl_data = ( "^XA\n" "^CI28\n" @@ -76,7 +76,7 @@ class TestWizardImportZpl2(TransactionCase): self.assertEqual(18, len(self.label.component_ids)) def test_wizard_import_zpl2_add(self): - """ Import ZPL2 from wizard ADD""" + """Import ZPL2 from wizard ADD""" self.env["printing.label.zpl2.component"].create( { "name": "ZPL II Label", diff --git a/printer_zpl2/tests/test_wizard_print_record_label.py b/printer_zpl2/tests/test_wizard_print_record_label.py index 7400cbc..b8b0c50 100644 --- a/printer_zpl2/tests/test_wizard_print_record_label.py +++ b/printer_zpl2/tests/test_wizard_print_record_label.py @@ -37,7 +37,7 @@ class TestWizardPrintRecordLabel(TransactionCase): @mock.patch("%s.cups" % model) def test_print_record_label(self, cups): - """ Check that printing a label using the generic wizard works """ + """Check that printing a label using the generic wizard works""" wizard_obj = self.Model.with_context( active_model="printing.printer", active_id=self.printer.id, diff --git a/printer_zpl2/wizard/print_record_label.py b/printer_zpl2/wizard/print_record_label.py index d360be3..9a92930 100644 --- a/printer_zpl2/wizard/print_record_label.py +++ b/printer_zpl2/wizard/print_record_label.py @@ -51,7 +51,7 @@ class PrintRecordLabel(models.TransientModel): return values def print_label(self): - """ Prints a label per selected record """ + """Prints a label per selected record""" record_model = self.env.context["active_model"] for record_id in self.env.context["active_ids"]: record = self.env[record_model].browse(record_id) From 0f7d5bcae482b4738ccdbd1dcc60e82c627149c3 Mon Sep 17 00:00:00 2001 From: Sylvain GARANCHER Date: Fri, 9 Sep 2022 09:55:19 +0200 Subject: [PATCH 49/54] [IMP] printer_zpl2: Include library inside the module Copied from https://github.com/subteno-it/python-zpl2, as there has been new release (1.2.1) that breaks current code without clear source (no commit on the repo). As the amount of code is not too much, we put it on the module itself, being able to control the whole chain, and to reduce the code with the considerations of current Odoo version. This commit has as author the main commmiter of the library repo. --- printer_zpl2/models/zpl2.py | 529 ++++++++++++++++++++++++++++++++++++ 1 file changed, 529 insertions(+) create mode 100644 printer_zpl2/models/zpl2.py diff --git a/printer_zpl2/models/zpl2.py b/printer_zpl2/models/zpl2.py new file mode 100644 index 0000000..00db219 --- /dev/null +++ b/printer_zpl2/models/zpl2.py @@ -0,0 +1,529 @@ +# Copyright (C) 2016 SYLEAM () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +# Copied from https://github.com/subteno-it/python-zpl2, as there has been new releases +# that breaks current code without clear source (no commit on the repo). As the amount +# of code is not too much, we put it on the module itself, being able to control the +# whole chain, and to reduce the code with the considerations of current Odoo version + +import binascii +import math + +try: + from PIL import ImageOps +except: + ImageOps = None + +try: + strcast = unicode +except: + strcast = str + +# Constants for the printer configuration management +CONF_RELOAD_FACTORY = "F" +CONF_RELOAD_NETWORK_FACTORY = "N" +CONF_RECALL_LAST_SAVED = "R" +CONF_SAVE_CURRENT = "S" + +# Command arguments names +ARG_FONT = "font" +ARG_HEIGHT = "height" +ARG_WIDTH = "width" +ARG_ORIENTATION = "orientation" +ARG_THICKNESS = "thickness" +ARG_BLOCK_WIDTH = "block_width" +ARG_BLOCK_LINES = "block_lines" +ARG_BLOCK_SPACES = "block_spaces" +ARG_BLOCK_JUSTIFY = "block_justify" +ARG_BLOCK_LEFT_MARGIN = "block_left_margin" +ARG_CHECK_DIGITS = "check_digits" +ARG_INTERPRETATION_LINE = "interpretation_line" +ARG_INTERPRETATION_LINE_ABOVE = "interpretation_line_above" +ARG_STARTING_MODE = "starting_mode" +ARG_SECURITY_LEVEL = "security_level" +ARG_COLUMNS_COUNT = "columns_count" +ARG_ROWS_COUNT = "rows_count" +ARG_TRUNCATE = "truncate" +ARG_MODE = "mode" +ARG_MODULE_WIDTH = "module_width" +ARG_BAR_WIDTH_RATIO = "bar_width_ratio" +ARG_REVERSE_PRINT = "reverse_print" +ARG_IN_BLOCK = "in_block" +ARG_COLOR = "color" +ARG_ROUNDING = "rounding" +ARG_DIAMETER = "diameter" +ARG_DIAGONAL_ORIENTATION = "diagonal_orientation" +ARG_MODEL = "model" +ARG_MAGNIFICATION_FACTOR = "magnification_factor" +ARG_ERROR_CORRECTION = "error_correction" +ARG_MASK_VALUE = "mask_value" + +# Model values +MODEL_ORIGINAL = 1 +MODEL_ENHANCED = 2 + +# Error Correction +ERROR_CORRECTION_ULTRA_HIGH = "H" +ERROR_CORRECTION_HIGH = "Q" +ERROR_CORRECTION_STANDARD = "M" +ERROR_CORRECTION_HIGH_DENSITY = "L" + +# Boolean values +BOOL_YES = "Y" +BOOL_NO = "N" + +# Orientation values +ORIENTATION_NORMAL = "N" +ORIENTATION_ROTATED = "R" +ORIENTATION_INVERTED = "I" +ORIENTATION_BOTTOM_UP = "B" + +# Diagonal lines orientation values +DIAGONAL_ORIENTATION_LEFT = "L" +DIAGONAL_ORIENTATION_RIGHT = "R" + +# Justify values +JUSTIFY_LEFT = "L" +JUSTIFY_CENTER = "C" +JUSTIFY_JUSTIFIED = "J" +JUSTIFY_RIGHT = "R" + +# Font values +FONT_DEFAULT = "0" +FONT_9X5 = "A" +FONT_11X7 = "B" +FONT_18X10 = "D" +FONT_28X15 = "E" +FONT_26X13 = "F" +FONT_60X40 = "G" +FONT_21X13 = "H" + +# Color values +COLOR_BLACK = "B" +COLOR_WHITE = "W" + +# Barcode types +BARCODE_CODE_11 = "code_11" +BARCODE_INTERLEAVED_2_OF_5 = "interleaved_2_of_5" +BARCODE_CODE_39 = "code_39" +BARCODE_CODE_49 = "code_49" +BARCODE_PDF417 = "pdf417" +BARCODE_EAN_8 = "ean-8" +BARCODE_UPC_E = "upc-e" +BARCODE_CODE_128 = "code_128" +BARCODE_EAN_13 = "ean-13" +BARCODE_QR_CODE = "qr_code" + + +class Zpl2(object): + """ZPL II management class + Allows to generate data for Zebra printers + """ + + def __init__(self): + self.encoding = "utf-8" + self.initialize() + + def initialize(self): + self._buffer = [] + + def output(self): + """Return the full contents to send to the printer""" + return "\n".encode(self.encoding).join(self._buffer) + + def _enforce(self, value, minimum=1, maximum=32000): + """Returns the value, forced between minimum and maximum""" + return min(max(minimum, value), maximum) + + def _write_command(self, data): + """Adds a complete command to buffer""" + self._buffer.append(strcast(data).encode(self.encoding)) + + def _generate_arguments(self, arguments, kwargs): + """Generate a zebra arguments from an argument names list and a dict of + values for these arguments + @param arguments : list of argument names, ORDER MATTERS + @param kwargs : list of arguments values + """ + command_arguments = [] + # Add all arguments in the list, if they exist + for argument in arguments: + if kwargs.get(argument, None) is not None: + if isinstance(kwargs[argument], bool): + kwargs[argument] = kwargs[argument] and BOOL_YES or BOOL_NO + command_arguments.append(kwargs[argument]) + + # Return a zebra formatted string, with a comma between each argument + return ",".join(map(str, command_arguments)) + + def print_width(self, label_width): + """Defines the print width setting on the printer""" + self._write_command("^PW%d" % label_width) + + def configuration_update(self, active_configuration): + """Set the active configuration on the printer""" + self._write_command("^JU%s" % active_configuration) + + def label_start(self): + """Adds the label start command to the buffer""" + self._write_command("^XA") + + def label_encoding(self): + """Adds the label encoding command to the buffer + Fixed value defined to UTF-8 + """ + self._write_command("^CI28") + + def label_end(self): + """Adds the label start command to the buffer""" + self._write_command("^XZ") + + def label_home(self, left, top): + """Define the label top left corner""" + self._write_command("^LH%d,%d" % (left, top)) + + def _field_origin(self, right, down): + """Define the top left corner of the data, from the top left corner of + the label + """ + return "^FO%d,%d" % (right, down) + + def _font_format(self, font_format): + """Send the commands which define the font to use for the current data""" + arguments = [ARG_FONT, ARG_HEIGHT, ARG_WIDTH] + # Add orientation in the font name (only place where there is + # no comma between values) + font_format[ARG_FONT] += font_format.get(ARG_ORIENTATION, ORIENTATION_NORMAL) + # Check that the height value fits in the allowed values + if font_format.get(ARG_HEIGHT) is not None: + font_format[ARG_HEIGHT] = self._enforce(font_format[ARG_HEIGHT], minimum=10) + # Check that the width value fits in the allowed values + if font_format.get(ARG_WIDTH) is not None: + font_format[ARG_WIDTH] = self._enforce(font_format[ARG_WIDTH], minimum=10) + # Generate the ZPL II command + return "^A" + self._generate_arguments(arguments, font_format) + + def _field_block(self, block_format): + """Define a maximum width to print some data""" + arguments = [ + ARG_BLOCK_WIDTH, + ARG_BLOCK_LINES, + ARG_BLOCK_SPACES, + ARG_BLOCK_JUSTIFY, + ARG_BLOCK_LEFT_MARGIN, + ] + return "^FB" + self._generate_arguments(arguments, block_format) + + def _barcode_format(self, barcodeType, barcode_format): + """Generate the commands to print a barcode + Each barcode type needs a specific function + """ + + def _code11(**kwargs): + arguments = [ + ARG_ORIENTATION, + ARG_CHECK_DIGITS, + ARG_HEIGHT, + ARG_INTERPRETATION_LINE, + ARG_INTERPRETATION_LINE_ABOVE, + ] + return "1" + self._generate_arguments(arguments, kwargs) + + def _interleaved2of5(**kwargs): + arguments = [ + ARG_ORIENTATION, + ARG_HEIGHT, + ARG_INTERPRETATION_LINE, + ARG_INTERPRETATION_LINE_ABOVE, + ARG_CHECK_DIGITS, + ] + return "2" + self._generate_arguments(arguments, kwargs) + + def _code39(**kwargs): + arguments = [ + ARG_ORIENTATION, + ARG_CHECK_DIGITS, + ARG_HEIGHT, + ARG_INTERPRETATION_LINE, + ARG_INTERPRETATION_LINE_ABOVE, + ] + return "3" + self._generate_arguments(arguments, kwargs) + + def _code49(**kwargs): + arguments = [ + ARG_ORIENTATION, + ARG_HEIGHT, + ARG_INTERPRETATION_LINE, + ARG_STARTING_MODE, + ] + # Use interpretation_line and interpretation_line_above to generate + # a specific interpretation_line value + if kwargs.get(ARG_INTERPRETATION_LINE) is not None: + if kwargs[ARG_INTERPRETATION_LINE]: + if kwargs[ARG_INTERPRETATION_LINE_ABOVE]: + # Interpretation line after + kwargs[ARG_INTERPRETATION_LINE] = "A" + else: + # Interpretation line before + kwargs[ARG_INTERPRETATION_LINE] = "B" + else: + # No interpretation line + kwargs[ARG_INTERPRETATION_LINE] = "N" + return "4" + self._generate_arguments(arguments, kwargs) + + def _pdf417(**kwargs): + arguments = [ + ARG_ORIENTATION, + ARG_HEIGHT, + ARG_SECURITY_LEVEL, + ARG_COLUMNS_COUNT, + ARG_ROWS_COUNT, + ARG_TRUNCATE, + ] + return "7" + self._generate_arguments(arguments, kwargs) + + def _ean8(**kwargs): + arguments = [ + ARG_ORIENTATION, + ARG_HEIGHT, + ARG_INTERPRETATION_LINE, + ARG_INTERPRETATION_LINE_ABOVE, + ] + return "8" + self._generate_arguments(arguments, kwargs) + + def _upce(**kwargs): + arguments = [ + ARG_ORIENTATION, + ARG_HEIGHT, + ARG_INTERPRETATION_LINE, + ARG_INTERPRETATION_LINE_ABOVE, + ARG_CHECK_DIGITS, + ] + return "9" + self._generate_arguments(arguments, kwargs) + + def _code128(**kwargs): + arguments = [ + ARG_ORIENTATION, + ARG_HEIGHT, + ARG_INTERPRETATION_LINE, + ARG_INTERPRETATION_LINE_ABOVE, + ARG_CHECK_DIGITS, + ARG_MODE, + ] + return "C" + self._generate_arguments(arguments, kwargs) + + def _ean13(**kwargs): + arguments = [ + ARG_ORIENTATION, + ARG_HEIGHT, + ARG_INTERPRETATION_LINE, + ARG_INTERPRETATION_LINE_ABOVE, + ] + return "E" + self._generate_arguments(arguments, kwargs) + + def _qrcode(**kwargs): + arguments = [ + ARG_ORIENTATION, + ARG_MODEL, + ARG_MAGNIFICATION_FACTOR, + ARG_ERROR_CORRECTION, + ARG_MASK_VALUE, + ] + return "Q" + self._generate_arguments(arguments, kwargs) + + barcodeTypes = { + BARCODE_CODE_11: _code11, + BARCODE_INTERLEAVED_2_OF_5: _interleaved2of5, + BARCODE_CODE_39: _code39, + BARCODE_CODE_49: _code49, + BARCODE_PDF417: _pdf417, + BARCODE_EAN_8: _ean8, + BARCODE_UPC_E: _upce, + BARCODE_CODE_128: _code128, + BARCODE_EAN_13: _ean13, + BARCODE_QR_CODE: _qrcode, + } + return "^B" + barcodeTypes[barcodeType](**barcode_format) + + def _barcode_field_default(self, barcode_format): + """Add the data start command to the buffer""" + arguments = [ + ARG_MODULE_WIDTH, + ARG_BAR_WIDTH_RATIO, + ] + return "^BY" + self._generate_arguments(arguments, barcode_format) + + def _field_data_start(self): + """Add the data start command to the buffer""" + return "^FD" + + def _field_reverse_print(self): + """Allows the printed data to appear white over black, or black over white""" + return "^FR" + + def _field_data_stop(self): + """Add the data stop command to the buffer""" + return "^FS" + + def _field_data(self, data): + """Add data to the buffer, between start and stop commands""" + command = "{start}{data}{stop}".format( + start=self._field_data_start(), + data=data, + stop=self._field_data_stop(), + ) + return command + + def font_data(self, right, down, field_format, data): + """Add a full text in the buffer, with needed formatting commands""" + reverse = "" + if field_format.get(ARG_REVERSE_PRINT, False): + reverse = self._field_reverse_print() + block = "" + if field_format.get(ARG_IN_BLOCK, False): + block = self._field_block(field_format) + command = "{origin}{font_format}{reverse}{block}{data}".format( + origin=self._field_origin(right, down), + font_format=self._font_format(field_format), + reverse=reverse, + block=block, + data=self._field_data(data), + ) + self._write_command(command) + + def barcode_data(self, right, down, barcodeType, barcode_format, data): + """Add a full barcode in the buffer, with needed formatting commands""" + command = "{default}{origin}{barcode_format}{data}".format( + default=self._barcode_field_default(barcode_format), + origin=self._field_origin(right, down), + barcode_format=self._barcode_format(barcodeType, barcode_format), + data=self._field_data(data), + ) + self._write_command(command) + + def graphic_box(self, right, down, graphic_format): + """Send the commands to draw a rectangle""" + arguments = [ + ARG_WIDTH, + ARG_HEIGHT, + ARG_THICKNESS, + ARG_COLOR, + ARG_ROUNDING, + ] + # Check that the thickness value fits in the allowed values + if graphic_format.get(ARG_THICKNESS) is not None: + graphic_format[ARG_THICKNESS] = self._enforce(graphic_format[ARG_THICKNESS]) + # Check that the width value fits in the allowed values + if graphic_format.get(ARG_WIDTH) is not None: + graphic_format[ARG_WIDTH] = self._enforce( + graphic_format[ARG_WIDTH], minimum=graphic_format[ARG_THICKNESS] + ) + # Check that the height value fits in the allowed values + if graphic_format.get(ARG_HEIGHT) is not None: + graphic_format[ARG_HEIGHT] = self._enforce( + graphic_format[ARG_HEIGHT], minimum=graphic_format[ARG_THICKNESS] + ) + # Check that the rounding value fits in the allowed values + if graphic_format.get(ARG_ROUNDING) is not None: + graphic_format[ARG_ROUNDING] = self._enforce( + graphic_format[ARG_ROUNDING], minimum=0, maximum=8 + ) + # Generate the ZPL II command + command = "{origin}{data}{stop}".format( + origin=self._field_origin(right, down), + data="^GB" + self._generate_arguments(arguments, graphic_format), + stop=self._field_data_stop(), + ) + self._write_command(command) + + def graphic_diagonal_line(self, right, down, graphic_format): + """Send the commands to draw a rectangle""" + arguments = [ + ARG_WIDTH, + ARG_HEIGHT, + ARG_THICKNESS, + ARG_COLOR, + ARG_DIAGONAL_ORIENTATION, + ] + # Check that the thickness value fits in the allowed values + if graphic_format.get(ARG_THICKNESS) is not None: + graphic_format[ARG_THICKNESS] = self._enforce(graphic_format[ARG_THICKNESS]) + # Check that the width value fits in the allowed values + if graphic_format.get(ARG_WIDTH) is not None: + graphic_format[ARG_WIDTH] = self._enforce( + graphic_format[ARG_WIDTH], minimum=3 + ) + # Check that the height value fits in the allowed values + if graphic_format.get(ARG_HEIGHT) is not None: + graphic_format[ARG_HEIGHT] = self._enforce( + graphic_format[ARG_HEIGHT], minimum=3 + ) + # Check the given orientation + graphic_format[ARG_DIAGONAL_ORIENTATION] = graphic_format.get( + ARG_DIAGONAL_ORIENTATION, DIAGONAL_ORIENTATION_LEFT + ) + # Generate the ZPL II command + command = "{origin}{data}{stop}".format( + origin=self._field_origin(right, down), + data="^GD" + self._generate_arguments(arguments, graphic_format), + stop=self._field_data_stop(), + ) + self._write_command(command) + + def graphic_circle(self, right, down, graphic_format): + """Send the commands to draw a circle""" + arguments = [ARG_DIAMETER, ARG_THICKNESS, ARG_COLOR] + # Check that the diameter value fits in the allowed values + if graphic_format.get(ARG_DIAMETER) is not None: + graphic_format[ARG_DIAMETER] = self._enforce( + graphic_format[ARG_DIAMETER], minimum=3, maximum=4095 + ) + # Check that the thickness value fits in the allowed values + if graphic_format.get(ARG_THICKNESS) is not None: + graphic_format[ARG_THICKNESS] = self._enforce( + graphic_format[ARG_THICKNESS], minimum=2, maximum=4095 + ) + # Generate the ZPL II command + command = "{origin}{data}{stop}".format( + origin=self._field_origin(right, down), + data="^GC" + self._generate_arguments(arguments, graphic_format), + stop=self._field_data_stop(), + ) + self._write_command(command) + + def graphic_field(self, right, down, pil_image): + """Encode a PIL image into an ASCII string suitable for ZPL printers""" + if ImageOps is None: + # Importing ImageOps from PIL didn't work + raise Exception( + "You must install Pillow to be able to use the graphic" + " fields feature" + ) + width, height = pil_image.size + rounded_width = int(math.ceil(width / 8.0) * 8) + # Transform the image : + # - Invert the colors (PIL uses 0 for black, ZPL uses 0 for white) + # - Convert to monochrome in case it is not already + # - Round the width to a multiple of 8 because ZPL needs an integer + # count of bytes per line (each pixel is a bit) + pil_image = ( + ImageOps.invert(pil_image).convert("1").crop((0, 0, rounded_width, height)) + ) + # Convert the image to a two-character hexadecimal values string + ascii_data = binascii.hexlify(pil_image.tobytes()).upper() + # Each byte is composed of two characters + bytes_per_row = rounded_width / 8 + total_bytes = bytes_per_row * height + graphic_image_command = ( + "^GFA,{total_bytes},{total_bytes},{bytes_per_row},{ascii_data}".format( + total_bytes=total_bytes, + bytes_per_row=bytes_per_row, + ascii_data=ascii_data, + ) + ) + # Generate the ZPL II command + command = "{origin}{data}{stop}".format( + origin=self._field_origin(right, down), + data=graphic_image_command, + stop=self._field_data_stop(), + ) + self._write_command(command) From 946221e2d9d5d3403cf86d7b6d9f62f4e410ad20 Mon Sep 17 00:00:00 2001 From: "Pedro M. Baeza" Date: Fri, 9 Sep 2022 11:10:59 +0200 Subject: [PATCH 50/54] [FIX] printer_zpl2: Adapt code about having the library embedded --- printer_zpl2/__manifest__.py | 1 - printer_zpl2/models/printing_label_zpl2.py | 7 ++----- .../models/printing_label_zpl2_component.py | 6 ++---- printer_zpl2/models/zpl2.py | 18 ++---------------- printer_zpl2/tests/test_printing_label_zpl2.py | 9 +-------- printer_zpl2/wizard/wizard_import_zpl2.py | 7 ++----- 6 files changed, 9 insertions(+), 39 deletions(-) diff --git a/printer_zpl2/__manifest__.py b/printer_zpl2/__manifest__.py index 7d3d916..4ed0f44 100644 --- a/printer_zpl2/__manifest__.py +++ b/printer_zpl2/__manifest__.py @@ -10,7 +10,6 @@ "Apertoso NV, Odoo Community Association (OCA)", "website": "https://github.com/OCA/report-print-send", "license": "AGPL-3", - "external_dependencies": {"python": ["zpl2"]}, "depends": ["base_report_to_printer"], "data": [ "security/ir.model.access.csv", diff --git a/printer_zpl2/models/printing_label_zpl2.py b/printer_zpl2/models/printing_label_zpl2.py index dfd860a..e9e5e9b 100644 --- a/printer_zpl2/models/printing_label_zpl2.py +++ b/printer_zpl2/models/printing_label_zpl2.py @@ -14,12 +14,9 @@ from odoo import _, api, exceptions, fields, models from odoo.exceptions import ValidationError from odoo.tools.safe_eval import safe_eval, wrap_module -_logger = logging.getLogger(__name__) +from . import zpl2 -try: - import zpl2 -except ImportError: - _logger.debug("Cannot `import zpl2`.") +_logger = logging.getLogger(__name__) class PrintingLabelZpl2(models.Model): diff --git a/printer_zpl2/models/printing_label_zpl2_component.py b/printer_zpl2/models/printing_label_zpl2_component.py index 9342ec5..eccd2ef 100644 --- a/printer_zpl2/models/printing_label_zpl2_component.py +++ b/printer_zpl2/models/printing_label_zpl2_component.py @@ -5,12 +5,10 @@ import logging from odoo import api, fields, models +from . import zpl2 + _logger = logging.getLogger(__name__) -try: - import zpl2 -except ImportError: - _logger.debug("Cannot `import zpl2`.") DEFAULT_PYTHON_CODE = """# Python One-Liners # - object: %s record on which the action is triggered; may be void diff --git a/printer_zpl2/models/zpl2.py b/printer_zpl2/models/zpl2.py index 00db219..d9600b6 100644 --- a/printer_zpl2/models/zpl2.py +++ b/printer_zpl2/models/zpl2.py @@ -8,15 +8,7 @@ import binascii import math -try: - from PIL import ImageOps -except: - ImageOps = None - -try: - strcast = unicode -except: - strcast = str +from PIL import ImageOps # Constants for the printer configuration management CONF_RELOAD_FACTORY = "F" @@ -136,7 +128,7 @@ class Zpl2(object): def _write_command(self, data): """Adds a complete command to buffer""" - self._buffer.append(strcast(data).encode(self.encoding)) + self._buffer.append(str(data).encode(self.encoding)) def _generate_arguments(self, arguments, kwargs): """Generate a zebra arguments from an argument names list and a dict of @@ -492,12 +484,6 @@ class Zpl2(object): def graphic_field(self, right, down, pil_image): """Encode a PIL image into an ASCII string suitable for ZPL printers""" - if ImageOps is None: - # Importing ImageOps from PIL didn't work - raise Exception( - "You must install Pillow to be able to use the graphic" - " fields feature" - ) width, height = pil_image.size rounded_width = int(math.ceil(width / 8.0) * 8) # Transform the image : diff --git a/printer_zpl2/tests/test_printing_label_zpl2.py b/printer_zpl2/tests/test_printing_label_zpl2.py index f78322d..41f1470 100644 --- a/printer_zpl2/tests/test_printing_label_zpl2.py +++ b/printer_zpl2/tests/test_printing_label_zpl2.py @@ -1,19 +1,12 @@ # Copyright 2016 LasLabs Inc. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -import logging - import mock from odoo import exceptions from odoo.tests.common import TransactionCase -_logger = logging.getLogger(__name__) - -try: - import zpl2 -except ImportError: - _logger.debug("Cannot `import zpl2`.") +from ..models import zpl2 model = "odoo.addons.base_report_to_printer.models.printing_server" diff --git a/printer_zpl2/wizard/wizard_import_zpl2.py b/printer_zpl2/wizard/wizard_import_zpl2.py index 6493564..f6adc79 100644 --- a/printer_zpl2/wizard/wizard_import_zpl2.py +++ b/printer_zpl2/wizard/wizard_import_zpl2.py @@ -11,12 +11,9 @@ from PIL import Image, ImageOps from odoo import _, fields, models -_logger = logging.getLogger(__name__) +from ..models import zpl2 -try: - import zpl2 -except ImportError: - _logger.debug("Cannot `import zpl2`.") +_logger = logging.getLogger(__name__) def _compute_arg(data, arg): From a6a30d8b5060b86f30cac96eea742ff8fccb865e Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Sun, 2 Oct 2022 15:14:54 +0000 Subject: [PATCH 51/54] printer_zpl2 14.0.2.0.0 --- printer_zpl2/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/printer_zpl2/__manifest__.py b/printer_zpl2/__manifest__.py index 4ed0f44..ec02765 100644 --- a/printer_zpl2/__manifest__.py +++ b/printer_zpl2/__manifest__.py @@ -3,7 +3,7 @@ { "name": "Printer ZPL II", - "version": "14.0.1.0.0", + "version": "14.0.2.0.0", "category": "Printer", "summary": "Add a ZPL II label printing feature", "author": "SUBTENO-IT, FLorent de Labarre, " From cb480f87b506ba5f45416a243a296c458d0b7a5e Mon Sep 17 00:00:00 2001 From: duyanh Date: Fri, 28 Oct 2022 12:52:47 +0700 Subject: [PATCH 52/54] [15.0][MIG] printer_zpl2: Migration to 15.0 --- printer_zpl2/README.rst | 14 +++++++------- printer_zpl2/__manifest__.py | 4 ++-- printer_zpl2/models/__init__.py | 2 +- printer_zpl2/models/printing_label_zpl2.py | 8 +++----- printer_zpl2/static/description/index.html | 8 ++++---- printer_zpl2/tests/test_printing_label_zpl2.py | 4 ++-- printer_zpl2/tests/test_test_mode.py | 4 ++-- .../tests/test_wizard_print_record_label.py | 4 ++-- printer_zpl2/views/printing_label_zpl2.xml | 4 ++-- setup/printer_zpl2/odoo/addons/printer_zpl2 | 1 + setup/printer_zpl2/setup.py | 6 ++++++ 11 files changed, 32 insertions(+), 27 deletions(-) create mode 120000 setup/printer_zpl2/odoo/addons/printer_zpl2 create mode 100644 setup/printer_zpl2/setup.py diff --git a/printer_zpl2/README.rst b/printer_zpl2/README.rst index 912d503..1eb6908 100644 --- a/printer_zpl2/README.rst +++ b/printer_zpl2/README.rst @@ -14,14 +14,14 @@ Printer ZPL II :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Freport--print--send-lightgray.png?logo=github - :target: https://github.com/OCA/report-print-send/tree/14.0/printer_zpl2 + :target: https://github.com/OCA/report-print-send/tree/15.0/printer_zpl2 :alt: OCA/report-print-send .. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png - :target: https://translation.odoo-community.org/projects/report-print-send-14-0/report-print-send-14-0-printer_zpl2 + :target: https://translation.odoo-community.org/projects/report-print-send-15-0/report-print-send-15-0-printer_zpl2 :alt: Translate me on Weblate -.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png - :target: https://runbot.odoo-community.org/runbot/144/14.0 - :alt: Try me on Runbot +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/webui/builds.html?repo=OCA/report-print-send&target_branch=15.0 + :alt: Try me on Runboat |badge1| |badge2| |badge3| |badge4| |badge5| @@ -97,7 +97,7 @@ Bug Tracker Bugs are tracked on `GitHub Issues `_. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us smashing it by providing a detailed and welcomed -`feedback `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -134,6 +134,6 @@ OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use. -This module is part of the `OCA/report-print-send `_ project on GitHub. +This module is part of the `OCA/report-print-send `_ project on GitHub. You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/printer_zpl2/__manifest__.py b/printer_zpl2/__manifest__.py index ec02765..e8b5c8c 100644 --- a/printer_zpl2/__manifest__.py +++ b/printer_zpl2/__manifest__.py @@ -1,9 +1,9 @@ -# Copyright (C) 2016 SUBTENO-IT () +# Copyright (C) 2016-2022 SUBTENO-IT () # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). { "name": "Printer ZPL II", - "version": "14.0.2.0.0", + "version": "15.0.1.0.0", "category": "Printer", "summary": "Add a ZPL II label printing feature", "author": "SUBTENO-IT, FLorent de Labarre, " diff --git a/printer_zpl2/models/__init__.py b/printer_zpl2/models/__init__.py index 4263857..69d41d1 100644 --- a/printer_zpl2/models/__init__.py +++ b/printer_zpl2/models/__init__.py @@ -1,5 +1,5 @@ # Copyright (C) 2016 SYLEAM () # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from . import printing_label_zpl2_component from . import printing_label_zpl2 +from . import printing_label_zpl2_component diff --git a/printer_zpl2/models/printing_label_zpl2.py b/printer_zpl2/models/printing_label_zpl2.py index e9e5e9b..4048c31 100644 --- a/printer_zpl2/models/printing_label_zpl2.py +++ b/printer_zpl2/models/printing_label_zpl2.py @@ -69,7 +69,7 @@ class PrintingLabelZpl2(models.Model): test_print_mode = fields.Boolean(string="Mode Print") test_labelary_mode = fields.Boolean(string="Mode Labelary") record_id = fields.Integer(string="Record ID", default=1) - extra = fields.Text(string="Extra", default="{}") + extra = fields.Text(default="{}") printer_id = fields.Many2one(comodel_name="printing.printer", string="Printer") labelary_image = fields.Binary( string="Image from Labelary", compute="_compute_labelary_image" @@ -123,10 +123,8 @@ class PrintingLabelZpl2(models.Model): def _get_component_data(self, record, component, eval_args): if component.data_autofill: - data = component.autofill_data(record, eval_args) - else: - data = component.data - return safe_eval(str(data), eval_args) or "" + return component.autofill_data(record, eval_args) + return safe_eval(str(component.data), eval_args) or "" def _get_to_data_to_print( self, diff --git a/printer_zpl2/static/description/index.html b/printer_zpl2/static/description/index.html index 561c610..ba29649 100644 --- a/printer_zpl2/static/description/index.html +++ b/printer_zpl2/static/description/index.html @@ -3,7 +3,7 @@ - + Printer ZPL II