Rebranded to Tailbone.

This commit is contained in:
Lance Edgar 2013-09-01 07:27:47 -07:00
parent 47944767dc
commit 40efd8a3bc
111 changed files with 188 additions and 209 deletions

37
tailbone/__init__.py Normal file
View file

@ -0,0 +1,37 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Rattail's Pyramid Framework
"""
from ._version import __version__
from edbob.pyramid import Session
def includeme(config):
config.include('tailbone.static')
config.include('tailbone.subscribers')
config.include('tailbone.views')

1
tailbone/_version.py Normal file
View file

@ -0,0 +1 @@
__version__ = '0.3.1'

View file

@ -0,0 +1,31 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Forms
"""
from .simpleform import *
from .fields import *
from .renderers import *

54
tailbone/forms/fields.py Normal file
View file

@ -0,0 +1,54 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
``tailbone.forms.fields`` -- FormAlchemy Fields
"""
from formalchemy import Field
__all__ = ['AssociationProxyField']
def AssociationProxyField(name, **kwargs):
"""
Returns a ``Field`` class which is aware of SQLAlchemy association proxies.
"""
class ProxyField(Field):
def sync(self):
if not self.is_readonly():
setattr(self.parent.model, self.name,
self.renderer.deserialize())
def value(model):
try:
return getattr(model, name)
except AttributeError:
return None
kwargs.setdefault('value', value)
return ProxyField(name, **kwargs)

View file

@ -0,0 +1,99 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
FormAlchemy Field Renderers
"""
from webhelpers.html import literal
from webhelpers.html import tags
import formalchemy
from edbob.pyramid.forms import pretty_datetime
from edbob.pyramid.forms.formalchemy.renderers import YesNoFieldRenderer
from .common import AutocompleteFieldRenderer, EnumFieldRenderer
from .products import GPCFieldRenderer, ProductFieldRenderer
from .users import UserFieldRenderer
__all__ = ['AutocompleteFieldRenderer', 'EnumFieldRenderer', 'YesNoFieldRenderer',
'GPCFieldRenderer', 'PersonFieldRenderer', 'PriceFieldRenderer',
'PriceWithExpirationFieldRenderer', 'ProductFieldRenderer', 'UserFieldRenderer']
def PersonFieldRenderer(url):
BaseRenderer = AutocompleteFieldRenderer(url)
class PersonFieldRenderer(BaseRenderer):
def render_readonly(self, **kwargs):
person = self.raw_value
if not person:
return ''
return tags.link_to(
str(person),
self.request.route_url('person.read', uuid=person.uuid))
return PersonFieldRenderer
class PriceFieldRenderer(formalchemy.TextFieldRenderer):
"""
Renderer for fields which reference a :class:`ProductPrice` instance.
"""
def render_readonly(self, **kwargs):
price = self.field.raw_value
if price:
if price.price is not None and price.pack_price is not None:
if price.multiple > 1:
return literal('$ %0.2f / %u&nbsp; ($ %0.2f / %u)' % (
price.price, price.multiple,
price.pack_price, price.pack_multiple))
return literal('$ %0.2f&nbsp; ($ %0.2f / %u)' % (
price.price, price.pack_price, price.pack_multiple))
if price.price is not None:
if price.multiple > 1:
return '$ %0.2f / %u' % (price.price, price.multiple)
return '$ %0.2f' % price.price
if price.pack_price is not None:
return '$ %0.2f / %u' % (price.pack_price, price.pack_multiple)
return ''
class PriceWithExpirationFieldRenderer(PriceFieldRenderer):
"""
Price field renderer which also displays the expiration date, if present.
"""
def render_readonly(self, **kwargs):
res = super(PriceWithExpirationFieldRenderer, self).render_readonly(**kwargs)
if res:
price = self.field.raw_value
if price.ends:
res += '&nbsp; (%s)' % pretty_datetime(price.ends, from_='utc')
return res

View file

@ -0,0 +1,80 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Common Field Renderers
"""
from formalchemy.fields import FieldRenderer, SelectFieldRenderer
__all__ = ['AutocompleteFieldRenderer', 'EnumFieldRenderer']
def AutocompleteFieldRenderer(service_url, field_value=None, field_display=None, width='300px'):
"""
Returns a custom renderer class for an autocomplete field.
"""
class AutocompleteFieldRenderer(FieldRenderer):
@property
def focus_name(self):
return self.name + '-textbox'
@property
def needs_focus(self):
return not bool(self.value or field_value)
def render(self, **kwargs):
kwargs.setdefault('field_name', self.name)
kwargs.setdefault('field_value', self.value or field_value)
kwargs.setdefault('field_display', self.raw_value or field_display)
kwargs.setdefault('service_url', service_url)
kwargs.setdefault('width', width)
return render('/forms/field_autocomplete.mako', kwargs)
return AutocompleteFieldRenderer
def EnumFieldRenderer(enum):
"""
Adds support for enumeration fields.
"""
class Renderer(SelectFieldRenderer):
def render_readonly(self, **kwargs):
value = self.raw_value
if value is None:
return ''
if value in enum:
return enum[value]
return str(value)
def render(self, **kwargs):
opts = [(enum[x], x) for x in sorted(enum)]
return SelectFieldRenderer.render(self, opts, **kwargs)
return Renderer

View file

@ -0,0 +1,56 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Product Field Renderers
"""
from formalchemy import TextFieldRenderer
from rattail.gpc import GPC
__all__ = ['GPCFieldRenderer', 'ProductFieldRenderer']
class GPCFieldRenderer(TextFieldRenderer):
"""
Renderer for :class:`rattail.gpc.GPC` fields.
"""
@property
def length(self):
# Hm, should maybe consider hard-coding this...?
return len(str(GPC(0)))
class ProductFieldRenderer(TextFieldRenderer):
"""
Renderer for fields which represent :class:`rattail.db.Product` instances.
"""
def render_readonly(self, **kwargs):
product = self.raw_value
if product is None:
return ''
return product.full_description

View file

@ -0,0 +1,44 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
User Field Renderers
"""
from formalchemy.fields import TextFieldRenderer
__all__ = ['UserFieldRenderer']
class UserFieldRenderer(TextFieldRenderer):
"""
Renderer for fields which represent ``User`` instances.
"""
def render_readonly(self, **kwargs):
user = self.raw_value
if user is None:
return u''
return unicode(user.display_name)

View file

@ -0,0 +1,61 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
``pyramid_simpleform`` Forms
"""
from pyramid_simpleform import renderers
from webhelpers.html import tags
from webhelpers.html import HTML
from edbob.util import prettify
__all__ = ['FormRenderer']
class FormRenderer(renderers.FormRenderer):
"""
Customized form renderer. Provides some extra methods for convenience.
"""
def field_div(self, name, field, label=None):
errors = self.errors_for(name)
if errors:
errors = [HTML.tag('div', class_='field-error', c=x) for x in errors]
errors = tags.literal('').join(errors)
label = HTML.tag('label', for_=name, c=label or prettify(name))
inner = HTML.tag('div', class_='field', c=field)
outer_class = 'field-wrapper'
if errors:
outer_class += ' error'
outer = HTML.tag('div', class_=outer_class, c=(errors or '') + label + inner)
return outer
def referrer_field(self):
return self.hidden('referrer', value=self.form.request.get_referrer())

View file

@ -0,0 +1,32 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Grids
"""
from .core import *
from .alchemy import *
from . import util
from . import search

122
tailbone/grids/alchemy.py Normal file
View file

@ -0,0 +1,122 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
FormAlchemy Grid Classes
"""
from webhelpers.html import tags
from webhelpers.html import HTML
import formalchemy
import edbob
from edbob.util import prettify
from .core import Grid
from .. import Session
from sqlalchemy.orm import object_session
__all__ = ['AlchemyGrid']
class AlchemyGrid(Grid):
sort_map = {}
pager = None
pager_format = '$link_first $link_previous ~1~ $link_next $link_last'
def __init__(self, request, cls, instances, **kwargs):
super(AlchemyGrid, self).__init__(request, **kwargs)
self._formalchemy_grid = formalchemy.Grid(
cls, instances, session=Session(), request=request)
self._formalchemy_grid.prettify = prettify
def __delattr__(self, attr):
delattr(self._formalchemy_grid, attr)
def __getattr__(self, attr):
return getattr(self._formalchemy_grid, attr)
def cell_class(self, field):
classes = [field.name]
return ' '.join(classes)
def checkbox(self, row):
return tags.checkbox('check-'+row.uuid)
def column_header(self, field):
class_ = None
label = field.label()
if field.key in self.sort_map:
class_ = 'sortable'
if field.key == self.config['sort']:
class_ += ' sorted ' + self.config['dir']
label = tags.link_to(label, '#')
return HTML.tag('th', class_=class_, field=field.key,
title=self.column_titles.get(field.key), c=label)
def view_route_kwargs(self, row):
return {'uuid': row.uuid}
def edit_route_kwargs(self, row):
return {'uuid': row.uuid}
def delete_route_kwargs(self, row):
return {'uuid': row.uuid}
def iter_fields(self):
return self._formalchemy_grid.render_fields.itervalues()
def iter_rows(self):
for row in self._formalchemy_grid.rows:
self._formalchemy_grid._set_active(row, object_session(row))
yield row
def page_count_options(self):
options = edbob.config.get('edbob.pyramid', 'grid.page_count_options')
if options:
options = options.split(',')
options = [int(x.strip()) for x in options]
else:
options = [5, 10, 20, 50, 100]
return options
def page_links(self):
return self.pager.pager(self.pager_format,
symbol_next='next',
symbol_previous='prev',
onclick="grid_navigate_page(this, '$partial_url'); return false;")
def render_field(self, field):
if self._formalchemy_grid.readonly:
return field.render_readonly()
return field.render()
def row_attrs(self, row, i):
attrs = super(AlchemyGrid, self).row_attrs(row, i)
if hasattr(row, 'uuid'):
attrs['uuid'] = row.uuid
return attrs

136
tailbone/grids/core.py Normal file
View file

@ -0,0 +1,136 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Core Grid Classes
"""
try:
from collections import OrderedDict
except ImportError:
from ordereddict import OrderedDict
from webhelpers.html import HTML
from webhelpers.html.builder import format_attrs
from pyramid.renderers import render
from edbob.core import Object
__all__ = ['Grid']
class Grid(Object):
full = False
hoverable = True
checkboxes = False
partial_only = False
viewable = False
view_route_name = None
view_route_kwargs = None
editable = False
edit_route_name = None
edit_route_kwargs = None
deletable = False
delete_route_name = None
delete_route_kwargs = None
def __init__(self, request, **kwargs):
kwargs.setdefault('fields', OrderedDict())
kwargs.setdefault('column_titles', {})
kwargs.setdefault('extra_columns', [])
super(Grid, self).__init__(**kwargs)
self.request = request
def add_column(self, name, label, callback):
self.extra_columns.append(
Object(name=name, label=label, callback=callback))
def column_header(self, field):
return HTML.tag('th', field=field.name,
title=self.column_titles.get(field.name),
c=field.label)
def div_attrs(self):
classes = ['grid']
if self.full:
classes.append('full')
if self.hoverable:
classes.append('hoverable')
return format_attrs(
class_=' '.join(classes),
url=self.request.current_route_url())
def get_view_url(self, row):
kwargs = {}
if self.view_route_kwargs:
if callable(self.view_route_kwargs):
kwargs = self.view_route_kwargs(row)
else:
kwargs = self.view_route_kwargs
return self.request.route_url(self.view_route_name, **kwargs)
def get_edit_url(self, row):
kwargs = {}
if self.edit_route_kwargs:
if callable(self.edit_route_kwargs):
kwargs = self.edit_route_kwargs(row)
else:
kwargs = self.edit_route_kwargs
return self.request.route_url(self.edit_route_name, **kwargs)
def get_delete_url(self, row):
kwargs = {}
if self.delete_route_kwargs:
if callable(self.delete_route_kwargs):
kwargs = self.delete_route_kwargs(row)
else:
kwargs = self.delete_route_kwargs
return self.request.route_url(self.delete_route_name, **kwargs)
def get_row_attrs(self, row, i):
attrs = self.row_attrs(row, i)
return format_attrs(**attrs)
def iter_fields(self):
return self.fields.itervalues()
def iter_rows(self):
raise NotImplementedError
def render(self, template='/grids/grid.mako', **kwargs):
kwargs.setdefault('grid', self)
return render(template, kwargs)
def render_field(self, field):
raise NotImplementedError
def row_attrs(self, row, i):
attrs = {'class_': 'odd' if i % 2 else 'even'}
return attrs

285
tailbone/grids/search.py Normal file
View file

@ -0,0 +1,285 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Grid Search Filters
"""
from sqlalchemy import or_
from webhelpers.html import tags
from webhelpers.html import literal
from pyramid.renderers import render
from pyramid_simpleform import Form
from pyramid_simpleform.renderers import FormRenderer
from edbob.core import Object
from edbob.util import prettify
class SearchFilter(Object):
"""
Base class and default implementation for search filters.
"""
def __init__(self, name, label=None, **kwargs):
Object.__init__(self, **kwargs)
self.name = name
self.label = label or prettify(name)
def types_select(self):
types = [
('is', "is"),
('nt', "is not"),
('lk', "contains"),
('nl', "doesn't contain"),
]
options = []
filter_map = self.search.filter_map[self.name]
for value, label in types:
if value in filter_map:
options.append((value, label))
return tags.select('filter_type_'+self.name,
self.search.config.get('filter_type_'+self.name),
options, class_='filter-type')
def value_control(self):
return tags.text(self.name, self.search.config.get(self.name))
class BooleanSearchFilter(SearchFilter):
"""
Boolean search filter.
"""
def value_control(self):
return tags.select(self.name, self.search.config.get(self.name),
["True", "False"])
def EnumSearchFilter(enum):
options = enum.items()
class EnumSearchFilter(SearchFilter):
def value_control(self):
return tags.select(self.name, self.search.config.get(self.name), options)
return EnumSearchFilter
class SearchForm(Form):
"""
Generic form class which aggregates :class:`SearchFilter` instances.
"""
def __init__(self, request, filter_map, config, *args, **kwargs):
super(SearchForm, self).__init__(request, *args, **kwargs)
self.filter_map = filter_map
self.config = config
self.filters = {}
def add_filter(self, filter_):
filter_.search = self
self.filters[filter_.name] = filter_
class SearchFormRenderer(FormRenderer):
"""
Renderer for :class:`SearchForm` instances.
"""
def __init__(self, form, *args, **kwargs):
super(SearchFormRenderer, self).__init__(form, *args, **kwargs)
self.request = form.request
self.filters = form.filters
self.config = form.config
def add_filter(self, visible):
options = ['add a filter']
for f in self.sorted_filters():
options.append((f.name, f.label))
return self.select('add-filter', options,
style='display: none;' if len(visible) == len(self.filters) else None)
def checkbox(self, name, checked=None, **kwargs):
if name.startswith('include_filter_'):
if checked is None:
checked = self.config[name]
return tags.checkbox(name, checked=checked, **kwargs)
if checked is None:
checked = False
return super(SearchFormRenderer, self).checkbox(name, checked=checked, **kwargs)
def render(self, **kwargs):
kwargs['search'] = self
return literal(render('/grids/search.mako', kwargs))
def sorted_filters(self):
return sorted(self.filters.values(), key=lambda x: x.label)
def text(self, name, **kwargs):
return tags.text(name, value=self.config.get(name), **kwargs)
def filter_exact(field):
"""
Convenience function which returns a filter map entry, with typical logic
built in for "exact match" queries applied to ``field``.
"""
return {
'is':
lambda q, v: q.filter(field == v) if v else q,
'nt':
lambda q, v: q.filter(field != v) if v else q,
}
def filter_ilike(field):
"""
Convenience function which returns a filter map entry, with typical logic
built in for "ILIKE" queries applied to ``field``.
"""
def ilike(query, value):
if value:
query = query.filter(field.ilike('%%%s%%' % value))
return query
def not_ilike(query, value):
if value:
query = query.filter(or_(
field == None,
~field.ilike('%%%s%%' % value),
))
return query
return {'lk': ilike, 'nl': not_ilike}
def get_filter_config(prefix, request, filter_map, **kwargs):
"""
Returns a configuration dictionary for a search form.
"""
config = {}
def update_config(dict_, prefix='', exclude_by_default=False):
"""
Updates the ``config`` dictionary based on the contents of ``dict_``.
"""
for field in filter_map:
if prefix+'include_filter_'+field in dict_:
include = dict_[prefix+'include_filter_'+field]
include = bool(include) and include != '0'
config['include_filter_'+field] = include
elif exclude_by_default:
config['include_filter_'+field] = False
if prefix+'filter_type_'+field in dict_:
config['filter_type_'+field] = dict_[prefix+'filter_type_'+field]
if prefix+field in dict_:
config[field] = dict_[prefix+field]
# Update config to exclude all filters by default.
for field in filter_map:
config['include_filter_'+field] = False
# Update config to honor default settings.
config.update(kwargs)
# Update config with data cached in session.
update_config(request.session, prefix=prefix+'.')
# Update config with data from GET/POST request.
if request.params.get('filters') == 'true':
update_config(request.params, exclude_by_default=True)
# Cache filter data in session.
for key in config:
if (not key.startswith('filter_factory_')
and not key.startswith('filter_label_')):
request.session[prefix+'.'+key] = config[key]
return config
def get_filter_map(cls, exact=[], ilike=[], **kwargs):
"""
Convenience function which returns a "filter map" for ``cls``.
``exact``, if provided, should be a list of field names for which "exact"
filtering is to be allowed.
``ilike``, if provided, should be a list of field names for which "ILIKE"
filtering is to be allowed.
Any remaining ``kwargs`` are assumed to be filter map entries themselves,
and are added directly to the map.
"""
fmap = {}
for name in exact:
fmap[name] = filter_exact(getattr(cls, name))
for name in ilike:
fmap[name] = filter_ilike(getattr(cls, name))
fmap.update(kwargs)
return fmap
def get_search_form(request, filter_map, config):
"""
Returns a :class:`SearchForm` instance with a :class:`SearchFilter` for
each filter in ``filter_map``, using configuration from ``config``.
"""
search = SearchForm(request, filter_map, config)
for field in filter_map:
factory = config.get('filter_factory_%s' % field, SearchFilter)
label = config.get('filter_label_%s' % field)
search.add_filter(factory(field, label=label))
return search
def filter_query(query, config, filter_map, join_map):
"""
Filters ``query`` according to ``config`` and ``filter_map``. ``join_map``
is used, if necessary, to join additional tables to the base query. The
filtered query is returned.
"""
joins = config.setdefault('joins', [])
for key in config:
if key.startswith('include_filter_') and config[key]:
field = key[15:]
if field in join_map and field not in joins:
query = join_map[field](query)
joins.append(field)
value = config.get(field)
if value:
fmap = filter_map[field]
filt = fmap[config['filter_type_'+field]]
query = filt(query, value)
return query

