diff --git a/product_print_zpl_barcode/__init__.py b/product_print_zpl_barcode/__init__.py new file mode 100644 index 0000000..3b4c3ed --- /dev/null +++ b/product_print_zpl_barcode/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import wizard diff --git a/product_print_zpl_barcode/__manifest__.py b/product_print_zpl_barcode/__manifest__.py new file mode 100644 index 0000000..4fda81c --- /dev/null +++ b/product_print_zpl_barcode/__manifest__.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +# Copyright 2016-2018 Akretion +# @author: Alexis de Lattre +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + 'name': 'Product Generate Price Weight Barcode', + 'version': '10.0.1.0.0', + 'category': 'Extra Tools', + 'license': 'AGPL-3', + 'summary': 'Add a wizard to print product barcode stickers on ZPL printer', + 'description': """ +Print product barcode stickers on ZPL printer +============================================= + +This module adds a wizard on product.product form view which allows to generate and print a product barcode sticker on a ZPL printer (such as Zebra GC420, GK420, ...). It can print: + +* regular product barcode stickers. These stickers will show: + * product name + * product price + * EAN13 barcode + +* price/weight barcode stickers. These stickers will show: + * product name + * weight (the wizard asks for this info) + * price + * price per kg + * EAN13 barcode + +Roadmap: It would be cool one to day use the OCA module *printer_zpl2* or the underlying *zpl2* lib. + +This module has been written by Alexis de Lattre from Akretion +. + """, + 'author': 'Akretion', + 'website': 'http://www.akretion.com', + # We depend on point_of_sale and not only 'product' + # because the weight barcode rules are added by the point_of_sale module + 'depends': [ + 'point_of_sale', + 'barcodes', + 'base_report_to_printer', + ], + 'data': [ + 'wizard/product_print_zpl_barcode_view.xml', + 'views/product.xml', + ], + 'installable': True, +} diff --git a/product_print_zpl_barcode/views/product.xml b/product_print_zpl_barcode/views/product.xml new file mode 100644 index 0000000..0b244ce --- /dev/null +++ b/product_print_zpl_barcode/views/product.xml @@ -0,0 +1,23 @@ + + + + + + + + generate.weight.price.barcode.product.product.form + product.product + + +
+
+
+
+ + +
diff --git a/product_print_zpl_barcode/wizard/__init__.py b/product_print_zpl_barcode/wizard/__init__.py new file mode 100644 index 0000000..7106895 --- /dev/null +++ b/product_print_zpl_barcode/wizard/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import product_print_zpl_barcode diff --git a/product_print_zpl_barcode/wizard/product_print_zpl_barcode.py b/product_print_zpl_barcode/wizard/product_print_zpl_barcode.py new file mode 100644 index 0000000..aff563c --- /dev/null +++ b/product_print_zpl_barcode/wizard/product_print_zpl_barcode.py @@ -0,0 +1,291 @@ +# -*- coding: utf-8 -*- +# © 2016 Akretion (Alexis de Lattre ) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models, fields, api, _ +from odoo.exceptions import UserError +from odoo.tools import float_compare, float_is_zero +import odoo.addons.decimal_precision as dp +import base64 +import re + + +class ProductPrintZplBarcode(models.TransientModel): + _name = 'product.print.zpl.barcode' + _description = 'Generate and print product barcodes in ZPL' + + @api.model + def default_get(self, fields_list): + res = super(ProductPrintZplBarcode, self).default_get(fields_list) + assert self._context.get('active_model') == 'product.product',\ + 'wrong active_model, should be product.product' + product_id = self._context.get('active_id') + product = self.env['product.product'].browse(product_id) + if not product: + raise UserError(_('Missing Product')) + if not product.barcode: + raise UserError(_( + "Product '%s' doesn't have a barcode") % product.display_name) + nomenclature = self.env.ref('barcodes.default_barcode_nomenclature') + company = self.env.user.company_id + posconfig = self.env['pos.config'].search( + [('company_id', '=', company.id)], limit=1) + if posconfig: + pricelist = posconfig.pricelist_id + else: + pricelist = self.env['product.pricelist'].search([ + '|', ('company_id', '=', False), + ('company_id', '=', company.id), + ], limit=1) + if not pricelist: + raise UserError(_( + "There are no pricelist in company %s ?") % company.name) + + printer = self.env['printing.printer'].get_default() + res.update({ + 'nomenclature_id': nomenclature.id, + 'pricelist_id': pricelist.id, + 'currency_id': pricelist.currency_id.id, + 'barcode': product.barcode, + 'product_name': product.name, + 'product_id': product_id, + 'zpl_printer_id': printer and printer.id or False, + }) + return res + + product_id = fields.Many2one( + 'product.product', string='Product', required=True, readonly=True) + uom_id = fields.Many2one( + related='product_id.uom_id', readonly=True) + # 1 line = un peu moins de 30 + product_name = fields.Char('Product Label', required=True, size=56) + nomenclature_id = fields.Many2one( + 'barcode.nomenclature', 'Barcode Nomenclature', required=True) + rule_id = fields.Many2one( + 'barcode.rule', string='Barcode Rule', readonly=True, + compute='_compute_rule_id') + barcode_type = fields.Selection( + related='rule_id.type', readonly=True, string="Barcode Type") + label_size = fields.Selection([ + ('38x25', '38x25 mm'), + ], required=True, default='38x25', string='Label Size') + pricelist_id = fields.Many2one( + 'product.pricelist', string='Pricelist', required=True) + currency_id = fields.Many2one( + related='pricelist_id.currency_id', readonly=True) + # TODO: for the moment, we only support weight, but... + quantity = fields.Float(digits=dp.get_precision('Stock Weight')) + price_uom = fields.Monetary( + readonly=True, string="Price per Unit of Measure", + compute='_compute_price') # given by pricelist + price = fields.Monetary(compute='_compute_price', readonly=True) + currency_id = fields.Many2one('res.currency', string='Currency') + state = fields.Selection([ + ('step1', 'Step1'), + ('step2', 'Step2'), + ], default='step1', readonly=True) + zpl_file = fields.Binary(string='ZPL File', readonly=True) + zpl_filename = fields.Char('ZPL Filename') + barcode = fields.Char(readonly=True) + copies = fields.Integer( + string='Number of Labels', default=1, required=True) + zpl_printer_id = fields.Many2one( + 'printing.printer', string='ZPL Printer') + + @api.depends('pricelist_id', 'quantity', 'product_id') + def _compute_price(self): + # for regular barcodes + for wiz in self: + if wiz.pricelist_id and wiz.product_id: + price_uom = wiz.pricelist_id.get_product_price( + wiz.product_id, 1, False) + wiz.price_uom = price_uom + wiz.price = price_uom * wiz.quantity + + @api.one + @api.depends('nomenclature_id') + def _compute_rule_id(self): + match_rule = False + if self.nomenclature_id and self.barcode: + for rule in self.nomenclature_id.rule_ids: + match = self.nomenclature_id.match_pattern( + self.barcode, rule.pattern) + if match.get('match'): + match_rule = rule.id + break + self.rule_id = match_rule + + def _prepare_price_weight_barcode_type(self): + dpo = self.env['decimal.precision'] + bno = self.env['barcode.nomenclature'] + prec = dpo.precision_get('Stock Weight') + value = self.quantity + pbarcode = self.barcode + if float_is_zero(value, precision_digits=prec): + raise UserError(_( + "The quantity (%s) must be positive !") % value) + # check prefix + pattern = self.rule_id.pattern + if '{' not in pattern: + raise UserError(_( + "The barcode rule '%s' has a pattern '%s' which doesn't " + "contain a integer and decimal part between '{}'.") + % (self.rule_id.name, pattern)) + prefix = pattern.split('{')[0] + assert len(prefix) >= 1 + if len(prefix) > len(pbarcode): + raise UserError(_( + "The barcode of the product (%s) has %d characters, " + "which is smaller than the %d characters of the prefix " + "of the barcode pattern (%s).") + % (pbarcode, len(pbarcode), len(prefix), prefix)) + barcode = pbarcode[0:len(prefix)] + # print "barcode=", barcode + # print "pattern=", pattern + m = re.search('\{N+D+\}', pattern) + # print "m=", m + assert m + pattern_val = m.group(0) + pattern_val = pattern_val[1:-1] + # print "pattern_val=", pattern_val + max_value = 10**pattern_val.count('N') + if float_compare(value, max_value, precision_digits=prec) != -1: + raise UserError(_( + "The value to encode in the barcode (%s) is superior " + "to the maximum value allowed by the barcode pattern (%s).") + % (value, max_value)) + value_u = unicode(value) + value_u_split = value_u.split('.') + assert len(value_u_split) == 2 + value_n = value_u_split[0] + value_d = value_u_split[1] + assert len(value_n) <= pattern_val.count('N') + barcode += value_n.zfill(pattern_val.count('N')) + # end fill doesn't exists... so: + # 1) make sure we have enough 0 after + value_d_ext = value_d + '0' * pattern_val.count('D') + # 2) cut at the right size + barcode += value_d_ext[0:pattern_val.count('D')] + # print "barcode=", barcode + # Add checksum + if self.rule_id.encoding == 'ean13': + barcode = bno.sanitize_ean(barcode) + # print "barcode FINAL=", barcode + zpl_unicode = self._price_weight_barcode_type_zpl() % { + 'product_name': self.product_name, + 'ean13_no_checksum': barcode[:12], + 'price_uom': self.price_uom, + 'price': self.price, + 'currency_symbol': self.currency_id.symbol, + 'copies': self.copies, + 'quantity': value, + 'uom_name': self.uom_id.name, + } + zpl_encoded = zpl_unicode.encode('utf-8') + vals = { + 'zpl_file': zpl_encoded.encode('base64'), + 'barcode': barcode, + } + return vals + + @api.model + def _price_weight_barcode_type_zpl(self): + label = u""" +^XA +^CI28 +^PW304 +^LL200 +^LH0,20 +^CF0,30 +^FO15,0^FB270,1,0,C^FD%(price).2f %(currency_symbol)s^FS +^CF0,20 +^FO15,30^FB270,3,0,C^FD%(product_name)s^FS +^CF0,25 +^FO15,75^FB270,1,0,C^FD%(quantity).3f %(uom_name)s %(price_uom).2f %(currency_symbol)s/%(uom_name)s^FS +^FO60,110^BEN,50^FD%(ean13_no_checksum)s^FS +^PQ%(copies)s +^XZ +""" + return label + + @api.model + def _product_barcode_type_zpl(self): + label = u""" +^XA +^CI28 +^PW304 +^LL200 +^LH0,20 +^CF0,30 +^FO15,0^FB270,1,0,C^FD%(price_uom).2f %(currency_symbol)s^FS +^CF0,20 +^FO15,30^FB270,3,0,C^FD%(product_name)s^FS +^FO60,100^BEN,60^FD%(ean13_no_checksum)s^FS +^PQ%(copies)s +^XZ +""" + return label + + def _prepare_product_barcode_type(self): + zpl_unicode = self._product_barcode_type_zpl() % { + 'product_name': self.product_name, + 'ean13_no_checksum': self.barcode[:12], + 'price_uom': self.price_uom, + 'currency_symbol': self.currency_id.symbol, # symbol is a required field + 'copies': self.copies, + } + zpl_encoded = zpl_unicode.encode('utf-8') + vals = { + 'zpl_file': zpl_encoded.encode('base64'), + 'barcode': self.barcode, # unchanged + } + return vals + + def generate(self): + assert self.barcode + if len(self.barcode) != 13: + raise UserError(_( + "This wizard only supports EAN13 for the moment. Barcode '%s' " + "has %d digits instead of 13") % ( + self.barcode, + len(self.barcode))) + if not self.copies: + raise UserError(_("The number of copies cannot be 0")) + if self.barcode_type in ('price', 'weight'): + vals = self._prepare_price_weight_barcode_type() + elif self.barcode_type == 'product': + vals = self._prepare_product_barcode_type() + else: + raise UserError(_( + "Barcode Type %s is not supported for the moment") + % self.barcode_type) + vals.update({ + 'state': 'step2', + 'zpl_filename': 'barcode_%s.zpl' % vals['barcode'], + }) + self.write(vals) + action = self.env['ir.actions.act_window'].for_xml_id( + 'product_print_zpl_barcode', + 'product_print_zpl_barcode_action') + action.update({ + 'res_id': self.id, + 'context': self._context, + 'views': False}) + return action + + def print_zpl(self): + if not self.zpl_printer_id: + raise UserError(_( + "You must select a ZPL Printer.")) + self.zpl_printer_id.print_document( + self.zpl_filename, base64.decodestring(self.zpl_file), 'raw') + action = True + if self._context.get('print_and_new'): + action = self.env['ir.actions.act_window'].for_xml_id( + 'product_print_zpl_barcode', + 'product_print_zpl_barcode_action') + action.update({ + 'views': False, + 'context': self._context, + }) + return action diff --git a/product_print_zpl_barcode/wizard/product_print_zpl_barcode_view.xml b/product_print_zpl_barcode/wizard/product_print_zpl_barcode_view.xml new file mode 100644 index 0000000..e5db3a6 --- /dev/null +++ b/product_print_zpl_barcode/wizard/product_print_zpl_barcode_view.xml @@ -0,0 +1,59 @@ + + + + + + + product_print_zpl_barcode.form + product.print.zpl.barcode + +
+ + + + + + + + + + + + + + + +
+ + +
+
+ + + + + + +
+
+
+
+
+ + + Generate Barcode + product.print.zpl.barcode + form + new + + +