From ca79c760df4a87b978036f9b7854a5ae1cafa7bb Mon Sep 17 00:00:00 2001 From: mreficent Date: Tue, 30 Jun 2020 11:44:48 +0200 Subject: [PATCH] [MIG] printer_zpl2 to v13 (end) --- printer_zpl2/__manifest__.py | 3 +- printer_zpl2/models/printing_label_zpl2.py | 178 +++++++++++------- .../models/printing_label_zpl2_component.py | 55 +++++- printer_zpl2/readme/CONTRIBUTORS.rst | 1 + .../tests/test_printing_label_zpl2.py | 5 +- printer_zpl2/tests/test_test_mode.py | 5 +- .../tests/test_wizard_print_record_label.py | 5 +- printer_zpl2/views/printing_label_zpl2.xml | 31 +-- printer_zpl2/wizard/print_record_label.py | 5 + printer_zpl2/wizard/print_record_label.xml | 8 +- printer_zpl2/wizard/wizard_import_zpl2.xml | 2 +- 11 files changed, 203 insertions(+), 95 deletions(-) diff --git a/printer_zpl2/__manifest__.py b/printer_zpl2/__manifest__.py index 72bf7f3..49cbc19 100644 --- a/printer_zpl2/__manifest__.py +++ b/printer_zpl2/__manifest__.py @@ -5,9 +5,10 @@ "name": "Printer ZPL II", "version": "13.0.1.0.0", "category": "Printer", + "summary": "Add a ZPL II label printing feature", "author": "SUBTENO-IT, FLorent de Labarre, " "Apertoso NV, Odoo Community Association (OCA)", - "website": "http://www.syleam.fr/", + "website": "https://github.com/OCA/report-print-send", "license": "AGPL-3", "external_dependencies": {"python": ["zpl2"]}, "depends": ["base_report_to_printer"], diff --git a/printer_zpl2/models/printing_label_zpl2.py b/printer_zpl2/models/printing_label_zpl2.py index 8f33ee9..473af3d 100644 --- a/printer_zpl2/models/printing_label_zpl2.py +++ b/printer_zpl2/models/printing_label_zpl2.py @@ -4,13 +4,16 @@ import base64 import datetime import io +import itertools import logging import time +from collections import defaultdict import requests from PIL import Image, ImageOps from odoo import _, api, exceptions, fields, models +from odoo.exceptions import ValidationError from odoo.tools.safe_eval import safe_eval _logger = logging.getLogger(__name__) @@ -63,15 +66,16 @@ class PrintingLabelZpl2(models.Model): default=True, ) action_window_id = fields.Many2one( - comodel_name="ir.actions.act_window", string="Action", readonly=True + 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', - compute='_compute_labelary_image') + labelary_image = fields.Binary( + string="Image from Labelary", compute="_compute_labelary_image" + ) labelary_dpmm = fields.Selection( selection=[ ("6dpmm", "6dpmm (152 pdi)"), @@ -85,15 +89,46 @@ class PrintingLabelZpl2(models.Model): ) labelary_width = fields.Float(string="Width in mm", default=140) labelary_height = fields.Float(string="Height in mm", default=70) - data_type = fields.Selection( - string="Labels components data type", - selection=[("und", "Undefined")], - help="This allows to specify the type of the data encoded in label components", - ) - @api.model - def _get_component_data(self, component, eval_args): - return safe_eval(component.data, eval_args) or "" + @api.constrains("component_ids") + def check_recursion(self): + cr = self._cr + self.flush(["component_ids"]) + query = ( # pylint: disable=E8103 + 'SELECT "{}", "{}" FROM "{}" ' + 'WHERE "{}" IN %s AND "{}" IS NOT NULL'.format( + "label_id", + "sublabel_id", + "printing_label_zpl2_component", + "label_id", + "sublabel_id", + ) + ) + + succs = defaultdict(set) # transitive closure of successors + preds = defaultdict(set) # transitive closure of predecessors + todo, done = set(self.ids), set() + while todo: + cr.execute(query, [tuple(todo)]) + done.update(todo) + todo.clear() + for id1, id2 in cr.fetchall(): + for x, y in itertools.product( + [id1] + list(preds[id1]), [id2] + list(succs[id2]) + ): + if x == y: + raise ValidationError(_("You can not create recursive labels.")) + succs[x].add(y) + preds[y].add(x) + if id2 not in done: + todo.add(id2) + + 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 "" def _get_to_data_to_print( self, @@ -116,8 +151,8 @@ class PrintingLabelZpl2(models.Model): "datetime": datetime, } ) - data = self._get_component_data(component, eval_args) - if data == "component_not_show": + data = self._get_component_data(record, component, eval_args) + if isinstance(data, str) and data == "component_not_show": continue # Generate a list of elements if the component is repeatable @@ -157,10 +192,6 @@ class PrintingLabelZpl2(models.Model): 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 = self._get_to_data_to_print( record, page_number, page_count, label_offset_x, label_offset_y, **extra ) @@ -259,7 +290,7 @@ class PrintingLabelZpl2(models.Model): component_offset_y += component.sublabel_id.origin_y component.sublabel_id._generate_zpl2_components_data( label_data, - data, + data if isinstance(data, models.BaseModel) else record, label_offset_x=component_offset_x, label_offset_y=component_offset_y, ) @@ -325,55 +356,70 @@ class PrintingLabelZpl2(models.Model): return label_data.output() - def fill_component(self, line): - for component in self.component_ids: - json = { - "product_barcode": line.product_barcode, - "lot_barcode": line.lot_barcode, - "uom": str(line.product_qty) + " " + line.product_id.uom_id.name, - "package_barcode": line.package_barcode, - "product_qty": line.product_qty, - } - component.data = json - 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) ) - if label.data_type == "und": - for component in self.component_ids: - eval_args = extra - eval_args.update( - {"object": record, "time": time, "datetime": datetime} - ) - data = safe_eval(component.data, eval_args) or "" - if data == "component_not_show": - continue - # Send the label to printer - label_contents = label._generate_zpl2_data( - record, page_count=1, **extra - ) - printer.print_document( - report=None, content=label_contents, doc_format="raw" - ) - + # Send the label to printer + label_contents = label._generate_zpl2_data( + record, page_count=page_count, **extra + ) + printer.print_document( + report=None, content=label_contents, doc_format="raw" + ) return True - def create_action(self): - for label in self.filtered(lambda record: not record.action_window_id): - label.action_window_id = self.env["ir.actions.act_window"].create( - { - "name": _("Print Label"), - "binding_model_id": label.model_id.id, - "res_model": "wizard.print.record.label", - "view_mode": "form", - "target": "new", - "binding_type": "action", - } - ) + @api.model + def new_action(self, model_id): + return self.env["ir.actions.act_window"].create( + { + "name": _("Print Label"), + "binding_model_id": model_id, + "res_model": "wizard.print.record.label", + "view_mode": "form", + "target": "new", + "binding_type": "action", + "context": "{'default_active_model_id': %s}" % model_id, + } + ) + @api.model + def add_action(self, model_id): + action = self.env["ir.actions.act_window"].search( + [ + ("binding_model_id", "=", model_id), + ("res_model", "=", "wizard.print.record.label"), + ("view_mode", "=", "form"), + ("binding_type", "=", "action"), + ] + ) + if not action: + action = self.new_action(model_id) + return action + + def create_action(self): + models = self.filtered(lambda record: not record.action_window_id).mapped( + "model_id" + ) + labels = self.with_context(active_test=False).search( + [("model_id", "in", models.ids), ("action_window_id", "=", False)] + ) + actions = self.env["ir.actions.act_window"].search( + [ + ("binding_model_id", "in", models.ids), + ("res_model", "=", "wizard.print.record.label"), + ("view_mode", "=", "form"), + ("binding_type", "=", "action"), + ] + ) + for model in models: + action = actions.filtered(lambda a: a.binding_model_id == model) + if not action: + action = self.new_action(model.id) + for label in labels.filtered(lambda l: l.model_id == model): + label.action_window_id = action return True def unlink_action(self): @@ -382,7 +428,6 @@ class PrintingLabelZpl2(models.Model): def import_zpl2(self): self.ensure_one() return { - "view_type": "form", "view_mode": "form", "res_model": "wizard.import.zpl2", "type": "ir.actions.act_window", @@ -409,8 +454,15 @@ class PrintingLabelZpl2(models.Model): label.print_label(label.printer_id, record, **extra) @api.depends( - 'record_id', 'labelary_dpmm', 'labelary_width', 'labelary_height', - 'component_ids', 'origin_x', 'origin_y', 'test_labelary_mode') + "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() @@ -456,8 +508,8 @@ class PrintingLabelZpl2(models.Model): return base64.b64encode(imgByteArr.getvalue()) else: _logger.warning( - _( - "Error with Labelary API. %s") % response.status_code) + _("Error with Labelary API. %s") % response.status_code + ) except Exception as e: _logger.warning(_("Error with Labelary API. %s") % e) diff --git a/printer_zpl2/models/printing_label_zpl2_component.py b/printer_zpl2/models/printing_label_zpl2_component.py index 9ad4660..9342ec5 100644 --- a/printer_zpl2/models/printing_label_zpl2_component.py +++ b/printer_zpl2/models/printing_label_zpl2_component.py @@ -3,7 +3,7 @@ import logging -from odoo import fields, models +from odoo import api, fields, models _logger = logging.getLogger(__name__) @@ -13,12 +13,12 @@ except ImportError: _logger.debug("Cannot `import zpl2`.") DEFAULT_PYTHON_CODE = """# Python One-Liners -# - object: record on which the action is triggered; may be be void +# - object: %s record on which the action is triggered; may be void # - page_number: Current Page # - page_count: Total Page # - time, datetime: Python libraries -# - return 'component_not_show' to don't show this component -# Exemple : object.name +# - write instead 'component_not_show' to don't show this component +# Example: object.name "" @@ -114,6 +114,10 @@ class PrintingLabelZpl2Component(models.Model): default=str(zpl2.DIAGONAL_ORIENTATION_LEFT), help="Orientation of the diagonal line.", ) + data_autofill = fields.Boolean( + string="Autofill Data", + help="Change 'data' with dictionary of the object information.", + ) check_digits = fields.Boolean( help="Check if you want to compute and print the check digit." ) @@ -165,8 +169,11 @@ class PrintingLabelZpl2Component(models.Model): help="Error correction for some barcode types like QR Code.", ) mask_value = fields.Integer(default=7, help="Mask Value, from 0 to 7.") + model_id = fields.Many2one( + comodel_name="ir.model", compute="_compute_model_id", string="Record's model" + ) data = fields.Text( - default=DEFAULT_PYTHON_CODE, + default=lambda self: self._compute_default_data(), required=True, help="Data to print on this component. Resource values can be " "inserted with %(object.field_name)s.", @@ -233,6 +240,44 @@ class PrintingLabelZpl2Component(models.Model): "If not set, the data field is evaluated.", ) + def process_model(self, model): + # Used for expansions of this module + return model + + @api.depends("label_id.model_id") + def _compute_model_id(self): + # it's 'compute' instead of 'related' because is easier to expand it + for component in self: + component.model_id = self.process_model(component.label_id.model_id) + + def _compute_default_data(self): + model_id = self.env.context.get("default_model_id") or self.model_id.id + model = self.env["ir.model"].browse(model_id) + model = self.process_model(model) + return DEFAULT_PYTHON_CODE % (model.model or "") + + @api.onchange("model_id", "data") + def _onchange_data(self): + for component in self.filtered(lambda c: not c.data): + component.data = component._compute_default_data() + + @api.onchange("component_type") + def _onchange_component_type(self): + for component in self: + if component.component_type == "qr_code": + component.data_autofill = True + else: + component.data_autofill = False + + @api.model + def autofill_data(self, record, eval_args): + data = {} + usual_fields = ["id", "create_date", record.display_name] + for field in usual_fields: + if hasattr(record, field): + data[field] = getattr(record, field) + return data + def action_plus_origin_x(self): self.ensure_one() self.origin_x += 10 diff --git a/printer_zpl2/readme/CONTRIBUTORS.rst b/printer_zpl2/readme/CONTRIBUTORS.rst index 6ef4003..aac53d8 100644 --- a/printer_zpl2/readme/CONTRIBUTORS.rst +++ b/printer_zpl2/readme/CONTRIBUTORS.rst @@ -2,3 +2,4 @@ * Florent de Labarre * Jos De Graeve * Rod Schouteden +* Miquel Raïch diff --git a/printer_zpl2/tests/test_printing_label_zpl2.py b/printer_zpl2/tests/test_printing_label_zpl2.py index 1d67e8a..2d1f092 100644 --- a/printer_zpl2/tests/test_printing_label_zpl2.py +++ b/printer_zpl2/tests/test_printing_label_zpl2.py @@ -67,11 +67,8 @@ 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" label.print_label(self.printer, self.printer) - cups.Connection().printFile.assert_called_once_with( - self.printer.system_name, file_name, file_name, options={} - ) + cups.Connection().printFile.assert_called_once() def test_empty_label_contents(self): """ Check contents of an empty label """ diff --git a/printer_zpl2/tests/test_test_mode.py b/printer_zpl2/tests/test_test_mode.py index d6d7c4e..db7ea22 100644 --- a/printer_zpl2/tests/test_test_mode.py +++ b/printer_zpl2/tests/test_test_mode.py @@ -51,11 +51,8 @@ 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() - cups.Connection().printFile.assert_called_once_with( - self.printer.system_name, file_name, file_name, options={} - ) + cups.Connection().printFile.assert_called_once() def test_emulation_without_params(self): """ Check if not execute next if not in this mode """ diff --git a/printer_zpl2/tests/test_wizard_print_record_label.py b/printer_zpl2/tests/test_wizard_print_record_label.py index 2c2308d..1babfaa 100644 --- a/printer_zpl2/tests/test_wizard_print_record_label.py +++ b/printer_zpl2/tests/test_wizard_print_record_label.py @@ -48,10 +48,7 @@ class TestWizardPrintRecordLabel(TransactionCase): self.assertEqual(wizard.printer_id, self.printer) self.assertEqual(wizard.label_id, self.label) wizard.print_label() - file_name = "test.zpl" - cups.Connection().printFile.assert_called_once_with( - self.printer.system_name, file_name, file_name, options={} - ) + 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 diff --git a/printer_zpl2/views/printing_label_zpl2.xml b/printer_zpl2/views/printing_label_zpl2.xml index d99917d..b8fd804 100644 --- a/printer_zpl2/views/printing_label_zpl2.xml +++ b/printer_zpl2/views/printing_label_zpl2.xml @@ -1,4 +1,4 @@ - +