138
tailbone/grids/util.py Normal file
View file

@ -0,0 +1,138 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Grid Utilities
"""
from sqlalchemy.orm.attributes import InstrumentedAttribute
from webhelpers.html import literal
from pyramid.response import Response
from .search import SearchFormRenderer
def get_sort_config(name, request, **kwargs):
"""
Returns a configuration dictionary for grid sorting.
"""
# Initial config uses some default values.
config = {
'dir': 'asc',
'per_page': 20,
'page': 1,
}
# Override with defaults provided by caller.
config.update(kwargs)
# Override with values from GET/POST request and/or session.
for key in config:
full_key = name+'_'+key
if request.params.get(key):
value = request.params[key]
config[key] = value
request.session[full_key] = value
elif request.session.get(full_key):
value = request.session[full_key]
config[key] = value
return config
def get_sort_map(cls, names=None, **kwargs):
"""
Convenience function which returns a sort map for ``cls``.
If ``names`` is not specified, the map will include all "standard" fields
present on the mapped class. Otherwise, the map will be limited to only
the fields which are named.
All remaining ``kwargs`` are assumed to be sort map entries, and will be
added to the map directly.
"""
smap = {}
if names is None:
names = []
for attr in cls.__dict__:
obj = getattr(cls, attr)
if isinstance(obj, InstrumentedAttribute):
if obj.key != 'uuid':
names.append(obj.key)
for name in names:
smap[name] = sorter(getattr(cls, name))
smap.update(kwargs)
return smap
def render_grid(grid, search_form=None, **kwargs):
"""
Convenience function to render ``grid`` (which should be a
:class:`tailbone.grids.Grid` instance).
This "usually" will return a dictionary to be used as context for rendering
the final view template.
However, if a partial grid is requested (or mandated), then the grid body
will be rendered and a :class:`pyramid.response.Response` object will be
returned instead.
"""
if grid.partial_only or grid.request.params.get('partial'):
return Response(body=grid.render(), content_type='text/html')
kwargs['grid'] = literal(grid.render())
if search_form:
kwargs['search'] = SearchFormRenderer(search_form)
return kwargs
def sort_query(query, config, sort_map, join_map={}):
"""
Sorts ``query`` according to ``config`` and ``sort_map``. ``join_map`` is
used, if necessary, to join additional tables to the base query. The
sorted query is returned.
"""
field = config.get('sort')
if not field:
return query
joins = config.setdefault('joins', [])
if field in join_map and field not in joins:
query = join_map[field](query)
joins.append(field)
sort = sort_map[field]
return sort(query, config['dir'])
def sorter(field):
"""
Returns a function suitable for a sort map callable, with typical logic
built in for sorting applied to ``field``.
"""
return lambda q, d: q.order_by(getattr(field, d)())

45
tailbone/helpers.py Normal file
View file

@ -0,0 +1,45 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Template Context Helpers
"""
import datetime
from decimal import Decimal
from webhelpers.html import *
from webhelpers.html.tags import *
from edbob.pyramid.forms import pretty_datetime
def pretty_date(date):
"""
Render a human-friendly date string.
"""
if not date:
return ''
return date.strftime('%a %d %b %Y')

View file

@ -0,0 +1,96 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html style="direction: ltr;" xmlns="http://www.w3.org/1999/xhtml" lang="en-us">
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
<title>Inventory Worksheet : ${department.name}</title>
<style type="text/css">
h1 {
font-size: 24px;
margin: 10px 0px;
}
h2 {
font-size: 20px;
margin: 0px;
padding: 0px;
}
table {
border-bottom: 1px solid #000000;
border-left: 1px solid #000000;
border-collapse: collapse;
empty-cells: show;
width: 100%;
}
th {
border-right: 1px solid #000000;
border-top: 1px solid #000000;
padding: 4px 8px;
}
th.subdepartment {
border-left: none;
font-size: 1.2em;
padding: 15px;
text-align: left;
}
td {
border-right: 1px solid #000000;
border-top: 1px solid #000000;
padding: 2px 4px;
white-space: nowrap;
}
td.upc {
text-align: center;
}
td.count {
width: 25%;
}
td.spacer {
height: 50px;
}
</style>
</head>
<body>
<h1>Inventory Worksheet</h1>
<h2>Department:&nbsp; ${department.name} (${department.number})</h2>
<h3>generated on ${date} at ${time}</h3>
<br clear="all" />
<table>
% for subdepartment in department.subdepartments:
<% products = get_products(subdepartment) %>
% if products:
<tr>
<th class="subdepartment" colspan="4">Subdepartment:&nbsp; ${subdepartment.name} (${subdepartment.number})</th>
</tr>
<tr>
<th>UPC</th>
<th>Brand</th>
<th>Description</th>
<th>Count</th>
</tr>
% for product in products:
<tr>
<td class="upc">${get_upc(product)}</td>
<td class="brand">${product.brand or ''}</td>
<td class="description">${product.description}</td>
<td class="count">&nbsp;</td>
</tr>
% endfor
<tr>
<td class="spacer" colspan="19">
</tr>
% endif
% endfor
</table>
</body>
</html>

View file

@ -0,0 +1,128 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html style="direction: ltr;" xmlns="http://www.w3.org/1999/xhtml" lang="en-us">
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
<title>Ordering Worksheet : ${vendor.name}</title>
<style type="text/css">
body {
font-family: sans-serif;
}
h1 {
font-size: 24px;
margin: 10px 0px;
}
h2 {
font-size: 20px;
margin: 0px;
padding: 0px;
}
table {
border-bottom: 1px solid #000000;
border-left: 1px solid #000000;
border-collapse: collapse;
empty-cells: show;
}
th {
border-right: 1px solid #000000;
border-top: 1px solid #000000;
padding: 4px 8px;
}
th.department {
border-left: none;
font-size: 1.2em;
padding: 15px;
text-align: left;
text-transform: uppercase;
}
th.subdepartment {
border-left: none;
padding: 15px;
text-align: left;
}
td {
border-right: 1px solid #000000;
border-top: 1px solid #000000;
padding: 2px 4px;
white-space: nowrap;
}
td.upc,
td.case-qty,
td.code,
td.preferred {
text-align: center;
}
td.code {
font-family: monospace;
font-size: 120%;
}
td.scratch_pad {
width: 20px;
}
td.spacer {
height: 50px;
}
</style>
</head>
<body>
<h1>Ordering Worksheet</h1>
<h2>Vendor:&nbsp; ${vendor.name} (${vendor.id})</h2>
<h2>Phone:&nbsp; ${vendor.phone or ''}</h2>
<h2>Contact:&nbsp; ${vendor.contact or ''}</h2>
<h3>generated on ${date} at ${time}</h3>
<br clear="all" />
<table>
% for dept in sorted(costs, key=lambda x: x.name):
<tr>
<th class="department" colspan="21">Department:&nbsp; ${dept.name} (${dept.number})</th>
</tr>
% for subdept in sorted(costs[dept], key=lambda x: x.name):
<tr>
<th class="subdepartment" colspan="21">Subdepartment:&nbsp; ${subdept.name} (${subdept.number})</th>
</tr>
<tr>
<th>UPC</th>
<th>Brand</th>
<th>Description</th>
<th>Size</th>
<th>Case</th>
<th>Vend. Code</th>
<th>Pref.</th>
<th colspan="14">Order Scratch Pad</th>
</tr>
% for cost in sorted(costs[dept][subdept], key=cost_sort_key):
<tr>
<td class="upc">${get_upc(cost.product)}</td>
<td class="brand">${cost.product.brand or ''}</td>
<td class="desc">${cost.product.description}</td>
<td class="size">${cost.product.size or ''}</td>
<td class="case-qty">${cost.case_size} ${rattail.UNIT_OF_MEASURE.get(cost.product.unit_of_measure, '')}</td>
<td class="code">${cost.code or ''}</td>
<td class="preferred">${'X' if cost.preference == 1 else ''}</td>
% for i in range(14):
<td class="scratch_pad">&nbsp;</td>
% endfor
</tr>
% endfor
<tr>
<td class="spacer" colspan="21">
</tr>
% endfor
% endfor
</table>
</body>
</html>

View file

@ -0,0 +1,31 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Static Assets
"""
def includeme(config):
config.add_static_view('tailbone', 'tailbone:static')

View file

@ -0,0 +1,168 @@
/******************************
* Grid Header
******************************/
table.grid-header {
padding-bottom: 5px;
width: 100%;
}
/******************************
* Form (Filters etc.)
******************************/
table.grid-header td.form {
vertical-align: bottom;
}
/******************************
* Context Menu
******************************/
table.grid-header td.context-menu {
vertical-align: top;
}
table.grid-header td.context-menu ul {
list-style-type: none;
margin: 0px;
text-align: right;
}
table.grid-header td.context-menu ul li {
line-height: 2em;
}
/******************************
* Tools
******************************/
table.grid-header td.tools {
padding-bottom: 10px;
text-align: right;
vertical-align: bottom;
}
table.grid-header td.tools div.buttons button {
margin-left: 5px;
}
/******************************
* Grid
******************************/
div.grid {
clear: both;
}
div.grid table {
border-top: 1px solid black;
border-left: 1px solid black;
border-collapse: collapse;
font-size: 9pt;
line-height: normal;
white-space: nowrap;
}
div.grid.full table {
width: 100%;
}
div.grid table th,
div.grid table td {
border-right: 1px solid black;
border-bottom: 1px solid black;
padding: 2px 3px;
}
div.grid table th.sortable a {
display: block;
padding-right: 18px;
}
div.grid table th.sorted {
background-position: right center;
background-repeat: no-repeat;
}
div.grid table th.sorted.asc {
background-image: url(../img/sort_arrow_up.png);
}
div.grid table th.sorted.desc {
background-image: url(../img/sort_arrow_down.png);
}
div.grid table tbody td {
text-align: left;
}
div.grid table tbody td.center {
text-align: center;
}
div.grid table tbody td.right {
float: none;
text-align: right;
}
div.grid table tr.odd {
background-color: #e0e0e0;
}
div.grid table tbody tr.hovering {
background-color: #bbbbbb;
}
div.grid table tbody tr td.checkbox {
text-align: center;
}
div.grid table tbody tr td.view,
div.grid table tbody tr td.edit,
div.grid table tbody tr td.delete {
background-repeat: no-repeat;
background-position: center;
cursor: pointer;
min-width: 18px;
text-align: center;
width: 18px;
}
div.grid table tbody tr td.view {
background-image: url(../img/view.png);
}
div.grid table tbody tr td.edit {
background-image: url(../img/edit.png);
}
div.grid table tbody tr td.delete {
background-image: url(../img/delete.png);
}
div.pager {
margin-bottom: 20px;
margin-top: 5px;
}
div.pager p {
font-size: 10pt;
margin: 0px;
}
div.pager p.showing {
float: left;
}
div.pager #grid-page-count {
font-size: 8pt;
}
div.pager p.page-links {
float: right;
}

Binary file not shown.

After

(image error) Size: 641 B

Binary file not shown.

After

(image error) Size: 533 B

Binary file not shown.

After

(image error) Size: 158 B

Binary file not shown.

After

(image error) Size: 169 B

Binary file not shown.

After

(image error) Size: 616 B

57
tailbone/subscribers.py Normal file
View file

@ -0,0 +1,57 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Event Subscribers
"""
from pyramid import threadlocal
import rattail
from . import helpers
def before_render(event):
"""
Adds goodies to the global template renderer context:
* ``rattail``
"""
# Import labels module so it's available if/when needed.
import rattail.labels
# Import SIL module so it's available if/when needed.
import rattail.sil
request = event.get('request') or threadlocal.get_current_request()
renderer_globals = event
renderer_globals['h'] = helpers
renderer_globals['rattail'] = rattail
def includeme(config):
config.add_subscriber('tailbone.subscribers:before_render',
'pyramid.events.BeforeRender')

View file

@ -0,0 +1,44 @@
<%def name="autocomplete(field_name, service_url, field_value=None, field_display=None, width='300px', selected=None, cleared=None)">
<div id="${field_name}-container" class="autocomplete-container">
${h.hidden(field_name, id=field_name, value=field_value)}
${h.text(field_name+'-textbox', id=field_name+'-textbox', value=field_display,
class_='autocomplete-textbox', style='display: none;' if field_value else '')}
<div id="${field_name}-display" class="autocomplete-display"${'' if field_value else ' style="display: none;"'|n}>
<span>${field_display or ''}</span>
<button type="button" id="${field_name}-change" class="autocomplete-change">Change</button>
</div>
</div>
<script type="text/javascript">
$(function() {
$('#${field_name}-textbox').autocomplete({
source: '${service_url}',
autoFocus: true,
focus: function(event, ui) {
return false;
},
select: function(event, ui) {
$('#${field_name}').val(ui.item.value);
$('#${field_name}-display span:first').text(ui.item.label);
$('#${field_name}-textbox').hide();
$('#${field_name}-display').show();
% if selected:
${selected}(ui.item.value, ui.item.label);
% endif
return false;
}
});
$('#${field_name}-change').click(function() {
$('#${field_name}').val('');
$('#${field_name}-display').hide();
with ($('#${field_name}-textbox')) {
val('');
show();
focus();
}
% if cleared:
${cleared}();
% endif
});
});
</script>
</%def>

View file

@ -0,0 +1,11 @@
<%inherit file="/crud.mako" />
<%def name="context_menu_items()">
<li>${h.link_to("Back to Batches", url('batches'))}</li>
<li>${h.link_to("View Batch Rows", url('batch.rows', uuid=form.fieldset.model.uuid))}</li>
% if not form.readonly:
<li>${h.link_to("View this Batch", url('batch.read', uuid=form.fieldset.model.uuid))}</li>
% endif
</%def>
${parent.body()}

View file

@ -0,0 +1,5 @@
<%inherit file="/grid.mako" />
<%def name="title()">Batches</%def>
${parent.body()}

View file

@ -0,0 +1,42 @@
<%inherit file="/base.mako" />
<%def name="title()">Batch Parameters</%def>
<%def name="head_tags()">
${parent.head_tags()}
<script language="javascript" type="text/javascript">
$(function() {
$('#create-batch').click(function() {
disable_button(this, "Creating batch");
disable_button('#cancel');
$('form').submit();
});
});
</script>
</%def>
<%def name="batch_params()"></%def>
<p>Please provide the following values for your new batch:</p>
<br />
<div class="form">
${h.form(request.get_referrer())}
${h.hidden('provider', value=provider)}
${h.hidden('params', value='True')}
${self.batch_params()}
<div class="buttons">
<button type="button" id="create-batch">Create Batch</button>
<button type="button" id="cancel" onclick="location.href = '${request.get_referrer()}';">Cancel</button>
</div>
${h.end_form()}
</div>

