Skip to content

Commit

Permalink
Merge commit 'refs/pull/521/head' of github.com:OCA/wms into merge-br…
Browse files Browse the repository at this point in the history
…anch-2477-bscos-4243-d0a32794
  • Loading branch information
TDu committed Nov 3, 2023
2 parents 322f5c7 + 7b5a0a3 commit 64443dc
Show file tree
Hide file tree
Showing 19 changed files with 408 additions and 1 deletion.
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
# generated from manifests external_dependencies
biip==2.3.0
1 change: 1 addition & 0 deletions setup/shopfloor_gs1/odoo/addons/shopfloor_gs1
6 changes: 6 additions & 0 deletions setup/shopfloor_gs1/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import setuptools

setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)
1 change: 1 addition & 0 deletions shopfloor_gs1/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
bot, please!
1 change: 1 addition & 0 deletions shopfloor_gs1/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import actions
25 changes: 25 additions & 0 deletions shopfloor_gs1/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Copyright 2022 Camptocamp SA (http://www.camptocamp.com)
# @author Simone Orsi <[email protected]>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

{
"name": "Shopfloor GS1",
"summary": "Integrate GS1 barcode scan into Shopfloor app",
"version": "14.0.1.0.0",
"development_status": "Beta",
"category": "Inventory",
"website": "https://github.com/OCA/wms",
"author": "Camptocamp, Odoo Community Association (OCA)",
"maintainers": ["simahawk", "sebalix"],
"license": "AGPL-3",
"depends": ["shopfloor"],
"external_dependencies": {
"python": [
# >= 2.3.0 required to use 'GS1Message.parse_hri' method
# and next version 3.0.0 has been refactored bringing
# incompatibility issues (to check later).
"biip==2.3.0"
]
},
"data": [],
}
1 change: 1 addition & 0 deletions shopfloor_gs1/actions/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import search
52 changes: 52 additions & 0 deletions shopfloor_gs1/actions/search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Copyright 2022 Camptocamp SA (http://www.camptocamp.com)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from odoo.addons.component.core import Component

from ..config import MAPPING_AI_TO_TYPE, MAPPING_TYPE_TO_AI
from ..utils import GS1Barcode


class SearchAction(Component):
_inherit = "shopfloor.search.action"

def _search_type_to_gs1_ai(self, _type):
"""Convert search type to AIs.
Each type can be mapped to multiple AIs.
For instance, you can search a product by barcode (01) or manufacturer code (240).
"""
return MAPPING_TYPE_TO_AI.get(_type)

def _gs1_ai_to_search_type(self, ai):
"""Convert back GS1 AI to search type."""
return MAPPING_AI_TO_TYPE[ai]

def find(self, barcode, types=None, handler_kw=None):
barcode = barcode or ""
# Try to find records via GS1 and fallback to normal search
res = self._find_gs1(barcode, types=types)
if res:
return res
return super().find(barcode, types=types, handler_kw=handler_kw)

def _find_gs1(self, barcode, types=None, handler_kw=None, safe=True):
types = types or ()
ai_whitelist = ()
# Collect all AIs by converting from search types
for _type in types:
ai = self._search_type_to_gs1_ai(_type)
if ai:
ai_whitelist += ai
if types and not ai_whitelist:
# A specific type was asked but no AI could be found.
return
parsed = GS1Barcode.parse(barcode, ai_whitelist=ai_whitelist, safe=safe)
# Return the 1st record found if parsing was successful
for item in parsed:
record = self.generic_find(
item.value,
types=(self._gs1_ai_to_search_type(item.ai),),
handler_kw=handler_kw,
)
if record:
return record
26 changes: 26 additions & 0 deletions shopfloor_gs1/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Copyright 2022 Camptocamp SA (http://www.camptocamp.com)
# @author Simone Orsi <[email protected]>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

# https://www.gs1.org/standards/barcodes/application-identifiers
# TODO: define other internal mappings by convention


# Each type can be mapped to multiple AIs.
# For instance, you can search a product by barcode (01) or manufacturer code (240).
MAPPING_TYPE_TO_AI = {
"product": ("01", "240"),
"lot": ("10",),
"production_date": ("11",),
"serial": ("21",),
"manuf_product_code": ("240",),
"location": ("254",),
}
MAPPING_AI_TO_TYPE = {
"01": "product",
"10": "lot",
"11": "production_date",
"21": "serial",
"240": "product",
"254": "location",
}
2 changes: 2 additions & 0 deletions shopfloor_gs1/readme/CONTRIBUTORS.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
* Simone Orsi <[email protected]>
* Sébastien Alix <[email protected]>
5 changes: 5 additions & 0 deletions shopfloor_gs1/readme/DESCRIPTION.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Add GS1 barcode support to Shopfloor.

Based on https://biip.readthedocs.io/

TODO....
1 change: 1 addition & 0 deletions shopfloor_gs1/readme/USAGE.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
TODO
3 changes: 3 additions & 0 deletions shopfloor_gs1/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from . import test_utils
from . import test_action_search
from . import test_scan_anything
8 changes: 8 additions & 0 deletions shopfloor_gs1/tests/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
PROD_BARCODE = "09506000117843"
MANUF_CODE = "K000075"
DATE = "141231"
LOT1 = "1234AB"
LOT2 = "1234AC"
GS1_GTIN_BARCODE_1 = f"(01){PROD_BARCODE}(11){DATE}(10){LOT1}"
GS1_GTIN_BARCODE_2 = f"(01){PROD_BARCODE}(11){DATE}(10){LOT2}"
GS1_MANUF_BARCODE = f"(240){MANUF_CODE}(11){DATE}(10){LOT1}"
73 changes: 73 additions & 0 deletions shopfloor_gs1/tests/test_action_search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Copyright 2022 Camptocamp SA (http://www.camptocamp.com)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from odoo.addons.shopfloor.tests.test_actions_search import TestSearchBaseCase

from .common import (
GS1_GTIN_BARCODE_1,
GS1_MANUF_BARCODE,
LOT1,
MANUF_CODE,
PROD_BARCODE,
)


class TestFind(TestSearchBaseCase):
@classmethod
def setUpClassBaseData(cls):
super().setUpClassBaseData()
cls.product_a.barcode = PROD_BARCODE

def test_find_picking(self):
ptype = self.env.ref("shopfloor.picking_type_single_pallet_transfer_demo")
rec = self._create_picking(picking_type=ptype)
res = self.search.find(rec.name, types=("picking",))
self.assertEqual(res.record, rec)

def test_find_location(self):
rec = self.customer_location
barcode = GS1_GTIN_BARCODE_1 + "(254)" + rec.name
res = self.search.find(barcode, types=("location",))
self.assertEqual(res.record, rec)
res = self.search.find(rec.name, types=("location",))
self.assertEqual(res.record, rec)

def test_find_package(self):
rec = self.env["stock.quant.package"].sudo().create({"name": "ABC1234"})
res = self.search.find(rec.name, types=("package",))
self.assertEqual(res.record, rec)

def test_find_product(self):
rec = self.product_a
res = self.search.find(GS1_GTIN_BARCODE_1, types=("product",))
self.assertEqual(res.record, rec)
rec.barcode = MANUF_CODE
res = self.search.find(GS1_MANUF_BARCODE, types=("product",))
self.assertEqual(res.record, rec)

def test_find_lot(self):
rec = (
self.env["stock.production.lot"]
.sudo()
.create(
{
"product_id": self.product_a.id,
"company_id": self.env.company.id,
"name": LOT1,
}
)
)
res = self.search.find(
GS1_GTIN_BARCODE_1,
types=("lot",),
handler_kw=dict(lot=dict(products=self.product_a)),
)
self.assertEqual(res.record, rec)

def test_find_generic_packaging(self):
rec = (
self.env["product.packaging"]
.sudo()
.create({"name": "TEST PKG", "barcode": "1234"})
)
res = self.search.find(rec.barcode, types=("delivery_packaging",))
self.assertEqual(res.record, rec)
71 changes: 71 additions & 0 deletions shopfloor_gs1/tests/test_scan_anything.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Copyright 2023 Camptocamp SA (http://www.camptocamp.com)
# @author Simone Orsi <[email protected]>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

from odoo.addons.shopfloor.tests.test_actions_data_base import ActionsDataDetailCaseBase
from odoo.addons.shopfloor_base.tests.common_misc import ScanAnythingTestMixin

from .common import (
GS1_GTIN_BARCODE_1,
GS1_MANUF_BARCODE,
LOT1,
MANUF_CODE,
PROD_BARCODE,
)


class ScanAnythingCase(ActionsDataDetailCaseBase, ScanAnythingTestMixin):
def test_scan_product(self):
record = self.product_b
record.barcode = PROD_BARCODE
record.default_code = MANUF_CODE
rec_type = "product"
data = self.data_detail.product_detail(record)
# All kinds of search supported
for identifier in (
GS1_GTIN_BARCODE_1,
GS1_MANUF_BARCODE,
record.barcode,
record.default_code,
):
self._test_response_ok(rec_type, data, identifier)

def test_find_location(self):
record = self.stock_location
rec_type = "location"
gs1_barcode = GS1_GTIN_BARCODE_1 + "(254)" + record.name
data = self.data_detail.location_detail(record)
for identifier in (gs1_barcode, record.name):
self._test_response_ok(rec_type, data, identifier)

def test_scan_package(self):
record = self.package
rec_type = "package"
identifier = record.name
data = self.data_detail.package_detail(record)
self._test_response_ok(rec_type, data, identifier)

def test_scan_lot(self):
record = (
self.env["stock.production.lot"]
.sudo()
.create(
{
"product_id": self.product_a.id,
"company_id": self.env.company.id,
"name": LOT1,
}
)
)
rec_type = "lot"
identifier = record.name
data = self.data_detail.lot_detail(record)
for identifier in (GS1_GTIN_BARCODE_1, record.name):
self._test_response_ok(rec_type, data, identifier)

def test_scan_transfer(self):
record = self.picking
rec_type = "transfer"
identifier = record.name
data = self.data_detail.picking_detail(record)
self._test_response_ok(rec_type, data, identifier)
59 changes: 59 additions & 0 deletions shopfloor_gs1/tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Copyright 2022 Camptocamp
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

import datetime

from odoo.tests.common import BaseCase

from ..utils import GS1Barcode


class TestUtils(BaseCase):
def test_parse1(self):
code = "(01)09506000117843(11)141231(10)1234AB"
res = GS1Barcode.parse(code)
self.assertEqual(len(res), 3, res)
item = [x for x in res if x.ai == "01"][0]
self.assertEqual(item.code, code)
self.assertEqual(item.value, "09506000117843")
self.assertEqual(item.raw_value, "09506000117843")
item = [x for x in res if x.ai == "11"][0]
self.assertEqual(item.code, code)
self.assertEqual(item.value, datetime.date(2014, 12, 31))
self.assertEqual(item.raw_value, "141231")
item = [x for x in res if x.ai == "10"][0]
self.assertEqual(item.code, code)
self.assertEqual(item.value, "1234AB")
self.assertEqual(item.raw_value, "1234AB")

def test_parse2(self):
code = "(01)09506000117843(11)141231(10)1234AB"
res = GS1Barcode.parse(code, ai_whitelist=("01",))
self.assertEqual(len(res), 1, res)
item = [x for x in res if x.ai == "01"][0]
self.assertEqual(item.code, code)
self.assertEqual(item.value, "09506000117843")
self.assertEqual(item.raw_value, "09506000117843")

def test_parse3(self):
code = "(240)K000075(11)230201(10)0000392"
res = GS1Barcode.parse(code, ai_whitelist=("240",))
self.assertEqual(len(res), 1, res)
item = [x for x in res if x.ai == "240"][0]
self.assertEqual(item.code, code)
self.assertEqual(item.value, "K000075")
self.assertEqual(item.raw_value, "K000075")

def test_parse_order(self):
"""Ensure ai whitelist order is respected"""
code = "(01)09506000117843(11)141231(10)1234AB"
res = GS1Barcode.parse(code, ai_whitelist=("10", "01", "11"))
self.assertEqual(len(res), 3, res)
self.assertEqual(res[0].ai, "10")
self.assertEqual(res[1].ai, "01")
self.assertEqual(res[2].ai, "11")
res = GS1Barcode.parse(code, ai_whitelist=("01", "11", "10"))
self.assertEqual(len(res), 3, res)
self.assertEqual(res[0].ai, "01")
self.assertEqual(res[1].ai, "11")
self.assertEqual(res[2].ai, "10")
Loading

0 comments on commit 64443dc

Please sign in to comment.