feat: basic CORE customer/product lookup, add CORE menu entries
This commit is contained in:
commit
ce54ca6bd6
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
*.pyc
|
||||
*~
|
||||
.coverage
|
||||
dist/
|
||||
docs/_build/
|
||||
.tox/
|
6
README.md
Normal file
6
README.md
Normal file
|
@ -0,0 +1,6 @@
|
|||
|
||||
# Sideshow-COREPOS
|
||||
|
||||
This package adds CORE-POS integration for Sideshow.
|
||||
|
||||
Full docs are at https://rattailproject.org/docs/sideshow-corepos/
|
60
pyproject.toml
Normal file
60
pyproject.toml
Normal file
|
@ -0,0 +1,60 @@
|
|||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "Sideshow-COREPOS"
|
||||
version = "0.0.0"
|
||||
description = "Case/Special Order Tracker for CORE-POS"
|
||||
readme = "README.md"
|
||||
authors = [
|
||||
{name = "Lance Edgar", email = "lance@wuttaproject.org"}
|
||||
]
|
||||
maintainers = [
|
||||
{name = "Lance Edgar", email = "lance@wuttaproject.org"}
|
||||
]
|
||||
classifiers = [
|
||||
"Development Status :: 3 - Alpha",
|
||||
"Environment :: Web Environment",
|
||||
"Framework :: Pyramid",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
|
||||
"Natural Language :: English",
|
||||
"Programming Language :: Python",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
]
|
||||
license = {text = "GNU General Public License v3+"}
|
||||
requires-python = ">= 3.8"
|
||||
dependencies = [
|
||||
"Sideshow",
|
||||
"Wutta-COREPOS[web]",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
docs = ["Sphinx", "furo"]
|
||||
tests = ["pytest-cov", "tox"]
|
||||
|
||||
|
||||
[project.entry-points."paste.app_factory"]
|
||||
"main" = "sideshow_corepos.web.app:main"
|
||||
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://wuttaproject.org/"
|
||||
Repository = "https://forgejo.wuttaproject.org/wutta/sideshow-corepos"
|
||||
Issues = "https://forgejo.wuttaproject.org/wutta/sideshow-corepos/issues"
|
||||
Changelog = "https://forgejo.wuttaproject.org/wutta/sideshow-corepos/src/branch/master/CHANGELOG.md"
|
||||
|
||||
|
||||
[tool.commitizen]
|
||||
version_provider = "pep621"
|
||||
tag_format = "v$version"
|
||||
update_changelog_on_bump = true
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/sideshow_corepos"]
|
25
src/sideshow_corepos/__init__.py
Normal file
25
src/sideshow_corepos/__init__.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# Sideshow-COREPOS -- Case/Special Order Tracker for CORE-POS
|
||||
# Copyright © 2025 Lance Edgar
|
||||
#
|
||||
# This file is part of Sideshow.
|
||||
#
|
||||
# Sideshow 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.
|
||||
#
|
||||
# Sideshow 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 Sideshow. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
Sideshow-COREPOS - Case/Special Order Tracker for CORE-POS
|
||||
"""
|
0
src/sideshow_corepos/batch/__init__.py
Normal file
0
src/sideshow_corepos/batch/__init__.py
Normal file
220
src/sideshow_corepos/batch/neworder.py
Normal file
220
src/sideshow_corepos/batch/neworder.py
Normal file
|
@ -0,0 +1,220 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# Sideshow-COREPOS -- Case/Special Order Tracker for CORE-POS
|
||||
# Copyright © 2025 Lance Edgar
|
||||
#
|
||||
# This file is part of Sideshow.
|
||||
#
|
||||
# Sideshow 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.
|
||||
#
|
||||
# Sideshow 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 Sideshow. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
New Order Batch Handler for CORE-POS
|
||||
"""
|
||||
|
||||
import decimal
|
||||
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import orm
|
||||
|
||||
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.
|
||||
|
||||
See parent class
|
||||
:class:`~sideshow:sideshow.batch.neworder.NewOrderBatchHandler`
|
||||
for more info.
|
||||
"""
|
||||
|
||||
def autocomplete_customers_external(self, session, term, user=None):
|
||||
""" """
|
||||
corepos = self.app.get_corepos_handler()
|
||||
op_model = corepos.get_model_office_op()
|
||||
op_session = corepos.make_session_office_op()
|
||||
|
||||
# base query
|
||||
query = op_session.query(op_model.CustomerClassic)\
|
||||
.join(op_model.MemberInfo,
|
||||
op_model.MemberInfo.card_number == op_model.CustomerClassic.card_number)
|
||||
|
||||
# filter query
|
||||
criteria = []
|
||||
for word in term.split():
|
||||
criteria.append(sa.or_(
|
||||
op_model.CustomerClassic.first_name.ilike(f'%{word}%'),
|
||||
op_model.CustomerClassic.last_name.ilike(f'%{word}%')))
|
||||
query = query.filter(sa.and_(*criteria))
|
||||
|
||||
# sort query
|
||||
query = query.order_by(op_model.CustomerClassic.first_name,
|
||||
op_model.CustomerClassic.last_name)
|
||||
|
||||
# get data
|
||||
# TODO: need max_results option
|
||||
customers = query.all()
|
||||
|
||||
# get results
|
||||
def result(customer):
|
||||
return {'value': str(customer.card_number),
|
||||
'label': str(customer)}
|
||||
results = [result(c) for c in customers]
|
||||
|
||||
op_session.close()
|
||||
return results
|
||||
|
||||
def refresh_batch_from_external_customer(self, batch):
|
||||
""" """
|
||||
corepos = self.app.get_corepos_handler()
|
||||
op_model = corepos.get_model_office_op()
|
||||
op_session = corepos.make_session_office_op()
|
||||
|
||||
if not batch.customer_id.isdigit():
|
||||
raise ValueError(f"invalid CORE-POS customer card number: {batch.customer_id}")
|
||||
|
||||
try:
|
||||
customer = op_session.query(op_model.CustomerClassic)\
|
||||
.join(op_model.MemberInfo,
|
||||
op_model.MemberInfo.card_number == op_model.CustomerClassic.card_number)\
|
||||
.filter(op_model.CustomerClassic.card_number == int(batch.customer_id))\
|
||||
.filter(op_model.CustomerClassic.person_number == 1)\
|
||||
.options(orm.joinedload(op_model.CustomerClassic.member_info))\
|
||||
.one()
|
||||
except orm.exc.NoResultFound:
|
||||
raise ValueError(f"CORE-POS Customer not found: {customer_id}")
|
||||
|
||||
batch.customer_name = str(customer)
|
||||
batch.phone_number = customer.member_info.phone
|
||||
batch.email_address = customer.member_info.email
|
||||
|
||||
op_session.close()
|
||||
|
||||
def autocomplete_products_external(self, session, term, user=None):
|
||||
""" """
|
||||
corepos = self.app.get_corepos_handler()
|
||||
op_model = corepos.get_model_office_op()
|
||||
op_session = corepos.make_session_office_op()
|
||||
|
||||
# base query
|
||||
query = op_session.query(op_model.Product)
|
||||
|
||||
# filter query
|
||||
criteria = []
|
||||
for word in term.split():
|
||||
criteria.append(sa.or_(
|
||||
op_model.Product.brand.ilike(f'%{word}%'),
|
||||
op_model.Product.description.ilike(f'%{word}%')))
|
||||
query = query.filter(sa.and_(*criteria))
|
||||
|
||||
# sort query
|
||||
query = query.order_by(op_model.Product.brand,
|
||||
op_model.Product.description)
|
||||
|
||||
# get data
|
||||
# TODO: need max_results option
|
||||
products = query.all()
|
||||
|
||||
# get results
|
||||
def result(product):
|
||||
return {'value': product.upc,
|
||||
'label': product.formatted_name}
|
||||
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()
|
||||
|
||||
try:
|
||||
product = op_session.query(op_model.Product)\
|
||||
.filter(op_model.Product.upc == product_id)\
|
||||
.one()
|
||||
except orm.exc.NoResultFound:
|
||||
raise ValueError(f"CORE-POS Product not found: {product_id}")
|
||||
|
||||
data = {
|
||||
'product_id': product.upc,
|
||||
'scancode': product.upc,
|
||||
'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),
|
||||
'weighed': product.scale,
|
||||
'special_order': False,
|
||||
'department_id': product.department_number,
|
||||
'department_name': product.department.name if product.department else None,
|
||||
'case_size': self.get_case_size_for_external_product(product),
|
||||
'unit_price_reg': self.get_unit_price_reg_for_external_product(product),
|
||||
# TODO
|
||||
# 'vendor_name': product.vendor_name,
|
||||
# 'vendor_item_code': product.vendor_item_code,
|
||||
}
|
||||
|
||||
op_session.close()
|
||||
return data
|
||||
|
||||
def refresh_row_from_external_product(self, row):
|
||||
""" """
|
||||
corepos = self.app.get_corepos_handler()
|
||||
op_model = corepos.get_model_office_op()
|
||||
op_session = corepos.make_session_office_op()
|
||||
|
||||
try:
|
||||
product = op_session.query(op_model.Product)\
|
||||
.filter(op_model.Product.upc == row.product_id)\
|
||||
.one()
|
||||
except orm.exc.NoResultFound:
|
||||
raise ValueError(f"CORE-POS Product not found: {row.product_id}")
|
||||
|
||||
row.product_scancode = product.upc
|
||||
row.product_brand = product.brand
|
||||
row.product_description = product.description
|
||||
row.product_size = product.size
|
||||
row.product_weighed = product.scale
|
||||
row.department_id = product.department_number
|
||||
row.department_name = product.department.name if product.department else None
|
||||
row.special_order = False
|
||||
row.case_size = self.get_case_size_for_external_product(product)
|
||||
row.unit_cost = product.cost
|
||||
row.unit_price_reg = self.get_unit_price_reg_for_external_product(product)
|
||||
|
||||
op_session.close()
|
||||
|
||||
def get_case_size_for_external_product(self, product):
|
||||
""" """
|
||||
if product.vendor_items:
|
||||
item = product.vendor_items[0]
|
||||
if item.units is not None:
|
||||
return decimal.Decimal(f'{item.units:0.4f}')
|
||||
|
||||
def get_unit_price_reg_for_external_product(self, product):
|
||||
""" """
|
||||
if product.normal_price is not None:
|
||||
return decimal.Decimal(f'{product.normal_price:0.3f}')
|
29
src/sideshow_corepos/web/__init__.py
Normal file
29
src/sideshow_corepos/web/__init__.py
Normal file
|
@ -0,0 +1,29 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# Sideshow-COREPOS -- Case/Special Order Tracker for CORE-POS
|
||||
# Copyright © 2025 Lance Edgar
|
||||
#
|
||||
# This file is part of Sideshow.
|
||||
#
|
||||
# Sideshow 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.
|
||||
#
|
||||
# Sideshow 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 Sideshow. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
Sideshow-COREPOS - Case/Special Order Tracker for CORE-POS
|
||||
"""
|
||||
|
||||
|
||||
def includeme(config):
|
||||
config.include('sideshow_corepos.web.views')
|
68
src/sideshow_corepos/web/app.py
Normal file
68
src/sideshow_corepos/web/app.py
Normal file
|
@ -0,0 +1,68 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# Sideshow-COREPOS -- Case/Special Order Tracker for CORE-POS
|
||||
# Copyright © 2025 Lance Edgar
|
||||
#
|
||||
# This file is part of Sideshow.
|
||||
#
|
||||
# Sideshow 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.
|
||||
#
|
||||
# Sideshow 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 Sideshow. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
Sideshow-COREPOS web app
|
||||
"""
|
||||
|
||||
from wuttaweb import app as base
|
||||
|
||||
from wutta_corepos.web.db import CoreOpSession
|
||||
|
||||
|
||||
def main(global_config, **settings):
|
||||
"""
|
||||
Make and return the WSGI app (Paste entry point).
|
||||
"""
|
||||
# prefer Sideshow templates over wuttaweb
|
||||
settings.setdefault('mako.directories', [
|
||||
# 'sideshow_corepos.web:templates',
|
||||
'sideshow.web:templates',
|
||||
'wuttaweb:templates',
|
||||
])
|
||||
|
||||
# make config objects
|
||||
wutta_config = base.make_wutta_config(settings)
|
||||
pyramid_config = base.make_pyramid_config(settings)
|
||||
|
||||
# configure DB sessions
|
||||
CoreOpSession.configure(bind=wutta_config.core_office_op_engine)
|
||||
|
||||
# bring in the rest of Sideshow
|
||||
pyramid_config.include('sideshow.web')
|
||||
pyramid_config.include('sideshow_corepos.web')
|
||||
|
||||
return pyramid_config.make_wsgi_app()
|
||||
|
||||
|
||||
def make_wsgi_app():
|
||||
"""
|
||||
Make and return the WSGI app (generic entry point).
|
||||
"""
|
||||
return base.make_wsgi_app(main)
|
||||
|
||||
|
||||
def make_asgi_app():
|
||||
"""
|
||||
Make and return the ASGI app.
|
||||
"""
|
||||
return base.make_asgi_app(main)
|
67
src/sideshow_corepos/web/menus.py
Normal file
67
src/sideshow_corepos/web/menus.py
Normal file
|
@ -0,0 +1,67 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# Sideshow-COREPOS -- Case/Special Order Tracker for CORE-POS
|
||||
# Copyright © 2025 Lance Edgar
|
||||
#
|
||||
# This file is part of Sideshow.
|
||||
#
|
||||
# Sideshow 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.
|
||||
#
|
||||
# Sideshow 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 Sideshow. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
Sideshow-COREPOS - custom menus
|
||||
"""
|
||||
|
||||
from sideshow.web import menus as base
|
||||
|
||||
|
||||
class SideshowMenuHandler(base.SideshowMenuHandler):
|
||||
"""
|
||||
Custom menu handler for Sideshow, which adds CORE-POS entries.
|
||||
"""
|
||||
|
||||
def make_customers_menu(self, request, **kwargs):
|
||||
"""
|
||||
This adds the entry for CORE-POS Members..
|
||||
"""
|
||||
menu = super().make_customers_menu(request, **kwargs)
|
||||
|
||||
menu['items'].extend([
|
||||
{'type': 'sep'},
|
||||
{
|
||||
'title': "CORE-POS Members",
|
||||
'route': 'corepos_members',
|
||||
'perm': 'corepos_members.list',
|
||||
},
|
||||
])
|
||||
|
||||
return menu
|
||||
|
||||
def make_products_menu(self, request, **kwargs):
|
||||
"""
|
||||
This adds the entry for CORE-POS Products..
|
||||
"""
|
||||
menu = super().make_products_menu(request, **kwargs)
|
||||
|
||||
menu['items'].extend([
|
||||
{'type': 'sep'},
|
||||
{
|
||||
'title': "CORE-POS Products",
|
||||
'route': 'corepos_products',
|
||||
'perm': 'corepos_products.list',
|
||||
},
|
||||
])
|
||||
|
||||
return menu
|
32
src/sideshow_corepos/web/views.py
Normal file
32
src/sideshow_corepos/web/views.py
Normal file
|
@ -0,0 +1,32 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# Sideshow-COREPOS -- Case/Special Order Tracker for CORE-POS
|
||||
# Copyright © 2025 Lance Edgar
|
||||
#
|
||||
# This file is part of Sideshow.
|
||||
#
|
||||
# Sideshow 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.
|
||||
#
|
||||
# Sideshow 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 Sideshow. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
Sideshow-COREPOS - custom views
|
||||
"""
|
||||
|
||||
|
||||
def includeme(config):
|
||||
|
||||
# CORE-POS views
|
||||
config.include('wutta_corepos.web.views.corepos.members')
|
||||
config.include('wutta_corepos.web.views.corepos.products')
|
28
tasks.py
Normal file
28
tasks.py
Normal file
|
@ -0,0 +1,28 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
"""
|
||||
Tasks for Sideshow-COREPOS
|
||||
"""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from invoke import task
|
||||
|
||||
|
||||
@task
|
||||
def release(c, skip_tests=False):
|
||||
"""
|
||||
Release a new version of Sideshow-COREPOS
|
||||
"""
|
||||
if not skip_tests:
|
||||
c.run('pytest')
|
||||
|
||||
# rebuild pkg
|
||||
if os.path.exists('dist'):
|
||||
shutil.rmtree('dist')
|
||||
if os.path.exists('Sideshow_COREPOS.egg-info'):
|
||||
shutil.rmtree('Sideshow_COREPOS.egg-info')
|
||||
c.run('python -m build --sdist')
|
||||
|
||||
# upload
|
||||
c.run('twine upload dist/*')
|
17
tox.ini
Normal file
17
tox.ini
Normal file
|
@ -0,0 +1,17 @@
|
|||
|
||||
[tox]
|
||||
envlist = py38, py39, py310, py311
|
||||
|
||||
[testenv]
|
||||
extras = tests
|
||||
commands = pytest {posargs}
|
||||
|
||||
[testenv:coverage]
|
||||
basepython = python3.11
|
||||
commands = pytest --cov=sideshow_corepos --cov-report=html --cov-fail-under=100
|
||||
|
||||
[testenv:docs]
|
||||
basepython = python3.11
|
||||
extras = docs
|
||||
changedir = docs
|
||||
commands = sphinx-build -b html -d {envtmpdir}/doctrees -W -T . {envtmpdir}/docs
|
Loading…
Reference in a new issue