[ADD] barcodes_gs1_nomenclature
In order to integrate and use GS1 nomenclature into `stock_barcode`, adds a new module with GS1 nomenclature and rules, and overrides parsing methods. task-1968113 closes odoo/odoo#65858 Signed-off-by: Arnold Moyaux <amoyaux@users.noreply.github.com> Co-authored-by: ryv-odoo<ryv@odoo.com>
This commit is contained in:
committed by
svs-odoo
parent
a09e370ec5
commit
1c0dec6d58
@@ -17,7 +17,6 @@ var BarcodeParser = Class.extend({
|
||||
// necessary to parse the barcodes. The BarcodeParser is operational
|
||||
// only when those data have been loaded
|
||||
load: function(){
|
||||
var self = this;
|
||||
if (!this.nomenclature_id) {
|
||||
return;
|
||||
}
|
||||
@@ -25,23 +24,21 @@ var BarcodeParser = Class.extend({
|
||||
return rpc.query({
|
||||
model: 'barcode.nomenclature',
|
||||
method: 'read',
|
||||
args: [[id], ['name','rule_ids','upc_ean_conv']],
|
||||
})
|
||||
.then(function (nomenclatures){
|
||||
self.nomenclature = nomenclatures[0];
|
||||
|
||||
args: [[id], this._barcodeNomenclatureFields()],
|
||||
}).then(nomenclatures => {
|
||||
this.nomenclature = nomenclatures[0];
|
||||
var args = [
|
||||
[['barcode_nomenclature_id', '=', self.nomenclature.id]],
|
||||
['name', 'sequence', 'type', 'encoding', 'pattern', 'alias'],
|
||||
[['barcode_nomenclature_id', '=', this.nomenclature.id]],
|
||||
this._barcodeRuleFields(),
|
||||
];
|
||||
return rpc.query({
|
||||
model: 'barcode.rule',
|
||||
method: 'search_read',
|
||||
args: args,
|
||||
});
|
||||
}).then(function(rules){
|
||||
}).then(rules => {
|
||||
rules = rules.sort(function(a, b){ return a.sequence - b.sequence; });
|
||||
self.nomenclature.rules = rules;
|
||||
this.nomenclature.rules = rules;
|
||||
});
|
||||
},
|
||||
|
||||
@@ -187,15 +184,16 @@ var BarcodeParser = Class.extend({
|
||||
return match;
|
||||
},
|
||||
|
||||
// attempts to interpret a barcode (string encoding a barcode Code-128)
|
||||
// it will return an object containing various information about the barcode.
|
||||
// most importantly :
|
||||
// - code : the barcode
|
||||
// - type : the type of the barcode (e.g. alias, unit product, weighted product...)
|
||||
//
|
||||
// - value : if the barcode encodes a numerical value, it will be put there
|
||||
// - base_code : the barcode with all the encoding parts set to zero; the one put on
|
||||
// the product in the backend
|
||||
/**
|
||||
* Attempts to interpret a barcode (string encoding a barcode Code-128)
|
||||
*
|
||||
* @param {string} barcode
|
||||
* @returns {Object} the returned object containing informations about the barcode:
|
||||
* - code: the barcode
|
||||
* - type: the type of the barcode (e.g. alias, unit product, weighted product...)
|
||||
* - value: if the barcode encodes a numerical value, it will be put there
|
||||
* - base_code: the barcode with all the encoding parts set to zero; the one put on the product in the backend
|
||||
*/
|
||||
parse_barcode: function(barcode){
|
||||
var parsed_result = {
|
||||
encoding: '',
|
||||
@@ -253,6 +251,29 @@ var BarcodeParser = Class.extend({
|
||||
}
|
||||
return parsed_result;
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
_barcodeNomenclatureFields: function () {
|
||||
return [
|
||||
'name',
|
||||
'rule_ids',
|
||||
'upc_ean_conv',
|
||||
];
|
||||
},
|
||||
|
||||
_barcodeRuleFields: function () {
|
||||
return [
|
||||
'name',
|
||||
'sequence',
|
||||
'type',
|
||||
'encoding',
|
||||
'pattern',
|
||||
'alias',
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
return BarcodeParser;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<field name="arch" type="xml">
|
||||
<form string="Barcode Nomenclature">
|
||||
<sheet>
|
||||
<group col="4">
|
||||
<group name="general_attributes" col="4">
|
||||
<field name="name" />
|
||||
<field name="upc_ean_conv"/>
|
||||
</group>
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
from . import models
|
||||
from . import tests
|
||||
@@ -0,0 +1,16 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
'name': 'Barcode - GS1 Nomenclature',
|
||||
'version': '1.0',
|
||||
'category': 'Hidden',
|
||||
'summary': 'Parse barcodes according to the GS1-128 specifications',
|
||||
'depends': ['barcodes', 'uom'],
|
||||
'data': [
|
||||
'data/barcodes_gs1_rules.xml',
|
||||
'views/barcodes_gs1_templates.xml',
|
||||
'views/barcodes_view.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<record id="default_gs1_nomenclature" model="barcode.nomenclature">
|
||||
<field name="name">Default GS1 Nomenclature</field>
|
||||
<field name="is_gs1_nomenclature">true</field>
|
||||
</record>
|
||||
|
||||
<!-- Identifier Barcodes -->
|
||||
<record id="barcode_rule_gs1_00" model="barcode.rule">
|
||||
<field name="name">Serial Shipping Container Code</field>
|
||||
<field name="barcode_nomenclature_id" ref="default_gs1_nomenclature"/>
|
||||
<field name="sequence">100</field>
|
||||
<field name="encoding">gs1-128</field>
|
||||
<field name="pattern">(00)(\d{18})</field>
|
||||
<field name="type">package</field>
|
||||
<field name="gs1_content_type">identifier</field>
|
||||
</record>
|
||||
|
||||
<record id="barcode_rule_gs1_01" model="barcode.rule">
|
||||
<field name="name">Global Trade Item Number (GTIN)</field>
|
||||
<field name="barcode_nomenclature_id" ref="default_gs1_nomenclature"/>
|
||||
<field name="sequence">101</field>
|
||||
<field name="encoding">gs1-128</field>
|
||||
<field name="pattern">(01)(\d{14})</field>
|
||||
<field name="type">product</field>
|
||||
<field name="gs1_content_type">identifier</field>
|
||||
</record>
|
||||
|
||||
<record id="barcode_rule_gs1_02" model="barcode.rule">
|
||||
<field name="name">GTIN of contained trade items</field>
|
||||
<field name="barcode_nomenclature_id" ref="default_gs1_nomenclature"/>
|
||||
<field name="sequence">101</field>
|
||||
<field name="encoding">gs1-128</field>
|
||||
<field name="pattern">(02)(\d{14})</field>
|
||||
<field name="type">product</field>
|
||||
<field name="gs1_content_type">identifier</field>
|
||||
</record>
|
||||
|
||||
<record id="barcode_rule_gs1_410" model="barcode.rule">
|
||||
<field name="name">Ship to / Deliver to Global Location Number (GLN)</field>
|
||||
<field name="barcode_nomenclature_id" ref="default_gs1_nomenclature"/>
|
||||
<field name="sequence">102</field>
|
||||
<field name="encoding">gs1-128</field>
|
||||
<field name="pattern">(410)(\d{13})</field>
|
||||
<field name="type">location_dest</field>
|
||||
<field name="gs1_content_type">identifier</field>
|
||||
</record>
|
||||
|
||||
<record id="barcode_rule_gs1_413" model="barcode.rule">
|
||||
<field name="name">Ship for / Deliver for - Forward to Global Location Number (GLN)</field>
|
||||
<field name="barcode_nomenclature_id" ref="default_gs1_nomenclature"/>
|
||||
<field name="sequence">103</field>
|
||||
<field name="encoding">gs1-128</field>
|
||||
<field name="pattern">(413)(\d{13})</field>
|
||||
<field name="type">location_dest</field>
|
||||
<field name="gs1_content_type">identifier</field>
|
||||
</record>
|
||||
|
||||
<record id="barcode_rule_gs1_414" model="barcode.rule">
|
||||
<field name="name">Identification of a physical location - Global Location Number (GLN)</field>
|
||||
<field name="barcode_nomenclature_id" ref="default_gs1_nomenclature"/>
|
||||
<field name="sequence">104</field>
|
||||
<field name="encoding">gs1-128</field>
|
||||
<field name="pattern">(414)(\d{13})</field>
|
||||
<field name="type">location</field>
|
||||
<field name="gs1_content_type">identifier</field>
|
||||
</record>
|
||||
|
||||
<!-- Alphanumeric Barcodes -->
|
||||
<record id="barcode_rule_gs1_10" model="barcode.rule">
|
||||
<field name="name">Batch or lot number</field>
|
||||
<field name="barcode_nomenclature_id" ref="default_gs1_nomenclature"/>
|
||||
<field name="sequence">105</field>
|
||||
<field name="encoding">gs1-128</field>
|
||||
<field name="pattern">(10)([!"%-/0-9:-?A-Z_a-z]{0,20})</field>
|
||||
<field name="type">lot</field>
|
||||
<field name="gs1_content_type">alpha</field>
|
||||
</record>
|
||||
|
||||
<record id="barcode_rule_gs1_21" model="barcode.rule">
|
||||
<field name="name">Serial number</field>
|
||||
<field name="barcode_nomenclature_id" ref="default_gs1_nomenclature"/>
|
||||
<field name="sequence">106</field>
|
||||
<field name="encoding">gs1-128</field>
|
||||
<field name="pattern">(21)([!"%-/0-9:-?A-Z_a-z]{0,20})</field>
|
||||
<field name="type">lot</field>
|
||||
<field name="gs1_content_type">alpha</field>
|
||||
</record>
|
||||
|
||||
<!-- Date Barcodes -->
|
||||
<record id="barcode_rule_gs1_13" model="barcode.rule">
|
||||
<field name="name">Packaging date (YYMMDD)</field>
|
||||
<field name="barcode_nomenclature_id" ref="default_gs1_nomenclature"/>
|
||||
<field name="sequence">107</field>
|
||||
<field name="encoding">gs1-128</field>
|
||||
<field name="pattern">(13)(\d{6})</field>
|
||||
<field name="type">packaging_date</field>
|
||||
<field name="gs1_content_type">date</field>
|
||||
</record>
|
||||
|
||||
<record id="barcode_rule_gs1_15" model="barcode.rule">
|
||||
<field name="name">Best before date (YYMMDD)</field>
|
||||
<field name="barcode_nomenclature_id" ref="default_gs1_nomenclature"/>
|
||||
<field name="sequence">108</field>
|
||||
<field name="encoding">gs1-128</field>
|
||||
<field name="pattern">(15)(\d{6})</field>
|
||||
<field name="type">use_date</field>
|
||||
<field name="gs1_content_type">date</field>
|
||||
</record>
|
||||
|
||||
<record id="barcode_rule_gs1_16" model="barcode.rule">
|
||||
<field name="name">Expiration date (YYMMDD)</field>
|
||||
<field name="barcode_nomenclature_id" ref="default_gs1_nomenclature"/>
|
||||
<field name="sequence">109</field>
|
||||
<field name="encoding">gs1-128</field>
|
||||
<field name="pattern">(16)(\d{6})</field>
|
||||
<field name="type">expiration_date</field>
|
||||
<field name="gs1_content_type">date</field>
|
||||
</record>
|
||||
|
||||
<!-- Quantity/Measure Barcode -->
|
||||
<record id="barcode_rule_gs1_30" model="barcode.rule">
|
||||
<field name="name">Variable count of items (variable measure trade item)</field>
|
||||
<field name="barcode_nomenclature_id" ref="default_gs1_nomenclature"/>
|
||||
<field name="sequence">110</field>
|
||||
<field name="encoding">gs1-128</field>
|
||||
<field name="pattern">(30)(\d{0,8})</field>
|
||||
<field name="associated_uom_id" ref="uom.product_uom_unit"/>
|
||||
<field name="type">qty_done</field>
|
||||
<field name="gs1_content_type">measure</field>
|
||||
<field name="gs1_decimal_usage">False</field>
|
||||
|
||||
</record>
|
||||
|
||||
<record id="barcode_rule_gs1_37" model="barcode.rule">
|
||||
<field name="name">Count of trade items or trade item pieces contained in a logistic unit</field>
|
||||
<field name="barcode_nomenclature_id" ref="default_gs1_nomenclature"/>
|
||||
<field name="sequence">111</field>
|
||||
<field name="encoding">gs1-128</field>
|
||||
<field name="pattern">(37)(\d{0,8})</field>
|
||||
<field name="associated_uom_id" ref="uom.product_uom_unit"/>
|
||||
<field name="type">qty_done</field>
|
||||
<field name="gs1_content_type">measure</field>
|
||||
<field name="gs1_decimal_usage">False</field>
|
||||
</record>
|
||||
|
||||
<record id="barcode_rule_gs1_310y" model="barcode.rule">
|
||||
<field name="name">Net weight, kilograms (variable measure trade item)</field>
|
||||
<field name="barcode_nomenclature_id" ref="default_gs1_nomenclature"/>
|
||||
<field name="sequence">112</field>
|
||||
<field name="encoding">gs1-128</field>
|
||||
<field name="pattern">(310[0-5])(\d{6})</field>
|
||||
<field name="associated_uom_id" ref="uom.product_uom_kgm"/>
|
||||
<field name="type">qty_done</field>
|
||||
<field name="gs1_content_type">measure</field>
|
||||
<field name="gs1_decimal_usage">True</field>
|
||||
</record>
|
||||
|
||||
<record id="barcode_rule_gs1_311y" model="barcode.rule">
|
||||
<field name="name">Length or first dimension, metres (variable measure trade item)</field>
|
||||
<field name="barcode_nomenclature_id" ref="default_gs1_nomenclature"/>
|
||||
<field name="sequence">113</field>
|
||||
<field name="encoding">gs1-128</field>
|
||||
<field name="pattern">(311[0-5])(\d{6})</field>
|
||||
<field name="associated_uom_id" ref="uom.product_uom_meter"/>
|
||||
<field name="type">qty_done</field>
|
||||
<field name="gs1_content_type">measure</field>
|
||||
<field name="gs1_decimal_usage">True</field>
|
||||
</record>
|
||||
|
||||
<record id="barcode_rule_gs1_315y" model="barcode.rule">
|
||||
<field name="name">Net volume, litres (variable measure trade item)</field>
|
||||
<field name="barcode_nomenclature_id" ref="default_gs1_nomenclature"/>
|
||||
<field name="sequence">114</field>
|
||||
<field name="encoding">gs1-128</field>
|
||||
<field name="pattern">(315[0-5])(\d{6})</field>
|
||||
<field name="associated_uom_id" ref="uom.product_uom_litre"/>
|
||||
<field name="type">qty_done</field>
|
||||
<field name="gs1_content_type">measure</field>
|
||||
<field name="gs1_decimal_usage">True</field>
|
||||
</record>
|
||||
|
||||
<record id="barcode_rule_gs1_321y" model="barcode.rule">
|
||||
<field name="name">Length or first dimension, inches (variable measure trade item)</field>
|
||||
<field name="barcode_nomenclature_id" ref="default_gs1_nomenclature"/>
|
||||
<field name="sequence">115</field>
|
||||
<field name="encoding">gs1-128</field>
|
||||
<field name="pattern">(321[0-5])(\d{6})</field>
|
||||
<field name="associated_uom_id" ref="uom.product_uom_inch"/>
|
||||
<field name="type">qty_done</field>
|
||||
<field name="gs1_content_type">measure</field>
|
||||
<field name="gs1_decimal_usage">True</field>
|
||||
</record>
|
||||
|
||||
<record id="barcode_rule_gs1_357y" model="barcode.rule">
|
||||
<field name="name">Net weight (or volume), ounces (variable measure trade item)</field>
|
||||
<field name="barcode_nomenclature_id" ref="default_gs1_nomenclature"/>
|
||||
<field name="sequence">116</field>
|
||||
<field name="encoding">gs1-128</field>
|
||||
<field name="pattern">(357[0-5])(\d{6})</field>
|
||||
<field name="associated_uom_id" ref="uom.product_uom_oz"/>
|
||||
<field name="type">qty_done</field>
|
||||
<field name="gs1_content_type">measure</field>
|
||||
<field name="gs1_decimal_usage">True</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -0,0 +1,2 @@
|
||||
from . import barcode_nomenclature
|
||||
from . import barcode_rule
|
||||
@@ -0,0 +1,126 @@
|
||||
import re
|
||||
import datetime
|
||||
import calendar
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
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 Seperator", trim=False,
|
||||
help="Alternative regex delimiter for the FNC1 (by default, if not set, it is <GS> ASCII 29 char). The seperator 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 Seperator 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(self.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)
|
||||
@@ -0,0 +1,66 @@
|
||||
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=[
|
||||
('qty_done', 'Quantity'),
|
||||
('location', 'Location'),
|
||||
('location_dest', 'Destination location'),
|
||||
('lot', 'Lot number'),
|
||||
('package', 'Package'),
|
||||
('use_date', 'Best before Date'),
|
||||
('expiration_date', 'Expiration Date'),
|
||||
('packaging_date', 'Packaging Date'),
|
||||
], ondelete={
|
||||
'qty_done': 'set default',
|
||||
'location': 'set default',
|
||||
'location_dest': 'set default',
|
||||
'lot': 'set default',
|
||||
'package': 'set default',
|
||||
'use_date': 'set default',
|
||||
'expiration_date': 'set default',
|
||||
'packaging_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 to what this rule data will be handle:\
|
||||
* 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 dertermine 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,155 @@
|
||||
odoo.define('barcodes_gs1_nomenclature/static/src/js/barcode_parser.js', function (require) {
|
||||
"use strict";
|
||||
|
||||
const BarcodeParser = require('barcodes.BarcodeParser');
|
||||
const FNC1_CHAR = String.fromCharCode(29);
|
||||
const {_lt} = require('web.core');
|
||||
|
||||
BarcodeParser.include({
|
||||
/**
|
||||
* Convert YYMMDD GS1 date into a Date object
|
||||
*
|
||||
* @param {string} gs1Date YYMMDD string date, length must be 6
|
||||
* @returns {Date}
|
||||
*/
|
||||
gs1_date_to_date: function(gs1Date) {
|
||||
// See 7.12 Determination of century in dates:
|
||||
// https://www.gs1.org/sites/default/files/docs/barcodes/GS1_General_Specifications.pdfDetermination of century
|
||||
const now = new Date();
|
||||
const substractYear = parseInt(gs1Date.slice(0, 2)) - (now.getFullYear() % 100);
|
||||
let century = Math.floor(now.getFullYear() / 100);
|
||||
if (51 <= substractYear && substractYear <= 99) {
|
||||
century--;
|
||||
} else if (-99 <= substractYear && substractYear <= -50) {
|
||||
century++;
|
||||
}
|
||||
const year = century * 100 + parseInt(gs1Date.slice(0, 2));
|
||||
const date = new Date(year, parseInt(gs1Date.slice(2, 4) - 1));
|
||||
|
||||
if (gs1Date.slice(-2) === '00'){
|
||||
// Day is not mandatory, when not set -> last day of the month
|
||||
date.setDate(new Date(year, parseInt(gs1Date.slice(2, 4)), 0).getDate());
|
||||
} else {
|
||||
date.setDate(parseInt(gs1Date.slice(-2)));
|
||||
}
|
||||
return date;
|
||||
},
|
||||
|
||||
/**
|
||||
* Perform interpretation of the barcode value depending ot the rule.gs1_content_type
|
||||
*
|
||||
* @param {Array} match Result of a regex match with atmost 2 groups (ia and value)
|
||||
* @param {Object} rule Matched Barcode Rule
|
||||
* @returns {Object|null}
|
||||
*/
|
||||
parse_gs1_rule_pattern: function(match, rule) {
|
||||
const result = {
|
||||
rule: Object.assign({}, rule),
|
||||
ai: match[1],
|
||||
string_value: match[2]
|
||||
};
|
||||
if (rule.gs1_content_type === 'measure'){
|
||||
let decimalPosition = 0; // Decimal position begin at the end, 0 means no decimal
|
||||
if (rule.gs1_decimal_usage){
|
||||
decimalPosition = parseInt(match[1][match[1].length - 1]);
|
||||
}
|
||||
if (decimalPosition > 0) {
|
||||
const integral = match[2].slice(0, match[2].length - decimalPosition);
|
||||
const decimal = match[2].slice(match[2].length - decimalPosition);
|
||||
result.value = parseFloat( integral + "." + decimal);
|
||||
} else {
|
||||
result.value = parseInt(match[2]);
|
||||
}
|
||||
} else if (rule.gs1_content_type === 'identifier'){
|
||||
if (parseInt(match[2][match[2].length - 1]) !== this.get_barcode_check_digit("0".repeat(18 - match[2].length) + match[2])){
|
||||
throw new Error(_lt("Invalid barcode: the check digit is incorrect"));
|
||||
// return {error: _lt("Invalid barcode: the check digit is incorrect")};
|
||||
}
|
||||
result.value = match[2];
|
||||
} else if (rule.gs1_content_type === 'date'){
|
||||
if (match[2].length !== 6){
|
||||
throw new Error(_lt("Invalid barcode: can't be formated as date"));
|
||||
// return {error: _lt("Invalid barcode: can't be formated as date")};
|
||||
}
|
||||
result.value = this.gs1_date_to_date(match[2]);
|
||||
} else {
|
||||
result.value = match[2];
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* Try to decompose the gs1 extanded barcode into several unit of information using gs1 rules.
|
||||
*
|
||||
* @param {string} barcode
|
||||
* @returns {Array} Array of object
|
||||
*/
|
||||
gs1_decompose_extanded: function(barcode) {
|
||||
const results = [];
|
||||
const rules = this.nomenclature.rules.filter(rule => rule.encoding === 'gs1-128');
|
||||
let separatorReg = FNC1_CHAR + "?";
|
||||
if (this.nomenclature.gs1_separator_fnc1 && this.nomenclature.gs1_separator_fnc1.trim()){
|
||||
separatorReg = `(?:${this.nomenclature.gs1_separator_fnc1})?`;
|
||||
}
|
||||
|
||||
while (barcode.length > 0) {
|
||||
const barcodeLength = barcode.length;
|
||||
for (const rule of rules) {
|
||||
const match = barcode.match("^" + rule.pattern + separatorReg);
|
||||
if (match && match.length >= 3) {
|
||||
const res = this.parse_gs1_rule_pattern(match, rule);
|
||||
if (res) {
|
||||
barcode = barcode.slice(match.index + match[0].length);
|
||||
results.push(res);
|
||||
if (barcode.length === 0) {
|
||||
return results; // Barcode completly parsed, no need to keep looping.
|
||||
}
|
||||
} else {
|
||||
throw new Error(_lt("This barcode can't be parsed by any barcode rules."));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (barcodeLength === barcode.length) {
|
||||
throw new Error(_lt("This barcode can't be partially or fully parsed."));
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
},
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @returns {Object|Array|null} If nomenclature is GS1, returns an array or null
|
||||
*/
|
||||
parse_barcode: function(barcode){
|
||||
if (this.nomenclature && this.nomenclature.is_gs1_nomenclature) {
|
||||
return this.gs1_decompose_extanded(barcode);
|
||||
}
|
||||
return this._super(...arguments);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_barcodeNomenclatureFields: function () {
|
||||
const fieldNames = this._super(...arguments);
|
||||
fieldNames.push('is_gs1_nomenclature', 'gs1_separator_fnc1');
|
||||
return fieldNames;
|
||||
},
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_barcodeRuleFields: function () {
|
||||
const fieldNames = this._super(...arguments);
|
||||
fieldNames.push('gs1_content_type', 'gs1_decimal_usage', 'associated_uom_id');
|
||||
return fieldNames;
|
||||
},
|
||||
});
|
||||
|
||||
return BarcodeParser;
|
||||
});
|
||||
@@ -0,0 +1,282 @@
|
||||
odoo.define('barcodes_gs1_nomenclature/static/src/js/tests/barcode_parser_tests.js', function (require) {
|
||||
"use strict";
|
||||
|
||||
const BarcodeParser = require('barcodes.BarcodeParser');
|
||||
|
||||
|
||||
QUnit.module('Barcodes', {}, function () {
|
||||
QUnit.module('Barcode GS1 Parser', {
|
||||
beforeEach: function () {
|
||||
this.data = {
|
||||
'barcode.nomenclature': {
|
||||
fields: {
|
||||
name: {type: 'char', string: 'Barcode Nomenclature'},
|
||||
rule_ids: {type: 'one2many', relation: 'barcode.rule'},
|
||||
upc_ean_conv: {type: 'selection', string: 'UPC/EAN Conversion'},
|
||||
is_gs1_nomenclature: {type: 'boolean', string: 'Is GS1 Nomenclature'},
|
||||
gs1_separator_fnc1: {type: 'char', string: 'FNC1 Seperator Alternative'}
|
||||
},
|
||||
records: [
|
||||
{id: 1, name: "normal", upc_ean_conv: "always", is_gs1_nomenclature: false, gs1_separator_fnc1: ''},
|
||||
{id: 2, name: "GS1", upc_ean_conv: "always", is_gs1_nomenclature: true, gs1_separator_fnc1: ''},
|
||||
],
|
||||
},
|
||||
'barcode.rule': {
|
||||
fields: {
|
||||
name: {type: 'char', string: 'Barcode Nomenclature'},
|
||||
barcode_nomenclature_id: {type: 'many2one', relation: 'barcode.nomenclature'},
|
||||
sequence: {type: 'integer', string: 'Sequence'},
|
||||
encoding: {type: 'selection', string: 'Encoding'},
|
||||
type: {type: 'selection', string: 'Type'},
|
||||
pattern: {type: 'Char', string: 'Pattern'},
|
||||
alias: {type: 'Char', string: 'Alias'},
|
||||
},
|
||||
records: [
|
||||
{id: 1, name: "Product Barcodes", barcode_nomenclature_id: 1, sequence: 90, type: 'product', encoding: 'any', pattern: ".*"},
|
||||
{
|
||||
id: 2,
|
||||
name: "Global Trade Item Number (GTIN)",
|
||||
barcode_nomenclature_id: 2,
|
||||
sequence: 100,
|
||||
encoding: "gs1-128",
|
||||
pattern: "(01)(\\d{14})",
|
||||
type: "product",
|
||||
gs1_content_type: "identifier",
|
||||
gs1_decimal_usage: false
|
||||
}, {
|
||||
id: 3,
|
||||
name: "GTIN of contained trade items",
|
||||
barcode_nomenclature_id: 2,
|
||||
sequence: 101,
|
||||
encoding: "gs1-128",
|
||||
pattern: "(02)(\\d{14})",
|
||||
type: "product",
|
||||
gs1_content_type: "identifier",
|
||||
gs1_decimal_usage: false
|
||||
}, {
|
||||
id: 4,
|
||||
name: "Batch or lot number",
|
||||
barcode_nomenclature_id: 2,
|
||||
sequence: 102,
|
||||
encoding: "gs1-128",
|
||||
pattern: "(10)([!\"%/0-9:?A-Za-z]{0,20})",
|
||||
type: "product",
|
||||
gs1_content_type: "alpha",
|
||||
gs1_decimal_usage: false
|
||||
}, {
|
||||
id: 5,
|
||||
name: "Serial number",
|
||||
barcode_nomenclature_id: 2,
|
||||
sequence: 105,
|
||||
encoding: "gs1-128",
|
||||
pattern: "(21)([!\"%/0-9:?A-Za-z]{0,20})",
|
||||
type: "product",
|
||||
gs1_content_type: "alpha",
|
||||
gs1_decimal_usage: false
|
||||
}, {
|
||||
id: 6,
|
||||
name: "Packaging date (YYMMDD)",
|
||||
barcode_nomenclature_id: 2,
|
||||
sequence: 103,
|
||||
encoding: "gs1-128",
|
||||
pattern: "(13)(\\d{6})",
|
||||
type: "packaging_date",
|
||||
gs1_content_type: "date",
|
||||
gs1_decimal_usage: false
|
||||
}, {
|
||||
id: 7,
|
||||
name: "Best before date (YYMMDD)",
|
||||
barcode_nomenclature_id: 2,
|
||||
sequence: 104,
|
||||
encoding: "gs1-128",
|
||||
pattern: "(15)(\\d{6})",
|
||||
type: "use_date",
|
||||
gs1_content_type: "date",
|
||||
gs1_decimal_usage: false
|
||||
}, {
|
||||
id: 8,
|
||||
name: "Expiration date (YYMMDD)",
|
||||
barcode_nomenclature_id: 2,
|
||||
sequence: 105,
|
||||
encoding: "gs1-128",
|
||||
pattern: "(16)(\\d{6})",
|
||||
type: "expiration_date",
|
||||
gs1_content_type: "date",
|
||||
gs1_decimal_usage: false
|
||||
}, {
|
||||
id: 9,
|
||||
name: "Variable count of items (variabl",
|
||||
barcode_nomenclature_id: 2,
|
||||
sequence: 105,
|
||||
encoding: "gs1-128",
|
||||
pattern: "(30)(\\d{0,8})",
|
||||
type: "product",
|
||||
gs1_content_type: "measure",
|
||||
gs1_decimal_usage: false
|
||||
}, {
|
||||
id: 10,
|
||||
name: "Count of trade items or trade it",
|
||||
barcode_nomenclature_id: 2,
|
||||
sequence: 105,
|
||||
encoding: "gs1-128",
|
||||
pattern: "(37)(\\d{0,8})",
|
||||
type: "product",
|
||||
gs1_content_type: "measure",
|
||||
gs1_decimal_usage: false
|
||||
}, {
|
||||
id: 11,
|
||||
name: "Net weight, kilograms (variable",
|
||||
barcode_nomenclature_id: 2,
|
||||
sequence: 105,
|
||||
encoding: "gs1-128",
|
||||
pattern: "(310[0-5])(\\d{6})",
|
||||
type: "product",
|
||||
gs1_content_type: "measure",
|
||||
gs1_decimal_usage: true
|
||||
}, {
|
||||
id: 12,
|
||||
name: "Length or first dimension, metre",
|
||||
barcode_nomenclature_id: 2,
|
||||
sequence: 105,
|
||||
encoding: "gs1-128",
|
||||
pattern: "(311[0-5])(\\d{6})",
|
||||
type: "product",
|
||||
gs1_content_type: "measure",
|
||||
gs1_decimal_usage: true
|
||||
}, {
|
||||
id: 13,
|
||||
name: "Net volume, litres (variable mea",
|
||||
barcode_nomenclature_id: 2,
|
||||
sequence: 105,
|
||||
encoding: "gs1-128",
|
||||
pattern: "(315[0-5])(\\d{6})",
|
||||
type: "product",
|
||||
gs1_content_type: "measure",
|
||||
gs1_decimal_usage: true
|
||||
}, {
|
||||
id: 14,
|
||||
name: "Length or first dimension, inche",
|
||||
barcode_nomenclature_id: 2,
|
||||
sequence: 105,
|
||||
encoding: "gs1-128",
|
||||
pattern: "(321[0-5])(\\d{6})",
|
||||
type: "product",
|
||||
gs1_content_type: "measure",
|
||||
gs1_decimal_usage: true
|
||||
}, {
|
||||
id: 15,
|
||||
name: "Net weight (or volume), ounces (",
|
||||
barcode_nomenclature_id: 2,
|
||||
sequence: 105,
|
||||
encoding: "gs1-128",
|
||||
pattern: "(357[0-5])(\\d{6})",
|
||||
type: "product",
|
||||
gs1_content_type: "measure",
|
||||
gs1_decimal_usage: true
|
||||
}
|
||||
],
|
||||
}
|
||||
};
|
||||
}
|
||||
}, function () {
|
||||
QUnit.test('Test gs1 date barcode', async function (assert) {
|
||||
assert.expect(9);
|
||||
const barcodeNomenclature = new BarcodeParser({'nomenclature_id': false});
|
||||
await barcodeNomenclature.loaded;
|
||||
|
||||
// 20/10/2015 -> 151020
|
||||
let dateGS1 = "151020";
|
||||
let date = barcodeNomenclature.gs1_date_to_date(dateGS1);
|
||||
assert.equal(date.getDate(), 20);
|
||||
assert.equal(date.getMonth() + 1, 10);
|
||||
assert.equal(date.getFullYear(), 2015);
|
||||
|
||||
// XX/03/2052 -> 520300 -> (if day no set take last day of the month -> 31)
|
||||
dateGS1 = "520300";
|
||||
date = barcodeNomenclature.gs1_date_to_date(dateGS1);
|
||||
assert.equal(date.getDate(), 31);
|
||||
assert.equal(date.getMonth() + 1, 3);
|
||||
assert.equal(date.getFullYear(), 2052);
|
||||
|
||||
// XX/02/2020 -> 520200 -> (if day no set take last day of the month -> 29)
|
||||
dateGS1 = "200200";
|
||||
date = barcodeNomenclature.gs1_date_to_date(dateGS1);
|
||||
assert.equal(date.getDate(), 29);
|
||||
assert.equal(date.getMonth() + 1, 2);
|
||||
assert.equal(date.getFullYear(), 2020);
|
||||
});
|
||||
|
||||
QUnit.test('Test gs1 decompose extanded', async function (assert) {
|
||||
assert.expect(19);
|
||||
const barcodeNomenclature = new BarcodeParser({'nomenclature_id': false});
|
||||
await barcodeNomenclature.loaded;
|
||||
|
||||
barcodeNomenclature.nomenclature = this.data['barcode.nomenclature'].records[0];
|
||||
barcodeNomenclature.nomenclature.rules = this.data['barcode.rule'].records;
|
||||
|
||||
// (01)94019097685457(10)33650100138(3102)002004(15)131018
|
||||
let code128 = "01940190976854571033650100138\x1D310200200415131018";
|
||||
let res = barcodeNomenclature.gs1_decompose_extanded(code128);
|
||||
assert.equal(res.length, 4);
|
||||
assert.equal(res[0].ai, "01");
|
||||
|
||||
assert.equal(res[1].ai, "10");
|
||||
|
||||
assert.equal(res[2].ai, "3102");
|
||||
assert.equal(res[2].value, 20.04);
|
||||
|
||||
assert.equal(res[3].ai, "15");
|
||||
assert.equal(typeof res[3].value.getFullYear, 'function');
|
||||
assert.equal(res[3].value.getFullYear(), 2013);
|
||||
assert.equal(res[3].value.getDate(), 18);
|
||||
assert.equal(res[3].value.getMonth() + 1, 10);
|
||||
|
||||
// (01)94019097685457(13)170119(30)17
|
||||
code128 = "0194019097685457131701193017";
|
||||
res = barcodeNomenclature.gs1_decompose_extanded(code128);
|
||||
assert.equal(res.length, 3);
|
||||
assert.equal(res[0].ai, "01");
|
||||
|
||||
assert.equal(res[1].ai, "13");
|
||||
assert.equal(typeof res[1].value.getFullYear, 'function');
|
||||
assert.equal(res[1].value.getFullYear(), 2017);
|
||||
assert.equal(res[1].value.getDate(), 19);
|
||||
assert.equal(res[1].value.getMonth() + 1, 1);
|
||||
|
||||
assert.equal(res[2].ai, "30");
|
||||
assert.equal(res[2].value, 17);
|
||||
|
||||
});
|
||||
|
||||
QUnit.test('Test Alternative GS1 Separator (fnc1)', async function (assert) {
|
||||
assert.expect(6);
|
||||
let barcodeNomenclature = new BarcodeParser({'nomenclature_id': 2});
|
||||
await barcodeNomenclature.loaded;
|
||||
|
||||
barcodeNomenclature.nomenclature = this.data['barcode.nomenclature'].records[0];
|
||||
barcodeNomenclature.nomenclature.gs1_separator_fnc1 = "#";
|
||||
barcodeNomenclature.nomenclature.rules = this.data['barcode.rule'].records;
|
||||
|
||||
// (21)12345(15)090101(16)100101
|
||||
let code128 = "2112345\x1D1509010116100101";
|
||||
let res;
|
||||
try {
|
||||
res = barcodeNomenclature.gs1_decompose_extanded(code128);
|
||||
} catch (error) {
|
||||
assert.ok(
|
||||
error instanceof Error,
|
||||
"Default separator shouldn't work"
|
||||
);
|
||||
}
|
||||
|
||||
code128 = "2112345#1509010116100101";
|
||||
res = barcodeNomenclature.gs1_decompose_extanded(code128);
|
||||
assert.equal(res.length, 3);
|
||||
assert.equal(res[0].ai, "21");
|
||||
assert.equal(res[0].value, "12345");
|
||||
assert.equal(res[1].ai, "15");
|
||||
assert.equal(res[2].ai, "16");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
from . import test_barcodes_gs1_nomenclature
|
||||
@@ -0,0 +1,130 @@
|
||||
from odoo.addons.barcodes.tests.test_barcode_nomenclature import TestBarcodeNomenclature
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class TestBarcodeGS1Nomenclature(TestBarcodeNomenclature):
|
||||
def test_gs1_date_to_date(self):
|
||||
barcode_nomenclature = self.env['barcode.nomenclature']
|
||||
# 20/10/2015 -> 151020
|
||||
date_gs1 = "151020"
|
||||
date = barcode_nomenclature.gs1_date_to_date(date_gs1)
|
||||
self.assertEqual(date.day, 20)
|
||||
self.assertEqual(date.month, 10)
|
||||
self.assertEqual(date.year, 2015)
|
||||
|
||||
# XX/03/2052 -> 520300 -> (if day no set take last day of the month -> 31)
|
||||
date_gs1 = "520300"
|
||||
date = barcode_nomenclature.gs1_date_to_date(date_gs1)
|
||||
self.assertEqual(date.day, 31)
|
||||
self.assertEqual(date.month, 3)
|
||||
self.assertEqual(date.year, 2052)
|
||||
|
||||
# XX/02/2020 -> 520200 -> (if day no set take last day of the month -> 29)
|
||||
date_gs1 = "200200"
|
||||
date = barcode_nomenclature.gs1_date_to_date(date_gs1)
|
||||
self.assertEqual(date.day, 29)
|
||||
self.assertEqual(date.month, 2)
|
||||
self.assertEqual(date.year, 2020)
|
||||
|
||||
def test_gs1_extanded_barcode_1(self):
|
||||
barcode_nomenclature = self.env['barcode.nomenclature'].browse(self.ref('barcodes_gs1_nomenclature.default_gs1_nomenclature'))
|
||||
# (01)94019097685457(10)33650100138(3102)002004(15)131018
|
||||
code128 = "01940190976854571033650100138\x1D310200200415131018"
|
||||
res = barcode_nomenclature.gs1_decompose_extanded(code128)
|
||||
self.assertEqual(len(res), 4)
|
||||
self.assertEqual(res[0]["ai"], "01")
|
||||
|
||||
self.assertEqual(res[1]["ai"], "10")
|
||||
|
||||
self.assertEqual(res[2]["ai"], "3102")
|
||||
self.assertEqual(res[2]["value"], 20.04)
|
||||
|
||||
self.assertEqual(res[3]["ai"], "15")
|
||||
self.assertEqual(res[3]["value"].year, 2013)
|
||||
self.assertEqual(res[3]["value"].day, 18)
|
||||
self.assertEqual(res[3]["value"].month, 10)
|
||||
|
||||
# (01)94019097685457(13)170119(30)17
|
||||
code128 = "0194019097685457131701193017"
|
||||
res = barcode_nomenclature.gs1_decompose_extanded(code128)
|
||||
self.assertEqual(len(res), 3)
|
||||
self.assertEqual(res[0]["ai"], "01")
|
||||
|
||||
self.assertEqual(res[1]["ai"], "13")
|
||||
self.assertEqual(res[1]["value"].year, 2017)
|
||||
self.assertEqual(res[1]["value"].day, 19)
|
||||
self.assertEqual(res[1]["value"].month, 1)
|
||||
|
||||
self.assertEqual(res[2]["ai"], "30")
|
||||
self.assertEqual(res[2]["value"], 17)
|
||||
|
||||
def test_gs1_extanded_barcode_2_decimal(self):
|
||||
""" Parses multiples barcode with (or without) a decimal value and
|
||||
checks for each of them the value is correctly parsed.
|
||||
"""
|
||||
# Configures a barcode GS1 nomenclature...
|
||||
barcode_nomenclature = self.env['barcode.nomenclature'].create({
|
||||
'name': "GS1 Nomenclature - Test",
|
||||
'is_gs1_nomenclature': True,
|
||||
})
|
||||
default_barcode_rule_vals = {
|
||||
'default_encoding': 'gs1-128',
|
||||
'default_barcode_nomenclature_id': barcode_nomenclature.id,
|
||||
'default_type': 'qty_done',
|
||||
'default_gs1_content_type': 'measure',
|
||||
}
|
||||
# Creates a rule who don't take any decimal.
|
||||
barcode_rule = self.env['barcode.rule'].with_context(default_barcode_rule_vals).create({
|
||||
'name': "GS1 Rule Test - No Decimal",
|
||||
'pattern': r'(300)(\d{5,8})',
|
||||
'gs1_decimal_usage': False,
|
||||
})
|
||||
# Creates a rule to take the four last digit as decimals.
|
||||
barcode_rule_decimal = self.env['barcode.rule'].with_context(default_barcode_rule_vals).create({
|
||||
'name': "GS1 Rule Test - Four Decimals",
|
||||
'pattern': r'(304)(\d{5,8})',
|
||||
'gs1_decimal_usage': True,
|
||||
})
|
||||
|
||||
# Checks barcodes without decimals.
|
||||
res = barcode_nomenclature.gs1_decompose_extanded('30000000')
|
||||
self.assertEqual(len(res), 1)
|
||||
self.assertEqual(res[0]['string_value'], '00000')
|
||||
self.assertEqual(res[0]['value'], 0)
|
||||
|
||||
res = barcode_nomenclature.gs1_decompose_extanded('30018789')
|
||||
self.assertEqual(len(res), 1)
|
||||
self.assertEqual(res[0]['string_value'], '18789')
|
||||
self.assertEqual(res[0]['value'], 18789)
|
||||
|
||||
res = barcode_nomenclature.gs1_decompose_extanded('3001515000')
|
||||
self.assertEqual(len(res), 1)
|
||||
self.assertEqual(res[0]['string_value'], '1515000')
|
||||
self.assertEqual(res[0]['value'], 1515000)
|
||||
|
||||
# Checks barcodes with decimals.
|
||||
res = barcode_nomenclature.gs1_decompose_extanded('30400000')
|
||||
self.assertEqual(len(res), 1)
|
||||
self.assertEqual(res[0]['string_value'], '00000')
|
||||
self.assertEqual(res[0]['value'], 0.0)
|
||||
|
||||
res = barcode_nomenclature.gs1_decompose_extanded('30418789')
|
||||
self.assertEqual(len(res), 1)
|
||||
self.assertEqual(res[0]['string_value'], '18789')
|
||||
self.assertEqual(res[0]['value'], 1.8789)
|
||||
|
||||
res = barcode_nomenclature.gs1_decompose_extanded('3041515000')
|
||||
self.assertEqual(len(res), 1)
|
||||
self.assertEqual(res[0]['string_value'], '1515000')
|
||||
self.assertEqual(res[0]['value'], 151.5)
|
||||
|
||||
# Checks wrong configs will raise an exception.
|
||||
barcode_rule_decimal.pattern = r'()(\d{0,4})'
|
||||
# Barcode rule uses decimals but AI doesn't precise what is the decimal position.
|
||||
with self.assertRaises(ValidationError):
|
||||
res = barcode_nomenclature.gs1_decompose_extanded('1234')
|
||||
|
||||
# The pattern is too permissive and can catch something which can't be casted as measurement
|
||||
barcode_rule.pattern = r'(300)(.*)'
|
||||
with self.assertRaises(ValidationError):
|
||||
res = barcode_nomenclature.gs1_decompose_extanded('300bilou4000')
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<template id="assets_backend" name="barcodes assets" inherit_id="web.assets_backend">
|
||||
<xpath expr="." position="inside">
|
||||
<script type="text/javascript" src="/barcodes_gs1_nomenclature/static/src/js/barcode_parser.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="qunit_suite" name="barcode_tests" inherit_id="web.qunit_suite_tests">
|
||||
<xpath expr="." position="inside">
|
||||
<script type="text/javascript" src="/barcodes_gs1_nomenclature/static/src/js/tests/barcode_parser_tests.js"/>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||
@@ -0,0 +1,60 @@
|
||||
<?xml version="1.0"?>
|
||||
<odoo>
|
||||
<record id="view_barcode_gs1_nomenclature_form" model="ir.ui.view">
|
||||
<field name="name">Barcode Nomenclatures</field>
|
||||
<field name="model">barcode.nomenclature</field>
|
||||
<field name="inherit_id" ref="barcodes.view_barcode_nomenclature_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<!-- Nomenclature form -->
|
||||
<xpath expr="//field[@name='upc_ean_conv']" position="attributes">
|
||||
<attribute name="attrs">{'invisible':[('is_gs1_nomenclature', '=', True)]}</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//group[@name='general_attributes']" position="after">
|
||||
<group name="gs1_attributes" col="4">
|
||||
<field name="is_gs1_nomenclature"/>
|
||||
<field name="gs1_separator_fnc1" attrs="{'invisible':[('is_gs1_nomenclature', '!=', True)]}"/>
|
||||
</group>
|
||||
</xpath>
|
||||
<!-- Rules table -->
|
||||
<xpath expr="//field[@name='rule_ids']" position="attributes">
|
||||
<attribute name="context">{'is_gs1': is_gs1_nomenclature}</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='encoding']" position="attributes">
|
||||
<attribute name="attrs">{'column_invisible': [('parent.is_gs1_nomenclature', '=', True)]}</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='pattern']" position="after">
|
||||
<field name="gs1_content_type" attrs="{'column_invisible': [('parent.is_gs1_nomenclature', '!=', True)]}"/>
|
||||
<field name="gs1_decimal_usage" attrs="{'column_invisible': [('parent.is_gs1_nomenclature', '!=', True)], 'invisible': [('gs1_content_type', '!=', 'measure')]}"/>
|
||||
<field name="associated_uom_id" attrs="{'column_invisible': [('parent.is_gs1_nomenclature', '!=', True)], 'invisible': [('gs1_content_type', '!=', 'measure')]}"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_barcode_gs1_nomenclature_tree" model="ir.ui.view">
|
||||
<field name="name">Barcode Nomenclatures</field>
|
||||
<field name="model">barcode.nomenclature</field>
|
||||
<field name="inherit_id" ref="barcodes.view_barcode_nomenclature_tree"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='name']" position="after">
|
||||
<field name="is_gs1_nomenclature"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_barcode_gs1_rule_form" model="ir.ui.view">
|
||||
<field name="name">Barcode Rule</field>
|
||||
<field name="model">barcode.rule</field>
|
||||
<field name="inherit_id" ref="barcodes.view_barcode_rule_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='encoding']" position="attributes">
|
||||
<attribute name="attrs">{'invisible': ['|', ('parent.is_gs1_nomenclature', '=', True), ('type', '=', 'alias')]}</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='alias']" position="after">
|
||||
<field name="gs1_content_type" attrs="{'invisible': [('parent.is_gs1_nomenclature', '!=', True)]}"/>
|
||||
<field name="gs1_decimal_usage" attrs="{'invisible': ['|', ('parent.is_gs1_nomenclature', '!=', True), ('gs1_content_type', '!=', 'measure')]}"/>
|
||||
<field name="associated_uom_id" attrs="{'invisible': ['|', ('parent.is_gs1_nomenclature', '!=', True), ('gs1_content_type', '!=', 'measure')]}"/>
|
||||
<field name="is_gs1_nomenclature" invisible="1"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -104,16 +104,19 @@ var BarcodeReader = core.Class.extend({
|
||||
const callbacks = Object.keys(this.exclusive_callbacks).length
|
||||
? this.exclusive_callbacks
|
||||
: this.action_callbacks;
|
||||
|
||||
const parsed_result = this.barcode_parser.parse_barcode(code);
|
||||
if (callbacks[parsed_result.type]) {
|
||||
[...callbacks[parsed_result.type]].map((cb) => cb(parsed_result));
|
||||
} else if (callbacks.error) {
|
||||
[...callbacks.error].map((cb) => cb(parsed_result));
|
||||
} else {
|
||||
console.warn('Ignored Barcode Scan:', parsed_result);
|
||||
let parsed_results = this.barcode_parser.parse_barcode(code);
|
||||
if (! Array.isArray(parsed_results)) {
|
||||
parsed_results = [parsed_results];
|
||||
}
|
||||
for (const parsed_result of parsed_results) {
|
||||
if (callbacks[parsed_result.type]) {
|
||||
[...callbacks[parsed_result.type]].map(cb => cb(parsed_result));
|
||||
} else if (callbacks.error) {
|
||||
[...callbacks.error].map(cb => cb(parsed_result));
|
||||
} else {
|
||||
console.warn('Ignored Barcode Scan:', parsed_result);
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
// the barcode scanner will listen on the hw_proxy/scanner interface for
|
||||
|
||||
Reference in New Issue
Block a user