feat: add basic readonly web views for CORE members, products

also adds canonical web app db sessions for CORE

also adds some methods to corepos handler, to get model / make session
This commit is contained in:
Lance Edgar 2025-01-12 20:04:19 -06:00
parent b134e340ff
commit 05f428586b
22 changed files with 655 additions and 2 deletions

View file

@ -0,0 +1,6 @@
``wutta_corepos.web.db``
========================
.. automodule:: wutta_corepos.web.db
:members:

View file

@ -0,0 +1,6 @@
``wutta_corepos.web``
=====================
.. automodule:: wutta_corepos.web
:members:

View file

@ -0,0 +1,6 @@
``wutta_corepos.web.views.corepos.master``
==========================================
.. automodule:: wutta_corepos.web.views.corepos.master
:members:

View file

@ -0,0 +1,6 @@
``wutta_corepos.web.views.corepos.members``
===========================================
.. automodule:: wutta_corepos.web.views.corepos.members
:members:

View file

@ -0,0 +1,6 @@
``wutta_corepos.web.views.corepos.products``
============================================
.. automodule:: wutta_corepos.web.views.corepos.products
:members:

View file

@ -0,0 +1,6 @@
``wutta_corepos.web.views.corepos``
===================================
.. automodule:: wutta_corepos.web.views.corepos
:members:

View file

@ -0,0 +1,6 @@
``wutta_corepos.web.views``
===========================
.. automodule:: wutta_corepos.web.views
:members:

View file

@ -27,6 +27,7 @@ templates_path = ['_templates']
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
intersphinx_mapping = {
'wuttaweb': ('https://rattailproject.org/docs/wuttaweb/', None),
'wuttjamaican': ('https://rattailproject.org/docs/wuttjamaican/', None),
}

View file

@ -5,7 +5,8 @@ Wutta-COREPOS
This package adds basic integration with `CORE-POS`_, using
`pyCOREPOS`_.
Its main purpose is to setup DB connections for CORE Office.
Its main purpose is to setup DB connections for CORE Office, but it
also contains basic readonly web views for some CORE tables.
.. _CORE-POS: https://www.core-pos.com/
@ -26,3 +27,10 @@ Its main purpose is to setup DB connections for CORE Office.
api/wutta_corepos.app
api/wutta_corepos.conf
api/wutta_corepos.handler
api/wutta_corepos.web
api/wutta_corepos.web.db
api/wutta_corepos.web.views
api/wutta_corepos.web.views.corepos
api/wutta_corepos.web.views.corepos.master
api/wutta_corepos.web.views.corepos.members
api/wutta_corepos.web.views.corepos.products

View file

