19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:32:34 +01:00
parent 5faf7397c5
commit 2696f14ed7
721 changed files with 220375 additions and 91221 deletions

View file

@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
from . import barcode_events_mixin
from . import barcode_nomenclature
from . import barcode_rule
from . import ir_http
from . import res_company

View file

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api
class BarcodesBarcode_Events_Mixin(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(self.env._("In order to use barcodes.barcode_events_mixin, method on_barcode_scanned must be implemented"))

View file

@ -0,0 +1,215 @@
import re
from odoo import models, fields, api, _
from odoo.exceptions import UserError
from odoo.tools.barcode import check_barcode_encoding, get_barcode_check_digit
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', 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.")
@api.model
def sanitize_ean(self, ean):
""" Returns a valid zero padded EAN-13 from an EAN prefix.
:type ean: str
"""
ean = ean[0:13].zfill(13)
return ean[0:-1] + str(get_barcode_check_digit(ean))
@api.model
def sanitize_upc(self, upc):
""" Returns a valid zero padded UPC-A from a UPC-A prefix.
:type upc: str
"""
return self.sanitize_ean('0' + upc)[1:]
def match_pattern(self, barcode, pattern):
"""Checks barcode matches the pattern and retrieves the optional numeric value in barcode.
:param barcode:
:type barcode: str
:param pattern:
:type pattern: str
:return: 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
:rtype: dict
"""
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'
if whole_part.isdigit():
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
def parse_barcode(self, barcode):
if re.match(r'^urn:', barcode):
return self.parse_uri(barcode)
return self.parse_nomenclature_barcode(barcode)
def parse_nomenclature_barcode(self, barcode):
""" Attempts to interpret and parse a barcode.
:param barcode:
:type barcode: str
:return: A object containing various information about the barcode, like as:
- code: the barcode
- type: the barcode's type
- 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
:rtype: dict
"""
parsed_result = {
'encoding': '',
'type': 'error',
'code': barcode,
'base_code': barcode,
'value': 0,
}
for rule in self.rule_ids:
cur_barcode = barcode
if rule.encoding == 'ean13' and check_barcode_encoding(barcode, 'upca') and self.upc_ean_conv in ['upc2ean', 'always']:
cur_barcode = '0' + cur_barcode
elif rule.encoding == 'upca' and check_barcode_encoding(barcode, 'ean13') and barcode[0] == '0' and self.upc_ean_conv in ['ean2upc', 'always']:
cur_barcode = cur_barcode[1:]
if not check_barcode_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
# RFID/URI stuff.
@api.model
def parse_uri(self, barcode):
""" Convert supported URI format (lgtin, sgtin, sgtin-96, sgtin-198,
sscc and ssacc-96) into a GS1 barcode.
:param barcode str: the URI as a string.
:rtype: str
"""
if not re.match(r'^urn:', barcode):
return barcode
identifier, data = (bc_part.strip() for bc_part in re.split(':', barcode)[-2:])
data = re.split(r'\.', data)
match identifier:
case 'lgtin' | 'sgtin':
barcode = self._convert_uri_gtin_data_into_tracking_number(barcode, data)
case 'sgtin-96' | 'sgtin-198':
# Same as SGTIN but we have to remove the filter.
barcode = self._convert_uri_gtin_data_into_tracking_number(barcode, data[1:])
case 'sscc':
barcode = self._convert_uri_sscc_data_into_package(barcode, data)
case 'sscc-96':
# Same as SSCC but we have to remove the filter.
barcode = self._convert_uri_sscc_data_into_package(barcode, data[1:])
return barcode
@api.model
def _convert_uri_gtin_data_into_tracking_number(self, base_code, data):
gs1_company_prefix, item_ref_and_indicator, tracking_number = data
indicator = item_ref_and_indicator[0]
item_ref = item_ref_and_indicator[1:]
product_barcode = indicator + gs1_company_prefix + item_ref
product_barcode += str(get_barcode_check_digit(product_barcode + '0'))
return [
{
'base_code': base_code,
'code': product_barcode,
'encoding': '',
'type': 'product',
'value': product_barcode,
},
{
'base_code': base_code,
'code': tracking_number,
'encoding': '',
'type': 'lot',
'value': tracking_number,
},
]
@api.model
def _convert_uri_sscc_data_into_package(self, base_code, data):
gs1_company_prefix, serial_reference = data
extension = serial_reference[0]
serial_ref = serial_reference[1:]
sscc = extension + gs1_company_prefix + serial_ref
sscc += str(get_barcode_check_digit(sscc + '0'))
return [{
'base_code': base_code,
'code': sscc,
'encoding': '',
'type': 'package',
'value': sscc,
}]
@api.ondelete(at_uninstall=False)
def _unlink_except_default(self):
default_record = self.env.ref("barcodes.default_barcode_nomenclature", raise_if_not_found=False)
if default_record and default_record in self:
raise UserError(_(
"You cannot delete '%(name)s' because it's the default barcode nomenclature.",
name=default_record.display_name
))

View file

@ -0,0 +1,47 @@
import re
from odoo import models, fields, api, _
from odoo.exceptions import ValidationError
class BarcodeRule(models.Model):
_name = 'barcode.rule'
_description = 'Barcode Rule'
_order = 'sequence asc, id'
name = fields.Char(string='Rule Name', required=True, help='An internal identification for this barcode nomenclature rule')
barcode_nomenclature_id = fields.Many2one('barcode.nomenclature', string='Barcode Nomenclature', index='btree_not_null')
sequence = fields.Integer(string='Sequence', help='Used to order rules such that rules with a smaller sequence match first')
encoding = fields.Selection(
string='Encoding', required=True, default='any', selection=[
('any', 'Any'),
('ean13', 'EAN-13'),
('ean8', 'EAN-8'),
('upca', 'UPC-A'),
], help='This rule will apply only if the barcode is encoded with the specified encoding')
type = fields.Selection(
string='Type', required=True, selection=[
('alias', 'Alias'),
('product', 'Unit Product'),
], default='product')
pattern = fields.Char(string='Barcode Pattern', help="The barcode matching pattern", required=True, default='.*')
alias = fields.Char(string='Alias', 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 '.*'?"))
try:
re.compile(re.sub('{N+D*}', '', p))
except re.error:
raise ValidationError(_("The barcode pattern %(pattern)s does not lead to a valid regular expression.", pattern=rule.pattern))

View file

@ -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._is_internal():
res['max_time_between_keys_in_ms'] = int(
self.env['ir.config_parameter'].sudo().get_param('barcode.max_time_between_keys_in_ms', default='150'))
return res

View file

@ -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,
)