Refactor products view(s) per new master pattern.
Finally!
This commit is contained in:
parent
254c68034a
commit
583548cad5
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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()}
|
||||||
|
|
|
@ -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()}
|
|
11
tailbone/templates/products/edit.mako
Normal file
11
tailbone/templates/products/edit.mako
Normal 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()}
|
|
@ -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
|
||||||
|
|
|
@ -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)}
|
|
@ -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()
|
||||||
|
|
|
@ -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]))
|
||||||
|
|
|
@ -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')
|
||||||
|
|
Loading…
Reference in a new issue