Initial commit: OCA Report packages (45 packages)

This commit is contained in:
Ernad Husremovic 2025-08-29 15:43:05 +02:00
commit 2f4db400df
2543 changed files with 469120 additions and 0 deletions

View file

@ -0,0 +1,46 @@
# Printer ZPL II
Odoo addon: printer_zpl2
## Installation
```bash
pip install odoo-bringout-oca-report-print-send-printer_zpl2
```
## Dependencies
This addon depends on:
- base_report_to_printer
## Manifest Information
- **Name**: Printer ZPL II
- **Version**: 16.0.1.1.1
- **Category**: Printer
- **License**: AGPL-3
- **Installable**: True
## Source
Based on [OCA/report-print-send](https://github.com/OCA/report-print-send) branch 16.0, addon `printer_zpl2`.
## License
This package maintains the original AGPL-3 license from the upstream Odoo project.
## Documentation
- Overview: doc/OVERVIEW.md
- Architecture: doc/ARCHITECTURE.md
- Models: doc/MODELS.md
- Controllers: doc/CONTROLLERS.md
- Wizards: doc/WIZARDS.md
- Reports: doc/REPORTS.md
- Security: doc/SECURITY.md
- Install: doc/INSTALL.md
- Usage: doc/USAGE.md
- Configuration: doc/CONFIGURATION.md
- Dependencies: doc/DEPENDENCIES.md
- Troubleshooting: doc/TROUBLESHOOTING.md
- FAQ: doc/FAQ.md

View file

@ -0,0 +1,32 @@
# Architecture
```mermaid
flowchart TD
U[Users] -->|HTTP| V[Views and QWeb Templates]
V --> C[Controllers]
V --> W[Wizards Transient Models]
C --> M[Models and ORM]
W --> M
M --> R[Reports]
DX[Data XML] --> M
S[Security ACLs and Groups] -. enforces .-> M
subgraph Printer_zpl2 Module - printer_zpl2
direction LR
M:::layer
W:::layer
C:::layer
V:::layer
R:::layer
S:::layer
DX:::layer
end
classDef layer fill:#eef8ff,stroke:#6ea8fe,stroke-width:1px
```
Notes
- Views include tree/form/kanban templates and report templates.
- Controllers provide website/portal routes when present.
- Wizards are UI flows implemented with `models.TransientModel`.
- Data XML loads data/demo records; Security defines groups and access.

View file

@ -0,0 +1,3 @@
# Configuration
Refer to Odoo settings for printer_zpl2. Configure related models, access rights, and options as needed.

View file

@ -0,0 +1,3 @@
# Controllers
This module does not define custom HTTP controllers.

View file

@ -0,0 +1,5 @@
# Dependencies
This addon depends on:
- [base_report_to_printer](../../odoo-bringout-oca-report-print-send-base_report_to_printer)

View file

@ -0,0 +1,4 @@
# FAQ
- Q: Which Odoo version? A: 16.0 (OCA/OCB packaged).
- Q: How to enable? A: Start server with --addon printer_zpl2 or install in UI.

View file

@ -0,0 +1,7 @@
# Install
```bash
pip install odoo-bringout-oca-report-print-send-printer_zpl2"
# or
uv pip install odoo-bringout-oca-report-print-send-printer_zpl2"
```

View file

@ -0,0 +1,13 @@
# Models
Detected core models and extensions in printer_zpl2.
```mermaid
classDiagram
class printing_label_zpl2
class printing_label_zpl2_component
```
Notes
- Classes show model technical names; fields omitted for brevity.
- Items listed under _inherit are extensions of existing models.

View file

@ -0,0 +1,6 @@
# Overview
Packaged Odoo addon: printer_zpl2. Provides features documented in upstream Odoo 16 under this addon.
- Source: OCA/OCB 16.0, addon printer_zpl2
- License: LGPL-3

View file

@ -0,0 +1,3 @@
# Reports
This module does not define custom reports.

View file

@ -0,0 +1,34 @@
# Security
Access control and security definitions in printer_zpl2.
## Access Control Lists (ACLs)
Model access permissions defined in:
- **[ir.model.access.csv](../printer_zpl2/security/ir.model.access.csv)**
- 7 model access rules
## Record Rules
Row-level security rules defined in:
```mermaid
graph TB
subgraph "Security Layers"
A[Users] --> B[Groups]
B --> C[Access Control Lists]
C --> D[Models]
B --> E[Record Rules]
E --> F[Individual Records]
end
```
Security files overview:
- **[ir.model.access.csv](../printer_zpl2/security/ir.model.access.csv)**
- Model access permissions (CRUD rights)
Notes
- Access Control Lists define which groups can access which models
- Record Rules provide row-level security (filter records by user/group)
- Security groups organize users and define permission sets
- All security is enforced at the ORM level by Odoo

View file

@ -0,0 +1,5 @@
# Troubleshooting
- Ensure Python and Odoo environment matches repo guidance.
- Check database connectivity and logs if startup fails.
- Validate that dependent addons listed in DEPENDENCIES.md are installed.

View file

@ -0,0 +1,7 @@
# Usage
Start Odoo including this addon (from repo root):
```bash
python3 scripts/nix_odoo_web_server.py --db-name mydb --addon printer_zpl2
```

View file

@ -0,0 +1,10 @@
# Wizards
Transient models exposed as UI wizards in printer_zpl2.
```mermaid
classDiagram
class PrintRecordLabel
class PrintRecordLabelLines
class WizardImportZPl2
```

View file

@ -0,0 +1,143 @@
==============
Printer ZPL II
==============
..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:959cd2e93d084b3fb9c30ec5d95b216eed7d1d372aa195ec5126d9c06031815e
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
: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/16.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-16-0/report-print-send-16-0-printer_zpl2
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/report-print-send&target_branch=16.0
:alt: Try me on Runboat
|badge1| |badge2| |badge3| |badge4| |badge5|
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.
**Table of contents**
.. contents::
:local:
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
#. 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 ::
<act_window id="action_wizard_purchase"
name="Print Label"
src_model="product.product"
res_model="wizard.print.record.label"
view_mode="form"
target="new"
key2="client_action_multi"/>
Usage
=====
To print a label, you need to call use the label printing method from anywhere (other modules, server actions, etc.).
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/12.0
Changelog
=========
13.0.1.0.0 (2019-09-30)
~~~~~~~~~~~~~~~~~~~~~~~
* [RELEASE] Port from V12.
* Selection lists do not support integers any longer
* Binary field now returns False when empty instead of none,
change tests to reflect this
* work around an appels vs oranges warning
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/report-print-send/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/OCA/report-print-send/issues/new?body=module:%20printer_zpl2%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
~~~~~~~
* SUBTENO-IT
* FLorent de Labarre
* Apertoso NV
Contributors
~~~~~~~~~~~~
* Sylvain Garancher <sylvain.garancher@syleam.fr>
* Florent de Labarre
* 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>
* Tran Quoc Duong <duontq@trobz.com>
Maintainers
~~~~~~~~~~~
This module is maintained by the OCA.
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
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 <https://github.com/OCA/report-print-send/tree/16.0/printer_zpl2>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View file

@ -0,0 +1,5 @@
# Copyright (C) 2016 SUBTENO-IT (<https://subteno-it.fr>)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import models
from . import wizard

View file

@ -0,0 +1,21 @@
# Copyright (C) 2016-2022 SUBTENO-IT (<https://subteno-it.fr>)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
"name": "Printer ZPL II",
"version": "16.0.1.1.1",
"category": "Printer",
"summary": "Add a ZPL II label printing feature",
"author": "SUBTENO-IT, FLorent de Labarre, "
"Apertoso NV, Odoo Community Association (OCA)",
"website": "https://github.com/OCA/report-print-send",
"license": "AGPL-3",
"depends": ["base_report_to_printer"],
"data": [
"security/ir.model.access.csv",
"views/printing_label_zpl2.xml",
"wizard/print_record_label.xml",
"wizard/wizard_import_zpl2.xml",
],
"installable": True,
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,5 @@
# Copyright (C) 2016 SYLEAM (<http://www.syleam.fr>)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import printing_label_zpl2
from . import printing_label_zpl2_component

View file

@ -0,0 +1,528 @@
# Copyright (C) 2016 SYLEAM (<http://www.syleam.fr>)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import base64
import io
import itertools
import logging
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, wrap_module
from . import zpl2
_logger = logging.getLogger(__name__)
class PrintingLabelZpl2(models.Model):
_name = "printing.label.zpl2"
_description = "ZPL II Label"
_order = "model_id, name, id"
name = fields.Char(required=True, help="Label Name.")
active = fields.Boolean(default=True)
description = fields.Char(help="Long description for this label.")
model_id = fields.Many2one(
comodel_name="ir.model",
string="Model",
required=True,
ondelete="cascade",
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.",
copy=True,
)
restore_saved_config = fields.Boolean(
string="Restore printer's configuration",
help="Restore printer's saved configuration and end of each label ",
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(default="{}")
printer_id = fields.Many2one(comodel_name="printing.printer", string="Printer")
labelary_image = fields.Binary(
string="Image from Labelary", compute="_compute_labelary_image"
)
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)
@api.constrains("component_ids")
def check_recursion(self):
cr = self._cr
self.flush_recordset(["component_ids"])
query = (
'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)]) # pylint: disable=E8103
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:
return component.autofill_data(record, eval_args)
return safe_eval(str(component.data), eval_args) or ""
def _get_to_data_to_print(
self,
record,
page_number=1,
page_count=1,
label_offset_x=0,
label_offset_y=0,
**extra
):
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": wrap_module(
__import__("time"), ["time", "strptime", "strftime"]
),
"datetime": wrap_module(
__import__("datetime"),
[
"date",
"datetime",
"time",
"timedelta",
"timezone",
"tzinfo",
"MAXYEAR",
"MINYEAR",
],
),
}
)
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
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,
)
)
return to_print
# flake8: noqa: C901
def _generate_zpl2_components_data(
self,
label_data,
record,
page_number=1,
page_count=1,
label_offset_x=0,
label_offset_y=0,
**extra
):
to_print = self._get_to_data_to_print(
record, page_number, page_count, label_offset_x, label_offset_y, **extra
)
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 = {
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 == "zpl2_raw":
label_data._write_command(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 == "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":
# During the on_change don't take the bin_size
image = (
component.with_context(bin_size_graphic_image=False).graphic_image
or data
)
try:
pil_image = Image.open(io.BytesIO(base64.b64decode(image))).convert(
"RGB"
)
except Exception:
continue
if component.width and component.height:
pil_image = pil_image.resize((component.width, component.height))
# Invert the colors
if component.reverse_print:
pil_image = ImageOps.invert(pil_image)
# Rotation (PIL rotates counter clockwise)
if component.orientation == zpl2.ORIENTATION_ROTATED:
pil_image = pil_image.transpose(Image.ROTATE_270)
elif component.orientation == zpl2.ORIENTATION_INVERTED:
pil_image = pil_image.transpose(Image.ROTATE_180)
elif component.orientation == zpl2.ORIENTATION_BOTTOM_UP:
pil_image = pil_image.transpose(Image.ROTATE_90)
label_data.graphic_field(
component_offset_x, component_offset_y, pil_image
)
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 if isinstance(data, models.BaseModel) else record,
label_offset_x=component_offset_x,
label_offset_y=component_offset_y,
)
else:
if component.component_type == zpl2.BARCODE_QR_CODE:
# Adding Control Arguments to QRCode data Label
data = "{}A,{}".format(component.error_correction, data)
barcode_arguments = {
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,
zpl2.ARG_MODEL,
zpl2.ARG_MAGNIFICATION_FACTOR,
zpl2.ARG_ERROR_CORRECTION,
zpl2.ARG_MASK_VALUE,
]
}
label_data.barcode_data(
component.origin_x + offset_x,
component.origin_y + offset_y,
component.component_type,
barcode_arguments,
data,
)
def _generate_zpl2_data(self, record, page_count=1, **extra):
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()
if not labelary_emul:
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,
**extra
)
# Restore printer's configuration and end the label
if self.restore_saved_config:
label_data.configuration_update(zpl2.CONF_RECALL_LAST_SAVED)
label_data.label_end()
return label_data.output()
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(
report=None, content=label_contents, doc_format="raw"
)
return True
@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):
self.mapped("action_window_id").unlink()
def import_zpl2(self):
self.ensure_one()
return {
"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]
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
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.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
and self.record_id
and self.labelary_width
and self.labelary_height
and self.labelary_dpmm
and self.component_ids
):
return False
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, timeout=30
)
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")
return base64.b64encode(imgByteArr.getvalue())
else:
_logger.warning(
_("Error with Labelary API. %s") % response.status_code
)
except Exception as e:
_logger.warning(_("Error with Labelary API. %s") % e)
return False