View file

@ -0,0 +1,19 @@
<%inherit file="/batches/params.mako" />
<%def name="batch_params()">
<div class="field-wrapper">
<label for="profile">Label Type</label>
<div class="field">
${h.select('profile', None, label_profiles)}
</div>
</div>
<div class="field-wrapper">
<label for="quantity">Quantity</label>
<div class="field">${h.text('quantity', value=1)}</div>
</div>
</%def>
${parent.body()}

View file

@ -0,0 +1,40 @@
<%inherit file="/batches/crud.mako" />
<%def name="context_menu_items()">
${parent.context_menu_items()}
<li>${h.link_to("Edit this Batch", url('batch.update', uuid=form.fieldset.model.uuid))}</li>
<li>${h.link_to("Delete this Batch", url('batch.delete', uuid=form.fieldset.model.uuid))}</li>
</%def>
${parent.body()}
<% batch = form.fieldset.model %>
<h2>Columns</h2>
<div class="grid full hoverable">
<table>
<thead>
<tr>
<th>Name</th>
<th>SIL Name</th>
<th>Display Name</th>
<th>Description</th>
<th>Data Type</th>
<th>Visible</th>
</tr>
</thead>
<tbody>
% for i, column in enumerate(batch.columns, 1):
<tr class="${'odd' if i % 2 else 'even'}">
<td>${column.name}</td>
<td>${column.sil_name}</td>
<td>${column.display_name}</td>
<td>${column.description}</td>
<td>${column.data_type}</td>
<td>${column.visible}</td>
</tr>
% endfor
</tbody>
</table>
</div>

View file

@ -0,0 +1,8 @@
<%inherit file="/crud.mako" />
<%def name="context_menu_items()">
<li>${h.link_to("Back to Batch", url('batch.read', uuid=form.fieldset.model.batch.uuid))}</li>
<li>${h.link_to("Back to Batch Rows", url('batch.rows', uuid=form.fieldset.model.batch.uuid))}</li>
</%def>
${parent.body()}

View file

@ -0,0 +1,46 @@
<%inherit file="/grid.mako" />
<%def name="title()">Batch Rows : ${batch.description}</%def>
<%def name="head_tags()">
${parent.head_tags()}
<script language="javascript" type="text/javascript">
$(function() {
$('#delete-results').click(function() {
var msg = "This will delete all rows matching the current search.\n\n"
+ "PLEASE NOTE that this may include some rows which are not visible "
+ "on your screen.\n(I.e., if there is more than one \"page\" of results.)\n\n"
+ "Are you sure you wish to delete these rows?";
if (confirm(msg)) {
disable_button(this, "Deleting rows");
location.href = '${url('batch.rows.delete', uuid=batch.uuid)}';
}
});
$('#execute-batch').click(function() {
if (confirm("Are you sure you wish to execute this batch?")) {
disable_button(this, "Executing batch");
location.href = '${url('batch.execute', uuid=batch.uuid)}';
}
});
});
</script>
</%def>
<%def name="context_menu_items()">
<li>${h.link_to("Back to Batches", url('batches'))}</li>
<li>${h.link_to("Back to Batch", url('batch.read', uuid=batch.uuid))}</li>
</%def>
<%def name="tools()">
<div class="buttons">
<button type="button" id="delete-results">Delete Results</button>
<button type="button" id="execute-batch">Execute Batch</button>
</div>
</%def>
${parent.body()}

View file

@ -0,0 +1,12 @@
<%inherit file="/crud.mako" />
<%def name="context_menu_items()">
<li>${h.link_to("Back to Brands", url('brands'))}</li>
% if form.readonly:
<li>${h.link_to("Edit this Brand", url('brand.update', uuid=form.fieldset.model.uuid))}</li>
% elif form.updating:
<li>${h.link_to("View this Brand", url('brand.read', uuid=form.fieldset.model.uuid))}</li>
% endif
</%def>
${parent.body()}

View file

@ -0,0 +1,11 @@
<%inherit file="/grid.mako" />
<%def name="title()">Brands</%def>
<%def name="context_menu_items()">
% if request.has_perm('brands.create'):
<li>${h.link_to("Create a new Brand", url('brand.create'))}</li>
% endif
</%def>
${parent.body()}

View file

@ -0,0 +1,12 @@
<%inherit file="/crud.mako" />
<%def name="context_menu_items()">
<li>${h.link_to("Back to Categories", url('categories'))}</li>
% if form.readonly:
<li>${h.link_to("Edit this Category", url('category.update', uuid=form.fieldset.model.uuid))}</li>
% elif form.updating:
<li>${h.link_to("View this Category", url('category.read', uuid=form.fieldset.model.uuid))}</li>
% endif
</%def>
${parent.body()}

View file

@ -0,0 +1,11 @@
<%inherit file="/grid.mako" />
<%def name="title()">Categories</%def>
<%def name="context_menu_items()">
% if request.has_perm('categories.create'):
<li>${h.link_to("Create a new Category", url('category.create'))}</li>
% endif
</%def>
${parent.body()}

View file

@ -0,0 +1,20 @@
<%inherit file="/form.mako" />
<%def name="title()">${"New "+form.pretty_name if form.creating else form.pretty_name+' : '+capture(self.model_title)}</%def>
<%def name="model_title()">${h.literal(str(form.fieldset.model))}</%def>
<%def name="head_tags()">
${parent.head_tags()}
<script type="text/javascript">
$(function() {
$('a.delete').click(function() {
if (! confirm("Do you really wish to delete this object?")) {
return false;
}
});
});
</script>
</%def>
${parent.body()}

View file

@ -0,0 +1,12 @@
<%inherit file="/crud.mako" />
<%def name="context_menu_items()">
<li>${h.link_to("Back to Customer Groups", url('customer_groups'))}</li>
% if form.readonly and request.has_perm('customer_groups.update'):
<li>${h.link_to("Edit this Customer Group", url('customer_group.update', uuid=form.fieldset.model.uuid))}</li>
% elif form.updating:
<li>${h.link_to("View this Customer Group", url('customer_group.read', uuid=form.fieldset.model.uuid))}</li>
% endif
</%def>
${parent.body()}

View file

@ -0,0 +1,11 @@
<%inherit file="/grid.mako" />
<%def name="title()">Customer Groups</%def>
<%def name="context_menu_items()">
% if request.has_perm('customer_groups.create'):
<li>${h.link_to("Create a new Customer Group", url('customer_group.create'))}</li>
% endif
</%def>
${parent.body()}

View file

@ -0,0 +1,12 @@
<%inherit file="/crud.mako" />
<%def name="context_menu_items()">
<li>${h.link_to("Back to Customers", url('customers'))}</li>
% if form.readonly and request.has_perm('customers.update'):
<li>${h.link_to("Edit this Customer", url('customer.update', uuid=form.fieldset.model.uuid))}</li>
% elif form.updating:
<li>${h.link_to("View this Customer", url('customer.read', uuid=form.fieldset.model.uuid))}</li>
% endif
</%def>
${parent.body()}

View file

@ -0,0 +1,5 @@
<%inherit file="/grid.mako" />
<%def name="title()">Customers</%def>
${parent.body()}

View file

@ -0,0 +1,51 @@
<%inherit file="/customers/crud.mako" />
${parent.body()}
<% customer = form.fieldset.model %>
<h2>People</h2>
% if customer.people:
<p>Customer account is associated with the following people:</p>
<div class="grid clickable">
<table>
<thead>
<th>First Name</th>
<th>Last Name</th>
</thead>
<tbody>
% for i, person in enumerate(customer.people, 1):
<tr class="${'odd' if i % 2 else 'even'}" url="${url('person.read', uuid=person.uuid)}">
<td>${person.first_name or ''}</td>
<td>${person.last_name or ''}</td>
</tr>
% endfor
</tbody>
</table>
</div>
% else:
<p>Customer account is not associated with any people.</p>
% endif
<h2>Groups</h2>
% if customer.groups:
<p>Customer account belongs to the following groups:</p>
<div class="grid clickable">
<table>
<thead>
<th>ID</th>
<th>Name</th>
</thead>
<tbody>
% for i, group in enumerate(customer.groups, 1):
<tr class="${'odd' if i % 2 else 'even'}" url="${url('customer_group.read', uuid=group.uuid)}">
<td>${group.id}</td>
<td>${group.name or ''}</td>
</tr>
% endfor
</tbody>
</table>
</div>
% else:
<p>Customer account doesn't belong to any groups.</p>
% endif

View file

@ -0,0 +1,12 @@
<%inherit file="/crud.mako" />
<%def name="context_menu_items()">
<li>${h.link_to("Back to Departments", url('departments'))}</li>
% if form.readonly:
<li>${h.link_to("Edit this Department", url('department.update', uuid=form.fieldset.model.uuid))}</li>
% elif form.updating:
<li>${h.link_to("View this Department", url('department.read', uuid=form.fieldset.model.uuid))}</li>
% endif
</%def>
${parent.body()}

View file

@ -0,0 +1,11 @@
<%inherit file="/grid.mako" />
<%def name="title()">Departments</%def>
<%def name="context_menu_items()">
% if request.has_perm('departments.create'):
<li>${h.link_to("Create a new Department", url('department.create'))}</li>
% endif
</%def>
${parent.body()}

View file

@ -0,0 +1,12 @@
<%inherit file="/crud.mako" />
<%def name="context_menu_items()">
<li>${h.link_to("Back to Employees", url('employees'))}</li>
% if form.readonly:
<li>${h.link_to("Edit this Employee", url('employee.update', uuid=form.fieldset.model.uuid))}</li>
% elif form.updating:
<li>${h.link_to("View this Employee", url('employee.read', uuid=form.fieldset.model.uuid))}</li>
% endif
</%def>
${parent.body()}

View file

@ -0,0 +1,5 @@
<%inherit file="/grid.mako" />
<%def name="title()">Employees</%def>
${parent.body()}

View file

@ -0,0 +1,3 @@
<%namespace file="/autocomplete.mako" import="autocomplete" />
${autocomplete(field_name, service_url, field_value, field_display, width=width, selected=selected, cleared=cleared)}

View file

@ -0,0 +1,63 @@
<div ${grid.div_attrs()}>
<table>
<thead>
<tr>
% if grid.checkboxes:
<th class="checkbox">${h.checkbox('check-all')}</th>
% endif
% for field in grid.iter_fields():
${grid.column_header(field)}
% endfor
% for col in grid.extra_columns:
<th>${col.label}</td>
% endfor
% if grid.viewable:
<th>&nbsp;</th>
% endif
% if grid.editable:
<th>&nbsp;</th>
% endif
% if grid.deletable:
<th>&nbsp;</th>
% endif
</tr>
</thead>
<tbody>
% for i, row in enumerate(grid.iter_rows(), 1):
<tr ${grid.get_row_attrs(row, i)}>
% if grid.checkboxes:
<td class="checkbox">${grid.checkbox(row)}</td>
% endif
% for field in grid.iter_fields():
<td class="${grid.cell_class(field)}">${grid.render_field(field)}</td>
% endfor
% for col in grid.extra_columns:
<td class="${col.name}">${col.callback(row)}</td>
% endfor
% if grid.viewable:
<td class="view" url="${grid.get_view_url(row)}">&nbsp;</td>
% endif
% if grid.editable:
<td class="edit" url="${grid.get_edit_url(row)}">&nbsp;</td>
% endif
% if grid.deletable:
<td class="delete" url="${grid.get_delete_url(row)}">&nbsp;</td>
% endif
</tr>
% endfor
</tbody>
</table>
% if grid.pager:
<div class="pager">
<p class="showing">
showing ${grid.pager.first_item} thru ${grid.pager.last_item} of ${grid.pager.item_count}
(page ${grid.pager.page} of ${grid.pager.page_count})
</p>
<p class="page-links">
${h.select('grid-page-count', grid.pager.items_per_page, grid.page_count_options())}
per page&nbsp;
${grid.page_links()}
</p>
</div>
% endif
</div>

View file

@ -0,0 +1,26 @@
<%inherit file="/crud.mako" />
<%def name="head_tags()">
${parent.head_tags()}
<style type="text/css">
div.form div.field-wrapper.format textarea {
width: auto;
}
</style>
</%def>
<%def name="context_menu_items()">
<li>${h.link_to("Back to Label Profiles", url('label_profiles'))}</li>
% if form.updating:
<% profile = form.fieldset.model %>
<% printer = profile.get_printer() %>
% if printer and printer.required_settings:
<li>${h.link_to("Edit Printer Settings", url('label_profile.printer_settings', uuid=profile.uuid))}</li>
% endif
<li>${h.link_to("View this Label Profile", url('label_profile.read', uuid=profile.uuid))}</li>
% endif
</%def>
${parent.body()}

View file

@ -0,0 +1,11 @@
<%inherit file="/grid.mako" />
<%def name="title()">Label Profiles</%def>
<%def name="context_menu_items()">
% if request.has_perm('label_profiles.create'):
<li>${h.link_to("Create a new Label Profile", url('label_profile.create'))}</li>
% endif
</%def>
${parent.body()}

View file

@ -0,0 +1,48 @@
<%inherit file="/base.mako" />
<%def name="title()">Printer Settings</%def>
<%def name="context_menu_items()">
<li>${h.link_to("Back to Label Profiles", url('label_profiles'))}</li>
<li>${h.link_to("View this Label Profile", url('label_profile.read', uuid=profile.uuid))}</li>
<li>${h.link_to("Edit this Label Profile", url('label_profile.update', uuid=profile.uuid))}</li>
</%def>
<div class="form-wrapper">
<ul class="context-menu">
${self.context_menu_items()}
</ul>
<div class="form">
<div class="field-wrapper">
<label>Label Profile</label>
<div class="field">${profile.description}</div>
</div>
<div class="field-wrapper">
<label>Printer Spec</label>
<div class="field">${profile.printer_spec}</div>
</div>
${h.form(request.current_route_url())}
% for name, display in printer.required_settings.iteritems():
<div class="field-wrapper">
<label for="${name}">${display}</label>
<div class="field">
${h.text(name, value=profile.get_printer_setting(name))}
</div>
</div>
% endfor
<div class="buttons">
${h.submit('update', "Update")}
<button type="button" onclick="location.href = '${url('label_profile.read', uuid=profile.uuid)}';">Cancel</button>
</div>
${h.end_form()}
</div>
</div>

View file

@ -0,0 +1,32 @@
<%inherit file="/labels/profiles/crud.mako" />
<%def name="context_menu_items()">
<li>${h.link_to("Back to Label Profiles", url('label_profiles'))}</li>
% if form.readonly and request.has_perm('label_profiles.update'):
<% profile = form.fieldset.model %>
<% printer = profile.get_printer() %>
<li>${h.link_to("Edit this Label Profile", url('label_profile.update', uuid=form.fieldset.model.uuid))}</li>
% if printer and printer.required_settings:
<li>${h.link_to("Edit Printer Settings", url('label_profile.printer_settings', uuid=profile.uuid))}</li>
% endif
% endif
</%def>
${parent.body()}
<% profile = form.fieldset.model %>
<% printer = profile.get_printer() %>
% if printer and printer.required_settings:
<h2>Printer Settings</h2>
<div class="form">
% for name, display in printer.required_settings.iteritems():
<div class="field-wrapper">
<label>${display}</label>
<div class="field">${profile.get_printer_setting(name) or ''}</div>
</div>
% endfor
</div>
% endif

View file

@ -0,0 +1,12 @@
<%inherit file="/crud.mako" />
<%def name="context_menu_items()">
<li>${h.link_to("Back to People", url('people'))}</li>
% if form.readonly:
<li>${h.link_to("Edit this Person", url('person.update', uuid=form.fieldset.model.uuid))}</li>
% elif form.updating:
<li>${h.link_to("View this Person", url('person.read', uuid=form.fieldset.model.uuid))}</li>
% endif
</%def>
${parent.body()}

View file

@ -0,0 +1,27 @@
<%inherit file="/base.mako" />
<%def name="title()">Create Products Batch</%def>
<%def name="context_menu_items()">
<li>${h.link_to("Back to Products", url('products'))}</li>
</%def>
<div class="form">
${h.form(request.current_route_url())}
<div class="field-wrapper">
<label for="provider">Batch Type</label>
<div class="field">
${h.select('provider', None, providers)}
</div>
</div>
<div class="buttons">
${h.submit('create', "Create Batch")}
<button type="button" onclick="location.href = '${url('products')}';">Cancel</button>
</div>
${h.end_form()}
</div>

