mirror of
https://github.com/OCA/report-print-send.git
synced 2025-02-16 07:11:31 +02:00
[MIG] printer_zpl2 to v13 (end)
This commit is contained in:
@@ -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"],
|
||||
|
||||
@@ -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.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",
|
||||
)
|
||||
)
|
||||
|
||||
@api.model
|
||||
def _get_component_data(self, component, eval_args):
|
||||
return safe_eval(component.data, eval_args) or ""
|
||||
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
|
||||
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(
|
||||
@api.model
|
||||
def new_action(self, model_id):
|
||||
return self.env["ir.actions.act_window"].create(
|
||||
{
|
||||
"name": _("Print Label"),
|
||||
"binding_model_id": label.model_id.id,
|
||||
"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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,3 +2,4 @@
|
||||
* Florent de Labarre
|
||||
* Jos De Graeve <Jos.DeGraeve@apertoso.be>
|
||||
* Rod Schouteden <rod.schouteden@dynapps.be>
|
||||
* Miquel Raïch <miquel.raich@forgeflow.com>
|
||||
|
||||
@@ -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 """
|
||||
|
||||
@@ -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 """
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" ?>
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<!--
|
||||
Copyright 2016 SUBTENO-IT
|
||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
@@ -56,7 +56,6 @@
|
||||
<field name="width" />
|
||||
<field name="origin_x" />
|
||||
<field name="origin_y" />
|
||||
<field name="data_type" />
|
||||
<field name="restore_saved_config" />
|
||||
</group>
|
||||
<group attrs="{'invisible':[('test_print_mode', '=', False)]}">
|
||||
@@ -69,7 +68,12 @@
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Components">
|
||||
<field name="component_ids" nolabel="1" colspan="4">
|
||||
<field
|
||||
name="component_ids"
|
||||
nolabel="1"
|
||||
colspan="4"
|
||||
context="{'default_model_id': model_id}"
|
||||
>
|
||||
<tree string="Label Component">
|
||||
<field name="sequence" />
|
||||
<field name="name" />
|
||||
@@ -106,6 +110,7 @@
|
||||
<group>
|
||||
<field name="name" />
|
||||
<field name="sequence" />
|
||||
<field name="model_id" invisible="1" />
|
||||
</group>
|
||||
<group>
|
||||
<field name="component_type" />
|
||||
@@ -113,6 +118,10 @@
|
||||
name="repeat"
|
||||
attrs="{'invisible': [('component_type', '=', 'zpl2_raw')]}"
|
||||
/>
|
||||
<field
|
||||
name="data_autofill"
|
||||
attrs="{'invisible': [('component_type', '!=', 'qr_code')]}"
|
||||
/>
|
||||
</group>
|
||||
<group
|
||||
attrs="{'invisible': [('component_type', '=', 'zpl2_raw')]}"
|
||||
@@ -132,7 +141,7 @@
|
||||
</group>
|
||||
</group>
|
||||
<group
|
||||
attrs="{'invisible': [('component_type', 'in', ('rectangle', 'diagonal', 'circle'))]}"
|
||||
attrs="{'invisible': ['|', ('data_autofill', '=', True), ('component_type', 'in', ('rectangle', 'diagonal', 'circle'))]}"
|
||||
string="Data"
|
||||
>
|
||||
<field
|
||||
|
||||
@@ -23,6 +23,11 @@ class PrintRecordLabel(models.TransientModel):
|
||||
],
|
||||
help="Label to print.",
|
||||
)
|
||||
active_model_id = fields.Many2one(
|
||||
comodel_name="ir.model",
|
||||
string="Model",
|
||||
domain=lambda self: [("model", "=", self.env.context.get("active_model"))],
|
||||
)
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields_list):
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" ?>
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<!--
|
||||
Copyright 2016 SYLEAM
|
||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
@@ -11,7 +11,11 @@
|
||||
<form string="Print Label">
|
||||
<group>
|
||||
<field name="printer_id" />
|
||||
<field name="label_id" />
|
||||
<field
|
||||
name="label_id"
|
||||
context="{'default_model_id': active_model_id}"
|
||||
/>
|
||||
<field name="active_model_id" invisible="1" />
|
||||
</group>
|
||||
<footer>
|
||||
<button type="special" special="cancel" string="Cancel" />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" ?>
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<record id="view_wizard_import_zpl2_form" model="ir.ui.view">
|
||||
<field name="name">wizard.import.zpl2.form</field>
|
||||
|
||||
Reference in New Issue
Block a user