From c2f76f6c97b1357454ac84e7094de7fc70ae9df8 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 13 Jan 2025 09:19:03 -0600 Subject: [PATCH] fix: add "other" menu entry, plus docs/tests --- docs/Makefile | 20 ++ docs/_static/.keepme | 0 docs/api/sideshow_corepos.batch.neworder.rst | 6 + docs/api/sideshow_corepos.batch.rst | 6 + docs/api/sideshow_corepos.rst | 6 + docs/api/sideshow_corepos.web.app.rst | 6 + docs/api/sideshow_corepos.web.menus.rst | 6 + docs/api/sideshow_corepos.web.rst | 6 + docs/api/sideshow_corepos.web.views.rst | 6 + docs/conf.py | 40 +++ docs/index.rst | 22 ++ docs/make.bat | 35 +++ src/sideshow_corepos/batch/neworder.py | 19 +- src/sideshow_corepos/web/app.py | 3 +- src/sideshow_corepos/web/menus.py | 23 +- src/sideshow_corepos/web/views.py | 2 + tests/batch/test_neworder.py | 261 +++++++++++++++++++ tests/web/test_app.py | 32 +++ tests/web/test_init.py | 11 + tests/web/test_menus.py | 51 ++++ 20 files changed, 546 insertions(+), 15 deletions(-) create mode 100644 docs/Makefile create mode 100644 docs/_static/.keepme create mode 100644 docs/api/sideshow_corepos.batch.neworder.rst create mode 100644 docs/api/sideshow_corepos.batch.rst create mode 100644 docs/api/sideshow_corepos.rst create mode 100644 docs/api/sideshow_corepos.web.app.rst create mode 100644 docs/api/sideshow_corepos.web.menus.rst create mode 100644 docs/api/sideshow_corepos.web.rst create mode 100644 docs/api/sideshow_corepos.web.views.rst create mode 100644 docs/conf.py create mode 100644 docs/index.rst create mode 100644 docs/make.bat create mode 100644 tests/batch/test_neworder.py create mode 100644 tests/web/test_app.py create mode 100644 tests/web/test_init.py create mode 100644 tests/web/test_menus.py diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d4bb2cb --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/_static/.keepme b/docs/_static/.keepme new file mode 100644 index 0000000..e69de29 diff --git a/docs/api/sideshow_corepos.batch.neworder.rst b/docs/api/sideshow_corepos.batch.neworder.rst new file mode 100644 index 0000000..cb69fb0 --- /dev/null +++ b/docs/api/sideshow_corepos.batch.neworder.rst @@ -0,0 +1,6 @@ + +``sideshow_corepos.batch.neworder`` +=================================== + +.. automodule:: sideshow_corepos.batch.neworder + :members: diff --git a/docs/api/sideshow_corepos.batch.rst b/docs/api/sideshow_corepos.batch.rst new file mode 100644 index 0000000..df4af01 --- /dev/null +++ b/docs/api/sideshow_corepos.batch.rst @@ -0,0 +1,6 @@ + +``sideshow_corepos.batch`` +========================== + +.. automodule:: sideshow_corepos.batch + :members: diff --git a/docs/api/sideshow_corepos.rst b/docs/api/sideshow_corepos.rst new file mode 100644 index 0000000..30dda1e --- /dev/null +++ b/docs/api/sideshow_corepos.rst @@ -0,0 +1,6 @@ + +``sideshow_corepos`` +==================== + +.. automodule:: sideshow_corepos + :members: diff --git a/docs/api/sideshow_corepos.web.app.rst b/docs/api/sideshow_corepos.web.app.rst new file mode 100644 index 0000000..535034b --- /dev/null +++ b/docs/api/sideshow_corepos.web.app.rst @@ -0,0 +1,6 @@ + +``sideshow_corepos.web.app`` +============================ + +.. automodule:: sideshow_corepos.web.app + :members: diff --git a/docs/api/sideshow_corepos.web.menus.rst b/docs/api/sideshow_corepos.web.menus.rst new file mode 100644 index 0000000..4f7fbdf --- /dev/null +++ b/docs/api/sideshow_corepos.web.menus.rst @@ -0,0 +1,6 @@ + +``sideshow_corepos.web.menus`` +============================== + +.. automodule:: sideshow_corepos.web.menus + :members: diff --git a/docs/api/sideshow_corepos.web.rst b/docs/api/sideshow_corepos.web.rst new file mode 100644 index 0000000..a7ac2bb --- /dev/null +++ b/docs/api/sideshow_corepos.web.rst @@ -0,0 +1,6 @@ + +``sideshow_corepos.web`` +======================== + +.. automodule:: sideshow_corepos.web + :members: diff --git a/docs/api/sideshow_corepos.web.views.rst b/docs/api/sideshow_corepos.web.views.rst new file mode 100644 index 0000000..d837759 --- /dev/null +++ b/docs/api/sideshow_corepos.web.views.rst @@ -0,0 +1,6 @@ + +``sideshow_corepos.web.views`` +============================== + +.. automodule:: sideshow_corepos.web.views + :members: diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..a5ee51b --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,40 @@ +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +from importlib.metadata import version as get_version + +project = 'Sideshow-COREPOS' +copyright = '2025, Lance Edgar' +author = 'Lance Edgar' +release = '0.1' +release = get_version('Sideshow-COREPOS') + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.intersphinx', + 'sphinx.ext.viewcode', + 'sphinx.ext.todo', +] + +templates_path = ['_templates'] +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +intersphinx_mapping = { + 'sideshow': ('https://rattailproject.org/docs/sideshow/', None), + 'wuttjamaican': ('https://rattailproject.org/docs/wuttjamaican/', None), +} + + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = 'furo' +html_static_path = ['_static'] diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..072a982 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,22 @@ + +Sideshow-COREPOS +================ + +This is `Sideshow`_ with integration for `CORE-POS`_. + +.. _Sideshow: https://pypi.org/project/Sideshow/ + +.. _CORE-POS: https://www.core-pos.com/ + + +.. toctree:: + :maxdepth: 1 + :caption: API + + api/sideshow_corepos + api/sideshow_corepos.batch + api/sideshow_corepos.batch.neworder + api/sideshow_corepos.web + api/sideshow_corepos.web.app + api/sideshow_corepos.web.menus + api/sideshow_corepos.web.views diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..32bb245 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/src/sideshow_corepos/batch/neworder.py b/src/sideshow_corepos/batch/neworder.py index 4b94ba8..694604c 100644 --- a/src/sideshow_corepos/batch/neworder.py +++ b/src/sideshow_corepos/batch/neworder.py @@ -34,8 +34,9 @@ from sideshow.batch import neworder as base class NewOrderBatchHandler(base.NewOrderBatchHandler): """ - Custom batch handler which can use CORE-POS as external data - source for customers and products. + Custom :term:`handler` for :term:`new order batches ` which can use CORE-POS as external data source for + customers and products. See parent class :class:`~sideshow:sideshow.batch.neworder.NewOrderBatchHandler` @@ -96,7 +97,7 @@ class NewOrderBatchHandler(base.NewOrderBatchHandler): .options(orm.joinedload(op_model.CustomerClassic.member_info))\ .one() except orm.exc.NoResultFound: - raise ValueError(f"CORE-POS Customer not found: {customer_id}") + raise ValueError(f"CORE-POS Customer not found: {batch.customer_id}") batch.customer_name = str(customer) batch.phone_number = customer.member_info.phone @@ -132,19 +133,16 @@ class NewOrderBatchHandler(base.NewOrderBatchHandler): # get results def result(product): return {'value': product.upc, - 'label': product.formatted_name} + 'label': self.app.make_full_name(product.brand, + product.description, + product.size)} results = [result(c) for c in products] op_session.close() return results def get_product_info_external(self, session, product_id, user=None): - """ - Returns basic info for an :term:`external product` as pertains - to ordering. - - There is no default logic here; subclass must implement. - """ + """ """ corepos = self.app.get_corepos_handler() op_model = corepos.get_model_office_op() op_session = corepos.make_session_office_op() @@ -162,7 +160,6 @@ class NewOrderBatchHandler(base.NewOrderBatchHandler): 'brand_name': product.brand, 'description': product.description, 'size': product.size, - # 'full_description': product.formatted_name, 'full_description': self.app.make_full_name(product.brand, product.description, product.size), diff --git a/src/sideshow_corepos/web/app.py b/src/sideshow_corepos/web/app.py index 0ff4fbd..6f4e327 100644 --- a/src/sideshow_corepos/web/app.py +++ b/src/sideshow_corepos/web/app.py @@ -35,7 +35,6 @@ def main(global_config, **settings): """ # prefer Sideshow templates over wuttaweb settings.setdefault('mako.directories', [ - # 'sideshow_corepos.web:templates', 'sideshow.web:templates', 'wuttaweb:templates', ]) @@ -63,6 +62,6 @@ def make_wsgi_app(): def make_asgi_app(): """ - Make and return the ASGI app. + Make and return the ASGI app (generic entry point). """ return base.make_asgi_app(main) diff --git a/src/sideshow_corepos/web/menus.py b/src/sideshow_corepos/web/menus.py index 342974b..81aa81d 100644 --- a/src/sideshow_corepos/web/menus.py +++ b/src/sideshow_corepos/web/menus.py @@ -34,7 +34,7 @@ class SideshowMenuHandler(base.SideshowMenuHandler): def make_customers_menu(self, request, **kwargs): """ - This adds the entry for CORE-POS Members.. + This adds the entry for CORE-POS Members. """ menu = super().make_customers_menu(request, **kwargs) @@ -51,7 +51,7 @@ class SideshowMenuHandler(base.SideshowMenuHandler): def make_products_menu(self, request, **kwargs): """ - This adds the entry for CORE-POS Products.. + This adds the entry for CORE-POS Products. """ menu = super().make_products_menu(request, **kwargs) @@ -65,3 +65,22 @@ class SideshowMenuHandler(base.SideshowMenuHandler): ]) return menu + + def make_other_menu(self, request, **kwargs): + """ + This adds the entry for CORE Office. + """ + menu = super().make_other_menu(request, **kwargs) + + corepos = self.app.get_corepos_handler() + url = corepos.get_office_url() + if url: + menu['items'].extend([ + { + 'title': "CORE Office", + 'url': url, + 'target': '_blank', + }, + ]) + + return menu diff --git a/src/sideshow_corepos/web/views.py b/src/sideshow_corepos/web/views.py index 2edda1e..268b20d 100644 --- a/src/sideshow_corepos/web/views.py +++ b/src/sideshow_corepos/web/views.py @@ -22,6 +22,8 @@ ################################################################################ """ Sideshow-COREPOS - custom views + +This adds config for readonly views for CORE-POS members and products. """ diff --git a/tests/batch/test_neworder.py b/tests/batch/test_neworder.py new file mode 100644 index 0000000..290d339 --- /dev/null +++ b/tests/batch/test_neworder.py @@ -0,0 +1,261 @@ +# -*- coding: utf-8; -*- + +import datetime +import decimal + +import sqlalchemy as sa + +from corepos.db.office_op import model as op_model, Session as OpSession + +from wuttjamaican.testing import DataTestCase + +from sideshow_corepos.batch import neworder as mod + + +class TestNewOrderBatchHandler(DataTestCase): + + def setUp(self): + super().setUp() + + self.op_engine = sa.create_engine('sqlite://') + self.config.core_office_op_engines = {'default': self.op_engine} + self.config.core_office_op_engine = self.op_engine + + op_model.Base.metadata.create_all(bind=self.op_engine) + + self.op_session = OpSession(bind=self.op_engine) + + def tearDown(self): + self.op_session.close() + super().tearDown() + + def make_config(self, **kwargs): + config = super().make_config(**kwargs) + config.setdefault('wutta.enum_spec', 'sideshow.enum') + return config + + def make_handler(self): + return mod.NewOrderBatchHandler(self.config) + + def test_autocomplete_cutomers_external(self): + handler = self.make_handler() + + # empty results by default + self.assertEqual(handler.autocomplete_customers_external(self.session, 'foo'), []) + + # add a member + member = op_model.MemberInfo(card_number=42) + self.op_session.add(member) + customer = op_model.CustomerClassic(first_name="Chuck", last_name="Norris", + last_change=datetime.datetime.now()) + member.customers.append(customer) + self.op_session.add(customer) + self.op_session.flush() + + # search for chuck finds chuck + results = handler.autocomplete_customers_external(self.session, 'chuck') + self.assertEqual(len(results), 1) + self.assertEqual(results[0], { + 'value': '42', + 'label': "Chuck Norris", + }) + + # search for sally finds nothing + self.assertEqual(handler.autocomplete_customers_external(self.session, 'sally'), []) + + def test_refresh_batch_from_external_customer(self): + model = self.app.model + handler = self.make_handler() + + user = model.User(username='barney') + self.session.add(user) + self.session.flush() + + batch = handler.make_batch(self.session, created_by=user) + self.session.add(batch) + self.session.flush() + + # add a member + member = op_model.MemberInfo(card_number=42, phone='555-1234', email='chuck@example.com') + self.op_session.add(member) + customer = op_model.CustomerClassic(first_name="Chuck", last_name="Norris", + last_change=datetime.datetime.now()) + member.customers.append(customer) + self.op_session.add(customer) + self.op_session.flush() + + # error if invalid customer_id + batch.customer_id = 'BreakThings!' + self.assertRaises(ValueError, handler.refresh_batch_from_external_customer, batch) + + # error if customer not found + batch.customer_id = '9999' + self.assertRaises(ValueError, handler.refresh_batch_from_external_customer, batch) + + # batch should reflect customer info + batch.customer_id = '42' + self.assertIsNone(batch.customer_name) + self.assertIsNone(batch.phone_number) + self.assertIsNone(batch.email_address) + handler.refresh_batch_from_external_customer(batch) + self.assertEqual(batch.customer_name, "Chuck Norris") + self.assertEqual(batch.phone_number, '555-1234') + self.assertEqual(batch.email_address, 'chuck@example.com') + + def test_autocomplete_products_local(self): + handler = self.make_handler() + + # empty results by default + self.assertEqual(handler.autocomplete_products_external(self.session, 'foo'), []) + + # add a product + product = op_model.Product(upc='07430500132', brand="Bragg's", + description="Vinegar", size='32oz') + self.op_session.add(product) + self.op_session.commit() + + # search for vinegar finds product + results = handler.autocomplete_products_external(self.session, 'vinegar') + self.assertEqual(len(results), 1) + self.assertEqual(results[0], { + 'value': '07430500132', + 'label': "Bragg's Vinegar 32oz", + }) + + # search for brag finds product + results = handler.autocomplete_products_external(self.session, 'brag') + self.assertEqual(len(results), 1) + self.assertEqual(results[0], { + 'value': '07430500132', + 'label': "Bragg's Vinegar 32oz", + }) + + # search for juice finds nothing + self.assertEqual(handler.autocomplete_products_external(self.session, 'juice'), []) + + def test_get_case_size_for_external_product(self): + handler = self.make_handler() + + # null + product = op_model.Product(upc='07430500132', brand="Bragg's", + description="Vinegar", size='32oz') + self.op_session.add(product) + self.op_session.commit() + self.op_session.refresh(product) + self.assertIsNone(handler.get_case_size_for_external_product(product)) + + # typical + vendor = op_model.Vendor(id=42, name='Acme Distributors') + self.op_session.add(vendor) + item = op_model.VendorItem(vendor=vendor, sku='1234', units=12.34, + vendor_item_id=1) + product.vendor_items.append(item) + self.op_session.commit() + self.op_session.refresh(product) + self.assertEqual(handler.get_case_size_for_external_product(product), + decimal.Decimal('12.3400')) + + def test_get_unit_price_reg_for_external_product(self): + handler = self.make_handler() + + # null + product = op_model.Product(upc='07430500132', brand="Bragg's", + description="Vinegar", size='32oz') + self.op_session.add(product) + self.op_session.commit() + self.op_session.refresh(product) + self.assertIsNone(handler.get_unit_price_reg_for_external_product(product)) + + # typical + product.normal_price = 4.19 + self.op_session.commit() + self.op_session.refresh(product) + self.assertEqual(handler.get_unit_price_reg_for_external_product(product), + decimal.Decimal('4.19')) + + def test_get_product_info_external(self): + model = self.app.model + handler = self.make_handler() + + user = model.User(username='barney') + self.session.add(user) + batch = handler.make_batch(self.session, created_by=user) + self.session.add(batch) + self.session.flush() + + vendor = op_model.Vendor(id=42, name='Acme Distributors') + self.op_session.add(vendor) + product = op_model.Product(upc='07430500132', brand="Bragg", + description="Vinegar", size='32oz', + normal_price=4.19) + item = op_model.VendorItem(vendor=vendor, sku='1234', units=12.34, + vendor_item_id=1) + product.vendor_items.append(item) + self.op_session.add(product) + self.op_session.commit() + + # typical + info = handler.get_product_info_external(self.session, '07430500132') + self.assertEqual(info['product_id'], '07430500132') + self.assertEqual(info['scancode'], '07430500132') + self.assertEqual(info['brand_name'], 'Bragg') + self.assertEqual(info['description'], 'Vinegar') + self.assertEqual(info['size'], '32oz') + self.assertEqual(info['full_description'], 'Bragg Vinegar 32oz') + self.assertEqual(info['case_size'], decimal.Decimal('12.3400')) + self.assertEqual(info['unit_price_reg'], decimal.Decimal('4.19')) + + # error if no product_id + self.assertRaises(ValueError, handler.get_product_info_external, self.session, None) + + # error if product not found + self.assertRaises(ValueError, handler.get_product_info_external, self.session, 'BADUPC') + + def test_refresh_row_from_external_product(self): + model = self.app.model + enum = self.app.enum + handler = self.make_handler() + + user = model.User(username='barney') + self.session.add(user) + batch = handler.make_batch(self.session, created_by=user) + self.session.add(batch) + row = handler.make_row(order_qty=1, order_uom=enum.ORDER_UOM_UNIT) + handler.add_row(batch, row) + self.session.add(row) + self.session.flush() + + vendor = op_model.Vendor(id=42, name='Acme Distributors') + self.op_session.add(vendor) + product = op_model.Product(upc='07430500132', brand="Bragg", + description="Vinegar", size='32oz', + normal_price=4.19) + item = op_model.VendorItem(vendor=vendor, sku='1234', units=12.34, + vendor_item_id=1) + product.vendor_items.append(item) + self.op_session.add(product) + self.op_session.commit() + + # error if invalid product_id + row.product_id = 'BreakThings!' + self.assertRaises(ValueError, handler.refresh_row_from_external_product, row) + + # error if product not found + row.product_id = '9999' + self.assertRaises(ValueError, handler.refresh_row_from_external_product, row) + + # row should reflect product info + row.product_id = '07430500132' + self.assertIsNone(row.product_scancode) + self.assertIsNone(row.product_brand) + self.assertIsNone(row.product_description) + self.assertIsNone(row.product_size) + self.assertIsNone(row.case_size) + self.assertIsNone(row.unit_price_reg) + handler.refresh_row_from_external_product(row) + self.assertEqual(row.product_scancode, '07430500132') + self.assertEqual(row.product_brand, "Bragg") + self.assertEqual(row.product_description, "Vinegar") + self.assertEqual(row.product_size, "32oz") + self.assertEqual(row.case_size, decimal.Decimal('12.3400')) + self.assertEqual(row.unit_price_reg, decimal.Decimal('4.19')) diff --git a/tests/web/test_app.py b/tests/web/test_app.py new file mode 100644 index 0000000..1b4790e --- /dev/null +++ b/tests/web/test_app.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8; -*- + +from wuttjamaican.testing import FileTestCase, ConfigTestCase + +from asgiref.wsgi import WsgiToAsgi +from pyramid.router import Router + +from sideshow_corepos.web import app as mod + + +class TestMain(FileTestCase): + + def test_basic(self): + global_config = None + myconf = self.write_file('my.conf', '') + settings = {'wutta.config': myconf} + app = mod.main(global_config, **settings) + self.assertIsInstance(app, Router) + + +class TestMakeWsgiApp(ConfigTestCase): + + def test_basic(self): + wsgi = mod.make_wsgi_app() + self.assertIsInstance(wsgi, Router) + + +class TestMakeAsgiApp(ConfigTestCase): + + def test_basic(self): + asgi = mod.make_asgi_app() + self.assertIsInstance(asgi, WsgiToAsgi) diff --git a/tests/web/test_init.py b/tests/web/test_init.py new file mode 100644 index 0000000..a5425d6 --- /dev/null +++ b/tests/web/test_init.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8; -*- + +from wuttaweb.testing import WebTestCase + +from sideshow_corepos import web as mod + + +class TestIncludeme(WebTestCase): + + def test_coverage(self): + mod.includeme(self.pyramid_config) diff --git a/tests/web/test_menus.py b/tests/web/test_menus.py new file mode 100644 index 0000000..96f7026 --- /dev/null +++ b/tests/web/test_menus.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8; -*- + +from wuttaweb.testing import WebTestCase + +from sideshow_corepos.web import menus as mod + + +class TestSideshowMenuHandler(WebTestCase): + + def make_handler(self): + return mod.SideshowMenuHandler(self.config) + + def test_make_customers_menu(self): + handler = self.make_handler() + menu = handler.make_customers_menu(self.request) + item = menu['items'][-1] + self.assertEqual(item, { + 'title': "CORE-POS Members", + 'route': 'corepos_members', + 'perm': 'corepos_members.list', + }) + + def test_make_products_menu(self): + handler = self.make_handler() + menu = handler.make_products_menu(self.request) + item = menu['items'][-1] + self.assertEqual(item, { + 'title': "CORE-POS Products", + 'route': 'corepos_products', + 'perm': 'corepos_products.list', + }) + + def test_make_other_menu(self): + handler = self.make_handler() + + # no url configured by default + menu = handler.make_other_menu(self.request) + if menu['items']: + item = menu['items'][-1] + self.assertNotEqual(item['title'], "CORE Office") + + # entry added if url configured + self.config.setdefault('corepos.office.url', 'http://localhost/fannie/') + menu = handler.make_other_menu(self.request) + item = menu['items'][-1] + self.assertEqual(item, { + 'title': "CORE Office", + # nb. trailing slash gets stripped + 'url': 'http://localhost/fannie', + 'target': '_blank', + })