View file

@ -0,0 +1,12 @@
<%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
</%def>
${parent.body()}

View file

@ -0,0 +1,107 @@
<%inherit file="/grid.mako" />
<%def name="title()">Products</%def>
<%def name="head_tags()">
${parent.head_tags()}
<style type="text/css">
table.grid-header td.tools table {
margin-left: auto;
}
table.grid-header td.tools table th,
table.grid-header td.tools table td {
padding: 0px;
text-align: left;
}
table.grid-header td.tools table #label-quantity {
text-align: right;
width: 30px;
}
div.grid table tbody td.size,
div.grid table tbody td.regular_price_uuid,
div.grid table tbody td.current_price_uuid {
padding-right: 6px;
text-align: right;
}
div.grid table tbody td.labels {
text-align: center;
}
</style>
% if label_profiles and request.has_perm('products.print_labels'):
<script language="javascript" 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;
});
});
</script>
% endif
</%def>
<%def name="tools()">
% if label_profiles and request.has_perm('products.print_labels'):
<table>
<thead>
<tr>
<td>Label</td>
<td>Qty.</td>
</tr>
</thead>
<tbody>
<td>
<select name="label-profile" id="label-profile">
% for profile in label_profiles:
<option value="${profile.uuid}">${profile.description}</option>
% endfor
</select>
</td>
<td>
<input type="text" name="label-quantity" id="label-quantity" value="1" />
</td>
</tbody>
</table>
% endif
</%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
% if request.has_perm('batches.create'):
<li>${h.link_to("Create Batch from Results", url('products.create_batch'))}</li>
% endif
</%def>
${parent.body()}

View file

@ -0,0 +1,59 @@
<%inherit file="/products/crud.mako" />
${parent.body()}
<% product = form.fieldset.model %>
<div id="product-codes">
<h2>Product Codes:</h2>
% if product.codes:
<div class="grid hoverable">
<table>
<thead>
<th>Code</th>
</thead>
<tbody>
% for i, code in enumerate(product.codes, 1):
<tr class="${'odd' if i % 2 else 'even'}">
<td>${code}</td>
</tr>
% endfor
</tbody>
</table>
</div>
% else:
<p>None on file.</p>
% endif
</div>
<div id="product-costs">
<h2>Product Costs:</h2>
% if product.costs:
<div class="grid hoverable">
<table>
<thead>
<th>Pref.</th>
<th>Vendor</th>
<th>Code</th>
<th>Case Size</th>
<th>Case Cost</th>
<th>Unit Cost</th>
</thead>
<tbody>
% for i, cost in enumerate(product.costs, 1):
<tr class="${'odd' if i % 2 else 'even'}">
<td class="center">${'X' if cost.preference == 1 else ''}</td>
<td>${cost.vendor}</td>
<td class="center">${cost.code}</td>
<td class="center">${cost.case_size}</td>
<td class="right">${'$ %0.2f' % cost.case_cost if cost.case_cost is not None else ''}</td>
<td class="right">${'$ %0.4f' % cost.unit_cost if cost.unit_cost is not None else ''}</td>
</tr>
% endfor
</tbody>
</table>
</div>
% else:
<p>None on file.</p>
% endif
</div>

View file

@ -0,0 +1,2 @@
<%inherit file="/base.mako" />
${parent.body()}

View file

@ -0,0 +1,29 @@
<%inherit file="/reports/base.mako" />
<%def name="title()">Report : Inventory Worksheet</%def>
<p>Please provide the following criteria to generate your report:</p>
<br />
${h.form(request.current_route_url())}
<div class="field-wrapper">
<label for="department">Department</label>
<div class="field">
<select name="department">
% for department in departments:
<option value="${department.uuid}">${department.name}</option>
% endfor
</select>
</div>
</div>
<div class="field">
${h.checkbox('weighted-only', label=h.literal("Include items sold by weight <strong>only</strong>."))}
</div>
<div class="buttons">
${h.submit('submit', "Generate Report")}
</div>
${h.end_form()}

View file

@ -0,0 +1,88 @@
<%inherit file="/reports/base.mako" />
<%def name="title()">Report : Ordering Worksheet</%def>
<%def name="head_tags()">
${parent.head_tags()}
<style type="text/css">
div.grid {
clear: none;
}
</style>
</%def>
<p>Please provide the following criteria to generate your report:</p>
<br />
${h.form(request.current_route_url())}
${h.hidden('departments', value='')}
<div class="field-wrapper">
${h.hidden('vendor', value='')}
<label for="vendor-name">Vendor:</label>
${h.text('vendor-name', size='40', value='')}
<div id="vendor-display" style="display: none;">
<span>(no vendor)</span>&nbsp;
<button type="button" id="change-vendor">Change</button>
</div>
</div>
<div class="field-wrapper">
<label>Departments:</label>
<div class="grid"></div>
</div>
<div class="field-wrapper">
${h.checkbox('preferred_only', label="Include only those products for which this vendor is preferred.", checked=True)}
</div>
<div class="buttons">
${h.submit('submit', "Generate Report")}
</div>
${h.end_form()}
<script type="text/javascript">
$(function() {
var autocompleter = $('#vendor-name').autocomplete({
serviceUrl: '${url('vendors.autocomplete')}',
width: 300,
onSelect: function(value, data) {
$('#vendor').val(data);
$('#vendor-name').hide();
$('#vendor-name').val('');
$('#vendor-display span').html(value);
$('#vendor-display').show();
loading($('div.grid'));
$('div.grid').load('${url('departments.by_vendor')}', {'uuid': data});
},
});
$('#vendor-name').focus();
$('#change-vendor').click(function() {
$('#vendor').val('');
$('#vendor-display').hide();
$('#vendor-name').show();
$('#vendor-name').focus();
$('div.grid').empty();
});
$('form').submit(function() {
var depts = [];
$('div.grid table tbody tr').each(function() {
if ($(this).find('td.checkbox input[type=checkbox]').is(':checked')) {
depts.push(get_uuid(this));
}
$('#departments').val(depts.toString());
return true;
});
});
});
</script>

View file

@ -0,0 +1,12 @@
<%inherit file="/crud.mako" />
<%def name="context_menu_items()">
<li>${h.link_to("Back to Stores", url('stores'))}</li>
% if form.readonly and request.has_perm('stores.update'):
<li>${h.link_to("Edit this Store", url('store.update', uuid=form.fieldset.model.uuid))}</li>
% elif form.updating:
<li>${h.link_to("View this Store", url('store.read', uuid=form.fieldset.model.uuid))}</li>
% endif
</%def>
${parent.body()}

View file

@ -0,0 +1,11 @@
<%inherit file="/grid.mako" />
<%def name="title()">Stores</%def>
<%def name="context_menu_items()">
% if request.has_perm('stores.create'):
<li>${h.link_to("Create a new Store", url('store.create'))}</li>
% endif
</%def>
${parent.body()}

View file

@ -0,0 +1,12 @@
<%inherit file="/crud.mako" />
<%def name="context_menu_items()">
<li>${h.link_to("Back to Subdepartments", url('subdepartments'))}</li>
% if form.readonly:
<li>${h.link_to("Edit this Subdepartment", url('subdepartment.update', uuid=form.fieldset.model.uuid))}</li>
% elif form.updating:
<li>${h.link_to("View this Subdepartment", url('subdepartment.read', uuid=form.fieldset.model.uuid))}</li>
% endif
</%def>
${parent.body()}

View file

@ -0,0 +1,11 @@
<%inherit file="/grid.mako" />
<%def name="title()">Subdepartments</%def>
<%def name="context_menu_items()">
% if request.has_perm('subdepartments.create'):
<li>${h.link_to("Create a new Subdepartment", url('subdepartment.create'))}</li>
% endif
</%def>
${parent.body()}

View file

@ -0,0 +1,12 @@
<%inherit file="/crud.mako" />
<%def name="context_menu_items()">
<li>${h.link_to("Back to Users", url('users'))}</li>
% if form.readonly:
<li>${h.link_to("Edit this User", url('user.update', uuid=form.fieldset.model.uuid))}</li>
% elif form.updating:
<li>${h.link_to("View this User", url('user.read', uuid=form.fieldset.model.uuid))}</li>
% endif
</%def>
${parent.body()}

12
tailbone/templates/vendors/crud.mako vendored Normal file
View file

@ -0,0 +1,12 @@
<%inherit file="/crud.mako" />
<%def name="context_menu_items()">
<li>${h.link_to("Back to Vendors", url('vendors'))}</li>
% if form.readonly:
<li>${h.link_to("Edit this Vendor", url('vendor.update', uuid=form.fieldset.model.uuid))}</li>
% elif form.updating:
<li>${h.link_to("View this Vendor", url('vendor.read', uuid=form.fieldset.model.uuid))}</li>
% endif
</%def>
${parent.body()}

11
tailbone/templates/vendors/index.mako vendored Normal file
View file

@ -0,0 +1,11 @@
<%inherit file="/grid.mako" />
<%def name="title()">Vendors</%def>
<%def name="context_menu_items()">
% if request.has_perm('vendors.create'):
<li>${h.link_to("Create a new Vendor", url('vendor.create'))}</li>
% endif
</%def>
${parent.body()}

View file

@ -0,0 +1,47 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Pyramid Views
"""
from .core import *
from .grids import *
from .crud import *
from .autocomplete import *
def includeme(config):
config.include('tailbone.views.batches')
# config.include('tailbone.views.categories')
config.include('tailbone.views.customergroups')
config.include('tailbone.views.customers')
config.include('tailbone.views.departments')
config.include('tailbone.views.employees')
config.include('tailbone.views.labels')
config.include('tailbone.views.products')
config.include('tailbone.views.roles')
config.include('tailbone.views.stores')
config.include('tailbone.views.subdepartments')
config.include('tailbone.views.vendors')

View file

@ -0,0 +1,61 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Autocomplete View
"""
from .core import View
from .. import Session
__all__ = ['AutocompleteView']
class AutocompleteView(View):
def filter_query(self, q):
return q
def make_query(self, term):
q = Session.query(self.mapped_class)
q = self.filter_query(q)
q = q.filter(getattr(self.mapped_class, self.fieldname).ilike('%%%s%%' % term))
q = q.order_by(getattr(self.mapped_class, self.fieldname))
return q
def query(self, term):
return self.make_query(term)
def display(self, instance):
return getattr(instance, self.fieldname)
def __call__(self):
term = self.request.params.get('term')
if term:
term = term.strip()
if not term:
return []
results = self.query(term).all()
return [{'label': self.display(x), 'value': x.uuid} for x in results]

View file

@ -0,0 +1,35 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Batch Views
"""
from .params import *
def includeme(config):
config.include('tailbone.views.batches.core')
config.include('tailbone.views.batches.params')
config.include('tailbone.views.batches.rows')

View file

@ -0,0 +1,203 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Core Batch Views
"""
from pyramid.httpexceptions import HTTPFound
from pyramid.renderers import render_to_response
from webhelpers.html import tags
from edbob.pyramid.forms import PrettyDateTimeFieldRenderer
from ...forms import EnumFieldRenderer
from ...grids.search import BooleanSearchFilter
from edbob.pyramid.progress import SessionProgress
from .. import SearchableAlchemyGridView, CrudView, View
import rattail
from rattail import batches
from ... import Session
from rattail.db.model import Batch
from rattail.threads import Thread
class BatchesGrid(SearchableAlchemyGridView):
mapped_class = Batch
config_prefix = 'batches'
sort = 'id'
def filter_map(self):
def executed_is(q, v):
if v == 'True':
return q.filter(Batch.executed != None)
else:
return q.filter(Batch.executed == None)
def executed_isnot(q, v):
if v == 'True':
return q.filter(Batch.executed == None)
else:
return q.filter(Batch.executed != None)
return self.make_filter_map(
exact=['id'],
ilike=['source', 'destination', 'description'],
executed={
'is': executed_is,
'nt': executed_isnot,
})
def filter_config(self):
return self.make_filter_config(
filter_label_id="ID",
filter_factory_executed=BooleanSearchFilter,
include_filter_executed=True,
filter_type_executed='is',
executed='False')
def sort_map(self):
return self.make_sort_map('source', 'id', 'destination', 'description', 'executed')
def grid(self):
g = self.make_grid()
g.executed.set(renderer=PrettyDateTimeFieldRenderer(from_='utc'))
g.configure(
include=[
g.source,
g.id.label("ID"),
g.destination,
g.description,
g.rowcount.label("Row Count"),
g.executed,
],
readonly=True)
if self.request.has_perm('batches.read'):
def rows(row):
return tags.link_to("View Rows", self.request.route_url(
'batch.rows', uuid=row.uuid))
g.add_column('rows', "", rows)
g.viewable = True
g.view_route_name = 'batch.read'
if self.request.has_perm('batches.update'):
g.editable = True
g.edit_route_name = 'batch.update'
if self.request.has_perm('batches.delete'):
g.deletable = True
g.delete_route_name = 'batch.delete'
return g
class BatchCrud(CrudView):
mapped_class = Batch
home_route = 'batches'
def fieldset(self, model):
fs = self.make_fieldset(model)
fs.action_type.set(renderer=EnumFieldRenderer(rattail.BATCH_ACTION))
fs.executed.set(renderer=PrettyDateTimeFieldRenderer(from_='utc'))
fs.configure(
include=[
fs.source,
fs.id.label("ID"),
fs.destination,
fs.action_type,
fs.description,
fs.rowcount.label("Row Count").readonly(),
fs.executed.readonly(),
])
return fs
def post_delete(self, batch):
batch.drop_table()
class ExecuteBatch(View):
def execute_batch(self, batch, progress):
from rattail.db import Session
session = Session()
batch = session.merge(batch)
if not batch.execute(progress):
session.rollback()
session.close()
return
session.commit()
session.refresh(batch)
session.close()
progress.session.load()
progress.session['complete'] = True
progress.session['success_msg'] = "Batch \"%s\" has been executed." % batch.description
progress.session['success_url'] = self.request.route_url('batches')
progress.session.save()
def __call__(self):
uuid = self.request.matchdict['uuid']
batch = Session.query(Batch).get(uuid) if uuid else None
if not batch:
return HTTPFound(location=self.request.route_url('batches'))
progress = SessionProgress(self.request.session, 'batch.execute')
thread = Thread(target=self.execute_batch, args=(batch, progress))
thread.start()
kwargs = {
'key': 'batch.execute',
'cancel_url': self.request.route_url('batch.rows', uuid=batch.uuid),
'cancel_msg': "Batch execution was canceled.",
}
return render_to_response('/progress.mako', kwargs, request=self.request)
def includeme(config):
config.add_route('batches', '/batches')
config.add_view(BatchesGrid, route_name='batches',
renderer='/batches/index.mako',
permission='batches.list')
config.add_route('batch.read', '/batches/{uuid}')
config.add_view(BatchCrud, attr='read',
route_name='batch.read',
renderer='/batches/read.mako',
permission='batches.read')
config.add_route('batch.update', '/batches/{uuid}/edit')
config.add_view(BatchCrud, attr='update', route_name='batch.update',
renderer='/batches/crud.mako',
permission='batches.update')
config.add_route('batch.delete', '/batches/{uuid}/delete')
config.add_view(BatchCrud, attr='delete', route_name='batch.delete',
permission='batches.delete')
config.add_route('batch.execute', '/batches/{uuid}/execute')
config.add_view(ExecuteBatch, route_name='batch.execute',
permission='batches.execute')

View file

@ -0,0 +1,52 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Batch Parameter Views
"""
from ... import View
__all__ = ['BatchParamsView']
class BatchParamsView(View):
provider_name = None
def render_kwargs(self):
return {}
def __call__(self):
if self.request.POST:
if self.set_batch_params():
return HTTPFound(location=self.request.get_referer())
kwargs = self.render_kwargs()
kwargs['provider'] = self.provider_name
return kwargs
def includeme(config):
config.include('tailbone.views.batches.params.labels')

View file

@ -0,0 +1,51 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Print Labels Batch
"""
from .... import Session
import rattail
from . import BatchParamsView
class PrintLabels(BatchParamsView):
provider_name = 'print_labels'
def render_kwargs(self):
q = Session.query(rattail.LabelProfile)
q = q.order_by(rattail.LabelProfile.ordinal)
profiles = [(x.code, x.description) for x in q]
return {'label_profiles': profiles}
def includeme(config):
config.add_route('batch_params.print_labels', '/batches/params/print-labels')
config.add_view(PrintLabels, route_name='batch_params.print_labels',
renderer='/batches/params/print_labels.mako',
permission='batches.print_labels')

View file

