Merge PR #299 into 14.0

Signed-off-by pedrobaeza
This commit is contained in:
OCA-git-bot
2022-10-02 15:12:09 +00:00
7 changed files with 522 additions and 24 deletions

View File

@@ -10,7 +10,6 @@
"Apertoso NV, Odoo Community Association (OCA)", "Apertoso NV, Odoo Community Association (OCA)",
"website": "https://github.com/OCA/report-print-send", "website": "https://github.com/OCA/report-print-send",
"license": "AGPL-3", "license": "AGPL-3",
"external_dependencies": {"python": ["zpl2"]},
"depends": ["base_report_to_printer"], "depends": ["base_report_to_printer"],
"data": [ "data": [
"security/ir.model.access.csv", "security/ir.model.access.csv",

View File

@@ -14,12 +14,9 @@ from odoo import _, api, exceptions, fields, models
from odoo.exceptions import ValidationError from odoo.exceptions import ValidationError
from odoo.tools.safe_eval import safe_eval, wrap_module from odoo.tools.safe_eval import safe_eval, wrap_module
_logger = logging.getLogger(__name__) from . import zpl2
try: _logger = logging.getLogger(__name__)
import zpl2
except ImportError:
_logger.debug("Cannot `import zpl2`.")
class PrintingLabelZpl2(models.Model): class PrintingLabelZpl2(models.Model):

View File

@@ -5,12 +5,10 @@ import logging
from odoo import api, fields, models from odoo import api, fields, models
from . import zpl2
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
try:
import zpl2
except ImportError:
_logger.debug("Cannot `import zpl2`.")
DEFAULT_PYTHON_CODE = """# Python One-Liners DEFAULT_PYTHON_CODE = """# Python One-Liners
# - object: %s record on which the action is triggered; may be void # - object: %s record on which the action is triggered; may be void

515
printer_zpl2/models/zpl2.py Normal file
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

@@ -1,19 +1,12 @@
# Copyright 2016 LasLabs Inc. # Copyright 2016 LasLabs Inc.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import logging
import mock import mock
from odoo import exceptions from odoo import exceptions
from odoo.tests.common import TransactionCase from odoo.tests.common import TransactionCase
_logger = logging.getLogger(__name__) from ..models import zpl2
try:
import zpl2
except ImportError:
_logger.debug("Cannot `import zpl2`.")
model = "odoo.addons.base_report_to_printer.models.printing_server" model = "odoo.addons.base_report_to_printer.models.printing_server"

View File

@@ -11,12 +11,9 @@ from PIL import Image, ImageOps
from odoo import _, fields, models from odoo import _, fields, models
_logger = logging.getLogger(__name__) from ..models import zpl2
try: _logger = logging.getLogger(__name__)
import zpl2
except ImportError:
_logger.debug("Cannot `import zpl2`.")
def _compute_arg(data, arg): def _compute_arg(data, arg):

View File

@@ -1,3 +1,2 @@
# generated from manifests external_dependencies # generated from manifests external_dependencies
pycups pycups
zpl2