# Copyright (C) 2018 Florent de Labarre () # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). import logging import re import base64 import binascii import io from PIL import Image, ImageOps from odoo import fields, models, _ _logger = logging.getLogger(__name__) try: import zpl2 except ImportError: _logger.debug('Cannot `import zpl2`.') 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, 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, '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