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
# 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

View file

@ -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()

View file

@ -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.

View file

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

View file

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

View file

@ -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()}
<style type="text/css">
@ -20,7 +22,12 @@
</style>
</%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)">
${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()))
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()

View file

@ -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]))

View file

@ -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')