Refactor products view(s) per new master pattern.

Finally!
This commit is contained in:
Lance Edgar 2016-02-12 20:44:41 -06:00
parent 254c68034a
commit 583548cad5
12 changed files with 421 additions and 394 deletions

View file

@ -2,7 +2,7 @@
################################################################################ ################################################################################
# #
# Rattail -- Retail Software Framework # Rattail -- Retail Software Framework
# Copyright © 2010-2015 Lance Edgar # Copyright © 2010-2016 Lance Edgar
# #
# This file is part of Rattail. # This file is part of Rattail.
# #
@ -24,18 +24,20 @@
FormAlchemy Grid Classes FormAlchemy Grid Classes
""" """
from __future__ import unicode_literals from __future__ import unicode_literals, absolute_import
import logging import logging
import sqlalchemy as sa import sqlalchemy as sa
from sqlalchemy import orm from sqlalchemy import orm
from edbob.util import prettify
from rattail.db.types import GPCType
import formalchemy import formalchemy
from webhelpers import paginate from webhelpers import paginate
from edbob.util import prettify
from tailbone.db import Session from tailbone.db import Session
from tailbone.newgrids import Grid, GridColumn, filters from tailbone.newgrids import Grid, GridColumn, filters
@ -64,7 +66,8 @@ class AlchemyGrid(Grid):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(AlchemyGrid, self).__init__(*args, **kwargs) super(AlchemyGrid, self).__init__(*args, **kwargs)
fa_grid = formalchemy.Grid(self.model_class, instances=self.data, 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 fa_grid.prettify = prettify
self._fa_grid = fa_grid self._fa_grid = fa_grid
@ -99,6 +102,8 @@ class AlchemyGrid(Grid):
factory = filters.AlchemyBooleanFilter factory = filters.AlchemyBooleanFilter
elif isinstance(column.type, (sa.Date, sa.DateTime)): elif isinstance(column.type, (sa.Date, sa.DateTime)):
factory = filters.AlchemyDateFilter factory = filters.AlchemyDateFilter
elif isinstance(column.type, GPCType):
factory = filters.AlchemyGPCFilter
return factory(key, column=column, **kwargs) return factory(key, column=column, **kwargs)
def iter_filters(self): def iter_filters(self):
@ -107,6 +112,24 @@ class AlchemyGrid(Grid):
""" """
return self.filters.itervalues() 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): def make_sorters(self, sorters):
""" """
Returns a mapping of sort options for the grid. Keyword args override Returns a mapping of sort options for the grid. Keyword args override

View file

@ -2,7 +2,7 @@
################################################################################ ################################################################################
# #
# Rattail -- Retail Software Framework # Rattail -- Retail Software Framework
# Copyright © 2010-2015 Lance Edgar # Copyright © 2010-2016 Lance Edgar
# #
# This file is part of Rattail. # This file is part of Rattail.
# #
@ -24,7 +24,7 @@
Core Grid Classes Core Grid Classes
""" """
from __future__ import unicode_literals from __future__ import unicode_literals, absolute_import
from edbob.util import prettify 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. 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 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. # 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) key = 'tailbone.{}.grid.{}.sortkey'.format(user.uuid, self.key)
return get_setting(Session(), key) is not None return get_setting(session, key) is not None
def apply_user_defaults(self, settings): def apply_user_defaults(self, settings):
""" """
@ -600,10 +606,11 @@ class Grid(object):
""" """
url = action.get_url(row) url = action.get_url(row)
if url: if url:
kwargs = {'class_': action.key}
if action.icon: if action.icon:
icon = HTML.tag('span', class_='ui-icon ui-icon-{0}'.format(action.icon)) icon = HTML.tag('span', class_='ui-icon ui-icon-{}'.format(action.icon))
return tags.link_to(icon + action.label, url) return tags.link_to(icon + action.label, url, **kwargs)
return tags.link_to(action.label, url) return tags.link_to(action.label, url, **kwargs)
def iter_rows(self): def iter_rows(self):
return self.make_visible_data() return self.make_visible_data()

View file

