barcodes_gs1_nomenclature/models/barcode_nomenclature.py

171 lines
7.6 KiB
Python
Raw Normal View History

import re
import datetime
import calendar
from odoo import api, fields, models, _
from odoo.exceptions import ValidationError
from odoo.tools import get_barcode_check_digit
FNC1_CHAR = '\x1D'
class BarcodeNomenclature(models.Model):
_inherit = 'barcode.nomenclature'
is_gs1_nomenclature = fields.Boolean(
string="Is GS1 Nomenclature",
help="This Nomenclature use the GS1 specification, only GS1-128 encoding rules is accepted is this kind of nomenclature.")
gs1_separator_fnc1 = fields.Char(
string="FNC1 Separator", trim=False, default=r'(Alt029|#|\x1D)',
help="Alternative regex delimiter for the FNC1. The separator must not match the begin/end of any related rules pattern.")
@api.constrains('gs1_separator_fnc1')
def _check_pattern(self):
for nom in self:
if nom.is_gs1_nomenclature and nom.gs1_separator_fnc1:
try:
re.compile("(?:%s)?" % nom.gs1_separator_fnc1)
except re.error as error:
raise ValidationError(_("The FNC1 Separator Alternative is not a valid Regex: ") + str(error))
@api.model
def gs1_date_to_date(self, gs1_date):
""" Converts a GS1 date into a datetime.date.
:param gs1_date: A year formated as yymmdd
:type gs1_date: str
:return: converted date
:rtype: datetime.date
"""
# See 7.12 Determination of century in dates:
# https://www.gs1.org/sites/default/files/docs/barcodes/GS1_General_Specifications.pdf
now = datetime.date.today()
current_century = now.year // 100
substract_year = int(gs1_date[0:2]) - (now.year % 100)
century = (51 <= substract_year <= 99 and current_century - 1) or\
(-99 <= substract_year <= -50 and current_century + 1) or\
current_century
year = century * 100 + int(gs1_date[0:2])
if gs1_date[-2:] == '00': # Day is not mandatory, when not set -> last day of the month
date = datetime.datetime.strptime(str(year) + gs1_date[2:4], '%Y%m')
date = date.replace(day=calendar.monthrange(year, int(gs1_date[2:4]))[1])
else:
date = datetime.datetime.strptime(str(year) + gs1_date[2:], '%Y%m%d')
return date.date()
def parse_gs1_rule_pattern(self, match, rule):
result = {
'rule': rule,
'ai': match.group(1),
'string_value': match.group(2),
}
if rule.gs1_content_type == 'measure':
try:
decimal_position = 0 # Decimal position begins at the end, 0 means no decimal.
if rule.gs1_decimal_usage:
decimal_position = int(match.group(1)[-1])
if decimal_position > 0:
result['value'] = float(match.group(2)[:-decimal_position] + "." + match.group(2)[-decimal_position:])
else:
result['value'] = int(match.group(2))
except Exception:
raise ValidationError(_(
"There is something wrong with the barcode rule \"%s\" pattern.\n"
"If this rule uses decimal, check it can't get sometime else than a digit as last char for the Application Identifier.\n"
"Check also the possible matched values can only be digits, otherwise the value can't be casted as a measure.",
rule.name))
elif rule.gs1_content_type == 'identifier':
# Check digit and remove it of the value
if match.group(2)[-1] != str(get_barcode_check_digit("0" * (18 - len(match.group(2))) + match.group(2))):
return None
result['value'] = match.group(2)
elif rule.gs1_content_type == 'date':
if len(match.group(2)) != 6:
return None
result['value'] = self.gs1_date_to_date(match.group(2))
else: # when gs1_content_type == 'alpha':
result['value'] = match.group(2)
return result
def gs1_decompose_extanded(self, barcode):
"""Try to decompose the gs1 extanded barcode into several unit of information using gs1 rules.
Return a ordered list of dict
"""
self.ensure_one()
separator_group = FNC1_CHAR + "?"
if self.gs1_separator_fnc1:
separator_group = "(?:%s)?" % self.gs1_separator_fnc1
results = []
gs1_rules = self.rule_ids.filtered(lambda r: r.encoding == 'gs1-128')
def find_next_rule(remaining_barcode):
for rule in gs1_rules:
match = re.search("^" + rule.pattern + separator_group, remaining_barcode)
# If match and contains 2 groups at minimun, the first one need to be the AI and the second the value
# We can't use regex nammed group because in JS, it is not the same regex syntax (and not compatible in all browser)
if match and len(match.groups()) >= 2:
res = self.parse_gs1_rule_pattern(match, rule)
if res:
return res, remaining_barcode[match.end():]
return None
while len(barcode) > 0:
res_bar = find_next_rule(barcode)
# Cannot continue -> Fail to decompose gs1 and return
if not res_bar or res_bar[1] == barcode:
return None
barcode = res_bar[1]
results.append(res_bar[0])
return results
def parse_barcode(self, barcode):
if self.is_gs1_nomenclature:
return self.gs1_decompose_extanded(barcode)
return super().parse_barcode(barcode)
@api.model
def _preprocess_gs1_search_args(self, args, barcode_types, field='barcode'):
"""Helper method to preprocess 'args' in _search method to add support to
search with GS1 barcode result.
Cut off the padding if using GS1 and searching on barcode. If the barcode
is only digits to keep the original barcode part only.
"""
nomenclature = self.env.company.nomenclature_id
if nomenclature.is_gs1_nomenclature:
for i, arg in enumerate(args):
if not isinstance(arg, (list, tuple)) or len(arg) != 3:
continue
field_name, operator, value = arg
if field_name != field or operator not in ['ilike', 'not ilike', '=', '!='] or value is False:
continue
parsed_data = []
try:
parsed_data += nomenclature.parse_barcode(value) or []
except (ValidationError, ValueError):
pass
replacing_operator = 'ilike' if operator in ['ilike', '='] else 'not ilike'
for data in parsed_data:
data_type = data['rule'].type
value = data['value']
if data_type in barcode_types:
if data_type == 'lot':
args[i] = (field_name, operator, value)
break
match = re.match('0*([0-9]+)$', str(value))
if match:
unpadded_barcode = match.groups()[0]
args[i] = (field_name, replacing_operator, unpadded_barcode)
break
# The barcode isn't a valid GS1 barcode, checks if it can be unpadded.
if not parsed_data:
match = re.match('0+([0-9]+)$', value)
if match:
args[i] = (field_name, replacing_operator, match.groups()[0])
return args