mirror of
https://github.com/bringout/oca-ocb-technical.git
synced 2026-04-22 01:32:10 +02:00
Initial commit: Technical packages
This commit is contained in:
commit
3473fa71a0
873 changed files with 297766 additions and 0 deletions
|
|
@ -0,0 +1,3 @@
|
|||
from . import barcode_nomenclature
|
||||
from . import barcode_rule
|
||||
from . import ir_http
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,178 @@
|
|||
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:
|
||||
try:
|
||||
date = datetime.datetime.strptime(str(year) + gs1_date[2:], '%Y%m%d')
|
||||
except ValueError as e:
|
||||
raise ValidationError(_(
|
||||
"A GS1 barcode nomenclature pattern was matched. However, the barcode failed to be converted to a valid date: '%(error_message)'",
|
||||
error_message=e
|
||||
))
|
||||
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
|
||||
# zxing-library patch, removing GS1 identifiers
|
||||
for identifier in [']C1', ']e0', ']d2', ']Q3', ']J1', FNC1_CHAR]:
|
||||
if barcode.startswith(identifier):
|
||||
barcode = barcode.replace(identifier, '', 1)
|
||||
break
|
||||
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:
|
||||
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
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
import re
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class BarcodeRule(models.Model):
|
||||
_inherit = 'barcode.rule'
|
||||
|
||||
def _default_encoding(self):
|
||||
return 'gs1-128' if self.env.context.get('is_gs1') else 'any'
|
||||
|
||||
encoding = fields.Selection(
|
||||
selection_add=[('gs1-128', 'GS1-128')], default=_default_encoding,
|
||||
ondelete={'gs1-128': 'set default'})
|
||||
type = fields.Selection(
|
||||
selection_add=[
|
||||
('quantity', 'Quantity'),
|
||||
('location', 'Location'),
|
||||
('location_dest', 'Destination location'),
|
||||
('lot', 'Lot number'),
|
||||
('package', 'Package'),
|
||||
('use_date', 'Best before Date'),
|
||||
('expiration_date', 'Expiration Date'),
|
||||
('package_type', 'Package Type'),
|
||||
('pack_date', 'Pack Date'),
|
||||
], ondelete={
|
||||
'quantity': 'set default',
|
||||
'location': 'set default',
|
||||
'location_dest': 'set default',
|
||||
'lot': 'set default',
|
||||
'package': 'set default',
|
||||
'use_date': 'set default',
|
||||
'expiration_date': 'set default',
|
||||
'package_type': 'set default',
|
||||
'pack_date': 'set default',
|
||||
})
|
||||
is_gs1_nomenclature = fields.Boolean(related="barcode_nomenclature_id.is_gs1_nomenclature")
|
||||
gs1_content_type = fields.Selection([
|
||||
('date', 'Date'),
|
||||
('measure', 'Measure'),
|
||||
('identifier', 'Numeric Identifier'),
|
||||
('alpha', 'Alpha-Numeric Name'),
|
||||
], string="GS1 Content Type",
|
||||
help="The GS1 content type defines what kind of data the rule will process the barcode as:\
|
||||
* Date: the barcode will be converted into a Odoo datetime;\
|
||||
* Measure: the barcode's value is related to a specific UoM;\
|
||||
* Numeric Identifier: fixed length barcode following a specific encoding;\
|
||||
* Alpha-Numeric Name: variable length barcode.")
|
||||
gs1_decimal_usage = fields.Boolean('Decimal', help="If True, use the last digit of AI to determine where the first decimal is")
|
||||
associated_uom_id = fields.Many2one('uom.uom')
|
||||
|
||||
@api.constrains('pattern')
|
||||
def _check_pattern(self):
|
||||
gs1_rules = self.filtered(lambda rule: rule.encoding == 'gs1-128')
|
||||
for rule in gs1_rules:
|
||||
try:
|
||||
re.compile(rule.pattern)
|
||||
except re.error as error:
|
||||
raise ValidationError(_("The rule pattern \"%s\" is not a valid Regex: ", rule.name) + str(error))
|
||||
groups = re.findall(r'\([^)]*\)', rule.pattern)
|
||||
if len(groups) != 2:
|
||||
raise ValidationError(_(
|
||||
"The rule pattern \"%s\" is not valid, it needs two groups:"
|
||||
"\n\t- A first one for the Application Identifier (usually 2 to 4 digits);"
|
||||
"\n\t- A second one to catch the value.",
|
||||
rule.name))
|
||||
|
||||
super(BarcodeRule, (self - gs1_rules))._check_pattern()
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# -*- 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'
|
||||
|
||||
# TODO: remove in master.
|
||||
def session_info(self):
|
||||
res = super().session_info()
|
||||
nomenclature = self.env.company.sudo().nomenclature_id
|
||||
if nomenclature.is_gs1_nomenclature:
|
||||
res['gs1_group_separator_encodings'] = nomenclature.gs1_separator_fnc1
|
||||
return res
|
||||
Loading…
Add table
Add a link
Reference in a new issue