View file

@ -0,0 +1,293 @@
# Copyright (C) 2016 SYLEAM (<http://www.syleam.fr>)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import logging
from odoo import api, fields, models
from . import zpl2
_logger = logging.getLogger(__name__)
DEFAULT_PYTHON_CODE = """# Python One-Liners
# - object: %s record on which the action is triggered; may be void
# - page_number: Current Page
# - page_count: Total Page
# - time, datetime: Python libraries
# - write instead 'component_not_show' to don't show this component
# Example: object.name
""
"""
class PrintingLabelZpl2Component(models.Model):
_name = "printing.label.zpl2.component"
_description = "ZPL II Label Component"
_order = "sequence, id"
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"),
("diagonal", "Diagonal Line"),
("circle", "Circle"),
("graphic", "Graphic"),
(str(zpl2.BARCODE_CODE_11), "Code 11"),
(str(zpl2.BARCODE_INTERLEAVED_2_OF_5), "Interleaved 2 of 5"),
(str(zpl2.BARCODE_CODE_39), "Code 39"),
(str(zpl2.BARCODE_CODE_49), "Code 49"),
(str(zpl2.BARCODE_PDF417), "PDF417"),
(str(zpl2.BARCODE_EAN_8), "EAN-8"),
(str(zpl2.BARCODE_UPC_E), "UPC-E"),
(str(zpl2.BARCODE_CODE_128), "Code 128"),
(str(zpl2.BARCODE_EAN_13), "EAN-13"),
(str(zpl2.BARCODE_QR_CODE), "QR Code"),
("sublabel", "Sublabel"),
("zpl2_raw", "ZPL2"),
],
string="Type",
required=True,
default="text",
help="Type of content, simple text or barcode.",
)
font = fields.Selection(
selection=[
(str(zpl2.FONT_DEFAULT), "Default"),
(str(zpl2.FONT_9X5), "9x5"),
(str(zpl2.FONT_11X7), "11x7"),
(str(zpl2.FONT_18X10), "18x10"),
(str(zpl2.FONT_28X15), "28x15"),
(str(zpl2.FONT_26X13), "26x13"),
(str(zpl2.FONT_60X40), "60x40"),
(str(zpl2.FONT_21X13), "21x13"),
],
required=True,
default=str(zpl2.FONT_DEFAULT),
help="Font to use, for text only.",
)
thickness = fields.Integer(help="Thickness of the line to draw.")
color = fields.Selection(
selection=[(str(zpl2.COLOR_BLACK), "Black"), (str(zpl2.COLOR_WHITE), "White")],
default=str(zpl2.COLOR_BLACK),
help="Color of the line to draw.",
)
orientation = fields.Selection(
selection=[
(str(zpl2.ORIENTATION_NORMAL), "Normal"),
(str(zpl2.ORIENTATION_ROTATED), "Rotated"),
(str(zpl2.ORIENTATION_INVERTED), "Inverted"),
(str(zpl2.ORIENTATION_BOTTOM_UP), "Read from Bottom up"),
],
required=True,
default=str(zpl2.ORIENTATION_NORMAL),
help="Orientation of the barcode.",
)
diagonal_orientation = fields.Selection(
selection=[
(str(zpl2.DIAGONAL_ORIENTATION_LEFT), "Left (\\)"),
(str(zpl2.DIAGONAL_ORIENTATION_RIGHT), "Right (/)"),
],
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."
)
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.")
model = fields.Selection(
selection=[
(str(zpl2.MODEL_ORIGINAL), "Original"),
(str(zpl2.MODEL_ENHANCED), "Enhanced"),
],
default=str(zpl2.MODEL_ENHANCED),
help="Barcode model, used by some barcode types like QR Code.",
)
magnification_factor = fields.Integer(
default=1, help="Magnification Factor, from 1 to 10."
)
only_product_barcode = fields.Boolean("Only product barcode data")
error_correction = fields.Selection(
selection=[
(str(zpl2.ERROR_CORRECTION_ULTRA_HIGH), "Ultra-high Reliability Level"),
(str(zpl2.ERROR_CORRECTION_HIGH), "High Reliability Level"),
(str(zpl2.ERROR_CORRECTION_STANDARD), "Standard Level"),
(str(zpl2.ERROR_CORRECTION_HIGH_DENSITY), "High Density Level"),
],
required=True,
default=str(zpl2.ERROR_CORRECTION_HIGH),
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=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.",
)
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=[
(str(zpl2.JUSTIFY_LEFT), "Left"),
(str(zpl2.JUSTIFY_CENTER), "Center"),
(str(zpl2.JUSTIFY_JUSTIFIED), "Justified"),
(str(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.",
)
graphic_image = fields.Binary(
string="Image",
attachment=True,
help="This field holds a static image to print. "
"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
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

View file

@ -0,0 +1,515 @@
# Copyright (C) 2016 SYLEAM (<http://www.syleam.fr>)
# 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
from PIL import ImageOps
# 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(str(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"""
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)

View file

@ -0,0 +1,17 @@
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 ::
<act_window id="action_wizard_purchase"
name="Print Label"
src_model="product.product"
res_model="wizard.print.record.label"
view_mode="form"
target="new"
key2="client_action_multi"/>

View file

@ -0,0 +1,7 @@
* Sylvain Garancher <sylvain.garancher@syleam.fr>
* Florent de Labarre
* 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>
* Tran Quoc Duong <duontq@trobz.com>

View file

@ -0,0 +1,5 @@
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.

View file

@ -0,0 +1,8 @@
13.0.1.0.0 (2019-09-30)
~~~~~~~~~~~~~~~~~~~~~~~
* [RELEASE] Port from V12.
* Selection lists do not support integers any longer
* Binary field now returns False when empty instead of none,
change tests to reflect this
* work around an appels vs oranges warning

View file

@ -0,0 +1 @@
Nothing special, just install the module.

View file

@ -0,0 +1,13 @@
To print a label, you need to call use the label printing method from anywhere (other modules, server actions, etc.).
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/12.0

View file

@ -0,0 +1,8 @@
"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
access_wizard_print_record_label_user,Print Record Label user,model_wizard_print_record_label,base_report_to_printer.printing_group_user,1,1,1,1
access_wizard_print_record_label_line_user,Print Record Label Line user,model_wizard_print_record_label_line,base_report_to_printer.printing_group_user,1,1,1,1
access_wizard_import_zpl2_user,Import ZPL2 user,model_wizard_import_zpl2,base_report_to_printer.printing_group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 printing_label_zpl2_user Printing Label ZPL2 User model_printing_label_zpl2 base_report_to_printer.printing_group_user 1 0 0 0
3 printing_label_zpl2_manager Printing Label ZPL2 Manager model_printing_label_zpl2 base_report_to_printer.printing_group_manager 1 1 1 1
4 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
5 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
6 access_wizard_print_record_label_user Print Record Label user model_wizard_print_record_label base_report_to_printer.printing_group_user 1 1 1 1
7 access_wizard_print_record_label_line_user Print Record Label Line user model_wizard_print_record_label_line base_report_to_printer.printing_group_user 1 1 1 1
8 access_wizard_import_zpl2_user Import ZPL2 user model_wizard_import_zpl2 base_report_to_printer.printing_group_user 1 1 1 1

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View file

@ -0,0 +1,493 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
<title>Printer ZPL II</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
Despite the name, some widely supported CSS2 features are used.
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: gray; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic, pre.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document" id="printer-zpl-ii">
<h1 class="title">Printer ZPL II</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:959cd2e93d084b3fb9c30ec5d95b216eed7d1d372aa195ec5126d9c06031815e
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/report-print-send/tree/16.0/printer_zpl2"><img alt="OCA/report-print-send" src="https://img.shields.io/badge/github-OCA%2Freport--print--send-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/report-print-send-16-0/report-print-send-16-0-printer_zpl2"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/report-print-send&amp;target_branch=16.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>This module extends the <strong>Report to printer</strong> (<tt class="docutils literal">base_report_to_printer</tt>)
module to add a ZPL II label printing feature.</p>
<p>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.</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#installation" id="toc-entry-1">Installation</a></li>
<li><a class="reference internal" href="#configuration" id="toc-entry-2">Configuration</a></li>
<li><a class="reference internal" href="#usage" id="toc-entry-3">Usage</a></li>
<li><a class="reference internal" href="#changelog" id="toc-entry-4">Changelog</a><ul>
<li><a class="reference internal" href="#section-1" id="toc-entry-5">13.0.1.0.0 (2019-09-30)</a></li>
</ul>
</li>
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-6">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-7">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-8">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="toc-entry-9">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-10">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="installation">
<h1><a class="toc-backref" href="#toc-entry-1">Installation</a></h1>
<p>Nothing special, just install the module.</p>
</div>
<div class="section" id="configuration">
<h1><a class="toc-backref" href="#toc-entry-2">Configuration</a></h1>
<p>To configure this module, you need to:</p>
<ol class="arabic simple">
<li>Go to <em>Settings &gt; Printing &gt; Labels &gt; ZPL II</em></li>
<li>Create new labels</li>
<li>Import ZPL2 code</li>
<li>Use the Test Mode tab during the creation</li>
</ol>
<p>Its also possible to add a label printing wizard on any model by creating a new <em>ir.actions.act_window</em> record.
For example, to add the printing wizard on the <em>product.product</em> model</p>
<pre class="literal-block">
&lt;act_window id=&quot;action_wizard_purchase&quot;
name=&quot;Print Label&quot;
src_model=&quot;product.product&quot;
res_model=&quot;wizard.print.record.label&quot;
view_mode=&quot;form&quot;
target=&quot;new&quot;
key2=&quot;client_action_multi&quot;/&gt;
</pre>
</div>
<div class="section" id="usage">
<h1><a class="toc-backref" href="#toc-entry-3">Usage</a></h1>
<p>To print a label, you need to call use the label printing method from anywhere (other modules, server actions, etc.).</p>
<p>Example : Print the label of a product</p>
<pre class="literal-block">
self.env['printing.label.zpl2'].browse(label_id).print_label(
self.env['printing.printer'].browse(printer_id),
self.env['product.product'].browse(product_id))
</pre>
<p>You can also use the generic label printing wizard, if added on some models.</p>
<a class="reference external image-reference" href="https://runbot.odoo-community.org/runbot/144/12.0">
<img alt="Try me on Runbot" src="https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas" />
</a>
</div>
<div class="section" id="changelog">
<h1><a class="toc-backref" href="#toc-entry-4">Changelog</a></h1>
<div class="section" id="section-1">
<h2><a class="toc-backref" href="#toc-entry-5">13.0.1.0.0 (2019-09-30)</a></h2>
<ul class="simple">
<li>[RELEASE] Port from V12.</li>
<li>Selection lists do not support integers any longer</li>
<li>Binary field now returns False when empty instead of none,
change tests to reflect this</li>
<li>work around an appels vs oranges warning</li>
</ul>
</div>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#toc-entry-6">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/report-print-send/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/OCA/report-print-send/issues/new?body=module:%20printer_zpl2%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h1><a class="toc-backref" href="#toc-entry-7">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#toc-entry-8">Authors</a></h2>
<ul class="simple">
<li>SUBTENO-IT</li>
<li>FLorent de Labarre</li>
<li>Apertoso NV</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#toc-entry-9">Contributors</a></h2>
<ul class="simple">
<li>Sylvain Garancher &lt;<a class="reference external" href="mailto:sylvain.garancher&#64;syleam.fr">sylvain.garancher&#64;syleam.fr</a>&gt;</li>
<li>Florent de Labarre</li>
<li>Jos De Graeve &lt;<a class="reference external" href="mailto:Jos.DeGraeve&#64;apertoso.be">Jos.DeGraeve&#64;apertoso.be</a>&gt;</li>
<li>Rod Schouteden &lt;<a class="reference external" href="mailto:rod.schouteden&#64;dynapps.be">rod.schouteden&#64;dynapps.be</a>&gt;</li>
<li>Miquel Raïch &lt;<a class="reference external" href="mailto:miquel.raich&#64;forgeflow.com">miquel.raich&#64;forgeflow.com</a>&gt;</li>
<li>Lois Rilo &lt;<a class="reference external" href="mailto:lois.rilo&#64;forgeflow.com">lois.rilo&#64;forgeflow.com</a>&gt;</li>
<li>Tran Quoc Duong &lt;<a class="reference external" href="mailto:duontq&#64;trobz.com">duontq&#64;trobz.com</a>&gt;</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#toc-entry-10">Maintainers</a></h2>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org">
<img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" />
</a>
<p>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.</p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/report-print-send/tree/16.0/printer_zpl2">OCA/report-print-send</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,8 @@
# Copyright (C) 2016 SYLEAM (<http://www.syleam.fr>)
# 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
from . import test_generate_action
from . import test_test_mode
from . import test_wizard_import_zpl2

View file

@ -0,0 +1,44 @@
# Copyright (C) 2018 Florent de Labarre (<https://github.com/fmdl>)
# 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_create_action(self):
"""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"""
self.label.unlink_action()
self.assertFalse(self.label.action_window_id)

View file

@ -0,0 +1,94 @@
# Copyright (C) 2018 Florent de Labarre (<https://github.com/fmdl>)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from unittest.mock import patch
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)
@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
self.assertIs(self.label.labelary_image, False)
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.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"}
)
component.unlink()
self.assertIs(self.label.labelary_image, False)
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.assertTrue(self.label.labelary_image)

