diff options
author | Arthur de Jong <arthur@arthurdejong.org> | 2020-08-08 15:44:08 +0200 |
---|---|---|
committer | Arthur de Jong <arthur@arthurdejong.org> | 2020-08-08 16:32:17 +0200 |
commit | 180788af207f394e38b458ad14fb68f4853f4a9a (patch) | |
tree | af53276fe196d1a7e4feb980953453466030e9b5 | |
parent | c2284f322679e9527794aaa81e8fbb57792c5a21 (diff) |
Add GS1-128 format
This adds validation, parsing and encoding functions for GS1-128. It is
based on the lists of formats as published by the GS1 organisation.
Based on the implementation provided by Sergi Almacellas Abellana
<sergi@koolpi.com>.
Closes https://github.com/arthurdejong/python-stdnum/pull/144
-rw-r--r-- | stdnum/gs1_128.py | 269 | ||||
-rw-r--r-- | stdnum/gs1_ai.dat | 170 | ||||
-rw-r--r-- | tests/test_gs1_128.doctest | 148 | ||||
-rwxr-xr-x | update/gs1_ai.py | 82 |
4 files changed, 669 insertions, 0 deletions
diff --git a/stdnum/gs1_128.py b/stdnum/gs1_128.py new file mode 100644 index 0000000..aee11d2 --- /dev/null +++ b/stdnum/gs1_128.py @@ -0,0 +1,269 @@ +# gs1_128.py - functions for handling GS1-128 codes +# +# Copyright (C) 2019 Sergi Almacellas Abellana +# Copyright (C) 2020 Arthur de Jong +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 USA + +"""GS1-128 (Standard to encode product information in Code 128 barcodes). + +The GS1-128 (also called EAN-128, UCC/EAN-128 or UCC-128) is an international +standard for embedding data such as best before dates, weights, etc. with +Application Identifiers (AI). + +The GS1-128 standard is used as a product identification code on bar codes. +It embeds data with Application Identifiers (AI) that defines the kind of +data, the type and length. The standard is also known as UCC/EAN-128, UCC-128 +and EAN-128. + +GS1-128 is a subset of Code 128 symbology. + +More information: + +* https://en.wikipedia.org/wiki/GS1-128 +* https://www.gs1.org/standards/barcodes/application-identifiers +* https://www.gs1.org/docs/barcodes/GS1_General_Specifications.pdf + +>>> compact('(01)38425876095074(17)181119(37)1 ') +'013842587609507417181119371' +>>> encode({'01': '38425876095074'}) +'0138425876095074' +>>> info('0138425876095074') +{'01': '38425876095074'} +>>> validate('(17)181119(01)38425876095074(37)1') +'013842587609507417181119371' +""" + +import datetime +import decimal +import re + +from stdnum import numdb +from stdnum.exceptions import * +from stdnum.util import clean + + +# our open copy of the application identifier database +_gs1_aidb = numdb.get('gs1_ai') + + +# Extra validation modules based on the application identifier +_ai_validators = { + '01': 'stdnum.ean', + '02': 'stdnum.ean', + '8007': 'stdnum.iban', +} + + +def compact(number): + """Convert the GS1-128 to the minimal representation. + + This strips the number of any valid separators and removes surrounding + whitespace. For a more consistent compact representation use + :func:`validate()`. + """ + return clean(number, '()').strip() + + +def _encode_value(fmt, _type, value): + """Encode the specified value given the format and type.""" + if _type == 'decimal': + if isinstance(value, (list, tuple)) and fmt.startswith('N3+'): + number = _encode_value(fmt[3:], _type, value[1]) + return number[0] + value[0].rjust(3, '0') + number[1:] + value = str(value) + if fmt.startswith('N..'): + length = int(fmt[3:]) + value = value[:length + 1] + number, digits = (value.split('.') + [''])[:2] + digits = digits[:9] + return str(len(digits)) + number + digits + else: + length = int(fmt[1:]) + value = value[:length + 1] + number, digits = (value.split('.') + [''])[:2] + digits = digits[:9] + return str(len(digits)) + (number + digits).rjust(length, '0') + elif _type == 'date': + if isinstance(value, (list, tuple)) and fmt == 'N6..12': + return '%s%s' % ( + _encode_value('N6', _type, value[0]), + _encode_value('N6', _type, value[1])) + elif isinstance(value, datetime.date): + if fmt == 'N10': + return value.strftime('%y%m%d%H%M') + elif fmt == 'N8+N..4': + value = datetime.datetime.strftime(value, '%y%m%d%H%M%S') + if value.endswith('00'): + value = value[:-2] + if value.endswith('00'): + value = value[:-2] + return value + return value.strftime('%y%m%d') + return str(value) + + +def _max_length(fmt, _type): + """Determine the maximum length based on the format ad type.""" + length = sum(int(re.match(r'^[NXY][0-9]*?[.]*([0-9]+)$', x).group(1)) for x in fmt.split('+')) + if _type == 'decimal': + length += 1 + return length + + +def _pad_value(fmt, _type, value): + """Pad the value to the maximum length for the format.""" + if _type in ('decimal', 'int'): + return value.rjust(_max_length(fmt, _type), '0') + return value.ljust(_max_length(fmt, _type)) + + +def _decode_value(fmt, _type, value): + """Decode the specified value given the fmt and type.""" + if _type == 'decimal': + if fmt.startswith('N3+'): + return (value[1:4], _decode_value(fmt[3:], _type, value[0] + value[4:])) + digits = int(value[0]) + value = value[1:] + if digits: + value = value[:-digits] + '.' + value[-digits:] + return decimal.Decimal(value) + elif _type == 'date': + if fmt == 'N8+N..4': + return datetime.datetime.strptime(value, '%y%m%d%H%M%S'[:len(value)]) + elif len(value) == 10: + return datetime.datetime.strptime(value, '%y%m%d%H%M') + elif len(value) == 12: + return (_decode_value(fmt, _type, value[:6]), _decode_value(fmt, _type, value[6:])) + return datetime.datetime.strptime(value, '%y%m%d').date() + elif _type == 'int': + return int(value) + return value.strip() + + +def info(number, separator=''): + """Return a dictionary containing the information from the GS1-128 code. + + The returned dictionary maps application identifiers to values with the + appropriate type (`str`, `int`, `Decimal`, `datetime.date` or + `datetime.datetime`). + + If a `separator` is provided it will be used as FNC1 to determine the end + of variable-sized values. + """ + number = compact(number) + data = {} + identifier = '' + # skip separator + if separator and number.startswith(separator): + number = number[len(separator):] + while number: + # extract the application identifier + ai, info = _gs1_aidb.info(number)[0] + if not info or not number.startswith(ai): + raise InvalidComponent() + number = number[len(ai):] + # figure out the value part + value = number[:_max_length(info['format'], info['type'])] + if separator and info.get('fnc1', False): + idx = number.find(separator) + if idx > 0: + value = number[:idx] + number = number[len(value):] + # validate the value if we have a custom module for it + if ai in _ai_validators: + mod = __import__(_ai_validators[ai], globals(), locals(), ['validate']) + mod.validate(value) + # convert the number + data[ai] = _decode_value(info['format'], info['type'], value) + # skip separator + if separator and number.startswith(separator): + number = number[len(separator):] + return data + + +def encode(data, separator='', parentheses=False): + """Generate a GS1-128 for the application identifiers supplied. + + The provided dictionary is expected to map application identifiers to + values. The supported value types and formats depend on the application + identifier. + + If a `separator` is provided it will be used as FNC1 representation, + otherwise variable-sized values will be expanded to their maximum size + with appropriate padding. + + If `parentheses` is set the application identifiers will be surrounded + by parentheses for readability. + """ + ai_fmt = '(%s)' if parentheses else '%s' + # we keep items sorted and keep fixed-sized values separate tot output + # them first + fixed_values = [] + variable_values = [] + for inputai, value in sorted(data.items()): + ai, info = _gs1_aidb.info(inputai)[0] + if not info: + raise InvalidComponent() + # validate the value if we have a custom module for it + if ai in _ai_validators: + mod = __import__(_ai_validators[ai], globals(), locals(), ['validate']) + mod.validate(value) + value = _encode_value(info['format'], info['type'], value) + # store variable-sized values separate from fixed-size values + if info.get('fnc1', False): + variable_values.append((ai_fmt % ai, info['format'], info['type'], value)) + else: + fixed_values.append(ai_fmt % ai + value) + # we need the separator for all but the last variable-sized value + # (or pad values if we don't have a separator) + return ''.join( + fixed_values + [ + ai + (value if separator else _pad_value(fmt, _type, value)) + separator + for ai, fmt, _type, value in variable_values[:-1] + ] + [ + ai + value + for ai, fmt, _type, value in variable_values[-1:] + ]) + + +def validate(number, separator=''): + """Check if the number provided is a valid GS1-128. + + This checks formatting of the number and values and returns a stable + representation. + + If a separator is provided it will be used as FNC1 for both parsing the + provided number and for encoding the returned number. + """ + try: + return encode(info(number, separator), separator) + except ValidationError: + raise + except Exception: + # We wrap all other exceptions to ensure that we only return + # exceptions that are a subclass of ValidationError + # (the info() and encode() functions expect some semblance of valid + # input) + raise InvalidFormat() + + +def is_valid(number, separator=''): + """Check if the number provided is a valid GS1-128.""" + try: + return bool(validate(number)) + except ValidationError: + return False diff --git a/stdnum/gs1_ai.dat b/stdnum/gs1_ai.dat new file mode 100644 index 0000000..0b1ae6c --- /dev/null +++ b/stdnum/gs1_ai.dat @@ -0,0 +1,170 @@ +# generated from https://www.gs1.org/standards/barcodes/application-identifiers +# on 2020-07-12 19:36:00.576283 +00 format="N18" type="str" name="SSCC" description="Serial Shipping Container Code (SSCC)" +01 format="N14" type="str" name="GTIN" description="Global Trade Item Number (GTIN)" +02 format="N14" type="str" name="CONTENT" description="GTIN of contained trade items" +10 format="X..20" type="str" fnc1="1" name="BATCH/LOT" description="Batch or lot number" +11 format="N6" type="date" name="PROD DATE" description="Production date (YYMMDD)" +12 format="N6" type="date" name="DUE DATE" description="Due date (YYMMDD)" +13 format="N6" type="date" name="PACK DATE" description="Packaging date (YYMMDD)" +15 format="N6" type="date" name="BEST BEFORE or BEST BY" description="Best before date (YYMMDD)" +16 format="N6" type="date" name="SELL BY" description="Sell by date (YYMMDD)" +17 format="N6" type="date" name="USE BY OR EXPIRY" description="Expiration date (YYMMDD)" +20 format="N2" type="str" name="VARIANT" description="Internal product variant" +21 format="X..20" type="str" fnc1="1" name="SERIAL" description="Serial number" +22 format="X..20" type="str" fnc1="1" name="CPV" description="Consumer product variant" +235 format="X..28" type="str" fnc1="1" name="TPX" description="Third Party Controlled, Serialised Extension of GTIN (TPX)" +240 format="X..30" type="str" fnc1="1" name="ADDITIONAL ID" description="Additional product identification assigned by the manufacturer" +241 format="X..30" type="str" fnc1="1" name="CUST. PART NO." description="Customer part number" +242 format="N..6" type="str" fnc1="1" name="MTO VARIANT" description="Made-to-Order variation number" +243 format="X..20" type="str" fnc1="1" name="PCN" description="Packaging component number" +250 format="X..30" type="str" fnc1="1" name="SECONDARY SERIAL" description="Secondary serial number" +251 format="X..30" type="str" fnc1="1" name="REF. TO SOURCE" description="Reference to source entity" +253 format="N13+X..17" type="str" fnc1="1" name="GDTI" description="Global Document Type Identifier (GDTI)" +254 format="X..20" type="str" fnc1="1" name="GLN EXTENSION COMPONENT" description="GLN extension component" +255 format="N13+N..12" type="str" fnc1="1" name="GCN" description="Global Coupon Number (GCN)" +30 format="N..8" type="int" fnc1="1" name="VAR. COUNT" description="Variable count of items (variable measure trade item)" +310 format="N6" type="decimal" name="NET WEIGHT (kg)" description="Net weight, kilograms (variable measure trade item)" +311 format="N6" type="decimal" name="LENGTH (m)" description="Length or first dimension, metres (variable measure trade item)" +312 format="N6" type="decimal" name="WIDTH (m)" description="Width, diameter, or second dimension, metres (variable measure trade item)" +313 format="N6" type="decimal" name="HEIGHT (m)" description="Depth, thickness, height, or third dimension, metres (variable measure trade item)" +314 format="N6" type="decimal" name="AREA (m<sup>2</sup>)" description="Area, square metres (variable measure trade item)" +315 format="N6" type="decimal" name="NET VOLUME (l)" description="Net volume, litres (variable measure trade item)" +316 format="N6" type="decimal" name="NET VOLUME (m<sup>3</sup>)" description="Net volume, cubic metres (variable measure trade item)" +320 format="N6" type="decimal" name="NET WEIGHT (lb)" description="Net weight, pounds (variable measure trade item)" +321 format="N6" type="decimal" name="LENGTH (in)" description="Length or first dimension, inches (variable measure trade item)" +322 format="N6" type="decimal" name="LENGTH (ft)" description="Length or first dimension, feet (variable measure trade item)" +323 format="N6" type="decimal" name="LENGTH (yd)" description="Length or first dimension, yards (variable measure trade item)" +324 format="N6" type="decimal" name="WIDTH (in)" description="Width, diameter, or second dimension, inches (variable measure trade item)" +325 format="N6" type="decimal" name="WIDTH (ft)" description="Width, diameter, or second dimension, feet (variable measure trade item)" +326 format="N6" type="decimal" name="WIDTH (yd)" description="Width, diameter, or second dimension, yards (variable measure trade item)" +327 format="N6" type="decimal" name="HEIGHT (in)" description="Depth, thickness, height, or third dimension, inches (variable measure trade item)" +328 format="N6" type="decimal" name="HEIGHT (ft)" description="Depth, thickness, height, or third dimension, feet (variable measure trade item)" +329 format="N6" type="decimal" name="HEIGHT (yd)" description="Depth, thickness, height, or third dimension, yards (variable measure trade item)" +330 format="N6" type="decimal" name="GROSS WEIGHT (kg)" description="Logistic weight, kilograms" +331 format="N6" type="decimal" name="LENGTH (m), log" description="Length or first dimension, metres" +332 format="N6" type="decimal" name="WIDTH (m), log" description="Width, diameter, or second dimension, metres" +333 format="N6" type="decimal" name="HEIGHT (m), log" description="Depth, thickness, height, or third dimension, metres" +334 format="N6" type="decimal" name="AREA (m<sup>2</sup>), log" description="Area, square metres" +335 format="N6" type="decimal" name="VOLUME (l), log" description="Logistic volume, litres" +336 format="N6" type="decimal" name="VOLUME (m<sup>3</sup>), log" description="Logistic volume, cubic metres" +337 format="N6" type="decimal" name="KG PER m<sup>2</sup>" description="Kilograms per square metre" +340 format="N6" type="decimal" name="GROSS WEIGHT (lb)" description="Logistic weight, pounds" +341 format="N6" type="decimal" name="LENGTH (in), log" description="Length or first dimension, inches" +342 format="N6" type="decimal" name="LENGTH (ft), log" description="Length or first dimension, feet" +343 format="N6" type="decimal" name="LENGTH (yd), log" description="Length or first dimension, yards" +344 format="N6" type="decimal" name="WIDTH (in), log" description="Width, diameter, or second dimension, inches" +345 format="N6" type="decimal" name="WIDTH (ft), log" description="Width, diameter, or second dimension, feet" +346 format="N6" type="decimal" name="WIDTH (yd), log" description="Width, diameter, or second dimension, yard" +347 format="N6" type="decimal" name="HEIGHT (in), log" description="Depth, thickness, height, or third dimension, inches" +348 format="N6" type="decimal" name="HEIGHT (ft), log" description="Depth, thickness, height, or third dimension, feet" +349 format="N6" type="decimal" name="HEIGHT (yd), log" description="Depth, thickness, height, or third dimension, yards" +350 format="N6" type="decimal" name="AREA (in<sup>2</sup>)" description="Area, square inches (variable measure trade item)" +351 format="N6" type="decimal" name="AREA (ft<sup>2</sup>)" description="Area, square feet (variable measure trade item)" +352 format="N6" type="decimal" name="AREA (yd<sup>2</sup>)" description="Area, square yards (variable measure trade item)" +353 format="N6" type="decimal" name="AREA (in<sup>2</sup>), log" description="Area, square inches" +354 format="N6" type="decimal" name="AREA (ft<sup>2</sup>), log" description="Area, square feet" +355 format="N6" type="decimal" name="AREA (yd<sup>2</sup>), log" description="Area, square yards" +356 format="N6" type="decimal" name="NET WEIGHT (t oz)" description="Net weight, troy ounces (variable measure trade item)" +357 format="N6" type="decimal" name="NET VOLUME (oz)" description="Net weight (or volume), ounces (variable measure trade item)" +360 format="N6" type="decimal" name="NET VOLUME (qt)" description="Net volume, quarts (variable measure trade item)" +361 format="N6" type="decimal" name="NET VOLUME (gal.)" description="Net volume, gallons U.S. (variable measure trade item)" +362 format="N6" type="decimal" name="VOLUME (qt), log" description="Logistic volume, quarts" +363 format="N6" type="decimal" name="VOLUME (gal.), log" description="Logistic volume, gallons U.S." +364 format="N6" type="decimal" name="VOLUME (in<sup>3</sup>)" description="Net volume, cubic inches (variable measure trade item)" +365 format="N6" type="decimal" name="VOLUME (ft<sup>3</sup>)" description="Net volume, cubic feet (variable measure trade item)" +366 format="N6" type="decimal" name="VOLUME (yd<sup>3</sup>)" description="Net volume, cubic yards (variable measure trade item)" +367 format="N6" type="decimal" name="VOLUME (in<sup>3</sup>), log" description="Logistic volume, cubic inches" +368 format="N6" type="decimal" name="VOLUME (ft<sup>3</sup>), log" description="Logistic volume, cubic feet" +369 format="N6" type="decimal" name="VOLUME (yd<sup>3</sup>), log" description="Logistic volume, cubic yards" +37 format="N..8" type="int" fnc1="1" name="COUNT" description="Count of trade items or trade item pieces contained in a logistic unit" +390 format="N..15" type="decimal" fnc1="1" name="AMOUNT" description="Applicable amount payable or Coupon value, local currency" +391 format="N3+N..15" type="decimal" fnc1="1" name="AMOUNT" description="Applicable amount payable with ISO currency code" +392 format="N..15" type="decimal" fnc1="1" name="PRICE" description="Applicable amount payable, single monetary area (variable measure trade item)" +393 format="N3+N..15" type="decimal" fnc1="1" name="PRICE" description="Applicable amount payable with ISO currency code (variable measure trade item)" +394 format="N4" type="decimal" fnc1="1" name="PRCNT OFF" description="Percentage discount of a coupon" +400 format="X..30" type="str" fnc1="1" name="ORDER NUMBER" description="Customers purchase order number" +401 format="X..30" type="str" fnc1="1" name="GINC" description="Global Identification Number for Consignment (GINC)" +402 format="N17" type="str" fnc1="1" name="GSIN" description="Global Shipment Identification Number (GSIN)" +403 format="X..30" type="str" fnc1="1" name="ROUTE" description="Routing code" +410 format="N13" type="str" name="SHIP TO LOC" description="Ship to - Deliver to Global Location Number" +411 format="N13" type="str" name="BILL TO" description="Bill to - Invoice to Global Location Number" +412 format="N13" type="str" name="PURCHASE FROM" description="Purchased from Global Location Number" +413 format="N13" type="str" name="SHIP FOR LOC" description="Ship for - Deliver for - Forward to Global Location Number" +414 format="N13" type="str" name="LOC No" description="Identification of a physical location - Global Location Number" +415 format="N13" type="str" name="PAY TO" description="Global Location Number of the invoicing party" +416 format="N13" type="str" name="PROD/SERV LOC" description="GLN of the production or service location" +417 format="N13" type="str" name="PARTY" description="Party GLN" +420 format="X..20" type="str" fnc1="1" name="SHIP TO POST" description="Ship to - Deliver to postal code within a single postal authority" +421 format="N3+X..9" type="str" fnc1="1" name="SHIP TO POST" description="Ship to - Deliver to postal code with ISO country code" +422 format="N3" type="int" fnc1="1" name="ORIGIN" description="Country of origin of a trade item" +423 format="N3+N..12" type="str" fnc1="1" name="COUNTRY - INITIAL PROCESS." description="Country of initial processing" +424 format="N3" type="int" fnc1="1" name="COUNTRY - PROCESS." description="Country of processing" +425 format="N3+N..12" type="str" fnc1="1" name="COUNTRY - DISASSEMBLY" description="Country of disassembly" +426 format="N3" type="int" fnc1="1" name="COUNTRY - FULL PROCESS" description="Country covering full process chain" +427 format="X..3" type="str" fnc1="1" name="ORIGIN SUBDIVISION" description="Country subdivision Of origin" +7001 format="N13" type="str" fnc1="1" name="NSN" description="NATO Stock Number (NSN)" +7002 format="X..30" type="str" fnc1="1" name="MEAT CUT" description="UN/ECE meat carcasses and cuts classification" +7003 format="N10" type="date" fnc1="1" name="EXPIRY TIME" description="Expiration date and time" +7004 format="N..4" type="str" fnc1="1" name="ACTIVE POTENCY" description="Active potency" +7005 format="X..12" type="str" fnc1="1" name="CATCH AREA" description="Catch area" +7006 format="N6" type="date" fnc1="1" name="FIRST FREEZE DATE" description="First freeze date" +7007 format="N6..12" type="date" fnc1="1" name="HARVEST DATE" description="Harvest date" +7008 format="X..3" type="str" fnc1="1" name="AQUATIC SPECIES" description="Species for fishery purposes" +7009 format="X..10" type="str" fnc1="1" name="FISHING GEAR TYPE" description="Fishing gear type" +7010 format="X..2" type="str" fnc1="1" name="PROD METHOD" description="Production method" +7020 format="X..20" type="str" fnc1="1" name="REFURB LOT" description="Refurbishment lot ID" +7021 format="X..20" type="str" fnc1="1" name="FUNC STAT" description="Functional status" +7022 format="X..20" type="str" fnc1="1" name="REV STAT" description="Revision status" +7023 format="X..30" type="str" fnc1="1" name="GIAI - ASSEMBLY" description="Global Individual Asset Identifier (GIAI) of an assembly" +7030 format="N3+X..27" type="str" fnc1="1" name="PROCESSOR # 0" description="Number of processor with ISO Country Code" +7031 format="N3+X..27" type="str" fnc1="1" name="PROCESSOR # 1" description="Number of processor with ISO Country Code" +7032 format="N3+X..27" type="str" fnc1="1" name="PROCESSOR # 2" description="Number of processor with ISO Country Code" +7033 format="N3+X..27" type="str" fnc1="1" name="PROCESSOR # 3" description="Number of processor with ISO Country Code" +7034 format="N3+X..27" type="str" fnc1="1" name="PROCESSOR # 4" description="Number of processor with ISO Country Code" +7035 format="N3+X..27" type="str" fnc1="1" name="PROCESSOR # 5" description="Number of processor with ISO Country Code" +7036 format="N3+X..27" type="str" fnc1="1" name="PROCESSOR # 6" description="Number of processor with ISO Country Code" +7037 format="N3+X..27" type="str" fnc1="1" name="PROCESSOR # 7" description="Number of processor with ISO Country Code" +7038 format="N3+X..27" type="str" fnc1="1" name="PROCESSOR # 8" description="Number of processor with ISO Country Code" +7039 format="N3+X..27" type="str" fnc1="1" name="PROCESSOR # 9" description="Number of processor with ISO Country Code" +7040 format="N1+X3" type="str" fnc1="1" name="UIC+EXT" description="GS1 UIC with Extension 1 and Importer index" +710 format="X..20" type="str" fnc1="1" name="NHRN PZN" description="National Healthcare Reimbursement Number (NHRN) - Germany PZN" +711 format="X..20" type="str" fnc1="1" name="NHRN CIP" description="National Healthcare Reimbursement Number (NHRN) - France CIP" +712 format="X..20" type="str" fnc1="1" name="NHRN CN" description="National Healthcare Reimbursement Number (NHRN) - Spain CN" +713 format="X..20" type="str" fnc1="1" name="NHRN DRN" description="National Healthcare Reimbursement Number (NHRN) - Brasil DRN" +714 format="X..20" type="str" fnc1="1" name="NHRN AIM" description="National Healthcare Reimbursement Number (NHRN) - Portugal AIM" +7230 format="X2+X..28" type="str" fnc1="1" name="CERT #1" description="Certification reference" +7231 format="X2+X..28" type="str" fnc1="1" name="CERT #2" description="Certification reference" +7232 format="X2+X..28" type="str" fnc1="1" name="CERT #3" description="Certification reference" +7233 format="X2+X..28" type="str" fnc1="1" name="CERT #4" description="Certification reference" +7234 format="X2+X..28" type="str" fnc1="1" name="CERT #5" description="Certification reference" +7235 format="X2+X..28" type="str" fnc1="1" name="CERT #6" description="Certification reference" +7236 format="X2+X..28" type="str" fnc1="1" name="CERT #7" description="Certification reference" +7237 format="X2+X..28" type="str" fnc1="1" name="CERT #8" description="Certification reference" +7238 format="X2+X..28" type="str" fnc1="1" name="CERT #9" description="Certification reference" +7239 format="X2+X..28" type="str" fnc1="1" name="CERT #10" description="Certification reference" +7240 format="X..20" type="str" fnc1="1" name="PROTOCOL" description="Protocol ID" +8001 format="N14" type="str" fnc1="1" name="DIMENSIONS" description="Roll products (width, length, core diameter, direction, splices)" +8002 format="X..20" type="str" fnc1="1" name="CMT No" description="Cellular mobile telephone identifier" +8003 format="N14+X..16" type="str" fnc1="1" name="GRAI" description="Global Returnable Asset Identifier (GRAI)" +8004 format="X..30" type="str" fnc1="1" name="GIAI" description="Global Individual Asset Identifier (GIAI)" +8005 format="N6" type="str" fnc1="1" name="PRICE PER UNIT" description="Price per unit of measure" +8006 format="N14+N2+N2" type="str" fnc1="1" name="ITIP" description="Identification of an individual trade item piece" +8007 format="X..34" type="str" fnc1="1" name="IBAN" description="International Bank Account Number (IBAN)" +8008 format="N8+N..4" type="date" fnc1="1" name="PROD TIME" description="Date and time of production" +8009 format="X..50" type="str" fnc1="1" name="OPTSEN" description="Optically Readable Sensor Indicator" +8010 format="Y..30" type="str" fnc1="1" name="CPID" description="Component/Part Identifier (CPID)" +8011 format="N..12" type="str" fnc1="1" name="CPID SERIAL" description="Component/Part Identifier serial number (CPID SERIAL)" +8012 format="X..20" type="str" fnc1="1" name="VERSION" description="Software version" +8013 format="X..30" type="str" fnc1="1" name="GMN (for medical devices, the default, global data title is BUDI-DI)" description="Global Model Number (GMN)" +8017 format="N18" type="str" fnc1="1" name="GSRN - PROVIDER" description="Global Service Relation Number to identify the relationship between an organisation offering services and the provider of services" +8018 format="N18" type="str" fnc1="1" name="GSRN - RECIPIENT" description="Global Service Relation Number to identify the relationship between an organisation offering services and the recipient of services" +8019 format="N..10" type="str" fnc1="1" name="SRIN" description="Service Relation Instance Number (SRIN)" +8020 format="X..25" type="str" fnc1="1" name="REF No" description="Payment slip reference number" +8026 format="N14+N2+N2" type="str" fnc1="1" name="ITIP CONTENT" description="Identification of pieces of a trade item (ITIP) contained in a logistic unit" +8110 format="X..70" type="str" fnc1="1" name="" description="Coupon code identification for use in North America" +8111 format="N4" type="str" fnc1="1" name="POINTS" description="Loyalty points of a coupon" +8112 format="X..70" type="str" fnc1="1" name="" description="Paperless coupon code identification for use in North America" +8200 format="X..70" type="str" fnc1="1" name="PRODUCT URL" description="Extended Packaging URL" +90 format="X..30" type="str" fnc1="1" name="INTERNAL" description="Information mutually agreed between trading partners" +91-99 format="X..90" type="str" fnc1="1" name="INTERNAL" description="Company internal information" diff --git a/tests/test_gs1_128.doctest b/tests/test_gs1_128.doctest new file mode 100644 index 0000000..8bfa9af --- /dev/null +++ b/tests/test_gs1_128.doctest @@ -0,0 +1,148 @@ +test_gs1_128.doctest - more detailed doctests for the stdnum.gs1_128 module + +Copyright (C) 2019 Sergi Almacellas Abellaan +Copyright (C) 2020 Arthur de Jong + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +02110-1301 USA + + +This file contains more detailed doctests for the stdnum.gs1_128 module. It +tries to test more corner cases and detailed functionality that is not +really useful as module documentation. + +>>> from decimal import Decimal +>>> import datetime +>>> import pprint +>>> from stdnum import gs1_128 + + +>>> gs1_128.compact('(01)38425876095074(17)181119(37)1 ') +'013842587609507417181119371' + + +We can create a GS1-128 code based on data we provide. Various data types +will be converted to the correct representation. + +>>> gs1_128.encode({'01': '38425876095074', '17': datetime.date(2018, 11, 19), '37': 1}, parentheses=True) +'(01)38425876095074(17)181119(37)1' +>>> gs1_128.encode({'02': '98412345678908', '310': 17.23, '37': 32}) +'029841234567890831020017233732' +>>> gs1_128.encode({'03': '1234'}) # unknown AI +Traceback (most recent call last): + ... +InvalidComponent: ... + +If we have a separator we use it to separate variable-length values, otherwise +we pad all variable-length values to the maximum length (except the last one). + +>>> gs1_128.encode({'01': '58425876097843', '10': '123456', '37': 18, '390': 42, '392': 12}, parentheses=True) +'(01)58425876097843(10)123456 (37)00000018(390)0000000000000042(392)012' +>>> gs1_128.encode({'01': '58425876097843', '10': '123456', '37': 18, '390': 42, '392': 12}, parentheses=True, separator='[FNC1]') +'(01)58425876097843(10)123456[FNC1](37)18[FNC1](390)042[FNC1](392)012' + +Numeric values can be provided in several forms and precision is encoded +properly. + +>>> gs1_128.encode({ +... '310': 17.23, # float +... '311': 456, # int +... '312': 1.0 / 3.0, # float with lots of digits +... '313': '123.456', # str +... '391': ('123', Decimal('123.456')), # currency number combo +... }, parentheses=True) +'(310)2001723(311)0000456(312)5033333(313)3123456(391)3123123456' + +We generate dates in various formats, depending on the AI. + +>>> gs1_128.encode({ +... '11': datetime.datetime(2018, 11, 19, 0, 0, 0), +... '12': '181119', # if you provide a string value, it is expected to be correct +... '7003': datetime.datetime(2018, 11, 19, 12, 45, 13), +... '7007': (datetime.date(2018, 11, 19), datetime.date(2018, 11, 21)), +... }, parentheses=True) +'(11)181119(12)181119(7003)1811191245(7007)181119181121' +>>> gs1_128.encode({'8008': datetime.datetime(2018, 11, 19, 12, 45, 13)}, parentheses=True) +'(8008)181119124513' +>>> gs1_128.encode({'8008': datetime.datetime(2018, 11, 19, 12, 45)}, parentheses=True) +'(8008)1811191245' +>>> gs1_128.encode({'8008': datetime.datetime(2018, 11, 19, 12, 0)}, parentheses=True) +'(8008)18111912' +>>> gs1_128.encode({'8008': datetime.datetime(2018, 11, 19, 0, 0)}, parentheses=True) +'(8008)18111900' + +If we try to encode an invalid EAN we will get an error. + +>>> gs1_128.encode({'01': '38425876095079'}, parentheses=True) +Traceback (most recent call last): + ... +InvalidChecksum: ... + + +We can decode (parse) the GS1-128 code to a dictionary with information about +the structure of the number. + +pprint.pprint(gs1_128.info('(01)38425876095074(17)181119(37)1 ')) +{'01': '38425876095074', '17': datetime.date(2018, 11, 19), '37': 1} +>>> pprint.pprint(gs1_128.info('013842587609507417181119371')) +{'01': '38425876095074', '17': datetime.date(2018, 11, 19), '37': 1} +>>> pprint.pprint(gs1_128.info('(02)98412345678908(310)3017230(37)32')) +{'02': '98412345678908', '310': Decimal('17.230'), '37': 32} +>>> pprint.pprint(gs1_128.info('(01)58425876097843(10)123456 (17)181119(37)18')) +{'01': '58425876097843', '10': '123456', '17': datetime.date(2018, 11, 19), '37': 18} +>>> pprint.pprint(gs1_128.info('|(01)58425876097843|(10)123456|(17)181119(37)18', separator='|')) +{'01': '58425876097843', '10': '123456', '17': datetime.date(2018, 11, 19), '37': 18} +>>> gs1_128.info('(03)38425876095074') # unknown AI +Traceback (most recent call last): + ... +InvalidComponent: ... + +We can decode decimal values from various formats. + +>>> pprint.pprint(gs1_128.info('(310)5033333')) +{'310': Decimal('0.33333')} +>>> pprint.pprint(gs1_128.info('(310)0033333')) +{'310': Decimal('33333')} +>>> pprint.pprint(gs1_128.info('(391)3123123456')) +{'391': ('123', Decimal('123.456'))} + +We an decode date files from various formats. + +>>> pprint.pprint(gs1_128.info('(11)181119')) +{'11': datetime.date(2018, 11, 19)} +>>> pprint.pprint(gs1_128.info('(7003)1811191245')) +{'7003': datetime.datetime(2018, 11, 19, 12, 45)} +>>> pprint.pprint(gs1_128.info('(7007)181119')) +{'7007': datetime.date(2018, 11, 19)} +>>> pprint.pprint(gs1_128.info('(7007)181119181121')) +{'7007': (datetime.date(2018, 11, 19), datetime.date(2018, 11, 21))} +>>> pprint.pprint(gs1_128.info('(8008)18111912')) +{'8008': datetime.datetime(2018, 11, 19, 12, 0)} + + +While the compact() function can clean up the number somewhat the validate() +function calls info() and then encode() to ensure an even more compact and +consistent format. + +>>> gs1_128.compact('(01)58425876097843(10)123456 (37)00000018') +'015842587609784310123456 3700000018' +>>> gs1_128.validate('(01)58425876097843(10)123456 (37)00000018') +'015842587609784310123456 3718' +>>> gs1_128.validate('(01)58425876097843(10)123456 (37)00000018', separator='|') +'015842587609784310123456|3718' +>>> gs1_128.validate('30aa') +Traceback (most recent call last): + ... +InvalidFormat: ... diff --git a/update/gs1_ai.py b/update/gs1_ai.py new file mode 100755 index 0000000..527a1c5 --- /dev/null +++ b/update/gs1_ai.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 + +# update/gs1_ai.py - script to get GS1 application identifiers +# +# Copyright (C) 2019 Arthur de Jong +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 USA + +"""This script downloads GS1 application identifiers from the GS1 web site.""" + +import datetime +import json +import re + +import lxml.html +import requests + + +# the location of the GS1 application identifiers +download_url = 'https://www.gs1.org/standards/barcodes/application-identifiers' + + +def fetch_ais(): + """Download application identifiers frm the GS1 website.""" + response = requests.get(download_url) + document = lxml.html.document_fromstring(response.content) + element = document.findall('.//script[@type="application/ld+json"]')[1] + for entry in json.loads(element.text)['@graph']: + yield ( + entry['skos:prefLabel'].strip(), # AI + entry['gs1meta:formatAIvalue'].strip()[3:], # format + entry['gs1meta:requiresFNC1'], # require FNC1 + [x['@value'] for x in entry['schema:name'] if x['@language'] == 'en'][0].strip(), + [x['@value'] for x in entry['schema:description'] if x['@language'] == 'en'][0].strip()) + + +def group_ai_ranges(): + """Combine downloaded application identifiers into ranges.""" + first = None + prev = (None, ) * 5 + for value in sorted(fetch_ais()): + if value[1:] != prev[1:]: + if first: + yield first, *prev + first = value[0] + prev = value + yield first, *prev + + +if __name__ == '__main__': + print('# generated from %s' % download_url) + print('# on %s' % datetime.datetime.utcnow()) + for ai1, ai2, format, require_fnc1, name, description in group_ai_ranges(): + _type = 'str' + if re.match(r'^(N8\+)?N[0-9]*[.]*[0-9]+$', format) and 'date' in description.lower(): + _type = 'date' + elif re.match(r'^N[.]*[0-9]+$', format) and 'count' in description.lower(): + _type = 'int' + ai = ai1 + if ai1 != ai2: + if len(ai1) == 4: + ai = ai1[:3] + _type = 'decimal' + else: + ai = '%s-%s' % (ai1, ai2) + print('%s format="%s" type="%s"%s name="%s" description="%s"' % ( + ai, format, _type, + ' fnc1="1"' if require_fnc1 else '', + name, description)) |