@ -0,0 +1,222 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Batch Row Views
"""
from pyramid.httpexceptions import HTTPFound
from ... import Session
from .. import SearchableAlchemyGridView, CrudView
import rattail
from ...forms import GPCFieldRenderer
def field_with_renderer(field, column):
if column.sil_name == 'F01': # UPC
field = field.with_renderer(GPCFieldRenderer)
elif column.sil_name == 'F95': # Shelf Tag Type
q = Session.query(rattail.LabelProfile)
q = q.order_by(rattail.LabelProfile.ordinal)
field = field.dropdown(options=[(x.description, x.code) for x in q])
return field
def BatchRowsGrid(request):
uuid = request.matchdict['uuid']
batch = Session.query(rattail.Batch).get(uuid) if uuid else None
if not batch:
return HTTPFound(location=request.route_url('batches'))
class BatchRowsGrid(SearchableAlchemyGridView):
mapped_class = batch.rowclass
config_prefix = 'batch.%s' % batch.uuid
sort = 'ordinal'
def filter_map(self):
fmap = self.make_filter_map()
for column in batch.columns:
if column.visible:
if column.data_type.startswith('CHAR'):
fmap[column.name] = self.filter_ilike(
getattr(batch.rowclass, column.name))
else:
fmap[column.name] = self.filter_exact(
getattr(batch.rowclass, column.name))
return fmap
def filter_config(self):
config = self.make_filter_config()
for column in batch.columns:
if column.visible:
config['filter_label_%s' % column.name] = column.display_name
return config
def grid(self):
g = self.make_grid()
include = [g.ordinal.label("Row")]
for column in batch.columns:
if column.visible:
field = getattr(g, column.name)
field = field_with_renderer(field, column)
field = field.label(column.display_name)
include.append(field)
g.column_titles[field.key] = '%s - %s - %s' % (
column.sil_name, column.description, column.data_type)
g.configure(include=include, readonly=True)
route_kwargs = lambda x: {'batch_uuid': x.batch.uuid, 'uuid': x.uuid}
if self.request.has_perm('batch_rows.read'):
g.viewable = True
g.view_route_name = 'batch_row.read'
g.view_route_kwargs = route_kwargs
if self.request.has_perm('batch_rows.update'):
g.editable = True
g.edit_route_name = 'batch_row.update'
g.edit_route_kwargs = route_kwargs
if self.request.has_perm('batch_rows.delete'):
g.deletable = True
g.delete_route_name = 'batch_row.delete'
g.delete_route_kwargs = route_kwargs
return g
def render_kwargs(self):
return {'batch': batch}
grid = BatchRowsGrid(request)
grid.batch = batch
return grid
def batch_rows_grid(request):
result = BatchRowsGrid(request)
if isinstance(result, HTTPFound):
return result
return result()
def batch_rows_delete(request):
grid = BatchRowsGrid(request)
grid._filter_config = grid.filter_config()
rows = grid.make_query()
count = rows.count()
rows.delete(synchronize_session=False)
grid.batch.rowcount -= count
request.session.flash("Deleted %d rows from batch." % count)
return HTTPFound(location=request.route_url('batch.rows', uuid=grid.batch.uuid))
def batch_row_crud(request, attr):
batch_uuid = request.matchdict['batch_uuid']
batch = Session.query(rattail.Batch).get(batch_uuid)
if not batch:
return HTTPFound(location=request.route_url('batches'))
row_uuid = request.matchdict['uuid']
row = Session.query(batch.rowclass).get(row_uuid)
if not row:
return HTTPFound(location=request.route_url('batch.read', uuid=batch.uuid))
class BatchRowCrud(CrudView):
mapped_class = batch.rowclass
pretty_name = "Batch Row"
@property
def home_url(self):
return self.request.route_url('batch.rows', uuid=batch.uuid)
@property
def cancel_url(self):
return self.home_url
def fieldset(self, model):
fs = self.make_fieldset(model)
include = [fs.ordinal.label("Row Number").readonly()]
for column in batch.columns:
field = getattr(fs, column.name)
field = field_with_renderer(field, column)
field = field.label(column.display_name)
include.append(field)
fs.configure(include=include)
return fs
def flash_delete(self, row):
self.request.session.flash("Batch Row %d has been deleted."
% row.ordinal)
def post_delete(self, model):
batch.rowcount -= 1
crud = BatchRowCrud(request)
return getattr(crud, attr)()
def batch_row_read(request):
return batch_row_crud(request, 'read')
def batch_row_update(request):
return batch_row_crud(request, 'update')
def batch_row_delete(request):
return batch_row_crud(request, 'delete')
def includeme(config):
config.add_route('batch.rows', '/batches/{uuid}/rows')
config.add_view(batch_rows_grid, route_name='batch.rows',
renderer='/batches/rows/index.mako',
permission='batches.read')
config.add_route('batch.rows.delete', '/batches/{uuid}/rows/delete')
config.add_view(batch_rows_delete, route_name='batch.rows.delete',
permission='batch_rows.delete')
config.add_route('batch_row.read', '/batches/{batch_uuid}/{uuid}')
config.add_view(batch_row_read, route_name='batch_row.read',
renderer='/batches/rows/crud.mako',
permission='batch_rows.read')
config.add_route('batch_row.update', '/batches/{batch_uuid}/{uuid}/edit')
config.add_view(batch_row_update, route_name='batch_row.update',
renderer='/batches/rows/crud.mako',
permission='batch_rows.update')
config.add_route('batch_row.delete', '/batches/{batch_uuid}/{uuid}/delete')
config.add_view(batch_row_delete, route_name='batch_row.delete',
permission='batch_rows.delete')

124
tailbone/views/brands.py Normal file
View file

@ -0,0 +1,124 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Brand Views
"""
from . import SearchableAlchemyGridView, CrudView, AutocompleteView
from rattail.db.model import Brand
class BrandsGrid(SearchableAlchemyGridView):
mapped_class = Brand
config_prefix = 'brands'
sort = 'name'
def filter_map(self):
return self.make_filter_map(ilike=['name'])
def filter_config(self):
return self.make_filter_config(
include_filter_name=True,
filter_type_name='lk')
def sort_map(self):
return self.make_sort_map('name')
def grid(self):
g = self.make_grid()
g.configure(
include=[
g.name,
],
readonly=True)
if self.request.has_perm('brands.read'):
g.viewable = True
g.view_route_name = 'brand.read'
if self.request.has_perm('brands.update'):
g.editable = True
g.edit_route_name = 'brand.update'
if self.request.has_perm('brands.delete'):
g.deletable = True
g.delete_route_name = 'brand.delete'
return g
class BrandCrud(CrudView):
mapped_class = Brand
home_route = 'brands'
def fieldset(self, model):
fs = self.make_fieldset(model)
fs.configure(
include=[
fs.name,
])
return fs
class BrandsAutocomplete(AutocompleteView):
mapped_class = Brand
fieldname = 'name'
def add_routes(config):
config.add_route('brands', '/brands')
config.add_route('brands.autocomplete', '/brands/autocomplete')
config.add_route('brand.create', '/brands/new')
config.add_route('brand.read', '/brands/{uuid}')
config.add_route('brand.update', '/brands/{uuid}/edit')
config.add_route('brand.delete', '/brands/{uuid}/delete')
def includeme(config):
add_routes(config)
config.add_view(BrandsGrid,
route_name='brands',
renderer='/brands/index.mako',
permission='brands.list')
config.add_view(BrandsAutocomplete,
route_name='brands.autocomplete',
renderer='json',
permission='brands.list')
config.add_view(BrandCrud, attr='create',
route_name='brand.create',
renderer='/brands/crud.mako',
permission='brands.create')
config.add_view(BrandCrud, attr='read',
route_name='brand.read',
renderer='/brands/crud.mako',
permission='brands.read')
config.add_view(BrandCrud, attr='update',
route_name='brand.update',
renderer='/brands/crud.mako',
permission='brands.update')
config.add_view(BrandCrud, attr='delete',
route_name='brand.delete',
permission='brands.delete')

View file

@ -0,0 +1,110 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Category Views
"""
from . import SearchableAlchemyGridView, CrudView
from rattail.db.model import Category
class CategoriesGrid(SearchableAlchemyGridView):
mapped_class = Category
config_prefix = 'categories'
sort = 'number'
def filter_map(self):
return self.make_filter_map(exact=['number'], ilike=['name'])
def filter_config(self):
return self.make_filter_config(
include_filter_name=True,
filter_type_name='lk')
def sort_map(self):
return self.make_sort_map('number', 'name')
def grid(self):
g = self.make_grid()
g.configure(
include=[
g.number,
g.name,
],
readonly=True)
if self.request.has_perm('categories.read'):
g.viewable = True
g.view_route_name = 'category.read'
if self.request.has_perm('categories.update'):
g.editable = True
g.edit_route_name = 'category.update'
if self.request.has_perm('categories.delete'):
g.deletable = True
g.delete_route_name = 'category.delete'
return g
class CategoryCrud(CrudView):
mapped_class = Category
home_route = 'categories'
def fieldset(self, model):
fs = self.make_fieldset(model)
fs.configure(
include=[
fs.number,
fs.name,
])
return fs
def includeme(config):
config.add_route('categories', '/categories')
config.add_view(CategoriesGrid, route_name='categories',
renderer='/categories/index.mako',
permission='categories.list')
config.add_route('category.create', '/categories/new')
config.add_view(CategoryCrud, attr='create', route_name='category.create',
renderer='/categories/crud.mako',
permission='categories.create')
config.add_route('category.read', '/categories/{uuid}')
config.add_view(CategoryCrud, attr='read', route_name='category.read',
renderer='/categories/crud.mako',
permission='categories.read')
config.add_route('category.update', '/categories/{uuid}/edit')
config.add_view(CategoryCrud, attr='update', route_name='category.update',
renderer='/categories/crud.mako',
permission='categories.update')
config.add_route('category.delete', '/categories/{uuid}/delete')
config.add_view(CategoryCrud, attr='delete', route_name='category.delete',
permission='categories.delete')

35
tailbone/views/core.py Normal file
View file

@ -0,0 +1,35 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Core View
"""
class View(object):
"""
Base for all class-based views.
"""
def __init__(self, request):
self.request = request

212
tailbone/views/crud.py Normal file
View file

@ -0,0 +1,212 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
CRUD View
"""
from pyramid.httpexceptions import HTTPFound
import formalchemy
from .. import Session
from edbob.pyramid.forms.formalchemy import AlchemyForm
from .core import View
from edbob.util import requires_impl, prettify
__all__ = ['CrudView']
class CrudView(View):
readonly = False
allow_successive_creates = False
update_cancel_route = None
@property
@requires_impl(is_property=True)
def mapped_class(self):
pass
@property
def pretty_name(self):
return self.mapped_class.__name__
@property
@requires_impl(is_property=True)
def home_route(self):
pass
@property
def home_url(self):
return self.request.route_url(self.home_route)
@property
def cancel_route(self):
return self.home_route
@property
def cancel_url(self):
return self.request.route_url(self.cancel_route)
def make_fieldset(self, model, **kwargs):
kwargs.setdefault('session', Session())
kwargs.setdefault('request', self.request)
fieldset = formalchemy.FieldSet(model, **kwargs)
fieldset.prettify = prettify
return fieldset
def fieldset(self, model):
return self.make_fieldset(model)
def make_form(self, model, **kwargs):
if self.readonly:
self.creating = False
self.updating = False
else:
self.creating = model is self.mapped_class
self.updating = not self.creating
fieldset = self.fieldset(model)
kwargs.setdefault('pretty_name', self.pretty_name)
kwargs.setdefault('action_url', self.request.current_route_url())
if self.updating and self.update_cancel_route:
kwargs.setdefault('cancel_url', self.request.route_url(
self.update_cancel_route, uuid=model.uuid))
else:
kwargs.setdefault('cancel_url', self.cancel_url)
kwargs.setdefault('creating', self.creating)
kwargs.setdefault('updating', self.updating)
form = AlchemyForm(self.request, fieldset, **kwargs)
if form.creating:
if hasattr(self, 'create_label'):
form.create_label = self.create_label
if self.allow_successive_creates:
form.allow_successive_creates = True
if hasattr(self, 'successive_create_label'):
form.successive_create_label = self.successive_create_label
return form
def form(self, model):
return self.make_form(model)
def crud(self, model, readonly=False):
if readonly:
self.readonly = True
form = self.form(model)
if readonly:
form.readonly = True
if not form.readonly and self.request.POST:
if form.validate():
form.save()
result = self.post_save(form)
if result:
return result
if form.creating:
self.flash_create(form.fieldset.model)
else:
self.flash_update(form.fieldset.model)
if (form.creating and form.allow_successive_creates
and self.request.params.get('create_and_continue')):
return HTTPFound(location=self.request.current_route_url())
return HTTPFound(location=self.post_save_url(form))
self.validation_failed(form)
kwargs = self.template_kwargs(form)
kwargs['form'] = form
return kwargs
def template_kwargs(self, form):
return {}
def post_save(self, form):
pass
def post_save_url(self, form):
return self.home_url
def validation_failed(self, form):
pass
def flash_create(self, model):
self.request.session.flash("%s \"%s\" has been created." %
(self.pretty_name, model))
def flash_delete(self, model):
self.request.session.flash("%s \"%s\" has been deleted." %
(self.pretty_name, model))
def flash_update(self, model):
self.request.session.flash("%s \"%s\" has been updated." %
(self.pretty_name, model))
def create(self):
return self.crud(self.mapped_class)
def get_model(self, key):
model = Session.query(self.mapped_class).get(key)
return model
def read(self):
key = self.request.matchdict['uuid']
model = self.get_model(key)
if not model:
return HTTPFound(location=self.home_url)
return self.crud(model, readonly=True)
def update(self):
uuid = self.request.matchdict['uuid']
model = Session.query(self.mapped_class).get(uuid) if uuid else None
assert model
return self.crud(model)
def pre_delete(self, model):
pass
def post_delete(self, model):
pass
def delete(self):
uuid = self.request.matchdict['uuid']
model = Session.query(self.mapped_class).get(uuid) if uuid else None
assert model
result = self.pre_delete(model)
if result:
return result
Session.delete(model)
Session.flush() # Don't set flash message if delete fails.
self.post_delete(model)
self.flash_delete(model)
return HTTPFound(location=self.home_url)

View file

@ -0,0 +1,119 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
CustomerGroup Views
"""
from . import SearchableAlchemyGridView, CrudView
from .. import Session
from rattail.db.model import CustomerGroup, CustomerGroupAssignment
class CustomerGroupsGrid(SearchableAlchemyGridView):
mapped_class = CustomerGroup
config_prefix = 'customer_groups'
sort = 'name'
def filter_map(self):
return self.make_filter_map(ilike=['name'])
def filter_config(self):
return self.make_filter_config(
include_filter_name=True,
filter_type_name='lk')
def sort_map(self):
return self.make_sort_map('id', 'name')
def grid(self):
g = self.make_grid()
g.configure(
include=[
g.id.label("ID"),
g.name,
],
readonly=True)
if self.request.has_perm('customer_groups.read'):
g.viewable = True
g.view_route_name = 'customer_group.read'
if self.request.has_perm('customer_groups.update'):
g.editable = True
g.edit_route_name = 'customer_group.update'
if self.request.has_perm('customer_groups.delete'):
g.deletable = True
g.delete_route_name = 'customer_group.delete'
return g
class CustomerGroupCrud(CrudView):
mapped_class = CustomerGroup
home_route = 'customer_groups'
pretty_name = "Customer Group"
def fieldset(self, model):
fs = self.make_fieldset(model)
fs.configure(
include=[
fs.id.label("ID"),
fs.name,
])
return fs
def pre_delete(self, group):
# First remove customer associations.
q = Session.query(CustomerGroupAssignment)\
.filter(CustomerGroupAssignment.group == group)
for assignment in q:
Session.delete(assignment)
def add_routes(config):
config.add_route('customer_groups', '/customer-groups')
config.add_route('customer_group.create', '/customer-groups/new')
config.add_route('customer_group.read', '/customer-groups/{uuid}')
config.add_route('customer_group.update', '/customer-groups/{uuid}/edit')
config.add_route('customer_group.delete', '/customer-groups/{uuid}/delete')
def includeme(config):
add_routes(config)
config.add_view(CustomerGroupsGrid, route_name='customer_groups',
renderer='/customergroups/index.mako',
permission='customer_groups.list')
config.add_view(CustomerGroupCrud, attr='create', route_name='customer_group.create',
renderer='/customergroups/crud.mako',
permission='customer_groups.create')
config.add_view(CustomerGroupCrud, attr='read', route_name='customer_group.read',
renderer='/customergroups/crud.mako',
permission='customer_groups.read')
config.add_view(CustomerGroupCrud, attr='update', route_name='customer_group.update',
renderer='/customergroups/crud.mako',
permission='customer_groups.update')
config.add_view(CustomerGroupCrud, attr='delete', route_name='customer_group.delete',
permission='customer_groups.delete')