View file

@ -0,0 +1,95 @@
# Copyright (C) 2018 Florent de Labarre (<https://github.com/fmdl>)
# 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))

View file

@ -0,0 +1,104 @@
# Copyright 2016 LasLabs Inc.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from unittest.mock import patch
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,
}
)
@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],
printer_zpl2_id=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],
printer_zpl2_id=self.printer.id,
)
wizard = wizard_obj.create({})
self.assertEqual(wizard.label_id, self.label)

View file

@ -0,0 +1,362 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
Copyright 2016 SUBTENO-IT
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->
<odoo>
<record id="view_printing_label_zpl2_tree" model="ir.ui.view">
<field name="model">printing.label.zpl2</field>
<field name="arch" type="xml">
<tree>
<field name="name" />
<field name="model_id" />
</tree>
</field>
</record>
<record id="view_printing_label_zpl2_form" model="ir.ui.view">
<field name="model">printing.label.zpl2</field>
<field name="arch" type="xml">
<form string="ZPL II Label">
<header>
<button name="import_zpl2" string="Import ZPL2" type="object" />
</header>
<sheet>
<widget
name="web_ribbon"
title="Archived"
bg_color="bg-danger"
attrs="{'invisible': [('active', '=', True)]}"
/>
<div class="oe_button_box" name="button_box">
<field name="active" invisible="1" />
<field name="action_window_id" invisible="1" />
<button
name="create_action"
string="Add in the 'Action' menu"
type="object"
attrs="{'invisible':[('action_window_id','!=',False)]}"
icon="fa-plus-square"
help="Display an option on related documents."
class="oe_stat_button"
/>
<button
name="unlink_action"
string="Remove from the 'Action' menu"
type="object"
attrs="{'invisible':[('action_window_id','=',False)]}"
icon="fa-minus-square"
help="Remove the contextual action."
class="oe_stat_button"
/>
</div>
<group col="4">
<field name="name" />
<field name="model_id" />
<field name="description" />
<field name="width" />
<field name="origin_x" />
<field name="origin_y" />
<field name="restore_saved_config" />
</group>
<group attrs="{'invisible':[('test_print_mode', '=', False)]}">
<button
name="print_test_label"
string="Print Test"
type="object"
class="oe_highlight"
/>
</group>
<notebook>
<page string="Components">
<field
name="component_ids"
nolabel="1"
colspan="4"
context="{'default_model_id': model_id}"
>
<tree>
<field name="sequence" />
<field name="name" />
<field name="component_type" />
<field name="origin_x" />
<button
name="action_minus_origin_x"
type="object"
string="-"
icon="fa-minus-square"
/>
<button
name="action_plus_origin_x"
type="object"
string="+"
icon="fa-plus-square"
/>
<field name="origin_y" />
<button
name="action_minus_origin_y"
type="object"
string="-"
icon="fa-minus-square"
/>
<button
name="action_plus_origin_y"
type="object"
string="+"
icon="fa-plus-square"
/>
</tree>
<form string="Label Component">
<group>
<group>
<field name="name" />
<field name="sequence" />
<field name="model_id" invisible="1" />
</group>
<group>
<field name="component_type" />
<field
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')]}"
>
<field name="origin_x" />
<field name="origin_y" />
</group>
<group>
<field
name="graphic_image"
attrs="{'invisible': [('component_type', '!=', 'graphic')]}"
/>
<field
name="sublabel_id"
attrs="{'invisible': [('component_type', '!=', 'sublabel')]}"
/>
</group>
</group>
<group
attrs="{'invisible': ['|', ('data_autofill', '=', True), ('component_type', 'in', ('rectangle', 'diagonal', 'circle'))]}"
string="Data"
>
<field
name="data"
widget="ace"
options="{'mode': 'python'}"
nolabel="1"
colspan="2"
/>
</group>
<notebook colspan="4">
<page
string="Format"
attrs="{'invisible': [('component_type', 'in', ('sublabel', 'qr_code', 'zpl2_raw'))]}"
>
<group>
<field name="height" />
<field name="only_product_barcode" />
<field
name="width"
attrs="{'invisible': [('component_type', 'not in', ('text', 'rectangle', 'diagonal', 'circle', 'graphic'))]}"
/>
<field name="reverse_print" />
<field
name="orientation"
attrs="{'invisible': [('component_type', 'in', ('rectangle', 'diagonal', 'circle'))]}"
/>
<field
name="font"
attrs="{'invisible': [('component_type', '!=', 'text')]}"
/>
<field
name="in_block"
attrs="{'invisible': [('component_type', '!=', 'text')]}"
/>
<field
name="thickness"
attrs="{'invisible': [('component_type', 'not in', ('rectangle', 'diagonal', 'circle'))]}"
/>
<field
name="color"
attrs="{'invisible': [('component_type', 'not in', ('rectangle', 'diagonal', 'circle'))]}"
/>
<field
name="diagonal_orientation"
attrs="{'invisible': [('component_type', '!=', 'diagonal')], 'required': [('component_type', '=', 'diagonal')]}"
/>
</group>
</page>
<!-- Barcode specific arguments -->
<page
string="Barcode Format"
attrs="{'invisible': [('component_type', 'in', ('text', 'rectangle', 'diagonal', 'circle', 'sublabel', 'graphic', 'qr_code', 'zpl2_raw'))]}"
>
<group>
<field name="check_digits" />
<field name="interpretation_line" />
<field
name="interpretation_line_above"
/>
<field name="module_width" />
<field name="bar_width_ratio" />
<field name="security_level" />
<field name="columns_count" />
<field name="rows_count" />
<field name="truncate" />
</group>
</page>
<!-- 2D Barcode arguments -->
<page
string="2D Barcode Arguments"
attrs="{'invisible': [('component_type', '!=', 'qr_code')]}"
>
<group>
<field name="model" />
<field name="magnification_factor" />
<field name="error_correction" />
<field name="mask_value" />
</group>
</page>
<!-- Text block specific arguments -->
<page
string="Text Block Format"
attrs="{'invisible': ['|', ('component_type', '!=', 'text'), ('in_block', '=', False)]}"
>
<group>
<field name="block_width" />
<field name="block_lines" />
<field name="block_spaces" />
<field name="block_justify" />
<field name="block_left_margin" />
</group>
</page>
<!-- Repeat specific arguments -->
<page
string="Repeat"
attrs="{'invisible': [('repeat', '=', False)]}"
>
<group>
<field name="repeat_offset" />
<field name="repeat_count" />
<field name="repeat_offset_x" />
<field name="repeat_offset_y" />
</group>
</page>
</notebook>
</form>
</field>
<group
string="Emulation"
attrs="{'invisible':[('test_labelary_mode', '=', False)]}"
>
<field
name="labelary_image"
widget="image"
nolabel="1"
force_save="1"
/>
<p class="oe_grey" colspan="4">
Note : It is an emulation from http://labelary.com/, the result on printer can be different.
</p>
</group>
</page>
<page string="Test Mode">
<group>
<group>
<field name="test_print_mode" />
<field name="test_labelary_mode" />
</group>
</group>
<group>
<group>
<field
name="record_id"
attrs="{'invisible':[('test_print_mode', '=', False), ('test_labelary_mode', '=', False)], 'required':['|', ('test_print_mode', '=', True), ('test_labelary_mode', '=', True)]}"
/>
<field
name="printer_id"
attrs="{'invisible':[('test_print_mode', '=', False)], 'required':[('test_print_mode', '=', True)]}"
/>
<field
name="labelary_dpmm"
attrs="{'invisible':[('test_labelary_mode', '=', False)], 'required':[('test_labelary_mode', '=', True)]}"
/>
<field
name="labelary_width"
attrs="{'invisible':[('test_labelary_mode', '=', False)], 'required':[('test_labelary_mode', '=', True)]}"
/>
<field
name="labelary_height"
attrs="{'invisible':[('test_labelary_mode', '=', False)], 'required':[('test_labelary_mode', '=', True)]}"
/>
</group>
</group>
<group string="Extra">
<field
name="extra"
nolabel="1"
widget="ace"
options="{'mode': 'python'}"
attrs="{'invisible':[('test_print_mode', '=', False), ('test_labelary_mode', '=', False)]}"
colspan="2"
/>
</group>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="view_printing_label_zpl2_search" model="ir.ui.view">
<field name="model">printing.label.zpl2</field>
<field name="arch" type="xml">
<search string="ZPL II Label">
<field name="name" />
<field name="model_id" />
<filter
string="Archived"
name="inactive"
domain="[('active','=',False)]"
/>
</search>
</field>
</record>
<record model="ir.actions.act_window" id="act_open_printing_label_zpl2_view">
<field name="name">ZPL II Labels</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">printing.label.zpl2</field>
<field name="view_mode">tree,form</field>
<field name="search_view_id" ref="view_printing_label_zpl2_search" />
<field name="domain">[]</field>
<field name="context">{}</field>
</record>
<record
model="ir.actions.act_window.view"
id="act_open_printing_label_zpl2_view_form"
>
<field name="act_window_id" ref="act_open_printing_label_zpl2_view" />
<field name="sequence" eval="20" />
<field name="view_mode">form</field>
<field name="view_id" ref="view_printing_label_zpl2_form" />
</record>
<record
model="ir.actions.act_window.view"
id="act_open_printing_label_zpl2_view_tree"
>
<field name="act_window_id" ref="act_open_printing_label_zpl2_view" />
<field name="sequence" eval="10" />
<field name="view_mode">tree</field>
<field name="view_id" ref="view_printing_label_zpl2_tree" />
</record>
<menuitem
id="menu_printing_label_zpl2"
parent="base_report_to_printer.printing_menu"
sequence="20"
action="act_open_printing_label_zpl2_view"
/>
</odoo>

