diff --git a/printer_zpl2/README.rst b/printer_zpl2/README.rst new file mode 100644 index 00000000000..271aec834c5 --- /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/10.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 00000000000..6b40cb02b11 --- /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/__manifest__.py b/printer_zpl2/__manifest__.py new file mode 100644 index 00000000000..dcf7ec5f459 --- /dev/null +++ b/printer_zpl2/__manifest__.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': '10.0.1.0.0', + 'category': 'Printer', + 'author': 'SYLEAM, Odoo Community Association (OCA)', + 'website': 'http://www.syleam.fr/', + 'license': 'AGPL-3', + 'external_dependencies': { + '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 00000000000..048ad6e3dc5 --- /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 00000000000..76aa22033f2 --- /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 odoo import api, exceptions, fields, models +from odoo.tools.translate import _ +from odoo.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='Width 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 00000000000..c62cad6bb0d --- /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 odoo 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 00000000000..acd42710c60 --- /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 00000000000..4483773a512 --- /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 00000000000..37a88b756ac --- /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 odoo import exceptions +from odoo.tests.common import TransactionCase + + +model = 'odoo.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 00000000000..874c7d9c1dc --- /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 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, + }) + + @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 00000000000..24b98aa32a5 --- /dev/null +++ b/printer_zpl2/views/printing_label_zpl2.xml @@ -0,0 +1,142 @@ + + + + + printing.label.zpl2.tree + printing.label.zpl2 + + + + + + + + + printing.label.zpl2.form + printing.label.zpl2 + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + printing.label.zpl2.search + printing.label.zpl2 + + + + + + + + + ZPL II Labels + 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 00000000000..5c68984b4df --- /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 00000000000..c1ad750d61e --- /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 odoo 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 00000000000..ed64c9092da --- /dev/null +++ b/printer_zpl2/wizard/print_record_label.xml @@ -0,0 +1,30 @@ + + + + + wizard.print.record.label.form + wizard.print.record.label + +
+ + + + +