165
tailbone/views/customers.py Normal file
View file

@ -0,0 +1,165 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Customer Views
"""
from sqlalchemy import and_
from edbob.enum import EMAIL_PREFERENCE
from . import SearchableAlchemyGridView
from ..forms import EnumFieldRenderer
import rattail
from .. import Session
from rattail.db.model import (
Customer, CustomerPerson, CustomerGroupAssignment,
CustomerEmailAddress, CustomerPhoneNumber)
from . import CrudView
class CustomersGrid(SearchableAlchemyGridView):
mapped_class = Customer
config_prefix = 'customers'
sort = 'name'
def join_map(self):
return {
'email':
lambda q: q.outerjoin(CustomerEmailAddress, and_(
CustomerEmailAddress.parent_uuid == Customer.uuid,
CustomerEmailAddress.preference == 1)),
'phone':
lambda q: q.outerjoin(CustomerPhoneNumber, and_(
CustomerPhoneNumber.parent_uuid == Customer.uuid,
CustomerPhoneNumber.preference == 1)),
}
def filter_map(self):
return self.make_filter_map(
exact=['id'],
ilike=['name'],
email=self.filter_ilike(CustomerEmailAddress.address),
phone=self.filter_ilike(CustomerPhoneNumber.number))
def filter_config(self):
return self.make_filter_config(
include_filter_name=True,
filter_type_name='lk',
filter_label_phone="Phone Number",
filter_label_email="Email Address",
filter_label_id="ID")
def sort_map(self):
return self.make_sort_map(
'id', 'name',
email=self.sorter(CustomerEmailAddress.address),
phone=self.sorter(CustomerPhoneNumber.number))
def grid(self):
g = self.make_grid()
g.configure(
include=[
g.id.label("ID"),
g.name,
g.phone.label("Phone Number"),
g.email.label("Email Address"),
],
readonly=True)
if self.request.has_perm('customers.read'):
g.viewable = True
g.view_route_name = 'customer.read'
if self.request.has_perm('customers.update'):
g.editable = True
g.edit_route_name = 'customer.update'
if self.request.has_perm('customers.delete'):
g.deletable = True
g.delete_route_name = 'customer.delete'
return g
class CustomerCrud(CrudView):
mapped_class = Customer
home_route = 'customers'
def get_model(self, key):
model = super(CustomerCrud, self).get_model(key)
if model:
return model
model = Session.query(Customer).filter_by(id=key).first()
if model:
return model
model = Session.query(CustomerPerson).get(key)
if model:
return model.customer
model = Session.query(CustomerGroupAssignment).get(key)
if model:
return model.customer
return None
def fieldset(self, model):
fs = self.make_fieldset(model)
fs.email_preference.set(renderer=EnumFieldRenderer(EMAIL_PREFERENCE))
fs.configure(
include=[
fs.id.label("ID"),
fs.name,
fs.phone.label("Phone Number").readonly(),
fs.email.label("Email Address").readonly(),
fs.email_preference,
])
return fs
def add_routes(config):
config.add_route('customers', '/customers')
config.add_route('customer.create', '/customers/new')
config.add_route('customer.read', '/customers/{uuid}')
config.add_route('customer.update', '/customers/{uuid}/edit')
config.add_route('customer.delete', '/customers/{uuid}/delete')
def includeme(config):
add_routes(config)
config.add_view(CustomersGrid, route_name='customers',
renderer='/customers/index.mako',
permission='customers.list')
config.add_view(CustomerCrud, attr='create', route_name='customer.create',
renderer='/customers/crud.mako',
permission='customers.create')
config.add_view(CustomerCrud, attr='read', route_name='customer.read',
renderer='/customers/read.mako',
permission='customers.read')
config.add_view(CustomerCrud, attr='update', route_name='customer.update',
renderer='/customers/crud.mako',
permission='customers.update')
config.add_view(CustomerCrud, attr='delete', route_name='customer.delete',
permission='customers.delete')

View file

@ -0,0 +1,160 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Department Views
"""
from . import SearchableAlchemyGridView, CrudView, AlchemyGridView, AutocompleteView
from rattail.db.model import Department, Product, ProductCost, Vendor
class DepartmentsGrid(SearchableAlchemyGridView):
mapped_class = Department
config_prefix = 'departments'
sort = 'name'
def filter_map(self):
return self.make_filter_map(ilike=['name'])
def filter_config(self):
return self.make_filter_config(
include_filter_name=True,
filter_type_name='lk')
def sort_map(self):
return self.make_sort_map('number', 'name')
def grid(self):
g = self.make_grid()
g.configure(
include=[
g.number,
g.name,
],
readonly=True)
if self.request.has_perm('departments.read'):
g.viewable = True
g.view_route_name = 'department.read'
if self.request.has_perm('departments.update'):
g.editable = True
g.edit_route_name = 'department.update'
if self.request.has_perm('departments.delete'):
g.deletable = True
g.delete_route_name = 'department.delete'
return g
class DepartmentCrud(CrudView):
mapped_class = Department
home_route = 'departments'
def fieldset(self, model):
fs = self.make_fieldset(model)
fs.configure(
include=[
fs.number,
fs.name,
])
return fs
class DepartmentsByVendorGrid(AlchemyGridView):
mapped_class = Department
config_prefix = 'departments.by_vendor'
checkboxes = True
partial_only = True
def query(self):
q = self.make_query()
q = q.outerjoin(Product)
q = q.join(ProductCost)
q = q.join(Vendor)
q = q.filter(Vendor.uuid == self.request.params['uuid'])
q = q.distinct()
q = q.order_by(Department.name)
return q
def grid(self):
g = self.make_grid()
g.configure(
include=[
g.name,
],
readonly=True)
return g
class DepartmentsAutocomplete(AutocompleteView):
mapped_class = Department
fieldname = 'name'
def includeme(config):
config.add_route('departments', '/departments')
config.add_view(DepartmentsGrid,
route_name='departments',
renderer='/departments/index.mako',
permission='departments.list')
config.add_route('departments.autocomplete', '/departments/autocomplete')
config.add_view(DepartmentsAutocomplete,
route_name='departments.autocomplete',
renderer='json',
permission='departments.list')
config.add_route('departments.by_vendor', '/departments/by-vendor')
config.add_view(DepartmentsByVendorGrid,
route_name='departments.by_vendor',
permission='departments.list')
config.add_route('department.create', '/departments/new')
config.add_view(DepartmentCrud, attr='create',
route_name='department.create',
renderer='/departments/crud.mako',
permission='departments.create')
config.add_route('department.read', '/departments/{uuid}')
config.add_view(DepartmentCrud, attr='read',
route_name='department.read',
renderer='/departments/crud.mako',
permission='departments.read')
config.add_route('department.update', '/departments/{uuid}/edit')
config.add_view(DepartmentCrud, attr='update',
route_name='department.update',
renderer='/departments/crud.mako',
permission='departments.update')
config.add_route('department.delete', '/departments/{uuid}/delete')
config.add_view(DepartmentCrud, attr='delete',
route_name='department.delete',
permission='departments.delete')

178
tailbone/views/employees.py Normal file
View file

@ -0,0 +1,178 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Employee Views
"""
from sqlalchemy import and_
from . import SearchableAlchemyGridView, CrudView
from ..grids.search import EnumSearchFilter
from ..forms import AssociationProxyField, EnumFieldRenderer
from rattail.db.model import (
Employee, EmployeePhoneNumber, EmployeeEmailAddress, Person)
from rattail.enum import EMPLOYEE_STATUS, EMPLOYEE_STATUS_CURRENT
class EmployeesGrid(SearchableAlchemyGridView):
mapped_class = Employee
config_prefix = 'employees'
sort = 'first_name'
def join_map(self):
return {
'phone':
lambda q: q.outerjoin(EmployeePhoneNumber, and_(
EmployeePhoneNumber.parent_uuid == Employee.uuid,
EmployeePhoneNumber.preference == 1)),
'email':
lambda q: q.outerjoin(EmployeeEmailAddress, and_(
EmployeeEmailAddress.parent_uuid == Employee.uuid,
EmployeeEmailAddress.preference == 1)),
}
def filter_map(self):
kwargs = dict(
first_name=self.filter_ilike(Person.first_name),
last_name=self.filter_ilike(Person.last_name),
phone=self.filter_ilike(EmployeePhoneNumber.number),
email=self.filter_ilike(EmployeeEmailAddress.address))
if self.request.has_perm('employees.edit'):
kwargs.update(dict(
exact=['id', 'status']))
return self.make_filter_map(**kwargs)
def filter_config(self):
kwargs = dict(
include_filter_first_name=True,
filter_type_first_name='lk',
include_filter_last_name=True,
filter_type_last_name='lk',
filter_label_phone="Phone Number",
filter_label_email="Email Address")
if self.request.has_perm('employees.edit'):
kwargs.update(dict(
filter_label_id="ID",
include_filter_status=True,
filter_type_status='is',
filter_factory_status=EnumSearchFilter(EMPLOYEE_STATUS),
status=EMPLOYEE_STATUS_CURRENT))
return self.make_filter_config(**kwargs)
def sort_map(self):
return self.make_sort_map(
first_name=self.sorter(Person.first_name),
last_name=self.sorter(Person.last_name),
phone=self.sorter(EmployeePhoneNumber.number),
email=self.sorter(EmployeeEmailAddress.address))
def query(self):
q = self.make_query()
q = q.join(Person)
if not self.request.has_perm('employees.edit'):
q = q.filter(Employee.status == EMPLOYEE_STATUS_CURRENT)
return q
def grid(self):
g = self.make_grid()
g.append(AssociationProxyField('first_name'))
g.append(AssociationProxyField('last_name'))
g.configure(
include=[
g.id.label("ID"),
g.first_name,
g.last_name,
g.phone.label("Phone Number"),
g.email.label("Email Address"),
g.status.with_renderer(EnumFieldRenderer(EMPLOYEE_STATUS)),
],
readonly=True)
# Hide ID and Status fields for unprivileged users.
if not self.request.has_perm('employees.edit'):
del g.id
del g.status
if self.request.has_perm('employees.read'):
g.viewable = True
g.view_route_name = 'employee.read'
if self.request.has_perm('employees.update'):
g.editable = True
g.edit_route_name = 'employee.update'
if self.request.has_perm('employees.delete'):
g.deletable = True
g.delete_route_name = 'employee.delete'
return g
class EmployeeCrud(CrudView):
mapped_class = Employee
home_route = 'employees'
def fieldset(self, model):
fs = self.make_fieldset(model)
fs.append(AssociationProxyField('first_name'))
fs.append(AssociationProxyField('last_name'))
fs.append(AssociationProxyField('display_name'))
fs.configure(
include=[
fs.id.label("ID"),
fs.first_name,
fs.last_name,
fs.phone.label("Phone Number").readonly(),
fs.email.label("Email Address").readonly(),
fs.status.with_renderer(EnumFieldRenderer(EMPLOYEE_STATUS)),
])
return fs
def add_routes(config):
config.add_route('employees', '/employees')
config.add_route('employee.create', '/employees/new')
config.add_route('employee.read', '/employees/{uuid}')
config.add_route('employee.update', '/employees/{uuid}/edit')
config.add_route('employee.delete', '/employees/{uuid}/delete')
def includeme(config):
add_routes(config)
config.add_view(EmployeesGrid, route_name='employees',
renderer='/employees/index.mako',
permission='employees.list')
config.add_view(EmployeeCrud, attr='create', route_name='employee.create',
renderer='/employees/crud.mako',
permission='employees.create')
config.add_view(EmployeeCrud, attr='read', route_name='employee.read',
renderer='/employees/crud.mako',
permission='employees.read')
config.add_view(EmployeeCrud, attr='update', route_name='employee.update',
renderer='/employees/crud.mako',
permission='employees.update')
config.add_view(EmployeeCrud, attr='delete', route_name='employee.delete',
permission='employees.delete')

View file

@ -0,0 +1,30 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Grid Views
"""
from .core import *
from .alchemy import *

View file

@ -0,0 +1,181 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
FormAlchemy Grid Views
"""
from webhelpers import paginate
from .core import GridView
from ... import grids
from ... import Session
__all__ = ['AlchemyGridView', 'SortableAlchemyGridView',
'PagedAlchemyGridView', 'SearchableAlchemyGridView']
class AlchemyGridView(GridView):
def make_query(self, session=Session):
query = session.query(self.mapped_class)
return self.modify_query(query)
def modify_query(self, query):
return query
def query(self):
return self.make_query()
def make_grid(self, **kwargs):
self.update_grid_kwargs(kwargs)
return grids.AlchemyGrid(
self.request, self.mapped_class, self._data, **kwargs)
def grid(self):
return self.make_grid()
def __call__(self):
self._data = self.query()
grid = self.grid()
return grids.util.render_grid(grid)
class SortableAlchemyGridView(AlchemyGridView):
sort = None
@property
def config_prefix(self):
raise NotImplementedError
def join_map(self):
return {}
def make_sort_map(self, *args, **kwargs):
return grids.util.get_sort_map(
self.mapped_class, names=args or None, **kwargs)
def sorter(self, field):
return grids.util.sorter(field)
def sort_map(self):
return self.make_sort_map()
def make_sort_config(self, **kwargs):
return grids.util.get_sort_config(
self.config_prefix, self.request, **kwargs)
def sort_config(self):
return self.make_sort_config(sort=self.sort)
def modify_query(self, query):
return grids.util.sort_query(
query, self._sort_config, self.sort_map(), self.join_map())
def make_grid(self, **kwargs):
self.update_grid_kwargs(kwargs)
return grids.AlchemyGrid(
self.request, self.mapped_class, self._data,
sort_map=self.sort_map(), config=self._sort_config, **kwargs)
def grid(self):
return self.make_grid()
def __call__(self):
self._sort_config = self.sort_config()
self._data = self.query()
grid = self.grid()
return grids.util.render_grid(grid)
class PagedAlchemyGridView(SortableAlchemyGridView):
full = True
def make_pager(self):
config = self._sort_config
query = self.query()
return paginate.Page(
query, item_count=query.count(),
items_per_page=int(config['per_page']),
page=int(config['page']),
url=paginate.PageURL_WebOb(self.request))
def __call__(self):
self._sort_config = self.sort_config()
self._data = self.make_pager()
grid = self.grid()
grid.pager = self._data
return grids.util.render_grid(grid)
class SearchableAlchemyGridView(PagedAlchemyGridView):
def filter_exact(self, field):
return grids.search.filter_exact(field)
def filter_ilike(self, field):
return grids.search.filter_ilike(field)
def make_filter_map(self, **kwargs):
return grids.search.get_filter_map(self.mapped_class, **kwargs)
def filter_map(self):
return self.make_filter_map()
def make_filter_config(self, **kwargs):
return grids.search.get_filter_config(
self.config_prefix, self.request, self.filter_map(), **kwargs)
def filter_config(self):
return self.make_filter_config()
def make_search_form(self):
return grids.search.get_search_form(
self.request, self.filter_map(), self._filter_config)
def search_form(self):
return self.make_search_form()
def modify_query(self, query):
join_map = self.join_map()
query = grids.search.filter_query(
query, self._filter_config, self.filter_map(), join_map)
if hasattr(self, '_sort_config'):
self._sort_config['joins'] = self._filter_config['joins']
query = grids.util.sort_query(
query, self._sort_config, self.sort_map(), join_map)
return query
def __call__(self):
self._filter_config = self.filter_config()
search = self.search_form()
self._sort_config = self.sort_config()
self._data = self.make_pager()
grid = self.grid()
grid.pager = self._data
kwargs = self.render_kwargs()
return grids.util.render_grid(grid, search, **kwargs)

View file

@ -0,0 +1,68 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Core Grid View
"""
from .. import View
from ... import grids
__all__ = ['GridView']
class GridView(View):
route_name = None
route_url = None
renderer = None
permission = None
full = False
checkboxes = False
deletable = False
partial_only = False
def update_grid_kwargs(self, kwargs):
kwargs.setdefault('full', self.full)
kwargs.setdefault('checkboxes', self.checkboxes)
kwargs.setdefault('deletable', self.deletable)
kwargs.setdefault('partial_only', self.partial_only)
def make_grid(self, **kwargs):
self.update_grid_kwargs(kwargs)
return grids.Grid(self.request, **kwargs)
def grid(self):
return self.make_grid()
def render_kwargs(self):
return {}
def __call__(self):
grid = self.grid()
kwargs = self.render_kwargs()
return grids.util.render_grid(grid, **kwargs)

192
tailbone/views/labels.py Normal file
View file

@ -0,0 +1,192 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Label Views
"""
from pyramid.httpexceptions import HTTPFound
import formalchemy
from webhelpers.html import HTML
from .. import Session
from . import SearchableAlchemyGridView, CrudView
from ..grids.search import BooleanSearchFilter
from edbob.pyramid.forms import StrippingFieldRenderer
from rattail.db.model import LabelProfile
class ProfilesGrid(SearchableAlchemyGridView):
mapped_class = LabelProfile
config_prefix = 'label_profiles'
sort = 'ordinal'
def filter_map(self):
return self.make_filter_map(
exact=['code', 'visible'],
ilike=['description'])
def filter_config(self):
return self.make_filter_config(
filter_factory_visible=BooleanSearchFilter)
def sort_map(self):
return self.make_sort_map('ordinal', 'code', 'description', 'visible')
def grid(self):
g = self.make_grid()
g.configure(
include=[
g.ordinal,
g.code,
g.description,
g.visible,
],
readonly=True)
if self.request.has_perm('label_profiles.read'):
g.viewable = True
g.view_route_name = 'label_profile.read'
if self.request.has_perm('label_profiles.update'):
g.editable = True
g.edit_route_name = 'label_profile.update'
if self.request.has_perm('label_profiles.delete'):
g.deletable = True
g.delete_route_name = 'label_profile.delete'
return g
class ProfileCrud(CrudView):
mapped_class = LabelProfile
home_route = 'label_profiles'
pretty_name = "Label Profile"
update_cancel_route = 'label_profile.read'
def fieldset(self, model):
class FormatFieldRenderer(formalchemy.TextAreaFieldRenderer):
def render_readonly(self, **kwargs):
value = self.raw_value
if not value:
return ''
return HTML.tag('pre', c=value)
def render(self, **kwargs):
kwargs.setdefault('size', (80, 8))
return super(FormatFieldRenderer, self).render(**kwargs)
fs = self.make_fieldset(model)
fs.printer_spec.set(renderer=StrippingFieldRenderer)
fs.formatter_spec.set(renderer=StrippingFieldRenderer)
fs.format.set(renderer=FormatFieldRenderer)
fs.configure(
include=[
fs.ordinal,
fs.code,
fs.description,
fs.printer_spec,
fs.formatter_spec,
fs.format,
fs.visible,
])
return fs
def post_save(self, form):
profile = form.fieldset.model
if not profile.format:
formatter = profile.get_formatter()
if formatter:
try:
profile.format = formatter.default_format
except NotImplementedError:
pass
def post_save_url(self, form):
return self.request.route_url('label_profile.read',
uuid=form.fieldset.model.uuid)
def printer_settings(request):
uuid = request.matchdict['uuid']
profile = Session.query(LabelProfile).get(uuid) if uuid else None
if not profile:
return HTTPFound(location=request.route_url('label_profiles'))
read_profile = HTTPFound(location=request.route_url(
'label_profile.read', uuid=profile.uuid))
printer = profile.get_printer()
if not printer:
request.session.flash("Label profile \"%s\" does not have a functional "
"printer spec." % profile)
return read_profile
if not printer.required_settings:
request.session.flash("Printer class for label profile \"%s\" does not "
"require any settings." % profile)
return read_profile
if request.POST:
for setting in printer.required_settings:
if setting in request.POST:
profile.save_printer_setting(setting, request.POST[setting])
return read_profile
return {'profile': profile, 'printer': printer}
def includeme(config):
config.add_route('label_profiles', '/labels/profiles')
config.add_view(ProfilesGrid, route_name='label_profiles',
renderer='/labels/profiles/index.mako',
permission='label_profiles.list')
config.add_route('label_profile.create', '/labels/profiles/new')
config.add_view(ProfileCrud, attr='create', route_name='label_profile.create',
renderer='/labels/profiles/crud.mako',
permission='label_profiles.create')
config.add_route('label_profile.read', '/labels/profiles/{uuid}')
config.add_view(ProfileCrud, attr='read', route_name='label_profile.read',
renderer='/labels/profiles/read.mako',
permission='label_profiles.read')
config.add_route('label_profile.update', '/labels/profiles/{uuid}/edit')
config.add_view(ProfileCrud, attr='update', route_name='label_profile.update',
renderer='/labels/profiles/crud.mako',
permission='label_profiles.update')
config.add_route('label_profile.delete', '/labels/profiles/{uuid}/delete')
config.add_view(ProfileCrud, attr='delete', route_name='label_profile.delete',
permission='label_profiles.delete')
config.add_route('label_profile.printer_settings', '/labels/profiles/{uuid}/printer')
config.add_view(printer_settings, route_name='label_profile.printer_settings',
renderer='/labels/profiles/printer.mako',
permission='label_profiles.update')

156
tailbone/views/people.py Normal file
View file

@ -0,0 +1,156 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Person Views
"""
from sqlalchemy import and_
from . import SearchableAlchemyGridView, CrudView, AutocompleteView
from .. import Session
from rattail.db.model import (Person, PersonEmailAddress, PersonPhoneNumber,
VendorContact)
class PeopleGrid(SearchableAlchemyGridView):
mapped_class = Person
config_prefix = 'people'
sort = 'first_name'
def join_map(self):
return {
'email':
lambda q: q.outerjoin(PersonEmailAddress, and_(
PersonEmailAddress.parent_uuid == Person.uuid,
PersonEmailAddress.preference == 1)),
'phone':
lambda q: q.outerjoin(PersonPhoneNumber, and_(
PersonPhoneNumber.parent_uuid == Person.uuid,
PersonPhoneNumber.preference == 1)),
}
def filter_map(self):
return self.make_filter_map(
ilike=['first_name', 'last_name', 'display_name'],
email=self.filter_ilike(PersonEmailAddress.address),
phone=self.filter_ilike(PersonPhoneNumber.number))
def filter_config(self):
return self.make_filter_config(
include_filter_first_name=True,
filter_type_first_name='lk',
include_filter_last_name=True,
filter_type_last_name='lk',
filter_label_phone="Phone Number",
filter_label_email="Email Address")
def sort_map(self):
return self.make_sort_map(
'first_name', 'last_name', 'display_name',
email=self.sorter(PersonEmailAddress.address),
phone=self.sorter(PersonPhoneNumber.number))
def grid(self):
g = self.make_grid()
g.configure(
include=[
g.first_name,
g.last_name,
g.display_name,
g.phone.label("Phone Number"),
g.email.label("Email Address"),
],
readonly=True)
if self.request.has_perm('people.read'):
g.viewable = True
g.view_route_name = 'person.read'
if self.request.has_perm('people.update'):
g.editable = True
g.edit_route_name = 'person.update'
# if self.request.has_perm('products.delete'):
# g.deletable = True
# g.delete_route_name = 'product.delete'
return g
class PersonCrud(CrudView):
mapped_class = Person
home_route = 'people'
def get_model(self, key):
model = super(PersonCrud, self).get_model(key)
if model:
return model
model = Session.query(VendorContact).get(key)
if model:
return model.person
return None
def fieldset(self, model):
fs = self.make_fieldset(model)
fs.configure(
include=[
fs.first_name,
fs.last_name,
fs.display_name,
fs.phone.label("Phone Number").readonly(),
fs.email.label("Email Address").readonly(),
])
return fs
class PeopleAutocomplete(AutocompleteView):
mapped_class = Person
fieldname = 'display_name'
def add_routes(config):
config.add_route('people', '/people')
config.add_route('people.autocomplete', '/people/autocomplete')
config.add_route('person.read', '/people/{uuid}')
config.add_route('person.update', '/people/{uuid}/edit')
def includeme(config):
add_routes(config)
config.add_view(PeopleGrid, route_name='people',
renderer='/people/index.mako',
permission='people.list')
config.add_view(PersonCrud, attr='read', route_name='person.read',
renderer='/people/crud.mako',
permission='people.read')
config.add_view(PersonCrud, attr='update', route_name='person.update',
renderer='/people/crud.mako',
permission='people.update')
config.add_view(PeopleAutocomplete, route_name='people.autocomplete',
renderer='json',
permission='people.list')

368
tailbone/views/products.py Normal file
View file

@ -0,0 +1,368 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Product Views
"""
from sqlalchemy import and_
from sqlalchemy.orm import joinedload
from webhelpers.html.tags import link_to
from pyramid.httpexceptions import HTTPFound
from pyramid.renderers import render_to_response
import edbob
from edbob.pyramid.progress import SessionProgress
from . import SearchableAlchemyGridView
import rattail.labels
from rattail import sil
from rattail import batches
from rattail.threads import Thread
from rattail.exceptions import LabelPrintingError
from rattail.db.model import (
Product, ProductPrice, ProductCost, ProductCode,
Brand, Vendor, Department, Subdepartment, LabelProfile)
from rattail.gpc import GPC
from .. import Session
from ..forms import AutocompleteFieldRenderer, GPCFieldRenderer, PriceFieldRenderer
from . import CrudView
class ProductsGrid(SearchableAlchemyGridView):
mapped_class = Product
config_prefix = 'products'
sort = 'description'
def join_map(self):
def join_vendor(q):
q = q.outerjoin(
ProductCost,
and_(
ProductCost.product_uuid == Product.uuid,
ProductCost.preference == 1,
))
q = q.outerjoin(Vendor)
return q
return {
'brand':
lambda q: q.outerjoin(Brand),
'department':
lambda q: q.outerjoin(Department,
Department.uuid == Product.department_uuid),
'subdepartment':
lambda q: q.outerjoin(Subdepartment,
Subdepartment.uuid == Product.subdepartment_uuid),
'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,
'code':
lambda q: q.outerjoin(ProductCode),
}
def filter_map(self):
def filter_upc():
def filter_is(q, v):
if not v:
return q
try:
return q.filter(Product.upc.in_((
GPC(v), GPC(v, calc_check_digit='upc'))))
except ValueError:
return q
def filter_not(q, v):
if not v:
return q
try:
return q.filter(~Product.upc.in_((
GPC(v), GPC(v, calc_check_digit='upc'))))
except ValueError:
return q
return {'is': filter_is, 'nt': filter_not}
return self.make_filter_map(
ilike=['description', 'size'],
upc=filter_upc(),
brand=self.filter_ilike(Brand.name),
department=self.filter_ilike(Department.name),
subdepartment=self.filter_ilike(Subdepartment.name),
vendor=self.filter_ilike(Vendor.name),
code=self.filter_ilike(ProductCode.code))
def filter_config(self):
return self.make_filter_config(
include_filter_upc=True,
filter_type_upc='eq',
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',
include_filter_vendor=True,
filter_type_vendor='lk')
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))
def query(self):
q = self.make_query()
q = q.options(joinedload(Product.brand))
q = q.options(joinedload(Product.department))
q = q.options(joinedload(Product.subdepartment))
q = q.options(joinedload(Product.regular_price))
q = q.options(joinedload(Product.current_price))
q = q.options(joinedload(Product.vendor))
return q
def grid(self):
g = self.make_grid()
g.upc.set(renderer=GPCFieldRenderer)
g.regular_price.set(renderer=PriceFieldRenderer)
g.current_price.set(renderer=PriceFieldRenderer)
g.configure(
include=[
g.upc.label("UPC"),
g.brand,
g.description,
g.size,
g.subdepartment,
g.vendor,
g.regular_price.label("Reg. Price"),
g.current_price.label("Cur. Price"),
],
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'
q = Session.query(LabelProfile)
if q.count():
def labels(row):
return link_to("Print", '#', class_='print-label')
g.add_column('labels', "Labels", labels)
return g
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()}
class ProductCrud(CrudView):
mapped_class = Product
home_route = 'products'
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 fieldset(self, model):
fs = self.make_fieldset(model)
fs.upc.set(renderer=GPCFieldRenderer)
fs.brand.set(renderer=AutocompleteFieldRenderer(
self.request.route_url('brands.autocomplete')))
fs.regular_price.set(renderer=PriceFieldRenderer)
fs.current_price.set(renderer=PriceFieldRenderer)
fs.configure(
include=[
fs.upc.label("UPC"),
fs.brand,
fs.description,
fs.size,
fs.department,
fs.subdepartment,
fs.regular_price,
fs.current_price,
])
if not self.readonly:
del fs.regular_price
del fs.current_price
return fs
def print_labels(request):
profile = request.params.get('profile')
profile = Session.query(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
if not product:
return {'error': "Product not found"}
quantity = request.params.get('quantity')
if not quantity.isdigit():
return {'error': "Quantity must be numeric"}
quantity = int(quantity)
printer = profile.get_printer()
if not printer:
return {'error': "Couldn't get printer from label profile"}
try:
printer.print_labels([(product, quantity)])
except Exception, error:
return {'error': str(error)}
return {}
class CreateProductsBatch(ProductsGrid):
def make_batch(self, provider, progress):
from rattail.db import Session
session = 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(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.session, '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 = edbob.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.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_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')

197
tailbone/views/reports.py Normal file
View file

@ -0,0 +1,197 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Report Views
"""
from .core import View
from mako.template import Template
from pyramid.response import Response
from .. import Session
from rattail.db.model import Vendor, Department, Product, ProductCost
import re
import rattail
from edbob.time import local_time
from rattail.files import resource_path
plu_upc_pattern = re.compile(r'^000000000(\d{5})$')
weighted_upc_pattern = re.compile(r'^002(\d{5})00000\d$')
def get_upc(product):
upc = '%014u' % product.upc
m = plu_upc_pattern.match(upc)
if m:
return str(int(m.group(1)))
m = weighted_upc_pattern.match(upc)
if m:
return str(int(m.group(1)))
return upc
class OrderingWorksheet(View):
"""
This is the "Ordering Worksheet" report.
"""
report_template_path = 'tailbone:reports/ordering_worksheet.mako'
upc_getter = staticmethod(get_upc)
def __call__(self):
if self.request.params.get('vendor'):
vendor = Session.query(Vendor).get(self.request.params['vendor'])
if vendor:
departments = []
uuids = self.request.params.get('departments')
if uuids:
for uuid in uuids.split(','):
dept = Session.query(Department).get(uuid)
if dept:
departments.append(dept)
preferred_only = self.request.params.get('preferred_only') == '1'
body = self.write_report(vendor, departments, preferred_only)
response = Response(content_type='text/html')
response.headers['Content-Length'] = len(body)
response.headers['Content-Disposition'] = 'attachment; filename=ordering.html'
response.text = body
return response
return {}
def write_report(self, vendor, departments, preferred_only):
"""
Rendering engine for the ordering worksheet report.
"""
q = Session.query(ProductCost)
q = q.join(Product)
q = q.filter(ProductCost.vendor == vendor)
q = q.filter(Product.department_uuid.in_([x.uuid for x in departments]))
if preferred_only:
q = q.filter(ProductCost.preference == 1)
costs = {}
for cost in q:
dept = cost.product.department
subdept = cost.product.subdepartment
costs.setdefault(dept, {})
costs[dept].setdefault(subdept, [])
costs[dept][subdept].append(cost)
def cost_sort_key(cost):
product = cost.product
brand = product.brand.name if product.brand else ''
key = '{0} {1}'.format(brand, product.description)
return key
now = local_time()
data = dict(
vendor=vendor,
costs=costs,
cost_sort_key=cost_sort_key,
date=now.strftime('%a %d %b %Y'),
time=now.strftime('%I:%M %p'),
get_upc=self.upc_getter,
rattail=rattail,
)
template_path = resource_path(self.report_template_path)
template = Template(filename=template_path)
return template.render(**data)
class InventoryWorksheet(View):
"""
This is the "Inventory Worksheet" report.
"""
report_template_path = 'tailbone:reports/inventory_worksheet.mako'
upc_getter = staticmethod(get_upc)
def __call__(self):
"""
This is the "Inventory Worksheet" report.
"""
departments = Session.query(Department)
if self.request.params.get('department'):
department = departments.get(self.request.params['department'])
if department:
body = self.write_report(department)
response = Response(content_type='text/html')
response.headers['Content-Length'] = len(body)
response.headers['Content-Disposition'] = 'attachment; filename=inventory.html'
response.text = body
return response
departments = departments.order_by(rattail.Department.name)
departments = departments.all()
return{'departments': departments}
def write_report(self, department):
"""
Generates the Inventory Worksheet report.
"""
def get_products(subdepartment):
q = Session.query(rattail.Product)
q = q.outerjoin(rattail.Brand)
q = q.filter(rattail.Product.subdepartment == subdepartment)
if self.request.params.get('weighted-only'):
q = q.filter(rattail.Product.unit_of_measure == rattail.UNIT_OF_MEASURE_POUND)
q = q.order_by(rattail.Brand.name, rattail.Product.description)
return q.all()
now = local_time()
data = dict(
date=now.strftime('%a %d %b %Y'),
time=now.strftime('%I:%M %p'),
department=department,
get_products=get_products,
get_upc=self.upc_getter,
)
template_path = resource_path(self.report_template_path)
template = Template(filename=template_path)
return template.render(**data)
def add_routes(config):
config.add_route('reports.ordering', '/reports/ordering')
config.add_route('reports.inventory', '/reports/inventory')
def includeme(config):
add_routes(config)
config.add_view(OrderingWorksheet, route_name='reports.ordering',
renderer='/reports/ordering.mako')
config.add_view(InventoryWorksheet, route_name='reports.inventory',
renderer='/reports/inventory.mako')

218
tailbone/views/roles.py Normal file
View file

@ -0,0 +1,218 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Role Views
"""
from pyramid.httpexceptions import HTTPFound
import formalchemy
from webhelpers.html import tags
from webhelpers.html.builder import HTML
from edbob.db import auth
from .. import Session
from . import SearchableAlchemyGridView, CrudView
from rattail.db.model import Role
default_permissions = [
("People", [
('people.list', "List People"),
('people.read', "View Person"),
('people.create', "Create Person"),
('people.update', "Edit Person"),
('people.delete', "Delete Person"),
]),
("Roles", [
('roles.list', "List Roles"),
('roles.read', "View Role"),
('roles.create', "Create Role"),
('roles.update', "Edit Role"),
('roles.delete', "Delete Role"),
]),
("Users", [
('users.list', "List Users"),
('users.read', "View User"),
('users.create', "Create User"),
('users.update', "Edit User"),
('users.delete', "Delete User"),
]),
]
class RolesGrid(SearchableAlchemyGridView):
mapped_class = Role
config_prefix = 'roles'
sort = 'name'
def filter_map(self):
return self.make_filter_map(ilike=['name'])
def filter_config(self):
return self.make_filter_config(
include_filter_name=True,
filter_type_name='lk')
def sort_map(self):
return self.make_sort_map('name')
def grid(self):
g = self.make_grid()
g.configure(
include=[
g.name,
],
readonly=True)
if self.request.has_perm('roles.read'):
g.viewable = True
g.view_route_name = 'role.read'
if self.request.has_perm('roles.update'):
g.editable = True
g.edit_route_name = 'role.update'
if self.request.has_perm('roles.delete'):
g.deletable = True
g.delete_route_name = 'role.delete'
return g
class PermissionsField(formalchemy.Field):
def sync(self):
if not self.is_readonly():
role = self.model
role.permissions = self.renderer.deserialize()
def PermissionsFieldRenderer(permissions, *args, **kwargs):
perms = permissions
class PermissionsFieldRenderer(formalchemy.FieldRenderer):
permissions = perms
def deserialize(self):
perms = []
i = len(self.name) + 1
for key in self.params:
if key.startswith(self.name):
perms.append(key[i:])
return perms
def _render(self, readonly=False, **kwargs):
role = self.field.model
admin = auth.administrator_role(Session())
if role is admin:
html = HTML.tag('p', c="This is the administrative role; "
"it has full access to the entire system.")
if not readonly:
html += tags.hidden(self.name, value='') # ugly hack..or good idea?
else:
html = ''
for group, perms in self.permissions:
inner = HTML.tag('p', c=group)
for perm, title in perms:
checked = auth.has_permission(
role, perm, include_guest=False, session=Session())
if readonly:
span = HTML.tag('span', c="[X]" if checked else "[ ]")
inner += HTML.tag('p', class_='perm', c=span + ' ' + title)
else:
inner += tags.checkbox(self.name + '-' + perm,
checked=checked, label=title)
html += HTML.tag('div', class_='group', c=inner)
return html
def render(self, **kwargs):
return self._render(**kwargs)
def render_readonly(self, **kwargs):
return self._render(readonly=True, **kwargs)
return PermissionsFieldRenderer
class RoleCrud(CrudView):
mapped_class = Role
home_route = 'roles'
permissions = default_permissions
def fieldset(self, role):
fs = self.make_fieldset(role)
fs.append(PermissionsField(
'permissions',
renderer=PermissionsFieldRenderer(self.permissions)))
fs.configure(
include=[
fs.name,
fs.permissions,
])
return fs
def pre_delete(self, model):
admin = auth.administrator_role(Session())
guest = auth.guest_role(Session())
if model in (admin, guest):
self.request.session.flash("You may not delete the %s role." % str(model), 'error')
return HTTPFound(location=self.request.get_referrer())
def includeme(config):
config.add_route('roles', '/roles')
config.add_view(RolesGrid, route_name='roles',
renderer='/roles/index.mako',
permission='roles.list')
settings = config.get_settings()
perms = settings.get('edbob.permissions')
if perms:
RoleCrud.permissions = perms
config.add_route('role.create', '/roles/new')
config.add_view(RoleCrud, attr='create', route_name='role.create',
renderer='/roles/crud.mako',
permission='roles.create')
config.add_route('role.read', '/roles/{uuid}')
config.add_view(RoleCrud, attr='read', route_name='role.read',
renderer='/roles/crud.mako',
permission='roles.read')
config.add_route('role.update', '/roles/{uuid}/edit')
config.add_view(RoleCrud, attr='update', route_name='role.update',
renderer='/roles/crud.mako',
permission='roles.update')
config.add_route('role.delete', '/roles/{uuid}/delete')
config.add_view(RoleCrud, attr='delete', route_name='role.delete',
permission='roles.delete')

134
tailbone/views/stores.py Normal file
View file

@ -0,0 +1,134 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Store Views
"""
from sqlalchemy import and_
from . import SearchableAlchemyGridView, CrudView
from rattail.db.model import Store, StoreEmailAddress, StorePhoneNumber
class StoresGrid(SearchableAlchemyGridView):
mapped_class = Store
config_prefix = 'stores'
sort = 'id'
def join_map(self):
return {
'email':
lambda q: q.outerjoin(StoreEmailAddress, and_(
StoreEmailAddress.parent_uuid == Store.uuid,
StoreEmailAddress.preference == 1)),
'phone':
lambda q: q.outerjoin(StorePhoneNumber, and_(
StorePhoneNumber.parent_uuid == Store.uuid,
StorePhoneNumber.preference == 1)),
}
def filter_map(self):
return self.make_filter_map(
exact=['id'],
ilike=['name'],
email=self.filter_ilike(StoreEmailAddress.address),
phone=self.filter_ilike(StorePhoneNumber.number))
def filter_config(self):
return self.make_filter_config(
include_filter_name=True,
filter_type_name='lk',
filter_label_id="ID")
def sort_map(self):
return self.make_sort_map(
'id', 'name',
email=self.sorter(StoreEmailAddress.address),
phone=self.sorter(StorePhoneNumber.number))
def grid(self):
g = self.make_grid()
g.configure(
include=[
g.id.label("ID"),
g.name,
g.phone.label("Phone Number"),
g.email.label("Email Address"),
],
readonly=True)
g.viewable = True
g.view_route_name = 'store.read'
if self.request.has_perm('stores.update'):
g.editable = True
g.edit_route_name = 'store.update'
if self.request.has_perm('stores.delete'):
g.deletable = True
g.delete_route_name = 'store.delete'
return g
class StoreCrud(CrudView):
mapped_class = Store
home_route = 'stores'
def fieldset(self, model):
fs = self.make_fieldset(model)
fs.configure(
include=[
fs.id.label("ID"),
fs.name,
fs.phone.label("Phone Number").readonly(),
fs.email.label("Email Address").readonly(),
])
return fs
def includeme(config):
config.add_route('stores', '/stores')
config.add_view(StoresGrid, route_name='stores',
renderer='/stores/index.mako',
permission='stores.list')
config.add_route('store.create', '/stores/new')
config.add_view(StoreCrud, attr='create', route_name='store.create',
renderer='/stores/crud.mako',
permission='stores.create')
config.add_route('store.read', '/stores/{uuid}')
config.add_view(StoreCrud, attr='read', route_name='store.read',
renderer='/stores/crud.mako',
permission='stores.read')
config.add_route('store.update', '/stores/{uuid}/edit')
config.add_view(StoreCrud, attr='update', route_name='store.update',
renderer='/stores/crud.mako',
permission='stores.update')
config.add_route('store.delete', '/stores/{uuid}/delete')
config.add_view(StoreCrud, attr='delete', route_name='store.delete',
permission='stores.delete')

View file

@ -0,0 +1,116 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Subdepartment Views
"""
from . import SearchableAlchemyGridView, CrudView
from rattail.db.model import Subdepartment
class SubdepartmentsGrid(SearchableAlchemyGridView):
mapped_class = Subdepartment
config_prefix = 'subdepartments'
sort = 'name'
def filter_map(self):
return self.make_filter_map(ilike=['name'])
def filter_config(self):
return self.make_filter_config(
include_filter_name=True,
filter_type_name='lk')
def sort_map(self):
return self.make_sort_map('number', 'name')
def grid(self):
g = self.make_grid()
g.configure(
include=[
g.number,
g.name,
g.department,
],
readonly=True)
if self.request.has_perm('subdepartments.read'):
g.viewable = True
g.view_route_name = 'subdepartment.read'
if self.request.has_perm('subdepartments.update'):
g.editable = True
g.edit_route_name = 'subdepartment.update'
if self.request.has_perm('subdepartments.delete'):
g.deletable = True
g.delete_route_name = 'subdepartment.delete'
return g
class SubdepartmentCrud(CrudView):
mapped_class = Subdepartment
home_route = 'subdepartments'
def fieldset(self, model):
fs = self.make_fieldset(model)
fs.configure(
include=[
fs.number,
fs.name,
fs.department,
])
return fs
def includeme(config):
config.add_route('subdepartments', '/subdepartments')
config.add_view(SubdepartmentsGrid, route_name='subdepartments',
renderer='/subdepartments/index.mako',
permission='subdepartments.list')
config.add_route('subdepartment.create', '/subdepartments/new')
config.add_view(SubdepartmentCrud, attr='create',
route_name='subdepartment.create',
renderer='/subdepartments/crud.mako',
permission='subdepartments.create')
config.add_route('subdepartment.read', '/subdepartments/{uuid}')
config.add_view(SubdepartmentCrud, attr='read',
route_name='subdepartment.read',
renderer='/subdepartments/crud.mako',
permission='subdepartments.read')
config.add_route('subdepartment.update', '/subdepartments/{uuid}/edit')
config.add_view(SubdepartmentCrud, attr='update',
route_name='subdepartment.update',
renderer='/subdepartments/crud.mako',
permission='subdepartments.update')
config.add_route('subdepartment.delete', '/subdepartments/{uuid}/delete')
config.add_view(SubdepartmentCrud, attr='delete',
route_name='subdepartment.delete',
permission='subdepartments.delete')

146
tailbone/views/users.py Normal file
View file

@ -0,0 +1,146 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
User Views
"""
import formalchemy
from edbob.pyramid.views import users
from . import SearchableAlchemyGridView, CrudView
from ..forms import PersonFieldRenderer
from rattail.db.model import User, Person
class UsersGrid(SearchableAlchemyGridView):
mapped_class = User
config_prefix = 'users'
sort = 'username'
def join_map(self):
return {
'person':
lambda q: q.outerjoin(Person),
}
def filter_map(self):
return self.make_filter_map(
ilike=['username'],
person=self.filter_ilike(Person.display_name))
def filter_config(self):
return self.make_filter_config(
include_filter_username=True,
filter_type_username='lk',
include_filter_person=True,
filter_type_person='lk')
def sort_map(self):
return self.make_sort_map(
'username',
person=self.sorter(Person.display_name))
def grid(self):
g = self.make_grid()
g.configure(
include=[
g.username,
g.person,
],
readonly=True)
if self.request.has_perm('users.read'):
g.viewable = True
g.view_route_name = 'user.read'
if self.request.has_perm('users.update'):
g.editable = True
g.edit_route_name = 'user.update'
if self.request.has_perm('users.delete'):
g.deletable = True
g.delete_route_name = 'user.delete'
return g
class UserCrud(CrudView):
mapped_class = User
home_route = 'users'
def fieldset(self, user):
fs = self.make_fieldset(user)
# Must set Person options to empty set to avoid unwanted magic.
fs.person.set(options=[])
fs.person.set(renderer=PersonFieldRenderer(
self.request.route_url('people.autocomplete')))
fs.append(users.PasswordField('password'))
fs.append(formalchemy.Field(
'confirm_password', renderer=users.PasswordFieldRenderer))
fs.append(users.RolesField(
'roles', renderer=users.RolesFieldRenderer(self.request)))
fs.configure(
include=[
fs.username,
fs.person,
fs.password.label("Set Password"),
fs.confirm_password,
fs.roles,
])
if self.readonly:
del fs.password
del fs.confirm_password
return fs
def includeme(config):
config.add_route('users', '/users')
config.add_view(UsersGrid, route_name='users',
renderer='/users/index.mako',
permission='users.list')
config.add_route('user.create', '/users/new')
config.add_view(UserCrud, attr='create', route_name='user.create',
renderer='/users/crud.mako',
permission='users.create')
config.add_route('user.read', '/users/{uuid}')
config.add_view(UserCrud, attr='read', route_name='user.read',
renderer='/users/crud.mako',
permission='users.read')
config.add_route('user.update', '/users/{uuid}/edit')
config.add_view(UserCrud, attr='update', route_name='user.update',
renderer='/users/crud.mako',
permission='users.update')
config.add_route('user.delete', '/users/{uuid}/delete')
config.add_view(UserCrud, attr='delete', route_name='user.delete',
permission='users.delete')

131
tailbone/views/vendors.py Normal file
View file

@ -0,0 +1,131 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Vendor Views
"""
from . import SearchableAlchemyGridView, CrudView, AutocompleteView
from ..forms import AssociationProxyField, PersonFieldRenderer
from rattail.db.model import Vendor
class VendorsGrid(SearchableAlchemyGridView):
mapped_class = Vendor
config_prefix = 'vendors'
sort = 'name'
def filter_map(self):
return self.make_filter_map(exact=['id'], ilike=['name'])
def filter_config(self):
return self.make_filter_config(
include_filter_name=True,
filter_type_name='lk',
filter_label_id="ID")
def sort_map(self):
return self.make_sort_map('id', 'name')
def grid(self):
g = self.make_grid()
g.append(AssociationProxyField('contact'))
g.configure(
include=[
g.id.label("ID"),
g.name,
g.phone.label("Phone Number"),
g.email.label("Email Address"),
g.contact,
],
readonly=True)
if self.request.has_perm('vendors.read'):
g.viewable = True
g.view_route_name = 'vendor.read'
if self.request.has_perm('vendors.update'):
g.editable = True
g.edit_route_name = 'vendor.update'
if self.request.has_perm('vendors.delete'):
g.deletable = True
g.delete_route_name = 'vendor.delete'
return g
class VendorCrud(CrudView):
mapped_class = Vendor
home_route = 'vendors'
def fieldset(self, model):
fs = self.make_fieldset(model)
fs.append(AssociationProxyField('contact'))
fs.contact.set(renderer=PersonFieldRenderer(
self.request.route_url('people.autocomplete')))
fs.configure(
include=[
fs.id.label("ID"),
fs.name,
fs.special_discount,
fs.phone.label("Phone Number").readonly(),
fs.email.label("Email Address").readonly(),
fs.contact.readonly(),
])
return fs
class VendorsAutocomplete(AutocompleteView):
mapped_class = Vendor
fieldname = 'name'
def add_routes(config):
config.add_route('vendors', '/vendors')
config.add_route('vendors.autocomplete', '/vendors/autocomplete')
config.add_route('vendor.create', '/vendors/new')
config.add_route('vendor.read', '/vendors/{uuid}')
config.add_route('vendor.update', '/vendors/{uuid}/edit')
config.add_route('vendor.delete', '/vendors/{uuid}/delete')
def includeme(config):
add_routes(config)
config.add_view(VendorsGrid, route_name='vendors',
renderer='/vendors/index.mako',
permission='vendors.list')
config.add_view(VendorsAutocomplete, route_name='vendors.autocomplete',
renderer='json', permission='vendors.list')
config.add_view(VendorCrud, attr='create', route_name='vendor.create',
renderer='/vendors/crud.mako',
permission='vendors.create')
config.add_view(VendorCrud, attr='read', route_name='vendor.read',
renderer='/vendors/crud.mako',
permission='vendors.read')
config.add_view(VendorCrud, attr='update', route_name='vendor.update',
renderer='/vendors/crud.mako',
permission='vendors.update')
config.add_view(VendorCrud, attr='delete', route_name='vendor.delete',
permission='vendors.delete')