View file

@ -0,0 +1,5 @@
# Copyright (C) 2016 SYLEAM (<http://www.syleam.fr>)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import print_record_label
from . import wizard_import_zpl2

View file

@ -0,0 +1,68 @@
# Copyright (C) 2016 SYLEAM (<http://www.syleam.fr>)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
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.",
)
active_model_id = fields.Many2one(
comodel_name="ir.model",
string="Model",
domain=lambda self: [("model", "=", self.env.context.get("active_model"))],
)
model = fields.Char(related="active_model_id.model", string="Model Name")
line_ids = fields.One2many("wizard.print.record.label.line", "label_header_id")
@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(
[("id", "=", self.env.context.get("printer_zpl2_id"))]
)
if not printers:
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
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)
class PrintRecordLabelLines(models.TransientModel):
_name = "wizard.print.record.label.line"
_description = "Print Record Label Line"
label_no = fields.Integer(string="# labels")
label_header_id = fields.Many2one(comodel_name="wizard.print.record.label")

View file

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
Copyright 2016 SYLEAM
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->
<odoo>
<record id="view_wizard_print_product_label_form" model="ir.ui.view">
<field name="name">wizard.print.record.label.form</field>
<field name="model">wizard.print.record.label</field>
<field name="arch" type="xml">
<form string="Print Label">
<group>
<field name="printer_id" />
<field
name="label_id"
context="{'default_model_id': active_model_id}"
/>
<field name="active_model_id" invisible="1" />
</group>
<field name="model" invisible="1" />
<footer>
<button type="special" special="cancel" string="Cancel" />
<button
string="Print label"
type="object"
name="print_label"
class="oe_highlight"
/>
</footer>
</form>
</field>
</record>
<record model="ir.actions.act_window" id="action_wizard_print_record_label_view">
<field name="name">Print Label</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">wizard.print.record.label</field>
<field name="view_mode">form</field>
<field name="domain">[]</field>
<field name="context">{}</field>
<field name="target">new</field>
</record>
</odoo>