@ -34,6 +34,7 @@ dependencies = [
[project.optional-dependencies]
web = ["WuttaWeb"]
docs = ["Sphinx", "furo"]
tests = ["pytest-cov", "tox"]

View file

@ -21,7 +21,7 @@
#
################################################################################
"""
CORE-POS Handler
CORE-POS Integration Handler
"""
from wuttjamaican.app import GenericHandler
@ -33,6 +33,72 @@ class CoreposHandler(GenericHandler):
:term:`handler`.
"""
def get_model_office_op(self):
"""
Returns the :term:`data model` module for CORE Office 'op' DB,
i.e. :mod:`pycorepos:corepos.db.office_op.model`.
"""
from corepos.db.office_op import model
return model
def get_model_office_trans(self):
"""
Returns the :term:`data model` module for CORE Office 'trans'
DB, i.e. :mod:`pycorepos:corepos.db.office_trans.model`.
"""
from corepos.db.office_trans import model
return model
def get_model_office_arch(self):
"""
Returns the :term:`data model` module for CORE Office 'arch'
DB, i.e. :mod:`pycorepos:corepos.db.office_arch.model`.
"""
from corepos.db.office_arch import model
return model
def make_session_office_op(self, dbkey='default', **kwargs):
"""
Make a new :term:`db session` for the CORE Office 'op' DB.
:returns: Instance of
:class:`pycorepos:corepos.db.office_op.Session`.
"""
from corepos.db.office_op import Session
if 'bind' not in kwargs:
kwargs['bind'] = self.config.core_office_op_engines[dbkey]
return Session(**kwargs)
def make_session_office_trans(self, dbkey='default', **kwargs):
"""
Make a new :term:`db session` for the CORE Office 'trans' DB.
:returns: Instance of
:class:`pycorepos:corepos.db.office_trans.Session`.
"""
from corepos.db.office_trans import Session
if 'bind' not in kwargs:
kwargs['bind'] = self.config.core_office_trans_engines[dbkey]
return Session(**kwargs)
def make_session_office_arch(self, dbkey='default', **kwargs):
"""
Make a new :term:`db session` for the CORE Office 'arch' DB.
:returns: Instance of
:class:`pycorepos:corepos.db.office_arch.Session`.
"""
from corepos.db.office_arch import Session
if 'bind' not in kwargs:
kwargs['bind'] = self.config.core_office_arch_engines[dbkey]
return Session(**kwargs)
def get_office_url(self, require=False):
"""
Returns the base URL for the CORE Office web app.

View file

@ -0,0 +1,29 @@
# -*- coding: utf-8; -*-
################################################################################
#
# Wutta-COREPOS -- Wutta Framework integration for CORE-POS
# Copyright © 2025 Lance Edgar
#
# This file is part of Wutta Framework.
#
# Wutta Framework is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Wutta Framework 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 General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with
# Wutta Framework. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Wutta-COREPOS -- wuttaweb features
"""
def includeme(config):
config.include('wutta_corepos.web.views')

View file

@ -0,0 +1,70 @@
# -*- coding: utf-8; -*-
################################################################################
#
# Wutta-COREPOS -- Wutta Framework integration for CORE-POS
# Copyright © 2025 Lance Edgar
#
# This file is part of Wutta Framework.
#
# Wutta Framework is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Wutta Framework 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 General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with
# Wutta Framework. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Wutta-COREPOS -- wuttaweb DB sessions
See :mod:`wuttaweb:wuttaweb.db.sess` for more info on web app sessions
in general.
.. class:: CoreOpSession
Primary web app :term:`db session` for CORE Office 'op' DB.
.. class:: CoreTransSession
Primary web app :term:`db session` for CORE Office 'trans' DB.
.. class:: CoreArchSession
Primary web app :term:`db session` for CORE Office 'arch' DB.
.. class:: ExtraCoreOpSessions
Dict of secondary CORE Office 'op' DB sessions, if applicable.
.. class:: ExtraCoreTransSessions
Dict of secondary CORE Office 'trans' DB sessions, if applicable.
.. class:: ExtraCoreArchSessions
Dict of secondary CORE Office 'arch' DB sessions, if applicable.
"""
from sqlalchemy.orm import sessionmaker, scoped_session
from zope.sqlalchemy import register
CoreOpSession = scoped_session(sessionmaker())
register(CoreOpSession)
CoreTransSession = scoped_session(sessionmaker())
register(CoreTransSession)
CoreArchSession = scoped_session(sessionmaker())
register(CoreArchSession)
# nb. these start out empty but may be populated on app startup
ExtraCoreOpSessions = {}
ExtraCoreTransSessions = {}
ExtraCoreArchSessions = {}

View file

@ -0,0 +1,29 @@
# -*- coding: utf-8; -*-
################################################################################
#
# Wutta-COREPOS -- Wutta Framework integration for CORE-POS
# Copyright © 2025 Lance Edgar
#
# This file is part of Wutta Framework.
#
# Wutta Framework is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Wutta Framework 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 General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with
# Wutta Framework. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Wutta-COREPOS -- wuttaweb views
"""
def includeme(config):
config.include('wutta_corepos.web.views.corepos')

View file

@ -0,0 +1,32 @@
# -*- coding: utf-8; -*-
################################################################################
#
# Wutta-COREPOS -- Wutta Framework integration for CORE-POS
# Copyright © 2025 Lance Edgar
#
# This file is part of Wutta Framework.
#
# Wutta Framework is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Wutta Framework 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 General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with
# Wutta Framework. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Views for CORE-POS
"""
from .master import CoreOpMasterView
def includeme(config):
config.include('wutta_corepos.web.views.corepos.members')
config.include('wutta_corepos.web.views.corepos.products')

View file

@ -0,0 +1,40 @@
# -*- coding: utf-8; -*-
################################################################################
#
# Wutta-COREPOS -- Wutta Framework integration for CORE-POS
# Copyright © 2025 Lance Edgar
#
# This file is part of Wutta Framework.
#
# Wutta Framework is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Wutta Framework 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 General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with
# Wutta Framework. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
CORE-POS master view base class
"""
from wuttaweb.views import MasterView
from wutta_corepos.web.db import CoreOpSession
class CoreOpMasterView(MasterView):
"""
Base class for master views which use the CORE Office 'op' DB.
"""
Session = CoreOpSession
def __init__(self, request, context=None):
super().__init__(request, context=context)
self.corepos_handler = self.app.get_corepos_handler()

View file

@ -0,0 +1,117 @@
# -*- coding: utf-8; -*-
################################################################################
#
# Wutta-COREPOS -- Wutta Framework integration for CORE-POS
# Copyright © 2025 Lance Edgar
#
# This file is part of Wutta Framework.
#
# Wutta Framework is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Wutta Framework 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 General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with
# Wutta Framework. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Views for CORE-POS Members
"""
import sqlalchemy as sa
from sqlalchemy import orm
from corepos.db.office_op.model import MemberInfo
from wutta_corepos.web.views.corepos import CoreOpMasterView
class MemberView(CoreOpMasterView):
"""
Master view for
:class:`~pycorepos:corepos.db.office_op.model.MemberInfo`; route
prefix is ``corepos_members``.
Notable URLs provided by this class:
* ``/corepos/members/``
* ``/corepos/members/XXX``
"""
model_class = MemberInfo
model_title = "CORE-POS Member"
route_prefix = 'corepos_members'
url_prefix = '/corepos/members'
# nb. this is just for readonly lookup
creatable = False
editable = False
deletable = False
grid_columns = [
'card_number',
'first_name',
'last_name',
'street',
'city',
'state',
'zip',
'phone',
'email',
]
filter_defaults = {
'card_number': {'active': True, 'verb': 'equal'},
'first_name': {'active': True, 'verb': 'contains'},
'last_name': {'active': True, 'verb': 'contains'},
}
sort_defaults = 'card_number'
def get_query(self, session=None):
""" """
query = super().get_query(session=session)
op_model = self.corepos_handler.get_model_office_op()
query = query.outerjoin(op_model.CustomerClassic,
sa.and_(
op_model.CustomerClassic.card_number == op_model.MemberInfo.card_number,
op_model.CustomerClassic.person_number == 1,
))\
.options(orm.joinedload(op_model.MemberInfo.customers))
return query
def configure_grid(self, g):
""" """
super().configure_grid(g)
op_model = self.corepos_handler.get_model_office_op()
# first_name
g.set_renderer('first_name', self.render_customer_attr)
g.set_sorter('first_name', op_model.CustomerClassic.first_name)
# last_name
g.set_renderer('last_name', self.render_customer_attr)
g.set_sorter('last_name', op_model.CustomerClassic.last_name)
def render_customer_attr(self, member, key, value):
""" """
customer = member.customers[0]
return getattr(customer, key)
def defaults(config, **kwargs):
base = globals()
MemberView = kwargs.get('MemberView', base['MemberView'])
MemberView.defaults(config)
def includeme(config):
defaults(config)

View file

@ -0,0 +1,97 @@
# -*- coding: utf-8; -*-
################################################################################
#
# Wutta-COREPOS -- Wutta Framework integration for CORE-POS
# Copyright © 2025 Lance Edgar
#
# This file is part of Wutta Framework.
#
# Wutta Framework is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Wutta Framework 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 General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with
# Wutta Framework. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Views for CORE-POS Products
"""
from corepos.db.office_op.model import Product
from wutta_corepos.web.views.corepos import CoreOpMasterView
class ProductView(CoreOpMasterView):
"""
Master view for
:class:`~pycorepos:corepos.db.office_op.model.Product`; route
prefix is ``corepos_products``.
Notable URLs provided by this class:
* ``/corepos/products/``
* ``/corepos/products/XXX``
"""
model_class = Product
model_title = "CORE-POS Product"
route_prefix = 'corepos_products'
url_prefix = '/corepos/products'
# nb. this is just for readonly lookup
creatable = False
editable = False
deletable = False
labels = {
'upc': "UPC",
}
grid_columns = [
'upc',
'brand',
'description',
'size',
'department',
'vendor',
'normal_price',
]
filter_defaults = {
'upc': {'active': True, 'verb': 'contains'},
'brand': {'active': True, 'verb': 'contains'},
'description': {'active': True, 'verb': 'contains'},
}
sort_defaults = 'upc'
def configure_grid(self, g):
""" """
super().configure_grid(g)
# normal_price
g.set_renderer('normal_price', 'currency')
# links
g.set_link('upc')
g.set_link('brand')
g.set_link('description')
g.set_link('size')
def defaults(config, **kwargs):
base = globals()
ProductView = kwargs.get('ProductView', base['ProductView'])
ProductView.defaults(config)
def includeme(config):
defaults(config)

View file

@ -1,5 +1,10 @@
# -*- coding: utf-8; -*-
from unittest.mock import patch
import sqlalchemy as sa
from sqlalchemy import orm
from wuttjamaican.testing import ConfigTestCase
from wuttjamaican.exc import ConfigurationError
@ -11,6 +16,51 @@ class TestCoreposHandler(ConfigTestCase):
def make_handler(self):
return mod.CoreposHandler(self.config)
def test_get_model_office_op(self):
from corepos.db.office_op import model
handler = self.make_handler()
op_model = handler.get_model_office_op()
self.assertIs(op_model, model)
def test_get_model_office_trans(self):
from corepos.db.office_trans import model
handler = self.make_handler()
trans_model = handler.get_model_office_trans()
self.assertIs(trans_model, model)
def test_get_model_office_arch(self):
from corepos.db.office_arch import model
handler = self.make_handler()
arch_model = handler.get_model_office_arch()
self.assertIs(arch_model, model)
def test_make_session_office_op(self):
handler = self.make_handler()
engine = sa.create_engine('sqlite://')
with patch.object(self.config, 'core_office_op_engines', create=True,
new={'default': engine}):
op_session = handler.make_session_office_op()
self.assertIsInstance(op_session, orm.Session)
self.assertIs(op_session.bind, engine)
def test_make_session_office_trans(self):
handler = self.make_handler()
engine = sa.create_engine('sqlite://')
with patch.object(self.config, 'core_office_trans_engines', create=True,
new={'default': engine}):
trans_session = handler.make_session_office_trans()
self.assertIsInstance(trans_session, orm.Session)
self.assertIs(trans_session.bind, engine)
def test_make_session_office_arch(self):
handler = self.make_handler()
engine = sa.create_engine('sqlite://')
with patch.object(self.config, 'core_office_arch_engines', create=True,
new={'default': engine}):
arch_session = handler.make_session_office_arch()
self.assertIsInstance(arch_session, orm.Session)
self.assertIs(arch_session.bind, engine)
def test_get_office_url(self):
handler = self.make_handler()

11
tests/web/test_init.py Normal file
View file

@ -0,0 +1,11 @@
# -*- coding: utf-8; -*-
from wuttaweb.testing import WebTestCase
from wutta_corepos import web as mod
class TestIncludeme(WebTestCase):
def test_coverage(self):
return mod.includeme(self.pyramid_config)

View file

@ -0,0 +1,39 @@
# -*- coding: utf-8; -*-
from sqlalchemy import orm
from corepos.db.office_op import model as op_model
from wuttaweb.testing import WebTestCase
from wutta_corepos.web.views.corepos import members as mod
class TestProductView(WebTestCase):
def make_view(self):
return mod.MemberView(self.request)
def test_includeme(self):
return mod.includeme(self.pyramid_config)
def test_get_query(self):
view = self.make_view()
query = view.get_query()
# TODO: not sure how to test the join other than doing data
# setup and full runn-thru...and i'm feeling lazy
self.assertIsInstance(query, orm.Query)
def test_configure_grid(self):
view = self.make_view()
grid = view.make_grid(model_class=view.model_class)
self.assertNotIn('first_name', grid.renderers)
view.configure_grid(grid)
self.assertIn('first_name', grid.renderers)
def test_render_customer_attr(self):
view = self.make_view()
member = op_model.MemberInfo()
customer = op_model.CustomerClassic(first_name="Fred")
member.customers.append(customer)
self.assertEqual(view.render_customer_attr(member, 'first_name', 'nope'), "Fred")

View file

@ -0,0 +1,21 @@
# -*- coding: utf-8; -*-
from wuttaweb.testing import WebTestCase
from wutta_corepos.web.views.corepos import products as mod
class TestProductView(WebTestCase):
def make_view(self):
return mod.ProductView(self.request)
def test_includeme(self):
return mod.includeme(self.pyramid_config)
def test_configure_grid(self):
view = self.make_view()
grid = view.make_grid(model_class=view.model_class)
self.assertNotIn('upc', grid.linked_columns)
view.configure_grid(grid)
self.assertIn('upc', grid.linked_columns)