diff --git a/tailbone/newgrids/alchemy.py b/tailbone/newgrids/alchemy.py
index 0663ec73..80f727c1 100644
--- a/tailbone/newgrids/alchemy.py
+++ b/tailbone/newgrids/alchemy.py
@@ -2,7 +2,7 @@
################################################################################
#
# Rattail -- Retail Software Framework
-# Copyright © 2010-2015 Lance Edgar
+# Copyright © 2010-2016 Lance Edgar
#
# This file is part of Rattail.
#
@@ -24,18 +24,20 @@
FormAlchemy Grid Classes
"""
-from __future__ import unicode_literals
+from __future__ import unicode_literals, absolute_import
import logging
import sqlalchemy as sa
from sqlalchemy import orm
+from edbob.util import prettify
+
+from rattail.db.types import GPCType
+
import formalchemy
from webhelpers import paginate
-from edbob.util import prettify
-
from tailbone.db import Session
from tailbone.newgrids import Grid, GridColumn, filters
@@ -64,7 +66,8 @@ class AlchemyGrid(Grid):
def __init__(self, *args, **kwargs):
super(AlchemyGrid, self).__init__(*args, **kwargs)
fa_grid = formalchemy.Grid(self.model_class, instances=self.data,
- session=Session(), request=self.request)
+ session=kwargs.get('session', Session()),
+ request=self.request)
fa_grid.prettify = prettify
self._fa_grid = fa_grid
@@ -99,6 +102,8 @@ class AlchemyGrid(Grid):
factory = filters.AlchemyBooleanFilter
elif isinstance(column.type, (sa.Date, sa.DateTime)):
factory = filters.AlchemyDateFilter
+ elif isinstance(column.type, GPCType):
+ factory = filters.AlchemyGPCFilter
return factory(key, column=column, **kwargs)
def iter_filters(self):
@@ -107,6 +112,24 @@ class AlchemyGrid(Grid):
"""
return self.filters.itervalues()
+ def filter_data(self, query):
+ """
+ Filter and return the given data set, according to current settings.
+ """
+ # This overrides the core version only slightly, in that it will only
+ # invoke a join if any particular filter(s) actually modifies the
+ # query. The main motivation for this is on the products page, where
+ # the tricky "vendor (any)" filter has a weird join and causes
+ # unpredictable results. Now we can skip the join for that unless the
+ # user actually enters some criteria for it.
+ for filtr in self.iter_active_filters():
+ original = query
+ query = filtr.filter(query)
+ if query is not original and filtr.key in self.joiners and filtr.key not in self.joined:
+ query = self.joiners[filtr.key](query)
+ self.joined.add(filtr.key)
+ return query
+
def make_sorters(self, sorters):
"""
Returns a mapping of sort options for the grid. Keyword args override
diff --git a/tailbone/newgrids/core.py b/tailbone/newgrids/core.py
index 45ad3393..38dc019a 100644
--- a/tailbone/newgrids/core.py
+++ b/tailbone/newgrids/core.py
@@ -2,7 +2,7 @@
################################################################################
#
# Rattail -- Retail Software Framework
-# Copyright © 2010-2015 Lance Edgar
+# Copyright © 2010-2016 Lance Edgar
#
# This file is part of Rattail.
#
@@ -24,7 +24,7 @@
Core Grid Classes
"""
-from __future__ import unicode_literals
+from __future__ import unicode_literals, absolute_import
from edbob.util import prettify
@@ -246,11 +246,17 @@ class Grid(object):
"""
Check to see if the current user has default settings on file for this grid.
"""
- if not self.request.user:
+ user = self.request.user
+ if not user:
return False
+
+ session = getattr(self, 'session', Session())
+ if user not in session:
+ user = session.merge(user)
+
# User defaults should have all or nothing, so just check one key.
- key = 'tailbone.{0}.grid.{1}.sortkey'.format(self.request.user.uuid, self.key)
- return get_setting(Session(), key) is not None
+ key = 'tailbone.{}.grid.{}.sortkey'.format(user.uuid, self.key)
+ return get_setting(session, key) is not None
def apply_user_defaults(self, settings):
"""
@@ -600,10 +606,11 @@ class Grid(object):
"""
url = action.get_url(row)
if url:
+ kwargs = {'class_': action.key}
if action.icon:
- icon = HTML.tag('span', class_='ui-icon ui-icon-{0}'.format(action.icon))
- return tags.link_to(icon + action.label, url)
- return tags.link_to(action.label, url)
+ icon = HTML.tag('span', class_='ui-icon ui-icon-{}'.format(action.icon))
+ return tags.link_to(icon + action.label, url, **kwargs)
+ return tags.link_to(action.label, url, **kwargs)
def iter_rows(self):
return self.make_visible_data()
diff --git a/tailbone/newgrids/filters.py b/tailbone/newgrids/filters.py
index fa5b71a7..2916abab 100644
--- a/tailbone/newgrids/filters.py
+++ b/tailbone/newgrids/filters.py
@@ -2,7 +2,7 @@
################################################################################
#
# Rattail -- Retail Software Framework
-# Copyright © 2010-2015 Lance Edgar
+# Copyright © 2010-2016 Lance Edgar
#
# This file is part of Rattail.
#
@@ -24,12 +24,13 @@
Grid Filters
"""
-from __future__ import unicode_literals
+from __future__ import unicode_literals, absolute_import
import sqlalchemy as sa
from edbob.util import prettify
+from rattail.gpc import GPC
from rattail.util import OrderedDict
from rattail.core import UNSPECIFIED
@@ -367,6 +368,44 @@ class AlchemyDateFilter(AlchemyGridFilter):
return self.filter_is_null(query, value)
+class AlchemyGPCFilter(AlchemyGridFilter):
+ """
+ GPC filter for SQLAlchemy.
+ """
+ default_verbs = ['equal', 'not_equal']
+
+ def filter_equal(self, query, value):
+ """
+ Filter data with an equal ('=') query.
+ """
+ if value is None or value == '':
+ return query
+ try:
+ return query.filter(self.column.in_((
+ GPC(value),
+ GPC(value, calc_check_digit='upc'))))
+ except ValueError:
+ return query
+
+ def filter_not_equal(self, query, value):
+ """
+ Filter data with a not eqaul ('!=') query.
+ """
+ if value is None or value == '':
+ return query
+
+ # When saying something is 'not equal' to something else, we must also
+ # include things which are nothing at all, in our result set.
+ try:
+ return query.filter(sa.or_(
+ ~self.column.in_((
+ GPC(value),
+ GPC(value, calc_check_digit='upc'))),
+ self.column == None))
+ except ValueError:
+ return query
+
+
class GridFilterSet(OrderedDict):
"""
Collection class for :class:`GridFilter` instances.
diff --git a/tailbone/static/css/newgrids.css b/tailbone/static/css/newgrids.css
index 54c96a2d..59327e24 100644
--- a/tailbone/static/css/newgrids.css
+++ b/tailbone/static/css/newgrids.css
@@ -17,6 +17,7 @@
.newgrid-wrapper .newfilters fieldset {
margin: -8px 0 5px 0;
padding: 1px 5px 5px 5px;
+ width: 80%;
}
.newgrid-wrapper .newfilters .filter {
diff --git a/tailbone/templates/products/batch.mako b/tailbone/templates/products/batch.mako
index a0aaeaa4..ee44d607 100644
--- a/tailbone/templates/products/batch.mako
+++ b/tailbone/templates/products/batch.mako
@@ -20,7 +20,7 @@
${h.submit('create', "Create Batch")}
-
+ ${h.link_to("Cancel", url('products'), class_='button')}
${h.end_form()}
diff --git a/tailbone/templates/products/crud.mako b/tailbone/templates/products/crud.mako
deleted file mode 100644
index 75a0421c..00000000
--- a/tailbone/templates/products/crud.mako
+++ /dev/null
@@ -1,16 +0,0 @@
-## -*- coding: utf-8 -*-
-<%inherit file="/crud.mako" />
-
-<%def name="context_menu_items()">
- ${h.link_to("Back to Products", url('products'))}
- % if form.readonly and request.has_perm('products.update'):
- ${h.link_to("Edit this Product", url('product.update', uuid=form.fieldset.model.uuid))}
- % elif form.updating:
- ${h.link_to("View this Product", url('product.read', uuid=form.fieldset.model.uuid))}
- % endif
- % if version_count is not Undefined and request.has_perm('product.versions.view'):
- ${h.link_to("View Change History ({0})".format(version_count), url('product.versions', uuid=form.fieldset.model.uuid))}
- % endif
-%def>
-
-${parent.body()}
diff --git a/tailbone/templates/products/edit.mako b/tailbone/templates/products/edit.mako
new file mode 100644
index 00000000..981d42ab
--- /dev/null
+++ b/tailbone/templates/products/edit.mako
@@ -0,0 +1,11 @@
+## -*- coding: utf-8 -*-
+<%inherit file="/master/edit.mako" />
+
+<%def name="context_menu_items()">
+ ${parent.context_menu_items()}
+ % if version_count is not Undefined and request.has_perm('product.versions.view'):
+ ${h.link_to("View Change History ({})".format(version_count), url('product.versions', uuid=instance.uuid))}
+ % endif
+%def>
+
+${parent.body()}
diff --git a/tailbone/templates/products/index.mako b/tailbone/templates/products/index.mako
index fec57c50..b4b2c61b 100644
--- a/tailbone/templates/products/index.mako
+++ b/tailbone/templates/products/index.mako
@@ -1,7 +1,5 @@
## -*- coding: utf-8 -*-
-<%inherit file="/grid.mako" />
-
-<%def name="title()">Products%def>
+<%inherit file="/master/index.mako" />
<%def name="head_tags()">
${parent.head_tags()}
@@ -35,49 +33,48 @@
% if label_profiles and request.has_perm('products.print_labels'):
-
% endif
%def>
-<%def name="tools()">
+<%def name="grid_tools()">
% if label_profiles and request.has_perm('products.print_labels'):
-
+
- Label |
- Qty. |
+ Label |
+ Qty. |
@@ -97,9 +94,7 @@
%def>
<%def name="context_menu_items()">
- % if request.has_perm('products.create'):
- ${h.link_to("Create a new Product", url('product.create'))}
- % endif
+ ${parent.context_menu_items()}
% if request.has_perm('batches.create'):
${h.link_to("Create Batch from Results", url('products.create_batch'))}
% endif
diff --git a/tailbone/templates/products/read.mako b/tailbone/templates/products/view.mako
similarity index 93%
rename from tailbone/templates/products/read.mako
rename to tailbone/templates/products/view.mako
index e084def3..b8414941 100644
--- a/tailbone/templates/products/read.mako
+++ b/tailbone/templates/products/view.mako
@@ -1,7 +1,9 @@
## -*- coding: utf-8 -*-
-<%inherit file="/products/crud.mako" />
+<%inherit file="/master/view.mako" />
<%namespace file="/forms/lib.mako" import="render_field_readonly" />
+<% product = instance %>
+
<%def name="head_tags()">
${parent.head_tags()}
%def>
-<% product = form.fieldset.model %>
+<%def name="context_menu_items()">
+ ${parent.context_menu_items()}
+ % if version_count is not Undefined and request.has_perm('product.versions.view'):
+ ${h.link_to("View Change History ({})".format(version_count), url('product.versions', uuid=product.uuid))}
+ % endif
+%def>
<%def name="render_organization_fields(form)">
${render_field_readonly(form.fieldset.department)}
diff --git a/tailbone/views/master.py b/tailbone/views/master.py
index ab847764..b216b543 100644
--- a/tailbone/views/master.py
+++ b/tailbone/views/master.py
@@ -330,20 +330,18 @@ class MasterView(View):
"""
return getattr(cls, 'grid_key', '{0}s'.format(cls.get_normalized_model_name()))
- def make_grid_kwargs(self):
+ def make_grid_kwargs(self, **kwargs):
"""
Return a dictionary of kwargs to be passed to the factory when creating
new grid instances.
"""
- return {
+ defaults = {
'width': 'full',
'filterable': self.filterable,
'sortable': True,
'default_sortkey': getattr(self, 'default_sortkey', None),
'sortdir': getattr(self, 'sortdir', 'asc'),
'pageable': self.pageable,
- 'main_actions': self.get_main_actions(),
- 'more_actions': self.get_more_actions(),
'checkboxes': self.checkboxes,
'checked': self.checked,
'row_attrs': self.get_row_attrs,
@@ -353,6 +351,15 @@ class MasterView(View):
'permission_prefix': self.get_permission_prefix(),
'route_prefix': self.get_route_prefix(),
}
+ if 'main_actions' not in kwargs and 'more_actions' not in kwargs:
+ main, more = self.get_grid_actions()
+ defaults['main_actions'] = main
+ defaults['more_actions'] = more
+ defaults.update(kwargs)
+ return defaults
+
+ def get_grid_actions(self):
+ return self.get_main_actions(), self.get_more_actions()
def get_row_attrs(self, row, i):
"""
@@ -379,7 +386,8 @@ class MasterView(View):
Return a list of 'main' actions for the grid.
"""
actions = []
- if self.viewable:
+ prefix = self.get_permission_prefix()
+ if self.viewable and self.request.has_perm('{}.view'.format(prefix)):
actions.append(self.make_action('view', icon='zoomin'))
return actions
@@ -388,9 +396,10 @@ class MasterView(View):
Return a list of 'more' actions for the grid.
"""
actions = []
- if self.editable:
+ prefix = self.get_permission_prefix()
+ if self.editable and self.request.has_perm('{}.edit'.format(prefix)):
actions.append(self.make_action('edit', icon='pencil'))
- if self.deletable:
+ if self.deletable and self.request.has_perm('{}.delete'.format(prefix)):
actions.append(self.make_action('delete', icon='trash', url=self.default_delete_url))
return actions
@@ -421,14 +430,14 @@ class MasterView(View):
values = [getattr(row, k) for k in keys]
return dict(zip(keys, values))
- def make_grid(self):
+ def make_grid(self, **kwargs):
"""
Make and return a new (configured) grid instance.
"""
factory = self.get_grid_factory()
key = self.get_grid_key()
- data = self.get_data()
- kwargs = self.make_grid_kwargs()
+ data = self.get_data(session=kwargs.get('session'))
+ kwargs = self.make_grid_kwargs(**kwargs)
grid = factory(key, self.request, data=data, model_class=self.get_model_class(error=False), **kwargs)
self.configure_grid(grid)
grid.load_settings()
diff --git a/tailbone/views/messages.py b/tailbone/views/messages.py
index c1b3dbba..3b357902 100644
--- a/tailbone/views/messages.py
+++ b/tailbone/views/messages.py
@@ -106,8 +106,12 @@ class TerseRecipientsFieldRenderer(formalchemy.FieldRenderer):
recipients = self.raw_value
if not recipients:
return ''
- recips = filter(lambda r: r.recipient is not self.request.user, recipients)
+ message = self.field.parent.model
+ recips = [r for r in recipients if r.recipient is not self.request.user]
recips = sorted([r.recipient.display_name for r in recips])
+ if len(recips) < len(recipients) and (
+ message.sender is not self.request.user or not recips):
+ recips.insert(0, 'you')
if len(recips) < 5:
return ', '.join(recips)
return "{}, ...".format(', '.join(recips[:4]))
diff --git a/tailbone/views/products.py b/tailbone/views/products.py
index 2dc17a7f..72f3378c 100644
--- a/tailbone/views/products.py
+++ b/tailbone/views/products.py
@@ -2,7 +2,7 @@
################################################################################
#
# Rattail -- Retail Software Framework
-# Copyright © 2010-2015 Lance Edgar
+# Copyright © 2010-2016 Lance Edgar
#
# This file is part of Rattail.
#
@@ -32,158 +32,126 @@ import re
import sqlalchemy as sa
from sqlalchemy import orm
+from rattail import enum, pod, sil, batches
+from rattail.db import model, api, auth, Session as RattailSession
+from rattail.gpc import GPC
+from rattail.threads import Thread
+from rattail.exceptions import LabelPrintingError
+
import formalchemy
-from pyramid.httpexceptions import HTTPFound
+from pyramid import httpexceptions
from pyramid.renderers import render_to_response
from webhelpers.html import tags
-import rattail.labels
-from rattail import enum
-from rattail import sil
-from rattail import batches
-from rattail.threads import Thread
-from rattail.exceptions import LabelPrintingError
-from rattail.db import model
-from rattail.db.model import (
- Product, ProductPrice, ProductCost, ProductCode,
- Brand, Vendor, Department, Subdepartment, LabelProfile)
-from rattail.gpc import GPC
-from rattail.db.api import get_product_by_upc
-from rattail.db.util import configure_session
-from rattail.pod import get_image_url, get_image_path
-
-from tailbone.views import SearchableAlchemyGridView, CrudView, AutocompleteView
-from tailbone.views.continuum import VersionView, version_defaults
-from tailbone.forms import EnumFieldRenderer, DateTimeFieldRenderer
+from tailbone import forms
from tailbone.db import Session
-from tailbone.forms import GPCFieldRenderer, BrandFieldRenderer, PriceFieldRenderer
-from tailbone.forms.renderers import products as forms
+from tailbone.views import MasterView, SearchableAlchemyGridView, AutocompleteView
+from tailbone.views.continuum import VersionView, version_defaults
+from tailbone.forms.renderers import products as products_forms
+from tailbone.newgrids import GridAction
from tailbone.progress import SessionProgress
-class ProductsGrid(SearchableAlchemyGridView):
+class ProductsView(MasterView):
+ """
+ Master view for the Product class.
+ """
+ model_class = model.Product
- mapped_class = Product
- config_prefix = 'products'
- sort = 'description'
+ # child_version_classes = [
+ # (model.ProductCode, 'product_uuid'),
+ # (model.ProductCost, 'product_uuid'),
+ # (model.ProductPrice, 'product_uuid'),
+ # ]
# These aliases enable the grid queries to filter products which may be
# purchased from *any* vendor, and yet sort by only the "preferred" vendor
# (since that's what shows up in the grid column).
- ProductCostAny = orm.aliased(ProductCost)
- VendorAny = orm.aliased(Vendor)
+ ProductCostAny = orm.aliased(model.ProductCost)
+ VendorAny = orm.aliased(model.Vendor)
- def join_map(self):
+ def __init__(self, request):
+ self.request = request
+ self.print_labels = request.rattail_config.getbool('tailbone', 'products.print_labels', default=False)
+
+ def query(self, session):
+ user = self.request.user
+ if user and user not in session:
+ user = session.merge(user)
+
+ query = session.query(model.Product)
+ if not auth.has_permission(session, user, 'products.view_deleted'):
+ query = query.filter(model.Product.deleted == False)
+
+ # TODO: This used to be a good idea I thought...but in dev it didn't
+ # seem to make much difference, except with a larger (50K) data set it
+ # totally bogged things down instead of helping...
+ # query = query\
+ # .options(orm.joinedload(model.Product.brand))\
+ # .options(orm.joinedload(model.Product.department))\
+ # .options(orm.joinedload(model.Product.subdepartment))\
+ # .options(orm.joinedload(model.Product.regular_price))\
+ # .options(orm.joinedload(model.Product.current_price))\
+ # .options(orm.joinedload(model.Product.vendor))
+
+ return query
+
+ def configure_grid(self, g):
def join_vendor(q):
- q = q.outerjoin(
- ProductCost,
- sa.and_(
- ProductCost.product_uuid == Product.uuid,
- ProductCost.preference == 1,
- ))
- q = q.outerjoin(Vendor)
- return q
+ return q.outerjoin(model.ProductCost,
+ sa.and_(
+ model.ProductCost.product_uuid == model.Product.uuid,
+ model.ProductCost.preference == 1))\
+ .outerjoin(model.Vendor)
def join_vendor_any(q):
- q = q.outerjoin(
- self.ProductCostAny,
- self.ProductCostAny.product_uuid == Product.uuid)
- q = q.outerjoin(
- self.VendorAny,
- self.VendorAny.uuid == self.ProductCostAny.vendor_uuid)
- return q
+ return q.outerjoin(self.ProductCostAny,
+ self.ProductCostAny.product_uuid == model.Product.uuid)\
+ .outerjoin(self.VendorAny,
+ self.VendorAny.uuid == self.ProductCostAny.vendor_uuid)
- return {
- 'brand':
- lambda q: q.outerjoin(Brand),
- 'family':
- lambda q: q.outerjoin(model.Family),
- 'department':
- lambda q: q.outerjoin(Department,
- Department.uuid == Product.department_uuid),
- 'subdepartment':
- lambda q: q.outerjoin(Subdepartment,
- Subdepartment.uuid == Product.subdepartment_uuid),
- u'report_code':
- lambda q: q.outerjoin(model.ReportCode),
- 'regular_price':
- lambda q: q.outerjoin(ProductPrice,
- ProductPrice.uuid == Product.regular_price_uuid),
- 'current_price':
- lambda q: q.outerjoin(ProductPrice,
- ProductPrice.uuid == Product.current_price_uuid),
- 'vendor':
- join_vendor,
- 'vendor_any':
- join_vendor_any,
- 'code':
- lambda q: q.outerjoin(ProductCode),
- }
+ g.joiners['brand'] = lambda q: q.outerjoin(model.Brand)
+ g.joiners['family'] = lambda q: q.outerjoin(model.Family)
+ g.joiners['department'] = lambda q: q.outerjoin(model.Department,
+ model.Department.uuid == model.Product.department_uuid)
+ g.joiners['subdepartment'] = lambda q: q.outerjoin(model.Subdepartment,
+ model.Subdepartment.uuid == model.Product.subdepartment_uuid)
+ g.joiners['report_code'] = lambda q: q.outerjoin(model.ReportCode)
+ g.joiners['regular_price'] = lambda q: q.outerjoin(model.ProductPrice,
+ model.ProductPrice.uuid == model.Product.regular_price_uuid)
+ g.joiners['current_price'] = lambda q: q.outerjoin(model.ProductPrice,
+ model.ProductPrice.uuid == model.Product.current_price_uuid)
+ g.joiners['vendor'] = join_vendor
+ g.joiners['vendor_any'] = join_vendor_any
+ g.joiners['code'] = lambda q: q.outerjoin(model.ProductCode)
- def filter_map(self):
- return self.make_filter_map(
- ilike=['description', 'size'],
- upc=self.filter_gpc(model.Product.upc),
- brand=self.filter_ilike(Brand.name),
- family=self.filter_ilike(model.Family.name),
- department=self.filter_ilike(Department.name),
- report_code=self.filter_ilike(model.ReportCode.name),
- subdepartment=self.filter_ilike(Subdepartment.name),
- vendor=self.filter_ilike(Vendor.name),
- vendor_any=self.filter_ilike(self.VendorAny.name),
- code=self.filter_ilike(ProductCode.code))
+ g.sorters['brand'] = g.make_sorter(model.Brand.name)
+ g.sorters['department'] = g.make_sorter(model.Department.name)
+ g.sorters['subdepartment'] = g.make_sorter(model.Subdepartment.name)
+ g.sorters['vendor'] = g.make_sorter(model.Vendor.name)
- def filter_config(self):
- return self.make_filter_config(
- include_filter_upc=True,
- filter_type_upc='is',
- filter_label_upc="UPC",
- include_filter_brand=True,
- filter_type_brand='lk',
- include_filter_description=True,
- filter_type_description='lk',
- include_filter_department=True,
- filter_type_department='lk',
- filter_label_vendor="Vendor (preferred)",
- include_filter_vendor_any=True,
- filter_label_vendor_any="Vendor (any)",
- filter_type_vendor_any='lk')
+ g.filters['upc'].default_active = True
+ g.filters['upc'].default_verb = 'equal'
+ g.filters['upc'].label = "UPC"
+ g.filters['description'].default_active = True
+ g.filters['description'].default_verb = 'contains'
+ g.filters['brand'] = g.make_filter('brand', model.Brand.name,
+ default_active=True, default_verb='contains')
+ g.filters['family'] = g.make_filter('family', model.Family.name)
+ g.filters['department'] = g.make_filter('department', model.Department.name,
+ default_active=True, default_verb='contains')
+ g.filters['subdepartment'] = g.make_filter('subdepartment', model.Subdepartment.name)
+ g.filters['report_code'] = g.make_filter('report_code', model.ReportCode.name)
+ g.filters['vendor'] = g.make_filter('vendor', model.Vendor.name, label="Vendor (preferred)")
+ g.filters['vendor_any'] = g.make_filter('vendor_any', self.VendorAny.name, label="Vendor (any)")
+ g.filters['code'] = g.make_filter('code', model.ProductCode.code)
- def sort_map(self):
- return self.make_sort_map(
- 'upc', 'description', 'size',
- brand=self.sorter(Brand.name),
- department=self.sorter(Department.name),
- subdepartment=self.sorter(Subdepartment.name),
- regular_price=self.sorter(ProductPrice.price),
- current_price=self.sorter(ProductPrice.price),
- vendor=self.sorter(Vendor.name))
+ g.default_sortkey = 'description'
- def query(self):
- q = self.make_query()
- if not self.request.has_perm('products.view_deleted'):
- q = q.filter(model.Product.deleted == False)
- q = q.options(orm.joinedload(Product.brand))
- q = q.options(orm.joinedload(Product.department))
- q = q.options(orm.joinedload(Product.subdepartment))
- q = q.options(orm.joinedload(Product.regular_price))
- q = q.options(orm.joinedload(Product.current_price))
- q = q.options(orm.joinedload(Product.vendor))
- return q
-
- def grid(self):
- def extra_row_class(row, i):
- cls = []
- if row.not_for_sale:
- cls.append('not-for-sale')
- if row.deleted:
- cls.append('deleted')
- return ' '.join(cls) if cls else None
- g = self.make_grid(extra_row_class=extra_row_class)
- g.upc.set(renderer=GPCFieldRenderer)
- g.regular_price.set(renderer=PriceFieldRenderer)
- g.current_price.set(renderer=PriceFieldRenderer)
+ g.upc.set(renderer=forms.renderers.GPCFieldRenderer)
+ g.regular_price.set(renderer=forms.renderers.PriceFieldRenderer)
+ g.current_price.set(renderer=forms.renderers.PriceFieldRenderer)
g.configure(
include=[
g.upc.label("UPC"),
@@ -197,80 +165,68 @@ class ProductsGrid(SearchableAlchemyGridView):
],
readonly=True)
- if self.request.has_perm('products.read'):
- g.viewable = True
- g.view_route_name = 'product.read'
- if self.request.has_perm('products.update'):
- g.editable = True
- g.edit_route_name = 'product.update'
- if self.request.has_perm('products.delete'):
- g.deletable = True
- g.delete_route_name = 'product.delete'
+ # TODO: need to check for 'print labels' permission here also
+ if self.print_labels:
+ g.more_actions.append(GridAction('print_label', icon='print'))
- # Maybe add Print Label column.
- if self.rattail_config.getbool('tailbone', 'products.print_labels', default=True):
- q = Session.query(LabelProfile)
- if q.count():
- def labels(row):
- return tags.link_to("Print", '#', class_='print-label')
- g.add_column('labels', "Labels", labels)
+ def template_kwargs_index(self, **kwargs):
+ if self.print_labels:
+ kwargs['label_profiles'] = Session.query(model.LabelProfile)\
+ .filter(model.LabelProfile.visible == True)\
+ .order_by(model.LabelProfile.ordinal)\
+ .all()
+ return kwargs
- return g
+ def row_attrs(self, row, i):
- def render_kwargs(self):
- q = Session.query(LabelProfile)
- q = q.filter(LabelProfile.visible == True)
- q = q.order_by(LabelProfile.ordinal)
- return {'label_profiles': q.all()}
+ attrs = {'uuid': row.uuid}
+ classes = []
+ if row.not_for_sale:
+ classes.append('not-for-sale')
+ if row.deleted:
+ classes.append('deleted')
+ if classes:
+ attrs['class_'] = ' '.join(classes)
-class ProductCrud(CrudView):
- """
- Product CRUD view class.
- """
- mapped_class = Product
- home_route = 'products'
- child_version_classes = [
- (model.ProductCode, 'product_uuid'),
- (model.ProductCost, 'product_uuid'),
- (model.ProductPrice, 'product_uuid'),
- ]
+ return attrs
- def get_model(self, key):
- model = super(ProductCrud, self).get_model(key)
- if model:
- return model
- model = Session.query(ProductPrice).get(key)
- if model:
- return model.product
- return None
+ def get_instance(self):
+ key = self.request.matchdict['uuid']
+ product = Session.query(model.Product).get(key)
+ if product:
+ return product
+ price = Session.query(model.ProductPrice).get(key)
+ if price:
+ return price.product
+ raise httpexceptions.HTTPNotFound()
- def fieldset(self, model):
- fs = self.make_fieldset(model)
- fs.upc.set(renderer=GPCFieldRenderer)
+ def configure_fieldset(self, fs):
+
+ fs.upc.set(renderer=forms.renderers.GPCFieldRenderer)
fs.brand.set(options=[])
- fs.unit_of_measure.set(renderer=EnumFieldRenderer(enum.UNIT_OF_MEASURE))
- fs.regular_price.set(renderer=PriceFieldRenderer)
- fs.current_price.set(renderer=PriceFieldRenderer)
+ fs.unit_of_measure.set(renderer=forms.renderers.EnumFieldRenderer(enum.UNIT_OF_MEASURE))
+ fs.regular_price.set(renderer=forms.renderers.PriceFieldRenderer)
+ fs.current_price.set(renderer=forms.renderers.PriceFieldRenderer)
+ fs.last_sold.set(renderer=forms.renderers.DateTimeFieldRenderer(self.rattail_config))
- fs.append(formalchemy.Field('current_price_ends'))
- fs.current_price_ends.set(value=lambda p: p.current_price.ends if p.current_price else None)
- fs.current_price_ends.set(renderer=DateTimeFieldRenderer(self.request.rattail_config))
+ fs.append(formalchemy.Field('current_price_ends',
+ value=lambda p: p.current_price.ends if p.current_price else None,
+ renderer=forms.renderers.DateTimeFieldRenderer(self.rattail_config)))
- fs.last_sold.set(renderer=DateTimeFieldRenderer(self.request.rattail_config))
fs.configure(
include=[
fs.upc.label("UPC"),
- fs.brand.with_renderer(BrandFieldRenderer),
+ fs.brand.with_renderer(forms.renderers.BrandFieldRenderer),
fs.description,
fs.unit_size,
fs.unit_of_measure.label("Unit of Measure"),
fs.size,
fs.weighed,
fs.case_pack,
- fs.department.with_renderer(forms.DepartmentFieldRenderer),
- fs.subdepartment.with_renderer(forms.SubdepartmentFieldRenderer),
- fs.category.with_renderer(forms.CategoryFieldRenderer),
+ fs.department.with_renderer(products_forms.DepartmentFieldRenderer),
+ fs.subdepartment.with_renderer(products_forms.SubdepartmentFieldRenderer),
+ fs.category.with_renderer(products_forms.CategoryFieldRenderer),
fs.family,
fs.report_code,
fs.regular_price,
@@ -285,33 +241,119 @@ class ProductCrud(CrudView):
fs.deleted,
fs.last_sold,
])
- if not self.readonly:
+ if not self.viewing:
del fs.regular_price
del fs.current_price
if not self.request.has_perm('products.view_deleted'):
del fs.deleted
- return fs
- def pre_crud(self, product):
- self.product_deleted = not self.creating and product.deleted
-
- def post_crud(self, product, form):
- if self.product_deleted:
- self.request.session.flash("This product is marked as deleted.", 'error')
-
- def template_kwargs(self, form):
- kwargs = super(ProductCrud, self).template_kwargs(form)
+ def template_kwargs_view(self, **kwargs):
kwargs['image'] = False
- product = form.fieldset.model
+ product = kwargs['instance']
if product.upc:
- kwargs['image_url'] = get_image_url(
- self.request.rattail_config, product.upc)
- kwargs['image_path'] = get_image_path(
- self.request.rattail_config, product.upc)
+ kwargs['image_url'] = pod.get_image_url(self.rattail_config, product.upc)
+ kwargs['image_path'] = pod.get_image_path(self.rattail_config, product.upc)
if os.path.exists(kwargs['image_path']):
kwargs['image'] = True
return kwargs
+ def edit(self):
+ # TODO: Should add some more/better hooks, so don't have to duplicate
+ # so much code here.
+ self.editing = True
+ instance = self.get_instance()
+ form = self.make_form(instance)
+ product_deleted = instance.deleted
+ if self.request.method == 'POST':
+ if form.validate():
+ self.save_form(form)
+ self.after_edit(instance)
+ self.request.session.flash("{} {} has been updated.".format(
+ self.get_model_title(), self.get_instance_title(instance)))
+ return self.redirect(self.get_action_url('view', instance))
+ if product_deleted:
+ self.request.session.flash("This product is marked as deleted.", 'error')
+ return self.render_to_response('edit', {'instance': instance,
+ 'instance_title': self.get_instance_title(instance),
+ 'form': form})
+
+ def make_batch(self):
+ if self.request.method == 'POST':
+ provider = self.request.POST.get('provider')
+ if provider:
+ provider = batches.get_provider(self.rattail_config, provider)
+ if provider:
+
+ if self.request.POST.get('params') == 'True':
+ provider.set_params(Session(), **self.request.POST)
+
+ else:
+ try:
+ url = self.request.route_url('batch_params.{}'.format(provider.name))
+ except KeyError:
+ pass
+ else:
+ self.request.session['referer'] = self.request.current_route_url()
+ return httpexceptions.HTTPFound(location=url)
+
+ progress = SessionProgress(self.request, 'products.batch')
+ thread = Thread(target=self.make_batch_thread, args=(provider, progress))
+ thread.start()
+ kwargs = {
+ 'key': 'products.batch',
+ 'cancel_url': self.request.route_url('products'),
+ 'cancel_msg': "Batch creation was canceled.",
+ }
+ return render_to_response('/progress.mako', kwargs, request=self.request)
+
+ enabled = self.rattail_config.get('rattail.pyramid', 'batches.providers')
+ if enabled:
+ enabled = enabled.split()
+
+ providers = []
+ for provider in batches.iter_providers():
+ if not enabled or provider.name in enabled:
+ providers.append((provider.name, provider.description))
+
+ return {'providers': providers}
+
+ def make_batch_thread(self, provider, progress):
+ """
+ Threat target for making a batch from current products query.
+ """
+ session = RattailSession()
+
+ grid = self.make_grid(session=session, pageable=False,
+ main_actions=[], more_actions=[])
+ products = grid._fa_grid.rows
+
+ batch = provider.make_batch(session, products, progress)
+ if not batch:
+ session.rollback()
+ session.close()
+ return
+
+ session.commit()
+ session.refresh(batch)
+ session.close()
+
+ progress.session.load()
+ progress.session['complete'] = True
+ progress.session['success_url'] = self.request.route_url('batch.read', uuid=batch.uuid)
+ progress.session['success_msg'] = 'Batch "{}" has been created.'.format(batch.description)
+ progress.session.save()
+
+ @classmethod
+ def defaults(cls, config):
+
+ # make batch from product query
+ config.add_route('products.create_batch', '/products/batch')
+ config.add_view(cls, attr='make_batch', route_name='products.create_batch',
+ renderer='/products/batch.mako',
+ permission='batches.create')
+
+ cls._defaults(config)
+
class ProductVersionView(VersionView):
"""
@@ -331,7 +373,7 @@ class ProductVersionView(VersionView):
"""
uuid = self.request.matchdict['uuid']
product = Session.query(model.Product).get(uuid)
- assert product, "No product found for UUID: {0}".format(repr(uuid))
+ assert product, "No product found for UUID: {}".format(repr(uuid))
if product.deleted:
self.request.session.flash("This product is marked as deleted.", 'error')
@@ -354,8 +396,8 @@ class ProductsAutocomplete(AutocompleteView):
def query(self, term):
q = Session.query(model.Product).outerjoin(model.Brand)
q = q.filter(sa.or_(
- model.Brand.name.ilike('%{0}%'.format(term)),
- model.Product.description.ilike('%{0}%'.format(term))))
+ model.Brand.name.ilike('%{}%'.format(term)),
+ model.Product.description.ilike('%{}%'.format(term))))
if not self.request.has_perm('products.view_deleted'):
q = q.filter(model.Product.deleted == False)
q = q.order_by(model.Brand.name, model.Product.description)
@@ -377,11 +419,11 @@ def products_search(request):
upc = request.GET.get('upc', '').strip()
upc = re.sub(r'\D', '', upc)
if upc:
- product = get_product_by_upc(Session, upc)
+ product = api.get_product_by_upc(Session(), upc)
if not product:
# Try again, assuming caller did not include check digit.
upc = GPC(upc, calc_check_digit='upc')
- product = get_product_by_upc(Session, upc)
+ product = api.get_product_by_upc(Session(), upc)
if product:
if product.deleted and not request.has_perm('products.view_deleted'):
product = None
@@ -396,12 +438,12 @@ def products_search(request):
def print_labels(request):
profile = request.params.get('profile')
- profile = Session.query(LabelProfile).get(profile) if profile else None
+ profile = Session.query(model.LabelProfile).get(profile) if profile else None
if not profile:
return {'error': "Label profile not found"}
product = request.params.get('product')
- product = Session.query(Product).get(product) if product else None
+ product = Session.query(model.Product).get(product) if product else None
if not product:
return {'error': "Product not found"}
@@ -421,114 +463,19 @@ def print_labels(request):
return {}
-class CreateProductsBatch(ProductsGrid):
-
- def make_batch(self, provider, progress):
- from rattail.db import Session
- session = Session()
- configure_session(self.request.rattail_config, session)
-
- self._filter_config = self.filter_config()
- self._sort_config = self.sort_config()
- products = self.make_query(session)
-
- batch = provider.make_batch(session, products, progress)
- if not batch:
- session.rollback()
- session.close()
- return
-
- session.commit()
- session.refresh(batch)
- session.close()
-
- progress.session.load()
- progress.session['complete'] = True
- progress.session['success_url'] = self.request.route_url('batch.read', uuid=batch.uuid)
- progress.session['success_msg'] = "Batch \"%s\" has been created." % batch.description
- progress.session.save()
-
- def __call__(self):
- if self.request.POST:
- provider = self.request.POST.get('provider')
- if provider:
- provider = batches.get_provider(self.request.rattail_config, provider)
- if provider:
-
- if self.request.POST.get('params') == 'True':
- provider.set_params(Session(), **self.request.POST)
-
- else:
- try:
- url = self.request.route_url('batch_params.%s' % provider.name)
- except KeyError:
- pass
- else:
- self.request.session['referer'] = self.request.current_route_url()
- return HTTPFound(location=url)
-
- progress = SessionProgress(self.request, 'products.batch')
- thread = Thread(target=self.make_batch, args=(provider, progress))
- thread.start()
- kwargs = {
- 'key': 'products.batch',
- 'cancel_url': self.request.route_url('products'),
- 'cancel_msg': "Batch creation was canceled.",
- }
- return render_to_response('/progress.mako', kwargs, request=self.request)
-
- enabled = self.request.rattail_config.get('rattail.pyramid', 'batches.providers')
- if enabled:
- enabled = enabled.split()
-
- providers = []
- for provider in batches.iter_providers():
- if not enabled or provider.name in enabled:
- providers.append((provider.name, provider.description))
-
- return {'providers': providers}
-
-
-def add_routes(config):
- config.add_route('products', '/products')
- config.add_route('products.autocomplete', '/products/autocomplete')
- config.add_route('products.search', '/products/search')
- config.add_route('products.print_labels', '/products/labels')
- config.add_route('products.create_batch', '/products/batch')
- config.add_route('product.create', '/products/new')
- config.add_route('product.read', '/products/{uuid}')
- config.add_route('product.update', '/products/{uuid}/edit')
- config.add_route('product.delete', '/products/{uuid}/delete')
-
-
def includeme(config):
- add_routes(config)
-
- config.add_view(ProductsGrid, route_name='products',
- renderer='/products/index.mako',
- permission='products.list')
+ config.add_route('products.autocomplete', '/products/autocomplete')
config.add_view(ProductsAutocomplete, route_name='products.autocomplete',
- renderer='json',
- permission='products.list')
+ renderer='json', permission='products.list')
+ config.add_route('products.print_labels', '/products/labels')
config.add_view(print_labels, route_name='products.print_labels',
renderer='json', permission='products.print_labels')
- config.add_view(CreateProductsBatch, route_name='products.create_batch',
- renderer='/products/batch.mako',
- permission='batches.create')
- config.add_view(ProductCrud, attr='create', route_name='product.create',
- renderer='/products/crud.mako',
- permission='products.create')
- config.add_view(ProductCrud, attr='read', route_name='product.read',
- renderer='/products/read.mako',
- permission='products.read')
- config.add_view(ProductCrud, attr='update', route_name='product.update',
- renderer='/products/crud.mako',
- permission='products.update')
- config.add_view(ProductCrud, attr='delete', route_name='product.delete',
- permission='products.delete')
+
+ config.add_route('products.search', '/products/search')
config.add_view(products_search, route_name='products.search',
renderer='json', permission='products.list')
+ ProductsView.defaults(config)
version_defaults(config, ProductVersionView, 'product')