View file

@ -0,0 +1,427 @@
# Copyright (C) 2018 Florent de Labarre (<https://github.com/fmdl>)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import base64
import binascii
import io
import logging
import re
from PIL import Image, ImageOps
from odoo import _, fields, models
from ..models import zpl2
_logger = logging.getLogger(__name__)
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})
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):
self.ensure_one()
Zpl2Component = self.env["printing.label.zpl2.component"]
if self.delete_component:
self.mapped("label_id.component_ids").unlink()
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,
"model": str(zpl2.MODEL_ENHANCED),
"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
Zpl2Component = self.env["printing.label.zpl2.component"]
model_fields = Zpl2Component.fields_get()
component = {}
for field, value in vals.items():
if field in model_fields.keys():
field_type = model_fields[field].get("type", False)
if field_type == "boolean":
if not 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

View file

@ -0,0 +1,29 @@
<?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>
<field name="model">wizard.import.zpl2</field>
<field name="arch" type="xml">
<form string="Print Label">
<group>
<group>
<field name="label_id" />
<field name="delete_component" />
</group>
</group>
<group string="ZPL2">
<field name="data" widget="ace" nolabel="1" colspan="2" />
</group>
<footer>
<button string="Cancel" class="btn-default" special="cancel" />
<button
string="Import"
type="object"
name="import_zpl2"
class="btn-primary"
/>
</footer>
</form>
</field>
</record>
</odoo>

View file

@ -0,0 +1,42 @@
[project]
name = "odoo-bringout-oca-report-print-send-printer_zpl2"
version = "16.0.0"
description = "Printer ZPL II - Add a ZPL II label printing feature"
authors = [
{ name = "Ernad Husremovic", email = "hernad@bring.out.ba" }
]
dependencies = [
"odoo-bringout-oca-report-print-send-base_report_to_printer>=16.0.0",
"requests>=2.25.1"
]
readme = "README.md"
requires-python = ">= 3.11"
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Office/Business",
]
[project.urls]
homepage = "https://github.com/bringout/0"
repository = "https://github.com/bringout/0"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.metadata]
allow-direct-references = true
[tool.hatch.build.targets.wheel]
packages = ["printer_zpl2"]
[tool.rye]
managed = true
dev-dependencies = [
"pytest>=8.4.1",
]