diff --git a/rattail/pyramid/templates/categories/base.mako b/rattail/pyramid/templates/categories/base.mako
new file mode 100644
index 00000000..27f7dd90
--- /dev/null
+++ b/rattail/pyramid/templates/categories/base.mako
@@ -0,0 +1,2 @@
+<%inherit file="/base.mako" />
+${parent.body()}
diff --git a/rattail/pyramid/templates/categories/category.mako b/rattail/pyramid/templates/categories/category.mako
new file mode 100644
index 00000000..2fd4d52d
--- /dev/null
+++ b/rattail/pyramid/templates/categories/category.mako
@@ -0,0 +1,10 @@
+<%inherit file="/categories/base.mako" />
+<%inherit file="/crud.mako" />
+
+<%def name="crud_name()">Category%def>
+
+<%def name="menu()">
+
${h.link_to("Back to Categories", url('categories.list'))}
+%def>
+
+${parent.body()}
diff --git a/rattail/pyramid/templates/categories/index.mako b/rattail/pyramid/templates/categories/index.mako
new file mode 100644
index 00000000..051f2853
--- /dev/null
+++ b/rattail/pyramid/templates/categories/index.mako
@@ -0,0 +1,12 @@
+<%inherit file="/categories/base.mako" />
+<%inherit file="/index.mako" />
+
+<%def name="title()">Categories%def>
+
+<%def name="menu()">
+ % if request.has_perm('categories.create'):
+ ${h.link_to("Create a new Category", url('category.new'))}
+ % endif
+%def>
+
+${parent.body()}
diff --git a/rattail/pyramid/views/__init__.py b/rattail/pyramid/views/__init__.py
index 220539ea..1b36d5cb 100644
--- a/rattail/pyramid/views/__init__.py
+++ b/rattail/pyramid/views/__init__.py
@@ -30,4 +30,5 @@
def includeme(config):
config.include('rattail.pyramid.views.batches')
config.include('rattail.pyramid.views.departments')
+ config.include('rattail.pyramid.views.categories')
config.include('rattail.pyramid.views.products')
diff --git a/rattail/pyramid/views/categories.py b/rattail/pyramid/views/categories.py
new file mode 100644
index 00000000..94380fbd
--- /dev/null
+++ b/rattail/pyramid/views/categories.py
@@ -0,0 +1,182 @@
+#!/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 .
+#
+################################################################################
+
+"""
+``rattail.pyramid.views.categories`` -- Category Views
+"""
+
+import transaction
+from pyramid.httpexceptions import HTTPFound
+
+from edbob.pyramid import filters
+from edbob.pyramid import forms
+from edbob.pyramid import grids
+from edbob.pyramid import Session
+
+import rattail
+
+
+def filter_map():
+ return filters.get_filter_map(
+ rattail.Category,
+ exact=['number'],
+ ilike=['name'])
+
+def search_config(request, fmap):
+ return filters.get_search_config(
+ 'categories.list', request, fmap,
+ include_filter_name=True,
+ filter_type_name='lk')
+
+def search_form(config):
+ return filters.get_search_form(config)
+
+def grid_config(request, search, fmap):
+ return grids.get_grid_config(
+ 'categories.list', request, search,
+ filter_map=fmap, sort='name',
+ deletable=True)
+
+def sort_map():
+ return grids.get_sort_map(
+ rattail.Category,
+ ['number', 'name'])
+
+def query(config):
+ smap = sort_map()
+ q = Session.query(rattail.Category)
+ q = filters.filter_query(q, config)
+ q = grids.sort_query(q, config, smap)
+ return q
+
+
+def categories(request):
+
+ fmap = filter_map()
+ config = search_config(request, fmap)
+ search = search_form(config)
+ config = grid_config(request, search, fmap)
+ categories = grids.get_pager(query, config)
+
+ g = forms.AlchemyGrid(
+ rattail.Category, categories, config,
+ gridurl=request.route_url('categories.list'),
+ objurl='category.edit', delurl='category.delete')
+
+ g.configure(
+ include=[
+ g.number,
+ g.name,
+ ],
+ readonly=True)
+
+ grid = g.render(class_='clickable categories')
+ return grids.render_grid(request, grid, search)
+
+
+def category_fieldset(category, request):
+ fs = forms.make_fieldset(category, url=request.route_url,
+ url_action=request.current_route_url(),
+ route_name='categories.list')
+ fs.configure(
+ include=[
+ fs.number,
+ fs.name,
+ ])
+ return fs
+
+
+def new_category(request):
+
+ fs = category_fieldset(rattail.Category, request)
+ if not fs.readonly and request.POST:
+ fs.rebind(data=request.params)
+ if fs.validate():
+
+ with transaction.manager:
+ fs.sync()
+ Session.add(fs.model)
+ Session.flush()
+ request.session.flash("%s \"%s\" has been %s." % (
+ fs.crud_title, fs.get_display_text(),
+ 'updated' if fs.edit else 'created'))
+
+ return HTTPFound(location=request.route_url('categories.list'))
+
+ return {'fieldset': fs, 'crud': True}
+
+
+def edit_category(request):
+ """
+ View for editing a :class:`rattail.Category` instance.
+ """
+
+ uuid = request.matchdict['uuid']
+ category = Session.query(rattail.Category).get(uuid) if uuid else None
+ assert category
+
+ fs = category_fieldset(category, request)
+ if request.POST:
+ fs.rebind(data=request.params)
+ if fs.validate():
+
+ with transaction.manager:
+ fs.sync()
+ fs.model = Session.merge(fs.model)
+ request.session.flash("%s \"%s\" has been %s." % (
+ fs.crud_title, fs.get_display_text(),
+ 'updated' if fs.edit else 'created'))
+ home = request.route_url('categories.list')
+
+ return HTTPFound(location=home)
+
+ return {'fieldset': fs, 'crud': True}
+
+
+def delete_category(request):
+ uuid = request.matchdict['uuid']
+ category = Session.query(rattail.Category).get(uuid) if uuid else None
+ assert category
+ with transaction.manager:
+ Session.delete(category)
+ return HTTPFound(location=request.route_url('categories.list'))
+
+
+def includeme(config):
+
+ config.add_route('categories.list', '/categories')
+ config.add_view(categories, route_name='categories.list', renderer='/categories/index.mako',
+ permission='categories.list', http_cache=0)
+
+ config.add_route('category.new', '/categories/new')
+ config.add_view(new_category, route_name='category.new', renderer='/categories/category.mako',
+ permission='categories.create', http_cache=0)
+
+ config.add_route('category.edit', '/categories/{uuid}/edit')
+ config.add_view(edit_category, route_name='category.edit', renderer='/categories/category.mako',
+ permission='category.edit', http_cache=0)
+
+ config.add_route('category.delete', '/categories/{uuid}/delete')
+ config.add_view(delete_category, route_name='category.delete',
+ permission='category.delete', http_cache=0)