@ -2,7 +2,7 @@
################################################################################ ################################################################################
# #
# Rattail -- Retail Software Framework # Rattail -- Retail Software Framework
# Copyright © 2010-2015 Lance Edgar # Copyright © 2010-2016 Lance Edgar
# #
# This file is part of Rattail. # This file is part of Rattail.
# #
@ -24,12 +24,13 @@
Grid Filters Grid Filters
""" """
from __future__ import unicode_literals from __future__ import unicode_literals, absolute_import
import sqlalchemy as sa import sqlalchemy as sa
from edbob.util import prettify from edbob.util import prettify
from rattail.gpc import GPC
from rattail.util import OrderedDict from rattail.util import OrderedDict
from rattail.core import UNSPECIFIED from rattail.core import UNSPECIFIED
@ -367,6 +368,44 @@ class AlchemyDateFilter(AlchemyGridFilter):
return self.filter_is_null(query, value) 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): class GridFilterSet(OrderedDict):
""" """
Collection class for :class:`GridFilter` instances. Collection class for :class:`GridFilter` instances.

View file

@ -17,6 +17,7 @@
.newgrid-wrapper .newfilters fieldset { .newgrid-wrapper .newfilters fieldset {
margin: -8px 0 5px 0; margin: -8px 0 5px 0;
padding: 1px 5px 5px 5px; padding: 1px 5px 5px 5px;
width: 80%;
} }
.newgrid-wrapper .newfilters .filter { .newgrid-wrapper .newfilters .filter {

View file

@ -20,7 +20,7 @@
<div class="buttons"> <div class="buttons">
${h.submit('create', "Create Batch")} ${h.submit('create', "Create Batch")}
<button type="button" onclick="location.href = '${url('products')}';">Cancel</button> ${h.link_to("Cancel", url('products'), class_='button')}
</div> </div>
${h.end_form()} ${h.end_form()}

View file

@ -1,16 +0,0 @@
## -*- coding: utf-8 -*-
<%inherit file="/crud.mako" />
<%def name="context_menu_items()">
<li>${h.link_to("Back to Products", url('products'))}</li>
% if form.readonly and request.has_perm('products.update'):
<li>${h.link_to("Edit this Product", url('product.update', uuid=form.fieldset.model.uuid))}</li>
% elif form.updating:
<li>${h.link_to("View this Product", url('product.read', uuid=form.fieldset.model.uuid))}</li>
% endif
% if version_count is not Undefined and request.has_perm('product.versions.view'):
<li>${h.link_to("View Change History ({0})".format(version_count), url('product.versions', uuid=form.fieldset.model.uuid))}</li>
% endif
</%def>
${parent.body()}

View file

@ -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'):
<li>${h.link_to("View Change History ({})".format(version_count), url('product.versions', uuid=instance.uuid))}</li>
% endif
</%def>
${parent.body()}

View file

@ -1,7 +1,5 @@
## -*- coding: utf-8 -*- ## -*- coding: utf-8 -*-
<%inherit file="/grid.mako" /> <%inherit file="/master/index.mako" />
<%def name="title()">Products</%def>
<%def name="head_tags()"> <%def name="head_tags()">
${parent.head_tags()} ${parent.head_tags()}
@ -35,49 +33,48 @@
</style> </style>
% if label_profiles and request.has_perm('products.print_labels'): % if label_profiles and request.has_perm('products.print_labels'):
<script language="javascript" type="text/javascript"> <script type="text/javascript">
$(function() { $(function() {
$('div.grid a.print-label').live('click', function() { $('.newgrid-wrapper').on('click', 'a.print_label', function() {
var quantity = $('#label-quantity').val(); var quantity = $('table.label-printing #label-quantity');
if (isNaN(quantity)) { if (isNaN(quantity.val())) {
alert("You must provide a valid label quantity."); alert("You must provide a valid label quantity.");
$('#label-quantity').select(); quantity.select();
$('#label-quantity').focus(); quantity.focus();
} else { } else {
$.ajax({ quantity = quantity.val();
url: '${url('products.print_labels')}', var data = {
data: { product: get_uuid(this),
'product': get_uuid(this), profile: $('#label-profile').val(),
'profile': $('#label-profile').val(), quantity: quantity
'quantity': quantity, };
}, console.log(data);
success: function(data) { $.get('${url('products.print_labels')}', data, function(data) {
if (data.error) { if (data.error) {
alert("An error occurred while attempting to print:\n\n" + data.error); alert("An error occurred while attempting to print:\n\n" + data.error);
} else if (quantity == '1') { } else if (quantity == '1') {
alert("1 label has been printed."); alert("1 label has been printed.");
} else { } else {
alert(quantity + " labels have been printed."); alert(quantity + " labels have been printed.");
} }
}, });
}); }
} return false;
return false; });
}); });
});
</script> </script>
% endif % endif
</%def> </%def>
<%def name="tools()"> <%def name="grid_tools()">
% if label_profiles and request.has_perm('products.print_labels'): % if label_profiles and request.has_perm('products.print_labels'):
<table> <table class="label-printing">
<thead> <thead>
<tr> <tr>
<td>Label</td> <th>Label</th>
<td>Qty.</td> <th>Qty.</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -97,9 +94,7 @@
</%def> </%def>
<%def name="context_menu_items()"> <%def name="context_menu_items()">
% if request.has_perm('products.create'): ${parent.context_menu_items()}
<li>${h.link_to("Create a new Product", url('product.create'))}</li>
% endif
% if request.has_perm('batches.create'): % if request.has_perm('batches.create'):
<li>${h.link_to("Create Batch from Results", url('products.create_batch'))}</li> <li>${h.link_to("Create Batch from Results", url('products.create_batch'))}</li>
% endif % endif

View file

@ -1,7 +1,9 @@
## -*- coding: utf-8 -*- ## -*- coding: utf-8 -*-
<%inherit file="/products/crud.mako" /> <%inherit file="/master/view.mako" />
<%namespace file="/forms/lib.mako" import="render_field_readonly" /> <%namespace file="/forms/lib.mako" import="render_field_readonly" />
<% product = instance %>
<%def name="head_tags()"> <%def name="head_tags()">
${parent.head_tags()} ${parent.head_tags()}
<style type="text/css"> <style type="text/css">
@ -20,7 +22,12 @@
</style> </style>
</%def> </%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'):
<li>${h.link_to("View Change History ({})".format(version_count), url('product.versions', uuid=product.uuid))}</li>
% endif
</%def>
<%def name="render_organization_fields(form)"> <%def name="render_organization_fields(form)">
${render_field_readonly(form.fieldset.department)} ${render_field_readonly(form.fieldset.department)}

View file

@ -330,20 +330,18 @@ class MasterView(View):
""" """
return getattr(cls, 'grid_key', '{0}s'.format(cls.get_normalized_model_name())) 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 Return a dictionary of kwargs to be passed to the factory when creating
new grid instances. new grid instances.
""" """
return { defaults = {
'width': 'full', 'width': 'full',
'filterable': self.filterable, 'filterable': self.filterable,
'sortable': True, 'sortable': True,
'default_sortkey': getattr(self, 'default_sortkey', None), 'default_sortkey': getattr(self, 'default_sortkey', None),
'sortdir': getattr(self, 'sortdir', 'asc'), 'sortdir': getattr(self, 'sortdir', 'asc'),
'pageable': self.pageable, 'pageable': self.pageable,
'main_actions': self.get_main_actions(),
'more_actions': self.get_more_actions(),
'checkboxes': self.checkboxes, 'checkboxes': self.checkboxes,
'checked': self.checked, 'checked': self.checked,
'row_attrs': self.get_row_attrs, 'row_attrs': self.get_row_attrs,
@ -353,6 +351,15 @@ class MasterView(View):
'permission_prefix': self.get_permission_prefix(), 'permission_prefix': self.get_permission_prefix(),
'route_prefix': self.get_route_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): def get_row_attrs(self, row, i):
""" """
@ -379,7 +386,8 @@ class MasterView(View):
Return a list of 'main' actions for the grid. Return a list of 'main' actions for the grid.
""" """
actions = [] 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')) actions.append(self.make_action('view', icon='zoomin'))
return actions return actions
@ -388,9 +396,10 @@ class MasterView(View):
Return a list of 'more' actions for the grid. Return a list of 'more' actions for the grid.
""" """
actions = [] 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')) 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)) actions.append(self.make_action('delete', icon='trash', url=self.default_delete_url))
return actions return actions
@ -421,14 +430,14 @@ class MasterView(View):
values = [getattr(row, k) for k in keys] values = [getattr(row, k) for k in keys]
return dict(zip(keys, values)) return dict(zip(keys, values))
def make_grid(self): def make_grid(self, **kwargs):
""" """
Make and return a new (configured) grid instance. Make and return a new (configured) grid instance.
""" """
factory = self.get_grid_factory() factory = self.get_grid_factory()
key = self.get_grid_key() key = self.get_grid_key()
data = self.get_data() data = self.get_data(session=kwargs.get('session'))
kwargs = self.make_grid_kwargs() kwargs = self.make_grid_kwargs(**kwargs)
grid = factory(key, self.request, data=data, model_class=self.get_model_class(error=False), **kwargs) grid = factory(key, self.request, data=data, model_class=self.get_model_class(error=False), **kwargs)
self.configure_grid(grid) self.configure_grid(grid)
grid.load_settings() grid.load_settings()

View file

@ -106,8 +106,12 @@ class TerseRecipientsFieldRenderer(formalchemy.FieldRenderer):
recipients = self.raw_value recipients = self.raw_value
if not recipients: if not recipients:
return '' 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]) 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: if len(recips) < 5:
return ', '.join(recips) return ', '.join(recips)
return "{}, ...".format(', '.join(recips[:4])) return "{}, ...".format(', '.join(recips[:4]))

View file

@ -2,7 +2,7 @@
################################################################################ ################################################################################
# #
# Rattail -- Retail Software Framework # Rattail -- Retail Software Framework
# Copyright © 2010-2015 Lance Edgar # Copyright © 2010-2016 Lance Edgar
# #
# This file is part of Rattail. # This file is part of Rattail.
# #
@ -32,158 +32,126 @@ import re
import sqlalchemy as sa import sqlalchemy as sa
from sqlalchemy import orm 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 import formalchemy
from pyramid.httpexceptions import HTTPFound from pyramid import httpexceptions
from pyramid.renderers import render_to_response from pyramid.renderers import render_to_response
from webhelpers.html import tags from webhelpers.html import tags
import rattail.labels from tailbone import forms
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.db import Session from tailbone.db import Session
from tailbone.forms import GPCFieldRenderer, BrandFieldRenderer, PriceFieldRenderer from tailbone.views import MasterView, SearchableAlchemyGridView, AutocompleteView
from tailbone.forms.renderers import products as forms 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 from tailbone.progress import SessionProgress
class ProductsGrid(SearchableAlchemyGridView): class ProductsView(MasterView):
"""
Master view for the Product class.
"""
model_class = model.Product
mapped_class = Product # child_version_classes = [
config_prefix = 'products' # (model.ProductCode, 'product_uuid'),
sort = 'description' # (model.ProductCost, 'product_uuid'),
# (model.ProductPrice, 'product_uuid'),
# ]
# These aliases enable the grid queries to filter products which may be # These aliases enable the grid queries to filter products which may be
# purchased from *any* vendor, and yet sort by only the "preferred" vendor # purchased from *any* vendor, and yet sort by only the "preferred" vendor
# (since that's what shows up in the grid column). # (since that's what shows up in the grid column).
ProductCostAny = orm.aliased(ProductCost) ProductCostAny = orm.aliased(model.ProductCost)
VendorAny = orm.aliased(Vendor) 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): def join_vendor(q):
q = q.outerjoin( return q.outerjoin(model.ProductCost,
ProductCost, sa.and_(
sa.and_( model.ProductCost.product_uuid == model.Product.uuid,
ProductCost.product_uuid == Product.uuid, model.ProductCost.preference == 1))\
ProductCost.preference == 1, .outerjoin(model.Vendor)
))
q = q.outerjoin(Vendor)
return q
def join_vendor_any(q): def join_vendor_any(q):
q = q.outerjoin( return q.outerjoin(self.ProductCostAny,
self.ProductCostAny, self.ProductCostAny.product_uuid == model.Product.uuid)\
self.ProductCostAny.product_uuid == Product.uuid) .outerjoin(self.VendorAny,
q = q.outerjoin( self.VendorAny.uuid == self.ProductCostAny.vendor_uuid)
self.VendorAny,
self.VendorAny.uuid == self.ProductCostAny.vendor_uuid)
return q
return { g.joiners['brand'] = lambda q: q.outerjoin(model.Brand)
'brand': g.joiners['family'] = lambda q: q.outerjoin(model.Family)
lambda q: q.outerjoin(Brand), g.joiners['department'] = lambda q: q.outerjoin(model.Department,
'family': model.Department.uuid == model.Product.department_uuid)
lambda q: q.outerjoin(model.Family), g.joiners['subdepartment'] = lambda q: q.outerjoin(model.Subdepartment,
'department': model.Subdepartment.uuid == model.Product.subdepartment_uuid)
lambda q: q.outerjoin(Department, g.joiners['report_code'] = lambda q: q.outerjoin(model.ReportCode)
Department.uuid == Product.department_uuid), g.joiners['regular_price'] = lambda q: q.outerjoin(model.ProductPrice,
'subdepartment': model.ProductPrice.uuid == model.Product.regular_price_uuid)
lambda q: q.outerjoin(Subdepartment, g.joiners['current_price'] = lambda q: q.outerjoin(model.ProductPrice,
Subdepartment.uuid == Product.subdepartment_uuid), model.ProductPrice.uuid == model.Product.current_price_uuid)
u'report_code': g.joiners['vendor'] = join_vendor
lambda q: q.outerjoin(model.ReportCode), g.joiners['vendor_any'] = join_vendor_any
'regular_price': g.joiners['code'] = lambda q: q.outerjoin(model.ProductCode)
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),
}
def filter_map(self): g.sorters['brand'] = g.make_sorter(model.Brand.name)
return self.make_filter_map( g.sorters['department'] = g.make_sorter(model.Department.name)
ilike=['description', 'size'], g.sorters['subdepartment'] = g.make_sorter(model.Subdepartment.name)
upc=self.filter_gpc(model.Product.upc), g.sorters['vendor'] = g.make_sorter(model.Vendor.name)
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))
def filter_config(self): g.filters['upc'].default_active = True
return self.make_filter_config( g.filters['upc'].default_verb = 'equal'
include_filter_upc=True, g.filters['upc'].label = "UPC"
filter_type_upc='is', g.filters['description'].default_active = True
filter_label_upc="UPC", g.filters['description'].default_verb = 'contains'
include_filter_brand=True, g.filters['brand'] = g.make_filter('brand', model.Brand.name,
filter_type_brand='lk', default_active=True, default_verb='contains')
include_filter_description=True, g.filters['family'] = g.make_filter('family', model.Family.name)
filter_type_description='lk', g.filters['department'] = g.make_filter('department', model.Department.name,
include_filter_department=True, default_active=True, default_verb='contains')
filter_type_department='lk', g.filters['subdepartment'] = g.make_filter('subdepartment', model.Subdepartment.name)
filter_label_vendor="Vendor (preferred)", g.filters['report_code'] = g.make_filter('report_code', model.ReportCode.name)
include_filter_vendor_any=True, g.filters['vendor'] = g.make_filter('vendor', model.Vendor.name, label="Vendor (preferred)")
filter_label_vendor_any="Vendor (any)", g.filters['vendor_any'] = g.make_filter('vendor_any', self.VendorAny.name, label="Vendor (any)")
filter_type_vendor_any='lk') g.filters['code'] = g.make_filter('code', model.ProductCode.code)
def sort_map(self): g.default_sortkey = 'description'
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))
def query(self): g.upc.set(renderer=forms.renderers.GPCFieldRenderer)
q = self.make_query() g.regular_price.set(renderer=forms.renderers.PriceFieldRenderer)
if not self.request.has_perm('products.view_deleted'): g.current_price.set(renderer=forms.renderers.PriceFieldRenderer)
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.configure( g.configure(
include=[ include=[
g.upc.label("UPC"), g.upc.label("UPC"),
@ -197,80 +165,68 @@ class ProductsGrid(SearchableAlchemyGridView):
], ],
readonly=True) readonly=True)
if self.request.has_perm('products.read'): # TODO: need to check for 'print labels' permission here also
g.viewable = True if self.print_labels:
g.view_route_name = 'product.read' g.more_actions.append(GridAction('print_label', icon='print'))
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'
# Maybe add Print Label column. def template_kwargs_index(self, **kwargs):
if self.rattail_config.getbool('tailbone', 'products.print_labels', default=True): if self.print_labels:
q = Session.query(LabelProfile) kwargs['label_profiles'] = Session.query(model.LabelProfile)\
if q.count(): .filter(model.LabelProfile.visible == True)\
def labels(row): .order_by(model.LabelProfile.ordinal)\
return tags.link_to("Print", '#', class_='print-label') .all()
g.add_column('labels', "Labels", labels) return kwargs
return g def row_attrs(self, row, i):
def render_kwargs(self): attrs = {'uuid': row.uuid}
q = Session.query(LabelProfile)
q = q.filter(LabelProfile.visible == True)
q = q.order_by(LabelProfile.ordinal)
return {'label_profiles': q.all()}
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): return attrs
"""
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'),
]
def get_model(self, key): def get_instance(self):
model = super(ProductCrud, self).get_model(key) key = self.request.matchdict['uuid']
if model: product = Session.query(model.Product).get(key)
return model if product:
model = Session.query(ProductPrice).get(key) return product
if model: price = Session.query(model.ProductPrice).get(key)
return model.product if price:
return None return price.product
raise httpexceptions.HTTPNotFound()
def fieldset(self, model): def configure_fieldset(self, fs):
fs = self.make_fieldset(model)
fs.upc.set(renderer=GPCFieldRenderer) fs.upc.set(renderer=forms.renderers.GPCFieldRenderer)
fs.brand.set(options=[]) fs.brand.set(options=[])
fs.unit_of_measure.set(renderer=EnumFieldRenderer(enum.UNIT_OF_MEASURE)) fs.unit_of_measure.set(renderer=forms.renderers.EnumFieldRenderer(enum.UNIT_OF_MEASURE))
fs.regular_price.set(renderer=PriceFieldRenderer) fs.regular_price.set(renderer=forms.renderers.PriceFieldRenderer)
fs.current_price.set(renderer=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.append(formalchemy.Field('current_price_ends',
fs.current_price_ends.set(value=lambda p: p.current_price.ends if p.current_price else None) value=lambda p: p.current_price.ends if p.current_price else None,
fs.current_price_ends.set(renderer=DateTimeFieldRenderer(self.request.rattail_config)) renderer=forms.renderers.DateTimeFieldRenderer(self.rattail_config)))
fs.last_sold.set(renderer=DateTimeFieldRenderer(self.request.rattail_config))
fs.configure( fs.configure(
include=[ include=[
fs.upc.label("UPC"), fs.upc.label("UPC"),
fs.brand.with_renderer(BrandFieldRenderer), fs.brand.with_renderer(forms.renderers.BrandFieldRenderer),
fs.description, fs.description,
fs.unit_size, fs.unit_size,
fs.unit_of_measure.label("Unit of Measure"), fs.unit_of_measure.label("Unit of Measure"),
fs.size, fs.size,
fs.weighed, fs.weighed,
fs.case_pack, fs.case_pack,
fs.department.with_renderer(forms.DepartmentFieldRenderer), fs.department.with_renderer(products_forms.DepartmentFieldRenderer),
fs.subdepartment.with_renderer(forms.SubdepartmentFieldRenderer), fs.subdepartment.with_renderer(products_forms.SubdepartmentFieldRenderer),
fs.category.with_renderer(forms.CategoryFieldRenderer), fs.category.with_renderer(products_forms.CategoryFieldRenderer),
fs.family, fs.family,
fs.report_code, fs.report_code,
fs.regular_price, fs.regular_price,
@ -285,33 +241,119 @@ class ProductCrud(CrudView):
fs.deleted, fs.deleted,
fs.last_sold, fs.last_sold,
]) ])
if not self.readonly: if not self.viewing:
del fs.regular_price del fs.regular_price
del fs.current_price del fs.current_price
if not self.request.has_perm('products.view_deleted'): if not self.request.has_perm('products.view_deleted'):
del fs.deleted del fs.deleted
return fs
def pre_crud(self, product): def template_kwargs_view(self, **kwargs):
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)
kwargs['image'] = False kwargs['image'] = False
product = form.fieldset.model product = kwargs['instance']
if product.upc: if product.upc:
kwargs['image_url'] = get_image_url( kwargs['image_url'] = pod.get_image_url(self.rattail_config, product.upc)
self.request.rattail_config, product.upc) kwargs['image_path'] = pod.get_image_path(self.rattail_config, product.upc)
kwargs['image_path'] = get_image_path(
self.request.rattail_config, product.upc)
if os.path.exists(kwargs['image_path']): if os.path.exists(kwargs['image_path']):
kwargs['image'] = True kwargs['image'] = True
return kwargs 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): class ProductVersionView(VersionView):
""" """
@ -331,7 +373,7 @@ class ProductVersionView(VersionView):
""" """
uuid = self.request.matchdict['uuid'] uuid = self.request.matchdict['uuid']
product = Session.query(model.Product).get(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: if product.deleted:
self.request.session.flash("This product is marked as deleted.", 'error') self.request.session.flash("This product is marked as deleted.", 'error')
@ -354,8 +396,8 @@ class ProductsAutocomplete(AutocompleteView):
def query(self, term): def query(self, term):
q = Session.query(model.Product).outerjoin(model.Brand) q = Session.query(model.Product).outerjoin(model.Brand)
q = q.filter(sa.or_( q = q.filter(sa.or_(
model.Brand.name.ilike('%{0}%'.format(term)), model.Brand.name.ilike('%{}%'.format(term)),
model.Product.description.ilike('%{0}%'.format(term)))) model.Product.description.ilike('%{}%'.format(term))))
if not self.request.has_perm('products.view_deleted'): if not self.request.has_perm('products.view_deleted'):
q = q.filter(model.Product.deleted == False) q = q.filter(model.Product.deleted == False)
q = q.order_by(model.Brand.name, model.Product.description) 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 = request.GET.get('upc', '').strip()
upc = re.sub(r'\D', '', upc) upc = re.sub(r'\D', '', upc)
if upc: if upc:
product = get_product_by_upc(Session, upc) product = api.get_product_by_upc(Session(), upc)
if not product: if not product:
# Try again, assuming caller did not include check digit. # Try again, assuming caller did not include check digit.
upc = GPC(upc, calc_check_digit='upc') 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:
if product.deleted and not request.has_perm('products.view_deleted'): if product.deleted and not request.has_perm('products.view_deleted'):
product = None product = None
@ -396,12 +438,12 @@ def products_search(request):
def print_labels(request): def print_labels(request):
profile = request.params.get('profile') 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: if not profile:
return {'error': "Label profile not found"} return {'error': "Label profile not found"}
product = request.params.get('product') 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: if not product:
return {'error': "Product not found"} return {'error': "Product not found"}
@ -421,114 +463,19 @@ def print_labels(request):
return {} 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): 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', config.add_view(ProductsAutocomplete, route_name='products.autocomplete',
renderer='json', renderer='json', permission='products.list')
permission='products.list')
config.add_route('products.print_labels', '/products/labels')
config.add_view(print_labels, route_name='products.print_labels', config.add_view(print_labels, route_name='products.print_labels',
renderer='json', permission='products.print_labels') renderer='json', permission='products.print_labels')
config.add_view(CreateProductsBatch, route_name='products.create_batch',
renderer='/products/batch.mako', config.add_route('products.search', '/products/search')
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_view(products_search, route_name='products.search', config.add_view(products_search, route_name='products.search',
renderer='json', permission='products.list') renderer='json', permission='products.list')
ProductsView.defaults(config)
version_defaults(config, ProductVersionView, 'product') version_defaults(config, ProductVersionView, 'product')