diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/barcodes/models | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/barcodes/models')
| -rw-r--r-- | addons/barcodes/models/__init__.py | 5 | ||||
| -rw-r--r-- | addons/barcodes/models/barcode_events_mixin.py | 25 | ||||
| -rw-r--r-- | addons/barcodes/models/barcodes.py | 204 | ||||
| -rw-r--r-- | addons/barcodes/models/ir_http.py | 15 | ||||
| -rw-r--r-- | addons/barcodes/models/res_company.py | 16 |
5 files changed, 265 insertions, 0 deletions
diff --git a/addons/barcodes/models/__init__.py b/addons/barcodes/models/__init__.py new file mode 100644 index 00000000..15631161 --- /dev/null +++ b/addons/barcodes/models/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +from . import barcodes +from . import barcode_events_mixin +from . import ir_http +from . import res_company diff --git a/addons/barcodes/models/barcode_events_mixin.py b/addons/barcodes/models/barcode_events_mixin.py new file mode 100644 index 00000000..e0e37f69 --- /dev/null +++ b/addons/barcodes/models/barcode_events_mixin.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- + +from odoo import models, fields, api + +class BarcodeEventsMixin(models.AbstractModel): + """ Mixin class for objects reacting when a barcode is scanned in their form views + which contains `<field name="_barcode_scanned" widget="barcode_handler"/>`. + Models using this mixin must implement the method on_barcode_scanned. It works + like an onchange and receives the scanned barcode in parameter. + """ + + _name = 'barcodes.barcode_events_mixin' + _description = 'Barcode Event Mixin' + + _barcode_scanned = fields.Char("Barcode Scanned", help="Value of the last barcode scanned.", store=False) + + @api.onchange('_barcode_scanned') + def _on_barcode_scanned(self): + barcode = self._barcode_scanned + if barcode: + self._barcode_scanned = "" + return self.on_barcode_scanned(barcode) + + def on_barcode_scanned(self, barcode): + raise NotImplementedError("In order to use barcodes.barcode_events_mixin, method on_barcode_scanned must be implemented") diff --git a/addons/barcodes/models/barcodes.py b/addons/barcodes/models/barcodes.py new file mode 100644 index 00000000..1d2ba68f --- /dev/null +++ b/addons/barcodes/models/barcodes.py @@ -0,0 +1,204 @@ +import logging +import re + +from odoo import tools, models, fields, api, _ +from odoo.exceptions import ValidationError + +_logger = logging.getLogger(__name__) + + +UPC_EAN_CONVERSIONS = [ + ('none','Never'), + ('ean2upc','EAN-13 to UPC-A'), + ('upc2ean','UPC-A to EAN-13'), + ('always','Always'), +] + +class BarcodeNomenclature(models.Model): + _name = 'barcode.nomenclature' + _description = 'Barcode Nomenclature' + + name = fields.Char(string='Barcode Nomenclature', size=32, required=True, help='An internal identification of the barcode nomenclature') + rule_ids = fields.One2many('barcode.rule', 'barcode_nomenclature_id', string='Rules', help='The list of barcode rules') + upc_ean_conv = fields.Selection(UPC_EAN_CONVERSIONS, string='UPC/EAN Conversion', required=True, default='always', + help="UPC Codes can be converted to EAN by prefixing them with a zero. This setting determines if a UPC/EAN barcode should be automatically converted in one way or another when trying to match a rule with the other encoding.") + + # returns the checksum of the ean13, or -1 if the ean has not the correct length, ean must be a string + def ean_checksum(self, ean): + code = list(ean) + if len(code) != 13: + return -1 + + oddsum = evensum = total = 0 + code = code[:-1] # Remove checksum + for i in range(len(code)): + if i % 2 == 0: + evensum += int(code[i]) + else: + oddsum += int(code[i]) + total = oddsum * 3 + evensum + return int((10 - total % 10) % 10) + + # returns the checksum of the ean8, or -1 if the ean has not the correct length, ean must be a string + def ean8_checksum(self,ean): + code = list(ean) + if len(code) != 8: + return -1 + + sum1 = int(ean[1]) + int(ean[3]) + int(ean[5]) + sum2 = int(ean[0]) + int(ean[2]) + int(ean[4]) + int(ean[6]) + total = sum1 + 3 * sum2 + return int((10 - total % 10) % 10) + + # returns true if the barcode is a valid EAN barcode + def check_ean(self, ean): + return re.match("^\d+$", ean) and self.ean_checksum(ean) == int(ean[-1]) + + # returns true if the barcode string is encoded with the provided encoding. + def check_encoding(self, barcode, encoding): + if encoding == 'ean13': + return len(barcode) == 13 and re.match("^\d+$", barcode) and self.ean_checksum(barcode) == int(barcode[-1]) + elif encoding == 'ean8': + return len(barcode) == 8 and re.match("^\d+$", barcode) and self.ean8_checksum(barcode) == int(barcode[-1]) + elif encoding == 'upca': + return len(barcode) == 12 and re.match("^\d+$", barcode) and self.ean_checksum("0"+barcode) == int(barcode[-1]) + elif encoding == 'any': + return True + else: + return False + + + # Returns a valid zero padded ean13 from an ean prefix. the ean prefix must be a string. + def sanitize_ean(self, ean): + ean = ean[0:13] + ean = ean + (13-len(ean))*'0' + return ean[0:12] + str(self.ean_checksum(ean)) + + # Returns a valid zero padded UPC-A from a UPC-A prefix. the UPC-A prefix must be a string. + def sanitize_upc(self, upc): + return self.sanitize_ean('0'+upc)[1:] + + # Checks if barcode matches the pattern + # Additionaly retrieves the optional numerical content in barcode + # Returns an object containing: + # - value: the numerical value encoded in the barcode (0 if no value encoded) + # - base_code: the barcode in which numerical content is replaced by 0's + # - match: boolean + def match_pattern(self, barcode, pattern): + match = { + "value": 0, + "base_code": barcode, + "match": False, + } + + barcode = barcode.replace("\\", "\\\\").replace("{", '\{').replace("}", "\}").replace(".", "\.") + numerical_content = re.search("[{][N]*[D]*[}]", pattern) # look for numerical content in pattern + + if numerical_content: # the pattern encodes a numerical content + num_start = numerical_content.start() # start index of numerical content + num_end = numerical_content.end() # end index of numerical content + value_string = barcode[num_start:num_end-2] # numerical content in barcode + + whole_part_match = re.search("[{][N]*[D}]", numerical_content.group()) # looks for whole part of numerical content + decimal_part_match = re.search("[{N][D]*[}]", numerical_content.group()) # looks for decimal part + whole_part = value_string[:whole_part_match.end()-2] # retrieve whole part of numerical content in barcode + decimal_part = "0." + value_string[decimal_part_match.start():decimal_part_match.end()-1] # retrieve decimal part + if whole_part == '': + whole_part = '0' + match['value'] = int(whole_part) + float(decimal_part) + + match['base_code'] = barcode[:num_start] + (num_end-num_start-2)*"0" + barcode[num_end-2:] # replace numerical content by 0's in barcode + match['base_code'] = match['base_code'].replace("\\\\", "\\").replace("\{", "{").replace("\}","}").replace("\.",".") + pattern = pattern[:num_start] + (num_end-num_start-2)*"0" + pattern[num_end:] # replace numerical content by 0's in pattern to match + + match['match'] = re.match(pattern, match['base_code'][:len(pattern)]) + + return match + + # Attempts to interpret an barcode (string encoding a barcode) + # It will return an object containing various information about the barcode. + # most importantly : + # - code : the barcode + # - type : the type of the barcode: + # - value : if the id encodes a numerical value, it will be put there + # - base_code : the barcode code with all the encoding parts set to zero; the one put on + # the product in the backend + def parse_barcode(self, barcode): + parsed_result = { + 'encoding': '', + 'type': 'error', + 'code': barcode, + 'base_code': barcode, + 'value': 0, + } + + rules = [] + for rule in self.rule_ids: + rules.append({'type': rule.type, 'encoding': rule.encoding, 'sequence': rule.sequence, 'pattern': rule.pattern, 'alias': rule.alias}) + + for rule in rules: + cur_barcode = barcode + if rule['encoding'] == 'ean13' and self.check_encoding(barcode,'upca') and self.upc_ean_conv in ['upc2ean','always']: + cur_barcode = '0'+cur_barcode + elif rule['encoding'] == 'upca' and self.check_encoding(barcode,'ean13') and barcode[0] == '0' and self.upc_ean_conv in ['ean2upc','always']: + cur_barcode = cur_barcode[1:] + + if not self.check_encoding(barcode,rule['encoding']): + continue + + match = self.match_pattern(cur_barcode, rule['pattern']) + if match['match']: + if rule['type'] == 'alias': + barcode = rule['alias'] + parsed_result['code'] = barcode + else: + parsed_result['encoding'] = rule['encoding'] + parsed_result['type'] = rule['type'] + parsed_result['value'] = match['value'] + parsed_result['code'] = cur_barcode + if rule['encoding'] == "ean13": + parsed_result['base_code'] = self.sanitize_ean(match['base_code']) + elif rule['encoding'] == "upca": + parsed_result['base_code'] = self.sanitize_upc(match['base_code']) + else: + parsed_result['base_code'] = match['base_code'] + return parsed_result + + return parsed_result + +class BarcodeRule(models.Model): + _name = 'barcode.rule' + _description = 'Barcode Rule' + _order = 'sequence asc' + + + name = fields.Char(string='Rule Name', size=32, required=True, help='An internal identification for this barcode nomenclature rule') + barcode_nomenclature_id = fields.Many2one('barcode.nomenclature', string='Barcode Nomenclature') + sequence = fields.Integer(string='Sequence', help='Used to order rules such that rules with a smaller sequence match first') + encoding = fields.Selection([ + ('any', 'Any'), + ('ean13', 'EAN-13'), + ('ean8', 'EAN-8'), + ('upca', 'UPC-A'), + ], string='Encoding', required=True, default='any', help='This rule will apply only if the barcode is encoded with the specified encoding') + type = fields.Selection([ + ('alias', 'Alias'), + ('product', 'Unit Product') + ], string='Type', required=True, default='product') + pattern = fields.Char(string='Barcode Pattern', size=32, help="The barcode matching pattern", required=True, default='.*') + alias = fields.Char(string='Alias', size=32, default='0', help='The matched pattern will alias to this barcode', required=True) + + @api.constrains('pattern') + def _check_pattern(self): + for rule in self: + p = rule.pattern.replace("\\\\", "X").replace("\{", "X").replace("\}", "X") + findall = re.findall("[{]|[}]", p) # p does not contain escaped { or } + if len(findall) == 2: + if not re.search("[{][N]*[D]*[}]", p): + raise ValidationError(_("There is a syntax error in the barcode pattern %(pattern)s: braces can only contain N's followed by D's.", pattern=rule.pattern)) + elif re.search("[{][}]", p): + raise ValidationError(_("There is a syntax error in the barcode pattern %(pattern)s: empty braces.", pattern=rule.pattern)) + elif len(findall) != 0: + raise ValidationError(_("There is a syntax error in the barcode pattern %(pattern)s: a rule can only contain one pair of braces.", pattern=rule.pattern)) + elif p == '*': + raise ValidationError(_(" '*' is not a valid Regex Barcode Pattern. Did you mean '.*' ?")) diff --git a/addons/barcodes/models/ir_http.py b/addons/barcodes/models/ir_http.py new file mode 100644 index 00000000..4f22a7be --- /dev/null +++ b/addons/barcodes/models/ir_http.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models + + +class IrHttp(models.AbstractModel): + _inherit = 'ir.http' + + def session_info(self): + res = super(IrHttp, self).session_info() + if self.env.user.has_group('base.group_user'): + res['max_time_between_keys_in_ms'] = int( + self.env['ir.config_parameter'].sudo().get_param('barcode.max_time_between_keys_in_ms', default='55')) + return res diff --git a/addons/barcodes/models/res_company.py b/addons/barcodes/models/res_company.py new file mode 100644 index 00000000..b510c6e0 --- /dev/null +++ b/addons/barcodes/models/res_company.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- + +from odoo import models, fields + + +class ResCompany(models.Model): + _inherit = 'res.company' + + def _get_default_nomenclature(self): + return self.env.ref('barcodes.default_barcode_nomenclature', raise_if_not_found=False) + + nomenclature_id = fields.Many2one( + 'barcode.nomenclature', + string="Nomenclature", + default=_get_default_nomenclature, + ) |
