Compare commits
No commits in common. "ac753a62f8a64abaaabf8f0a7db284db486ea2f7" and "2c95d3ce28e1a12725759584f9f5f669bbc82b6c" have entirely different histories.
ac753a62f8
...
2c95d3ce28
|
@ -49,15 +49,9 @@ sideshow_libcache = "sideshow.web.static:libcache"
|
||||||
[project.entry-points."paste.app_factory"]
|
[project.entry-points."paste.app_factory"]
|
||||||
"main" = "sideshow.web.app:main"
|
"main" = "sideshow.web.app:main"
|
||||||
|
|
||||||
[project.entry-points."wutta.batch.neworder"]
|
|
||||||
"sideshow" = "sideshow.batch.neworder:NewOrderBatchHandler"
|
|
||||||
|
|
||||||
[project.entry-points."wutta.config.extensions"]
|
[project.entry-points."wutta.config.extensions"]
|
||||||
"sideshow" = "sideshow.config:SideshowConfig"
|
"sideshow" = "sideshow.config:SideshowConfig"
|
||||||
|
|
||||||
[project.entry-points."wutta.web.menus"]
|
|
||||||
sideshow = "sideshow.web.menus:SideshowMenuHandler"
|
|
||||||
|
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
Homepage = "https://wuttaproject.org/"
|
Homepage = "https://wuttaproject.org/"
|
||||||
|
|
|
@ -77,65 +77,6 @@ class NewOrderBatchHandler(BatchHandler):
|
||||||
return self.config.get_bool('sideshow.orders.allow_unknown_products',
|
return self.config.get_bool('sideshow.orders.allow_unknown_products',
|
||||||
default=True)
|
default=True)
|
||||||
|
|
||||||
def autocomplete_customers_external(self, session, term, user=None):
|
|
||||||
"""
|
|
||||||
Return autocomplete search results for :term:`external
|
|
||||||
customer` records.
|
|
||||||
|
|
||||||
There is no default logic here; subclass must implement.
|
|
||||||
|
|
||||||
:param session: Current app :term:`db session`.
|
|
||||||
|
|
||||||
:param term: Search term string from user input.
|
|
||||||
|
|
||||||
:param user:
|
|
||||||
:class:`~wuttjamaican:wuttjamaican.db.model.auth.User` who
|
|
||||||
is doing the search, if known.
|
|
||||||
|
|
||||||
:returns: List of search results; each should be a dict with
|
|
||||||
``value`` and ``label`` keys.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def autocomplete_customers_local(self, session, term, user=None):
|
|
||||||
"""
|
|
||||||
Return autocomplete search results for
|
|
||||||
:class:`~sideshow.db.model.customers.LocalCustomer` records.
|
|
||||||
|
|
||||||
:param session: Current app :term:`db session`.
|
|
||||||
|
|
||||||
:param term: Search term string from user input.
|
|
||||||
|
|
||||||
:param user:
|
|
||||||
:class:`~wuttjamaican:wuttjamaican.db.model.auth.User` who
|
|
||||||
is doing the search, if known.
|
|
||||||
|
|
||||||
:returns: List of search results; each should be a dict with
|
|
||||||
``value`` and ``label`` keys.
|
|
||||||
"""
|
|
||||||
model = self.app.model
|
|
||||||
|
|
||||||
# base query
|
|
||||||
query = session.query(model.LocalCustomer)
|
|
||||||
|
|
||||||
# filter query
|
|
||||||
criteria = [model.LocalCustomer.full_name.ilike(f'%{word}%')
|
|
||||||
for word in term.split()]
|
|
||||||
query = query.filter(sa.and_(*criteria))
|
|
||||||
|
|
||||||
# sort query
|
|
||||||
query = query.order_by(model.LocalCustomer.full_name)
|
|
||||||
|
|
||||||
# get data
|
|
||||||
# TODO: need max_results option
|
|
||||||
customers = query.all()
|
|
||||||
|
|
||||||
# get results
|
|
||||||
def result(customer):
|
|
||||||
return {'value': customer.uuid.hex,
|
|
||||||
'label': customer.full_name}
|
|
||||||
return [result(c) for c in customers]
|
|
||||||
|
|
||||||
def set_customer(self, batch, customer_info, user=None):
|
def set_customer(self, batch, customer_info, user=None):
|
||||||
"""
|
"""
|
||||||
Set/update customer info for the batch.
|
Set/update customer info for the batch.
|
||||||
|
@ -150,14 +91,14 @@ class NewOrderBatchHandler(BatchHandler):
|
||||||
:class:`~sideshow.db.model.customers.PendingCustomer` record
|
:class:`~sideshow.db.model.customers.PendingCustomer` record
|
||||||
is created if necessary.
|
is created if necessary.
|
||||||
|
|
||||||
And then it will update customer-related attributes via one of:
|
And then it will update these accordingly:
|
||||||
|
|
||||||
* :meth:`refresh_batch_from_external_customer()`
|
* :attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.customer_name`
|
||||||
* :meth:`refresh_batch_from_local_customer()`
|
* :attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.phone_number`
|
||||||
* :meth:`refresh_batch_from_pending_customer()`
|
* :attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.email_address`
|
||||||
|
|
||||||
Note that ``customer_info`` may be ``None``, which will cause
|
Note that ``customer_info`` may be ``None``, which will cause
|
||||||
customer attributes to be set to ``None`` also.
|
all the above to be set to ``None`` also.
|
||||||
|
|
||||||
:param batch:
|
:param batch:
|
||||||
:class:`~sideshow.db.model.batch.neworder.NewOrderBatch` to
|
:class:`~sideshow.db.model.batch.neworder.NewOrderBatch` to
|
||||||
|
@ -188,11 +129,13 @@ class NewOrderBatchHandler(BatchHandler):
|
||||||
if not customer:
|
if not customer:
|
||||||
raise ValueError("local customer not found")
|
raise ValueError("local customer not found")
|
||||||
batch.local_customer = customer
|
batch.local_customer = customer
|
||||||
self.refresh_batch_from_local_customer(batch)
|
batch.customer_name = customer.full_name
|
||||||
|
batch.phone_number = customer.phone_number
|
||||||
|
batch.email_address = customer.email_address
|
||||||
|
|
||||||
else: # external customer_id
|
else: # external customer_id
|
||||||
batch.customer_id = customer_info
|
#batch.customer_id = customer_info
|
||||||
self.refresh_batch_from_external_customer(batch)
|
raise NotImplementedError
|
||||||
|
|
||||||
elif customer_info:
|
elif customer_info:
|
||||||
|
|
||||||
|
@ -217,7 +160,9 @@ class NewOrderBatchHandler(BatchHandler):
|
||||||
if 'full_name' not in customer_info:
|
if 'full_name' not in customer_info:
|
||||||
customer.full_name = self.app.make_full_name(customer.first_name,
|
customer.full_name = self.app.make_full_name(customer.first_name,
|
||||||
customer.last_name)
|
customer.last_name)
|
||||||
self.refresh_batch_from_pending_customer(batch)
|
batch.customer_name = customer.full_name
|
||||||
|
batch.phone_number = customer.phone_number
|
||||||
|
batch.email_address = customer.email_address
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|
||||||
|
@ -230,203 +175,6 @@ class NewOrderBatchHandler(BatchHandler):
|
||||||
|
|
||||||
session.flush()
|
session.flush()
|
||||||
|
|
||||||
def refresh_batch_from_external_customer(self, batch):
|
|
||||||
"""
|
|
||||||
Update customer-related attributes on the batch, from its
|
|
||||||
:term:`external customer` record.
|
|
||||||
|
|
||||||
This is called automatically from :meth:`set_customer()`.
|
|
||||||
|
|
||||||
There is no default logic here; subclass must implement.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def refresh_batch_from_local_customer(self, batch):
|
|
||||||
"""
|
|
||||||
Update customer-related attributes on the batch, from its
|
|
||||||
:attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.local_customer`
|
|
||||||
record.
|
|
||||||
|
|
||||||
This is called automatically from :meth:`set_customer()`.
|
|
||||||
"""
|
|
||||||
customer = batch.local_customer
|
|
||||||
batch.customer_name = customer.full_name
|
|
||||||
batch.phone_number = customer.phone_number
|
|
||||||
batch.email_address = customer.email_address
|
|
||||||
|
|
||||||
def refresh_batch_from_pending_customer(self, batch):
|
|
||||||
"""
|
|
||||||
Update customer-related attributes on the batch, from its
|
|
||||||
:attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.pending_customer`
|
|
||||||
record.
|
|
||||||
|
|
||||||
This is called automatically from :meth:`set_customer()`.
|
|
||||||
"""
|
|
||||||
customer = batch.pending_customer
|
|
||||||
batch.customer_name = customer.full_name
|
|
||||||
batch.phone_number = customer.phone_number
|
|
||||||
batch.email_address = customer.email_address
|
|
||||||
|
|
||||||
def autocomplete_products_external(self, session, term, user=None):
|
|
||||||
"""
|
|
||||||
Return autocomplete search results for :term:`external
|
|
||||||
product` records.
|
|
||||||
|
|
||||||
There is no default logic here; subclass must implement.
|
|
||||||
|
|
||||||
:param session: Current app :term:`db session`.
|
|
||||||
|
|
||||||
:param term: Search term string from user input.
|
|
||||||
|
|
||||||
:param user:
|
|
||||||
:class:`~wuttjamaican:wuttjamaican.db.model.auth.User` who
|
|
||||||
is doing the search, if known.
|
|
||||||
|
|
||||||
:returns: List of search results; each should be a dict with
|
|
||||||
``value`` and ``label`` keys.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def autocomplete_products_local(self, session, term, user=None):
|
|
||||||
"""
|
|
||||||
Return autocomplete search results for
|
|
||||||
:class:`~sideshow.db.model.products.LocalProduct` records.
|
|
||||||
|
|
||||||
:param session: Current app :term:`db session`.
|
|
||||||
|
|
||||||
:param term: Search term string from user input.
|
|
||||||
|
|
||||||
:param user:
|
|
||||||
:class:`~wuttjamaican:wuttjamaican.db.model.auth.User` who
|
|
||||||
is doing the search, if known.
|
|
||||||
|
|
||||||
:returns: List of search results; each should be a dict with
|
|
||||||
``value`` and ``label`` keys.
|
|
||||||
"""
|
|
||||||
model = self.app.model
|
|
||||||
|
|
||||||
# base query
|
|
||||||
query = session.query(model.LocalProduct)
|
|
||||||
|
|
||||||
# filter query
|
|
||||||
criteria = []
|
|
||||||
for word in term.split():
|
|
||||||
criteria.append(sa.or_(
|
|
||||||
model.LocalProduct.brand_name.ilike(f'%{word}%'),
|
|
||||||
model.LocalProduct.description.ilike(f'%{word}%')))
|
|
||||||
query = query.filter(sa.and_(*criteria))
|
|
||||||
|
|
||||||
# sort query
|
|
||||||
query = query.order_by(model.LocalProduct.brand_name,
|
|
||||||
model.LocalProduct.description)
|
|
||||||
|
|
||||||
# get data
|
|
||||||
# TODO: need max_results option
|
|
||||||
products = query.all()
|
|
||||||
|
|
||||||
# get results
|
|
||||||
def result(product):
|
|
||||||
return {'value': product.uuid.hex,
|
|
||||||
'label': product.full_description}
|
|
||||||
return [result(c) for c in products]
|
|
||||||
|
|
||||||
def get_product_info_external(self, session, product_id, user=None):
|
|
||||||
"""
|
|
||||||
Returns basic info for an :term:`external product` as pertains
|
|
||||||
to ordering.
|
|
||||||
|
|
||||||
When user has located a product via search, and must then
|
|
||||||
choose order quantity and UOM based on case size, pricing
|
|
||||||
etc., this method is called to retrieve the product info.
|
|
||||||
|
|
||||||
There is no default logic here; subclass must implement.
|
|
||||||
|
|
||||||
:param session: Current app :term:`db session`.
|
|
||||||
|
|
||||||
:param product_id: Product ID string for which to retrieve
|
|
||||||
info.
|
|
||||||
|
|
||||||
:param user:
|
|
||||||
:class:`~wuttjamaican:wuttjamaican.db.model.auth.User` who
|
|
||||||
is performing the action, if known.
|
|
||||||
|
|
||||||
:returns: Dict of product info. Should raise error instead of
|
|
||||||
returning ``None`` if product not found.
|
|
||||||
|
|
||||||
This method should only be called after a product has been
|
|
||||||
identified via autocomplete/search lookup; therefore the
|
|
||||||
``product_id`` should be valid, and the caller can expect this
|
|
||||||
method to *always* return a dict. If for some reason the
|
|
||||||
product cannot be found here, an error should be raised.
|
|
||||||
|
|
||||||
The dict should contain as much product info as is available
|
|
||||||
and needed; if some are missing it should not cause too much
|
|
||||||
trouble in the app. Here is a basic example::
|
|
||||||
|
|
||||||
def get_product_info_external(self, session, product_id, user=None):
|
|
||||||
ext_model = get_external_model()
|
|
||||||
ext_session = make_external_session()
|
|
||||||
|
|
||||||
ext_product = ext_session.get(ext_model.Product, product_id)
|
|
||||||
if not ext_product:
|
|
||||||
ext_session.close()
|
|
||||||
raise ValueError(f"external product not found: {product_id}")
|
|
||||||
|
|
||||||
info = {
|
|
||||||
'product_id': product_id,
|
|
||||||
'scancode': product.scancode,
|
|
||||||
'brand_name': product.brand_name,
|
|
||||||
'description': product.description,
|
|
||||||
'size': product.size,
|
|
||||||
'weighed': product.sold_by_weight,
|
|
||||||
'special_order': False,
|
|
||||||
'department_id': str(product.department_number),
|
|
||||||
'department_name': product.department_name,
|
|
||||||
'case_size': product.case_size,
|
|
||||||
'unit_price_reg': product.unit_price_reg,
|
|
||||||
'vendor_name': product.vendor_name,
|
|
||||||
'vendor_item_code': product.vendor_item_code,
|
|
||||||
}
|
|
||||||
|
|
||||||
ext_session.close()
|
|
||||||
return info
|
|
||||||
"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def get_product_info_local(self, session, uuid, user=None):
|
|
||||||
"""
|
|
||||||
Returns basic info for a
|
|
||||||
:class:`~sideshow.db.model.products.LocalProduct` as pertains
|
|
||||||
to ordering.
|
|
||||||
|
|
||||||
When user has located a product via search, and must then
|
|
||||||
choose order quantity and UOM based on case size, pricing
|
|
||||||
etc., this method is called to retrieve the product info.
|
|
||||||
|
|
||||||
See :meth:`get_product_info_external()` for more explanation.
|
|
||||||
"""
|
|
||||||
model = self.app.model
|
|
||||||
product = session.get(model.LocalProduct, uuid)
|
|
||||||
if not product:
|
|
||||||
raise ValueError(f"Local Product not found: {uuid}")
|
|
||||||
|
|
||||||
return {
|
|
||||||
'product_id': product.uuid.hex,
|
|
||||||
'scancode': product.scancode,
|
|
||||||
'brand_name': product.brand_name,
|
|
||||||
'description': product.description,
|
|
||||||
'size': product.size,
|
|
||||||
'full_description': product.full_description,
|
|
||||||
'weighed': product.weighed,
|
|
||||||
'special_order': product.special_order,
|
|
||||||
'department_id': product.department_id,
|
|
||||||
'department_name': product.department_name,
|
|
||||||
'case_size': product.case_size,
|
|
||||||
'unit_price_reg': product.unit_price_reg,
|
|
||||||
'vendor_name': product.vendor_name,
|
|
||||||
'vendor_item_code': product.vendor_item_code,
|
|
||||||
}
|
|
||||||
|
|
||||||
def add_item(self, batch, product_info, order_qty, order_uom, user=None):
|
def add_item(self, batch, product_info, order_qty, order_uom, user=None):
|
||||||
"""
|
"""
|
||||||
Add a new item/row to the batch, for given product and quantity.
|
Add a new item/row to the batch, for given product and quantity.
|
||||||
|
@ -476,7 +224,8 @@ class NewOrderBatchHandler(BatchHandler):
|
||||||
row.local_product = local
|
row.local_product = local
|
||||||
|
|
||||||
else: # external product_id
|
else: # external product_id
|
||||||
row.product_id = product_info
|
#row.product_id = product_info
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# pending_product
|
# pending_product
|
||||||
|
@ -564,7 +313,8 @@ class NewOrderBatchHandler(BatchHandler):
|
||||||
row.local_product = local
|
row.local_product = local
|
||||||
|
|
||||||
else: # external product_id
|
else: # external product_id
|
||||||
row.product_id = product_info
|
#row.product_id = product_info
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# pending_product
|
# pending_product
|
||||||
|
@ -743,7 +493,6 @@ class NewOrderBatchHandler(BatchHandler):
|
||||||
There is no default logic here; subclass must implement as
|
There is no default logic here; subclass must implement as
|
||||||
needed.
|
needed.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def remove_row(self, row):
|
def remove_row(self, row):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -46,12 +46,8 @@ class SideshowConfig(WuttaConfigExtension):
|
||||||
config.setdefault(f'{config.appname}.model_spec', 'sideshow.db.model')
|
config.setdefault(f'{config.appname}.model_spec', 'sideshow.db.model')
|
||||||
config.setdefault(f'{config.appname}.enum_spec', 'sideshow.enum')
|
config.setdefault(f'{config.appname}.enum_spec', 'sideshow.enum')
|
||||||
|
|
||||||
# batch handlers
|
|
||||||
config.setdefault(f'{config.appname}.batch.neworder.handler.default_spec',
|
|
||||||
'sideshow.batch.neworder:NewOrderBatchHandler')
|
|
||||||
|
|
||||||
# web app menu
|
# web app menu
|
||||||
config.setdefault(f'{config.appname}.web.menus.handler.default_spec',
|
config.setdefault(f'{config.appname}.web.menus.handler_spec',
|
||||||
'sideshow.web.menus:SideshowMenuHandler')
|
'sideshow.web.menus:SideshowMenuHandler')
|
||||||
|
|
||||||
# web app libcache
|
# web app libcache
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
# -*- coding: utf-8; -*-
|
|
||||||
################################################################################
|
|
||||||
#
|
|
||||||
# Sideshow -- Case/Special Order Tracker
|
|
||||||
# Copyright © 2024 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 web app
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
|
||||||
config.include('sideshow.web.static')
|
|
||||||
config.include('wuttaweb.subscribers')
|
|
||||||
config.include('sideshow.web.views')
|
|
|
@ -42,7 +42,9 @@ def main(global_config, **settings):
|
||||||
pyramid_config = base.make_pyramid_config(settings)
|
pyramid_config = base.make_pyramid_config(settings)
|
||||||
|
|
||||||
# bring in the rest of Sideshow
|
# bring in the rest of Sideshow
|
||||||
pyramid_config.include('sideshow.web')
|
pyramid_config.include('sideshow.web.static')
|
||||||
|
pyramid_config.include('wuttaweb.subscribers')
|
||||||
|
pyramid_config.include('sideshow.web.views')
|
||||||
|
|
||||||
return pyramid_config.make_wsgi_app()
|
return pyramid_config.make_wsgi_app()
|
||||||
|
|
||||||
|
|
|
@ -39,7 +39,6 @@ class SideshowMenuHandler(base.MenuHandler):
|
||||||
self.make_customers_menu(request),
|
self.make_customers_menu(request),
|
||||||
self.make_products_menu(request),
|
self.make_products_menu(request),
|
||||||
self.make_batch_menu(request),
|
self.make_batch_menu(request),
|
||||||
self.make_other_menu(request),
|
|
||||||
self.make_admin_menu(request),
|
self.make_admin_menu(request),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -128,16 +127,6 @@ class SideshowMenuHandler(base.MenuHandler):
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
def make_other_menu(self, request, **kwargs):
|
|
||||||
"""
|
|
||||||
Generate the "Other" menu.
|
|
||||||
"""
|
|
||||||
return {
|
|
||||||
'title': "Other",
|
|
||||||
'type': 'menu',
|
|
||||||
'items': [],
|
|
||||||
}
|
|
||||||
|
|
||||||
def make_admin_menu(self, request, **kwargs):
|
def make_admin_menu(self, request, **kwargs):
|
||||||
""" """
|
""" """
|
||||||
kwargs['include_people'] = True
|
kwargs['include_people'] = True
|
||||||
|
|
|
@ -3,38 +3,15 @@
|
||||||
|
|
||||||
<%def name="form_content()">
|
<%def name="form_content()">
|
||||||
|
|
||||||
<h3 class="block is-size-3">Customers</h3>
|
|
||||||
<div class="block" style="padding-left: 2rem;">
|
|
||||||
|
|
||||||
<b-field label="Customer Source">
|
|
||||||
<b-select name="sideshow.orders.use_local_customers"
|
|
||||||
v-model="simpleSettings['sideshow.orders.use_local_customers']"
|
|
||||||
@input="settingsNeedSaved = true">
|
|
||||||
<option value="true">Local Customers (in Sideshow)</option>
|
|
||||||
<option value="false">External Customers (e.g. in POS)</option>
|
|
||||||
</b-select>
|
|
||||||
</b-field>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 class="block is-size-3">Products</h3>
|
<h3 class="block is-size-3">Products</h3>
|
||||||
<div class="block" style="padding-left: 2rem;">
|
<div class="block" style="padding-left: 2rem;">
|
||||||
|
|
||||||
<b-field label="Product Source">
|
<b-field message="If set, user can enter details of an arbitrary new "pending" product.">
|
||||||
<b-select name="sideshow.orders.use_local_products"
|
|
||||||
v-model="simpleSettings['sideshow.orders.use_local_products']"
|
|
||||||
@input="settingsNeedSaved = true">
|
|
||||||
<option value="true">Local Products (in Sideshow)</option>
|
|
||||||
<option value="false">External Products (e.g. in POS)</option>
|
|
||||||
</b-select>
|
|
||||||
</b-field>
|
|
||||||
|
|
||||||
<b-field label="New/Unknown Products"
|
|
||||||
message="If set, user can enter details of an arbitrary new "pending" product.">
|
|
||||||
<b-checkbox name="sideshow.orders.allow_unknown_products"
|
<b-checkbox name="sideshow.orders.allow_unknown_products"
|
||||||
v-model="simpleSettings['sideshow.orders.allow_unknown_products']"
|
v-model="simpleSettings['sideshow.orders.allow_unknown_products']"
|
||||||
native-value="true"
|
native-value="true"
|
||||||
@input="settingsNeedSaved = true">
|
@input="settingsNeedSaved = true">
|
||||||
Allow creating orders for new/unknown products
|
Allow creating orders for "unknown" products
|
||||||
</b-checkbox>
|
</b-checkbox>
|
||||||
</b-field>
|
</b-field>
|
||||||
|
|
||||||
|
@ -61,32 +38,4 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 class="block is-size-3">Batches</h3>
|
|
||||||
<div class="block" style="padding-left: 2rem;">
|
|
||||||
|
|
||||||
<b-field label="New Order Batch Handler">
|
|
||||||
<input type="hidden"
|
|
||||||
name="wutta.batch.neworder.handler.spec"
|
|
||||||
:value="simpleSettings['wutta.batch.neworder.handler.spec']" />
|
|
||||||
<b-select v-model="simpleSettings['wutta.batch.neworder.handler.spec']"
|
|
||||||
@input="settingsNeedSaved = true">
|
|
||||||
<option :value="null">(use default)</option>
|
|
||||||
<option v-for="handler in batchHandlers"
|
|
||||||
:key="handler.spec"
|
|
||||||
:value="handler.spec">
|
|
||||||
{{ handler.spec }}
|
|
||||||
</option>
|
|
||||||
</b-select>
|
|
||||||
</b-field>
|
|
||||||
</div>
|
|
||||||
</%def>
|
|
||||||
|
|
||||||
<%def name="modify_vue_vars()">
|
|
||||||
${parent.modify_vue_vars()}
|
|
||||||
<script>
|
|
||||||
|
|
||||||
ThisPageData.batchHandlers = ${json.dumps(batch_handlers)|n}
|
|
||||||
|
|
||||||
</script>
|
|
||||||
</%def>
|
</%def>
|
||||||
|
|
|
@ -836,11 +836,11 @@
|
||||||
orderEmailAddress: ${json.dumps(email_address)|n},
|
orderEmailAddress: ${json.dumps(email_address)|n},
|
||||||
refreshingCustomer: false,
|
refreshingCustomer: false,
|
||||||
|
|
||||||
newCustomerFullName: ${json.dumps(new_customer_full_name or None)|n},
|
newCustomerFullName: ${json.dumps(new_customer_full_name)|n},
|
||||||
newCustomerFirstName: ${json.dumps(new_customer_first_name or None)|n},
|
newCustomerFirstName: ${json.dumps(new_customer_first_name)|n},
|
||||||
newCustomerLastName: ${json.dumps(new_customer_last_name or None)|n},
|
newCustomerLastName: ${json.dumps(new_customer_last_name)|n},
|
||||||
newCustomerPhone: ${json.dumps(new_customer_phone or None)|n},
|
newCustomerPhone: ${json.dumps(new_customer_phone)|n},
|
||||||
newCustomerEmail: ${json.dumps(new_customer_email or None)|n},
|
newCustomerEmail: ${json.dumps(new_customer_email)|n},
|
||||||
|
|
||||||
editNewCustomerShowDialog: false,
|
editNewCustomerShowDialog: false,
|
||||||
editNewCustomerFirstName: null,
|
editNewCustomerFirstName: null,
|
||||||
|
|
|
@ -57,11 +57,6 @@ class OrderView(MasterView):
|
||||||
|
|
||||||
Note that the "edit" view is not exposed here; user must perform
|
Note that the "edit" view is not exposed here; user must perform
|
||||||
various other workflow actions to modify the order.
|
various other workflow actions to modify the order.
|
||||||
|
|
||||||
.. attribute:: batch_handler
|
|
||||||
|
|
||||||
Reference to the new order batch handler, as returned by
|
|
||||||
:meth:`get_batch_handler()`. This gets set in the constructor.
|
|
||||||
"""
|
"""
|
||||||
model_class = Order
|
model_class = Order
|
||||||
editable = False
|
editable = False
|
||||||
|
@ -158,22 +153,6 @@ class OrderView(MasterView):
|
||||||
# total_price
|
# total_price
|
||||||
g.set_renderer('total_price', g.render_currency)
|
g.set_renderer('total_price', g.render_currency)
|
||||||
|
|
||||||
def get_batch_handler(self):
|
|
||||||
"""
|
|
||||||
Returns the configured :term:`handler` for :term:`new order
|
|
||||||
batches <new order batch>`.
|
|
||||||
|
|
||||||
You normally would not need to call this, and can use
|
|
||||||
:attr:`batch_handler` instead.
|
|
||||||
|
|
||||||
:returns:
|
|
||||||
:class:`~sideshow.batch.neworder.NewOrderBatchHandler`
|
|
||||||
instance.
|
|
||||||
"""
|
|
||||||
if hasattr(self, 'batch_handler'):
|
|
||||||
return self.batch_handler
|
|
||||||
return self.app.get_batch_handler('neworder')
|
|
||||||
|
|
||||||
def create(self):
|
def create(self):
|
||||||
"""
|
"""
|
||||||
Instead of the typical "create" view, this displays a "wizard"
|
Instead of the typical "create" view, this displays a "wizard"
|
||||||
|
@ -206,7 +185,7 @@ class OrderView(MasterView):
|
||||||
"""
|
"""
|
||||||
enum = self.app.enum
|
enum = self.app.enum
|
||||||
self.creating = True
|
self.creating = True
|
||||||
self.batch_handler = self.get_batch_handler()
|
self.batch_handler = NewOrderBatchHandler(self.config)
|
||||||
batch = self.get_current_batch()
|
batch = self.get_current_batch()
|
||||||
|
|
||||||
context = self.get_context_customer(batch)
|
context = self.get_context_customer(batch)
|
||||||
|
@ -244,7 +223,6 @@ class OrderView(MasterView):
|
||||||
try:
|
try:
|
||||||
result = getattr(self, action)(batch, data)
|
result = getattr(self, action)(batch, data)
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
log.warning("error calling json action for order", exc_info=True)
|
|
||||||
result = {'error': self.app.render_error(error)}
|
result = {'error': self.app.render_error(error)}
|
||||||
return self.json_response(result)
|
return self.json_response(result)
|
||||||
|
|
||||||
|
@ -301,49 +279,91 @@ class OrderView(MasterView):
|
||||||
"""
|
"""
|
||||||
AJAX view for customer autocomplete, when entering new order.
|
AJAX view for customer autocomplete, when entering new order.
|
||||||
|
|
||||||
This invokes one of the following on the
|
This should invoke a configured handler for the autocomplete
|
||||||
:attr:`batch_handler`:
|
behavior, but that is not yet implemented. For now it uses
|
||||||
|
built-in logic only, which queries the
|
||||||
* :meth:`~sideshow.batch.neworder.NewOrderBatchHandler.autocomplete_customers_external()`
|
:class:`~sideshow.db.model.customers.LocalCustomer` table.
|
||||||
* :meth:`~sideshow.batch.neworder.NewOrderBatchHandler.autocomplete_customers_local()`
|
|
||||||
|
|
||||||
:returns: List of search results; each should be a dict with
|
|
||||||
``value`` and ``label`` keys.
|
|
||||||
"""
|
"""
|
||||||
session = self.Session()
|
session = self.Session()
|
||||||
term = self.request.GET.get('term', '').strip()
|
term = self.request.GET.get('term', '').strip()
|
||||||
if not term:
|
if not term:
|
||||||
return []
|
return []
|
||||||
|
return self.mock_autocomplete_customers(session, term, user=self.request.user)
|
||||||
|
|
||||||
handler = self.get_batch_handler()
|
# TODO: move this to some handler
|
||||||
if handler.use_local_customers():
|
def mock_autocomplete_customers(self, session, term, user=None):
|
||||||
return handler.autocomplete_customers_local(session, term, user=self.request.user)
|
""" """
|
||||||
else:
|
import sqlalchemy as sa
|
||||||
return handler.autocomplete_customers_external(session, term, user=self.request.user)
|
|
||||||
|
model = self.app.model
|
||||||
|
|
||||||
|
# base query
|
||||||
|
query = session.query(model.LocalCustomer)
|
||||||
|
|
||||||
|
# filter query
|
||||||
|
criteria = [model.LocalCustomer.full_name.ilike(f'%{word}%')
|
||||||
|
for word in term.split()]
|
||||||
|
query = query.filter(sa.and_(*criteria))
|
||||||
|
|
||||||
|
# sort query
|
||||||
|
query = query.order_by(model.LocalCustomer.full_name)
|
||||||
|
|
||||||
|
# get data
|
||||||
|
# TODO: need max_results option
|
||||||
|
customers = query.all()
|
||||||
|
|
||||||
|
# get results
|
||||||
|
def result(customer):
|
||||||
|
return {'value': customer.uuid.hex,
|
||||||
|
'label': customer.full_name}
|
||||||
|
return [result(c) for c in customers]
|
||||||
|
|
||||||
def product_autocomplete(self):
|
def product_autocomplete(self):
|
||||||
"""
|
"""
|
||||||
AJAX view for product autocomplete, when entering new order.
|
AJAX view for product autocomplete, when entering new order.
|
||||||
|
|
||||||
This invokes one of the following on the
|
This should invoke a configured handler for the autocomplete
|
||||||
:attr:`batch_handler`:
|
behavior, but that is not yet implemented. For now it uses
|
||||||
|
built-in logic only, which queries the
|
||||||
* :meth:`~sideshow.batch.neworder.NewOrderBatchHandler.autocomplete_products_external()`
|
:class:`~sideshow.db.model.products.LocalProduct` table.
|
||||||
* :meth:`~sideshow.batch.neworder.NewOrderBatchHandler.autocomplete_products_local()`
|
|
||||||
|
|
||||||
:returns: List of search results; each should be a dict with
|
|
||||||
``value`` and ``label`` keys.
|
|
||||||
"""
|
"""
|
||||||
session = self.Session()
|
session = self.Session()
|
||||||
term = self.request.GET.get('term', '').strip()
|
term = self.request.GET.get('term', '').strip()
|
||||||
if not term:
|
if not term:
|
||||||
return []
|
return []
|
||||||
|
return self.mock_autocomplete_products(session, term, user=self.request.user)
|
||||||
|
|
||||||
handler = self.get_batch_handler()
|
# TODO: move this to some handler
|
||||||
if handler.use_local_products():
|
def mock_autocomplete_products(self, session, term, user=None):
|
||||||
return handler.autocomplete_products_local(session, term, user=self.request.user)
|
""" """
|
||||||
else:
|
import sqlalchemy as sa
|
||||||
return handler.autocomplete_products_external(session, term, user=self.request.user)
|
|
||||||
|
model = self.app.model
|
||||||
|
|
||||||
|
# base query
|
||||||
|
query = session.query(model.LocalProduct)
|
||||||
|
|
||||||
|
# filter query
|
||||||
|
criteria = []
|
||||||
|
for word in term.split():
|
||||||
|
criteria.append(sa.or_(
|
||||||
|
model.LocalProduct.brand_name.ilike(f'%{word}%'),
|
||||||
|
model.LocalProduct.description.ilike(f'%{word}%')))
|
||||||
|
query = query.filter(sa.and_(*criteria))
|
||||||
|
|
||||||
|
# sort query
|
||||||
|
query = query.order_by(model.LocalProduct.brand_name,
|
||||||
|
model.LocalProduct.description)
|
||||||
|
|
||||||
|
# get data
|
||||||
|
# TODO: need max_results option
|
||||||
|
products = query.all()
|
||||||
|
|
||||||
|
# get results
|
||||||
|
def result(product):
|
||||||
|
return {'value': product.uuid.hex,
|
||||||
|
'label': product.full_description}
|
||||||
|
return [result(c) for c in products]
|
||||||
|
|
||||||
def get_pending_product_required_fields(self):
|
def get_pending_product_required_fields(self):
|
||||||
""" """
|
""" """
|
||||||
|
@ -511,12 +531,11 @@ class OrderView(MasterView):
|
||||||
if not product_id:
|
if not product_id:
|
||||||
return {'error': "Must specify a product ID"}
|
return {'error': "Must specify a product ID"}
|
||||||
|
|
||||||
session = self.Session()
|
|
||||||
use_local = self.batch_handler.use_local_products()
|
use_local = self.batch_handler.use_local_products()
|
||||||
if use_local:
|
if use_local:
|
||||||
data = self.batch_handler.get_product_info_local(session, product_id)
|
data = self.get_local_product_info(product_id)
|
||||||
else:
|
else:
|
||||||
data = self.batch_handler.get_product_info_external(session, product_id)
|
raise NotImplementedError("TODO: add integration handler")
|
||||||
|
|
||||||
if 'error' in data:
|
if 'error' in data:
|
||||||
return data
|
return data
|
||||||
|
@ -552,6 +571,32 @@ class OrderView(MasterView):
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
# TODO: move this to some handler
|
||||||
|
def get_local_product_info(self, product_id):
|
||||||
|
""" """
|
||||||
|
model = self.app.model
|
||||||
|
session = self.Session()
|
||||||
|
product = session.get(model.LocalProduct, product_id)
|
||||||
|
if not product:
|
||||||
|
return {'error': "Product not found"}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'product_id': product.uuid.hex,
|
||||||
|
'scancode': product.scancode,
|
||||||
|
'brand_name': product.brand_name,
|
||||||
|
'description': product.description,
|
||||||
|
'size': product.size,
|
||||||
|
'full_description': product.full_description,
|
||||||
|
'weighed': product.weighed,
|
||||||
|
'special_order': product.special_order,
|
||||||
|
'department_id': product.department_id,
|
||||||
|
'department_name': product.department_name,
|
||||||
|
'case_size': product.case_size,
|
||||||
|
'unit_price_reg': product.unit_price_reg,
|
||||||
|
'vendor_name': product.vendor_name,
|
||||||
|
'vendor_item_code': product.vendor_item_code,
|
||||||
|
}
|
||||||
|
|
||||||
def add_item(self, batch, data):
|
def add_item(self, batch, data):
|
||||||
"""
|
"""
|
||||||
This adds a row to the user's current new order batch.
|
This adds a row to the user's current new order batch.
|
||||||
|
@ -680,9 +725,6 @@ class OrderView(MasterView):
|
||||||
'product_brand': row.product_brand,
|
'product_brand': row.product_brand,
|
||||||
'product_description': row.product_description,
|
'product_description': row.product_description,
|
||||||
'product_size': row.product_size,
|
'product_size': row.product_size,
|
||||||
'product_full_description': self.app.make_full_name(row.product_brand,
|
|
||||||
row.product_description,
|
|
||||||
row.product_size),
|
|
||||||
'product_weighed': row.product_weighed,
|
'product_weighed': row.product_weighed,
|
||||||
'department_display': row.department_name,
|
'department_display': row.department_name,
|
||||||
'special_order': row.special_order,
|
'special_order': row.special_order,
|
||||||
|
@ -709,6 +751,15 @@ class OrderView(MasterView):
|
||||||
else:
|
else:
|
||||||
data['product_id'] = row.product_id
|
data['product_id'] = row.product_id
|
||||||
|
|
||||||
|
# product_full_description
|
||||||
|
if use_local:
|
||||||
|
if row.local_product:
|
||||||
|
data['product_full_description'] = row.local_product.full_description
|
||||||
|
else: # use external
|
||||||
|
pass # TODO
|
||||||
|
if not data.get('product_id') and row.pending_product:
|
||||||
|
data['product_full_description'] = row.pending_product.full_description
|
||||||
|
|
||||||
# vendor_name
|
# vendor_name
|
||||||
if use_local:
|
if use_local:
|
||||||
if row.local_product:
|
if row.local_product:
|
||||||
|
@ -861,20 +912,7 @@ class OrderView(MasterView):
|
||||||
""" """
|
""" """
|
||||||
settings = [
|
settings = [
|
||||||
|
|
||||||
# batches
|
|
||||||
{'name': 'wutta.batch.neworder.handler.spec'},
|
|
||||||
|
|
||||||
# customers
|
|
||||||
{'name': 'sideshow.orders.use_local_customers',
|
|
||||||
# nb. this is really a bool but we present as string in config UI
|
|
||||||
#'type': bool,
|
|
||||||
'default': 'true'},
|
|
||||||
|
|
||||||
# products
|
# products
|
||||||
{'name': 'sideshow.orders.use_local_products',
|
|
||||||
# nb. this is really a bool but we present as string in config UI
|
|
||||||
#'type': bool,
|
|
||||||
'default': 'true'},
|
|
||||||
{'name': 'sideshow.orders.allow_unknown_products',
|
{'name': 'sideshow.orders.allow_unknown_products',
|
||||||
'type': bool,
|
'type': bool,
|
||||||
'default': True},
|
'default': True},
|
||||||
|
@ -896,10 +934,6 @@ class OrderView(MasterView):
|
||||||
|
|
||||||
context['pending_product_fields'] = self.PENDING_PRODUCT_ENTRY_FIELDS
|
context['pending_product_fields'] = self.PENDING_PRODUCT_ENTRY_FIELDS
|
||||||
|
|
||||||
handlers = self.app.get_batch_handler_specs('neworder')
|
|
||||||
handlers = [{'spec': spec} for spec in handlers]
|
|
||||||
context['batch_handlers'] = handlers
|
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -1011,6 +1045,8 @@ class OrderItemView(MasterView):
|
||||||
'department_id',
|
'department_id',
|
||||||
'department_name',
|
'department_name',
|
||||||
'special_order',
|
'special_order',
|
||||||
|
'order_qty',
|
||||||
|
'order_uom',
|
||||||
'case_size',
|
'case_size',
|
||||||
'unit_cost',
|
'unit_cost',
|
||||||
'unit_price_reg',
|
'unit_price_reg',
|
||||||
|
@ -1018,8 +1054,6 @@ class OrderItemView(MasterView):
|
||||||
'sale_ends',
|
'sale_ends',
|
||||||
'unit_price_quoted',
|
'unit_price_quoted',
|
||||||
'case_price_quoted',
|
'case_price_quoted',
|
||||||
'order_qty',
|
|
||||||
'order_uom',
|
|
||||||
'discount_percent',
|
'discount_percent',
|
||||||
'total_price',
|
'total_price',
|
||||||
'status_code',
|
'status_code',
|
||||||
|
|
|
@ -50,34 +50,6 @@ class TestNewOrderBatchHandler(DataTestCase):
|
||||||
config.setdefault('sideshow.orders.allow_unknown_products', 'false')
|
config.setdefault('sideshow.orders.allow_unknown_products', 'false')
|
||||||
self.assertFalse(handler.allow_unknown_products())
|
self.assertFalse(handler.allow_unknown_products())
|
||||||
|
|
||||||
def test_autocomplete_customers_external(self):
|
|
||||||
handler = self.make_handler()
|
|
||||||
self.assertRaises(NotImplementedError, handler.autocomplete_customers_external,
|
|
||||||
self.session, 'jack')
|
|
||||||
|
|
||||||
def test_autocomplete_cutomers_local(self):
|
|
||||||
model = self.app.model
|
|
||||||
handler = self.make_handler()
|
|
||||||
|
|
||||||
# empty results by default
|
|
||||||
self.assertEqual(handler.autocomplete_customers_local(self.session, 'foo'), [])
|
|
||||||
|
|
||||||
# add a customer
|
|
||||||
customer = model.LocalCustomer(full_name="Chuck Norris")
|
|
||||||
self.session.add(customer)
|
|
||||||
self.session.flush()
|
|
||||||
|
|
||||||
# search for chuck finds chuck
|
|
||||||
results = handler.autocomplete_customers_local(self.session, 'chuck')
|
|
||||||
self.assertEqual(len(results), 1)
|
|
||||||
self.assertEqual(results[0], {
|
|
||||||
'value': customer.uuid.hex,
|
|
||||||
'label': "Chuck Norris",
|
|
||||||
})
|
|
||||||
|
|
||||||
# search for sally finds nothing
|
|
||||||
self.assertEqual(handler.autocomplete_customers_local(self.session, 'sally'), [])
|
|
||||||
|
|
||||||
def test_set_customer(self):
|
def test_set_customer(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
handler = self.make_handler()
|
handler = self.make_handler()
|
||||||
|
@ -174,83 +146,6 @@ class TestNewOrderBatchHandler(DataTestCase):
|
||||||
self.assertIsNone(batch.phone_number)
|
self.assertIsNone(batch.phone_number)
|
||||||
self.assertIsNone(batch.email_address)
|
self.assertIsNone(batch.email_address)
|
||||||
|
|
||||||
def test_autocomplete_products_external(self):
|
|
||||||
handler = self.make_handler()
|
|
||||||
self.assertRaises(NotImplementedError, handler.autocomplete_products_external,
|
|
||||||
self.session, 'cheese')
|
|
||||||
|
|
||||||
def test_autocomplete_products_local(self):
|
|
||||||
model = self.app.model
|
|
||||||
handler = self.make_handler()
|
|
||||||
|
|
||||||
# empty results by default
|
|
||||||
self.assertEqual(handler.autocomplete_products_local(self.session, 'foo'), [])
|
|
||||||
|
|
||||||
# add a product
|
|
||||||
product = model.LocalProduct(brand_name="Bragg's", description="Vinegar")
|
|
||||||
self.session.add(product)
|
|
||||||
self.session.flush()
|
|
||||||
|
|
||||||
# search for vinegar finds product
|
|
||||||
results = handler.autocomplete_products_local(self.session, 'vinegar')
|
|
||||||
self.assertEqual(len(results), 1)
|
|
||||||
self.assertEqual(results[0], {
|
|
||||||
'value': product.uuid.hex,
|
|
||||||
'label': "Bragg's Vinegar",
|
|
||||||
})
|
|
||||||
|
|
||||||
# search for brag finds product
|
|
||||||
results = handler.autocomplete_products_local(self.session, 'brag')
|
|
||||||
self.assertEqual(len(results), 1)
|
|
||||||
self.assertEqual(results[0], {
|
|
||||||
'value': product.uuid.hex,
|
|
||||||
'label': "Bragg's Vinegar",
|
|
||||||
})
|
|
||||||
|
|
||||||
# search for juice finds nothing
|
|
||||||
self.assertEqual(handler.autocomplete_products_local(self.session, 'juice'), [])
|
|
||||||
|
|
||||||
def test_get_product_info_external(self):
|
|
||||||
handler = self.make_handler()
|
|
||||||
self.assertRaises(NotImplementedError, handler.get_product_info_external,
|
|
||||||
self.session, '07430500132')
|
|
||||||
|
|
||||||
def test_get_product_info_local(self):
|
|
||||||
model = self.app.model
|
|
||||||
handler = self.make_handler()
|
|
||||||
|
|
||||||
user = model.User(username='barney')
|
|
||||||
self.session.add(user)
|
|
||||||
local = model.LocalProduct(scancode='07430500132',
|
|
||||||
brand_name='Bragg',
|
|
||||||
description='Vinegar',
|
|
||||||
size='32oz',
|
|
||||||
case_size=12,
|
|
||||||
unit_price_reg=decimal.Decimal('5.99'))
|
|
||||||
self.session.add(local)
|
|
||||||
self.session.flush()
|
|
||||||
|
|
||||||
batch = handler.make_batch(self.session, created_by=user)
|
|
||||||
self.session.add(batch)
|
|
||||||
|
|
||||||
# typical, for local product
|
|
||||||
info = handler.get_product_info_local(self.session, local.uuid.hex)
|
|
||||||
self.assertEqual(info['product_id'], local.uuid.hex)
|
|
||||||
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'], 12)
|
|
||||||
self.assertEqual(info['unit_price_reg'], decimal.Decimal('5.99'))
|
|
||||||
|
|
||||||
# error if no product_id
|
|
||||||
self.assertRaises(ValueError, handler.get_product_info_local, self.session, None)
|
|
||||||
|
|
||||||
# error if product not found
|
|
||||||
mock_uuid = self.app.make_true_uuid()
|
|
||||||
self.assertRaises(ValueError, handler.get_product_info_local, self.session, mock_uuid.hex)
|
|
||||||
|
|
||||||
def test_add_item(self):
|
def test_add_item(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
enum = self.app.enum
|
enum = self.app.enum
|
||||||
|
@ -824,8 +719,10 @@ class TestNewOrderBatchHandler(DataTestCase):
|
||||||
self.session.add(row)
|
self.session.add(row)
|
||||||
self.session.flush()
|
self.session.flush()
|
||||||
# STATUS_OK
|
# STATUS_OK
|
||||||
row = handler.add_item(batch, {'scancode': '07430500132'}, 1, enum.ORDER_UOM_UNIT)
|
row = handler.make_row(product_id=42, order_qty=1, order_uom=enum.ORDER_UOM_UNIT)
|
||||||
self.session.flush()
|
handler.add_row(batch, row)
|
||||||
|
self.session.add(row)
|
||||||
|
self.session.commit()
|
||||||
|
|
||||||
# only 1 effective row
|
# only 1 effective row
|
||||||
rows = handler.get_effective_rows(batch)
|
rows = handler.get_effective_rows(batch)
|
||||||
|
|
|
@ -9,12 +9,4 @@ class TestSideshowMenuHandler(WebTestCase):
|
||||||
def test_make_menus(self):
|
def test_make_menus(self):
|
||||||
handler = mod.SideshowMenuHandler(self.config)
|
handler = mod.SideshowMenuHandler(self.config)
|
||||||
menus = handler.make_menus(self.request)
|
menus = handler.make_menus(self.request)
|
||||||
titles = [menu['title'] for menu in menus]
|
self.assertEqual(len(menus), 5)
|
||||||
self.assertEqual(titles, [
|
|
||||||
'Orders',
|
|
||||||
'Customers',
|
|
||||||
'Products',
|
|
||||||
'Batches',
|
|
||||||
'Other',
|
|
||||||
'Admin',
|
|
||||||
])
|
|
||||||
|
|
|
@ -42,8 +42,6 @@ class TestOrderView(WebTestCase):
|
||||||
|
|
||||||
def test_create(self):
|
def test_create(self):
|
||||||
self.pyramid_config.include('sideshow.web.views')
|
self.pyramid_config.include('sideshow.web.views')
|
||||||
self.config.setdefault('wutta.batch.neworder.handler.spec',
|
|
||||||
'sideshow.batch.neworder:NewOrderBatchHandler')
|
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
enum = self.app.enum
|
enum = self.app.enum
|
||||||
view = self.make_view()
|
view = self.make_view()
|
||||||
|
@ -179,9 +177,7 @@ class TestOrderView(WebTestCase):
|
||||||
|
|
||||||
def test_customer_autocomplete(self):
|
def test_customer_autocomplete(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
handler = self.make_handler()
|
|
||||||
view = self.make_view()
|
view = self.make_view()
|
||||||
view.batch_handler = handler
|
|
||||||
|
|
||||||
with patch.object(view, 'Session', return_value=self.session):
|
with patch.object(view, 'Session', return_value=self.session):
|
||||||
|
|
||||||
|
@ -209,16 +205,9 @@ class TestOrderView(WebTestCase):
|
||||||
result = view.customer_autocomplete()
|
result = view.customer_autocomplete()
|
||||||
self.assertEqual(result, [])
|
self.assertEqual(result, [])
|
||||||
|
|
||||||
# external lookup not implemented by default
|
|
||||||
with patch.object(handler, 'use_local_customers', return_value=False):
|
|
||||||
with patch.object(self.request, 'GET', new={'term': 'sally'}, create=True):
|
|
||||||
self.assertRaises(NotImplementedError, view.customer_autocomplete)
|
|
||||||
|
|
||||||
def test_product_autocomplete(self):
|
def test_product_autocomplete(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
handler = self.make_handler()
|
|
||||||
view = self.make_view()
|
view = self.make_view()
|
||||||
view.batch_handler = handler
|
|
||||||
|
|
||||||
with patch.object(view, 'Session', return_value=self.session):
|
with patch.object(view, 'Session', return_value=self.session):
|
||||||
|
|
||||||
|
@ -255,11 +244,6 @@ class TestOrderView(WebTestCase):
|
||||||
result = view.product_autocomplete()
|
result = view.product_autocomplete()
|
||||||
self.assertEqual(result, [])
|
self.assertEqual(result, [])
|
||||||
|
|
||||||
# external lookup not implemented by default
|
|
||||||
with patch.object(handler, 'use_local_products', return_value=False):
|
|
||||||
with patch.object(self.request, 'GET', new={'term': 'juice'}, create=True):
|
|
||||||
self.assertRaises(NotImplementedError, view.product_autocomplete)
|
|
||||||
|
|
||||||
def test_get_pending_product_required_fields(self):
|
def test_get_pending_product_required_fields(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
view = self.make_view()
|
view = self.make_view()
|
||||||
|
@ -545,27 +529,20 @@ class TestOrderView(WebTestCase):
|
||||||
self.assertEqual(context['case_size'], 12)
|
self.assertEqual(context['case_size'], 12)
|
||||||
self.assertEqual(context['unit_price_reg'], 5.99)
|
self.assertEqual(context['unit_price_reg'], 5.99)
|
||||||
|
|
||||||
|
# error if local product missing
|
||||||
|
mock_uuid = self.app.make_true_uuid()
|
||||||
|
context = view.get_product_info(batch, {'product_id': mock_uuid.hex})
|
||||||
|
self.assertEqual(context, {'error': "Product not found"})
|
||||||
|
|
||||||
# error if no product_id
|
# error if no product_id
|
||||||
context = view.get_product_info(batch, {})
|
context = view.get_product_info(batch, {})
|
||||||
self.assertEqual(context, {'error': "Must specify a product ID"})
|
self.assertEqual(context, {'error': "Must specify a product ID"})
|
||||||
|
|
||||||
# error if product not found
|
# external lookup not implemented (yet)
|
||||||
mock_uuid = self.app.make_true_uuid()
|
|
||||||
self.assertRaises(ValueError, view.get_product_info,
|
|
||||||
batch, {'product_id': mock_uuid.hex})
|
|
||||||
|
|
||||||
with patch.object(handler, 'use_local_products', return_value=False):
|
with patch.object(handler, 'use_local_products', return_value=False):
|
||||||
|
|
||||||
# external lookup not implemented by default
|
|
||||||
self.assertRaises(NotImplementedError, view.get_product_info,
|
self.assertRaises(NotImplementedError, view.get_product_info,
|
||||||
batch, {'product_id': '42'})
|
batch, {'product_id': '42'})
|
||||||
|
|
||||||
# external lookup may return its own error
|
|
||||||
with patch.object(handler, 'get_product_info_external',
|
|
||||||
return_value={'error': "something smells fishy"}):
|
|
||||||
context = view.get_product_info(batch, {'product_id': '42'})
|
|
||||||
self.assertEqual(context, {'error': "something smells fishy"})
|
|
||||||
|
|
||||||
def test_add_item(self):
|
def test_add_item(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
enum = self.app.enum
|
enum = self.app.enum
|
||||||
|
@ -993,47 +970,33 @@ class TestOrderView(WebTestCase):
|
||||||
|
|
||||||
# the next few tests will morph 2nd row..
|
# the next few tests will morph 2nd row..
|
||||||
|
|
||||||
def refresh_external(row):
|
|
||||||
row.product_scancode = '012345'
|
|
||||||
row.product_brand = 'Acme'
|
|
||||||
row.product_description = 'Bricks'
|
|
||||||
row.product_size = '1 ton'
|
|
||||||
row.product_weighed = True
|
|
||||||
row.department_id = 1
|
|
||||||
row.department_name = "Bricks & Mortar"
|
|
||||||
row.special_order = False
|
|
||||||
row.case_size = None
|
|
||||||
row.unit_cost = decimal.Decimal('599.99')
|
|
||||||
row.unit_price_reg = decimal.Decimal('999.99')
|
|
||||||
|
|
||||||
# typical, external product
|
# typical, external product
|
||||||
|
row2.product_id = '42'
|
||||||
with patch.object(handler, 'use_local_products', return_value=False):
|
with patch.object(handler, 'use_local_products', return_value=False):
|
||||||
with patch.object(handler, 'refresh_row_from_external_product', new=refresh_external):
|
|
||||||
handler.update_item(row2, '42', 1, enum.ORDER_UOM_UNIT)
|
|
||||||
data = view.normalize_row(row2)
|
data = view.normalize_row(row2)
|
||||||
self.assertEqual(data['uuid'], row2.uuid.hex)
|
self.assertEqual(data['uuid'], row2.uuid.hex)
|
||||||
self.assertEqual(data['sequence'], 2)
|
self.assertEqual(data['sequence'], 2)
|
||||||
self.assertEqual(data['product_id'], '42')
|
self.assertEqual(data['product_id'], '42')
|
||||||
self.assertEqual(data['product_scancode'], '012345')
|
self.assertIsNone(data['product_scancode'])
|
||||||
self.assertEqual(data['product_full_description'], 'Acme Bricks 1 ton')
|
self.assertNotIn('product_full_description', data) # TODO
|
||||||
self.assertIsNone(data['case_size'])
|
self.assertIsNone(data['case_size'])
|
||||||
self.assertNotIn('vendor_name', data) # TODO
|
self.assertNotIn('vendor_name', data) # TODO
|
||||||
self.assertEqual(data['order_qty'], 1)
|
self.assertEqual(data['order_qty'], 1)
|
||||||
self.assertEqual(data['order_uom'], 'EA')
|
self.assertEqual(data['order_uom'], 'EA')
|
||||||
self.assertEqual(data['order_qty_display'], '1 Units')
|
self.assertEqual(data['order_qty_display'], '1 Units')
|
||||||
self.assertEqual(data['unit_price_reg'], 999.99)
|
self.assertEqual(data['unit_price_reg'], 3.29)
|
||||||
self.assertEqual(data['unit_price_reg_display'], '$999.99')
|
self.assertEqual(data['unit_price_reg_display'], '$3.29')
|
||||||
self.assertNotIn('unit_price_sale', data)
|
self.assertNotIn('unit_price_sale', data)
|
||||||
self.assertNotIn('unit_price_sale_display', data)
|
self.assertNotIn('unit_price_sale_display', data)
|
||||||
self.assertNotIn('sale_ends', data)
|
self.assertNotIn('sale_ends', data)
|
||||||
self.assertNotIn('sale_ends_display', data)
|
self.assertNotIn('sale_ends_display', data)
|
||||||
self.assertEqual(data['unit_price_quoted'], 999.99)
|
self.assertEqual(data['unit_price_quoted'], 3.29)
|
||||||
self.assertEqual(data['unit_price_quoted_display'], '$999.99')
|
self.assertEqual(data['unit_price_quoted_display'], '$3.29')
|
||||||
self.assertIsNone(data['case_price_quoted'])
|
self.assertIsNone(data['case_price_quoted'])
|
||||||
self.assertEqual(data['case_price_quoted_display'], '')
|
self.assertEqual(data['case_price_quoted_display'], '')
|
||||||
self.assertEqual(data['total_price'], 999.99)
|
self.assertEqual(data['total_price'], 3.29)
|
||||||
self.assertEqual(data['total_price_display'], '$999.99')
|
self.assertEqual(data['total_price_display'], '$3.29')
|
||||||
self.assertFalse(data['special_order'])
|
self.assertIsNone(data['special_order'])
|
||||||
self.assertEqual(data['status_code'], row2.STATUS_OK)
|
self.assertEqual(data['status_code'], row2.STATUS_OK)
|
||||||
self.assertNotIn('pending_product', data)
|
self.assertNotIn('pending_product', data)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue