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

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