171 lines
7.6 KiB
Python
171 lines
7.6 KiB
Python
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
|