Rebranded to Tailbone.
This commit is contained in:
parent
47944767dc
commit
40efd8a3bc
111 changed files with 188 additions and 209 deletions
tailbone
__init__.py_version.py
forms
grids
helpers.pyreports
static
subscribers.pytemplates
autocomplete.mako
batches
brands
categories
crud.makocustomergroups
customers
departments
employees
forms
grids
labels/profiles
people
products
reports
stores
subdepartments
users
vendors
views
37
tailbone/__init__.py
Normal file
37
tailbone/__init__.py
Normal file
|
@ -0,0 +1,37 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2012 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
# Rattail is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
Rattail's Pyramid Framework
|
||||
"""
|
||||
|
||||
from ._version import __version__
|
||||
|
||||
from edbob.pyramid import Session
|
||||
|
||||
|
||||
def includeme(config):
|
||||
config.include('tailbone.static')
|
||||
config.include('tailbone.subscribers')
|
||||
config.include('tailbone.views')
|
1
tailbone/_version.py
Normal file
1
tailbone/_version.py
Normal file
|
@ -0,0 +1 @@
|
|||
__version__ = '0.3.1'
|
31
tailbone/forms/__init__.py
Normal file
31
tailbone/forms/__init__.py
Normal file
|
@ -0,0 +1,31 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2012 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
# Rattail is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
Forms
|
||||
"""
|
||||
|
||||
from .simpleform import *
|
||||
from .fields import *
|
||||
from .renderers import *
|
54
tailbone/forms/fields.py
Normal file
54
tailbone/forms/fields.py
Normal file
|
@ -0,0 +1,54 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2012 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
# Rattail is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
``tailbone.forms.fields`` -- FormAlchemy Fields
|
||||
"""
|
||||
|
||||
from formalchemy import Field
|
||||
|
||||
|
||||
__all__ = ['AssociationProxyField']
|
||||
|
||||
|
||||
def AssociationProxyField(name, **kwargs):
|
||||
"""
|
||||
Returns a ``Field`` class which is aware of SQLAlchemy association proxies.
|
||||
"""
|
||||
|
||||
class ProxyField(Field):
|
||||
|
||||
def sync(self):
|
||||
if not self.is_readonly():
|
||||
setattr(self.parent.model, self.name,
|
||||
self.renderer.deserialize())
|
||||
|
||||
def value(model):
|
||||
try:
|
||||
return getattr(model, name)
|
||||
except AttributeError:
|
||||
return None
|
||||
|
||||
kwargs.setdefault('value', value)
|
||||
return ProxyField(name, **kwargs)
|
99
tailbone/forms/renderers/__init__.py
Normal file
99
tailbone/forms/renderers/__init__.py
Normal file
|
@ -0,0 +1,99 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2012 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
# Rattail is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
FormAlchemy Field Renderers
|
||||
"""
|
||||
|
||||
from webhelpers.html import literal
|
||||
from webhelpers.html import tags
|
||||
|
||||
import formalchemy
|
||||
|
||||
from edbob.pyramid.forms import pretty_datetime
|
||||
from edbob.pyramid.forms.formalchemy.renderers import YesNoFieldRenderer
|
||||
|
||||
from .common import AutocompleteFieldRenderer, EnumFieldRenderer
|
||||
from .products import GPCFieldRenderer, ProductFieldRenderer
|
||||
from .users import UserFieldRenderer
|
||||
|
||||
|
||||
__all__ = ['AutocompleteFieldRenderer', 'EnumFieldRenderer', 'YesNoFieldRenderer',
|
||||
'GPCFieldRenderer', 'PersonFieldRenderer', 'PriceFieldRenderer',
|
||||
'PriceWithExpirationFieldRenderer', 'ProductFieldRenderer', 'UserFieldRenderer']
|
||||
|
||||
|
||||
def PersonFieldRenderer(url):
|
||||
|
||||
BaseRenderer = AutocompleteFieldRenderer(url)
|
||||
|
||||
class PersonFieldRenderer(BaseRenderer):
|
||||
|
||||
def render_readonly(self, **kwargs):
|
||||
person = self.raw_value
|
||||
if not person:
|
||||
return ''
|
||||
return tags.link_to(
|
||||
str(person),
|
||||
self.request.route_url('person.read', uuid=person.uuid))
|
||||
|
||||
return PersonFieldRenderer
|
||||
|
||||
|
||||
class PriceFieldRenderer(formalchemy.TextFieldRenderer):
|
||||
"""
|
||||
Renderer for fields which reference a :class:`ProductPrice` instance.
|
||||
"""
|
||||
|
||||
def render_readonly(self, **kwargs):
|
||||
price = self.field.raw_value
|
||||
if price:
|
||||
if price.price is not None and price.pack_price is not None:
|
||||
if price.multiple > 1:
|
||||
return literal('$ %0.2f / %u ($ %0.2f / %u)' % (
|
||||
price.price, price.multiple,
|
||||
price.pack_price, price.pack_multiple))
|
||||
return literal('$ %0.2f ($ %0.2f / %u)' % (
|
||||
price.price, price.pack_price, price.pack_multiple))
|
||||
if price.price is not None:
|
||||
if price.multiple > 1:
|
||||
return '$ %0.2f / %u' % (price.price, price.multiple)
|
||||
return '$ %0.2f' % price.price
|
||||
if price.pack_price is not None:
|
||||
return '$ %0.2f / %u' % (price.pack_price, price.pack_multiple)
|
||||
return ''
|
||||
|
||||
|
||||
class PriceWithExpirationFieldRenderer(PriceFieldRenderer):
|
||||
"""
|
||||
Price field renderer which also displays the expiration date, if present.
|
||||
"""
|
||||
|
||||
def render_readonly(self, **kwargs):
|
||||
res = super(PriceWithExpirationFieldRenderer, self).render_readonly(**kwargs)
|
||||
if res:
|
||||
price = self.field.raw_value
|
||||
if price.ends:
|
||||
res += ' (%s)' % pretty_datetime(price.ends, from_='utc')
|
||||
return res
|
80
tailbone/forms/renderers/common.py
Normal file
80
tailbone/forms/renderers/common.py
Normal file
|
@ -0,0 +1,80 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2012 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
# Rattail is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
Common Field Renderers
|
||||
"""
|
||||
|
||||
from formalchemy.fields import FieldRenderer, SelectFieldRenderer
|
||||
|
||||
|
||||
__all__ = ['AutocompleteFieldRenderer', 'EnumFieldRenderer']
|
||||
|
||||
|
||||
def AutocompleteFieldRenderer(service_url, field_value=None, field_display=None, width='300px'):
|
||||
"""
|
||||
Returns a custom renderer class for an autocomplete field.
|
||||
"""
|
||||
|
||||
class AutocompleteFieldRenderer(FieldRenderer):
|
||||
|
||||
@property
|
||||
def focus_name(self):
|
||||
return self.name + '-textbox'
|
||||
|
||||
@property
|
||||
def needs_focus(self):
|
||||
return not bool(self.value or field_value)
|
||||
|
||||
def render(self, **kwargs):
|
||||
kwargs.setdefault('field_name', self.name)
|
||||
kwargs.setdefault('field_value', self.value or field_value)
|
||||
kwargs.setdefault('field_display', self.raw_value or field_display)
|
||||
kwargs.setdefault('service_url', service_url)
|
||||
kwargs.setdefault('width', width)
|
||||
return render('/forms/field_autocomplete.mako', kwargs)
|
||||
|
||||
return AutocompleteFieldRenderer
|
||||
|
||||
|
||||
def EnumFieldRenderer(enum):
|
||||
"""
|
||||
Adds support for enumeration fields.
|
||||
"""
|
||||
|
||||
class Renderer(SelectFieldRenderer):
|
||||
|
||||
def render_readonly(self, **kwargs):
|
||||
value = self.raw_value
|
||||
if value is None:
|
||||
return ''
|
||||
if value in enum:
|
||||
return enum[value]
|
||||
return str(value)
|
||||
|
||||
def render(self, **kwargs):
|
||||
opts = [(enum[x], x) for x in sorted(enum)]
|
||||
return SelectFieldRenderer.render(self, opts, **kwargs)
|
||||
|
||||
return Renderer
|
56
tailbone/forms/renderers/products.py
Normal file
56
tailbone/forms/renderers/products.py
Normal file
|
@ -0,0 +1,56 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2012 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
# Rattail is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
Product Field Renderers
|
||||
"""
|
||||
|
||||
from formalchemy import TextFieldRenderer
|
||||
from rattail.gpc import GPC
|
||||
|
||||
|
||||
__all__ = ['GPCFieldRenderer', 'ProductFieldRenderer']
|
||||
|
||||
|
||||
class GPCFieldRenderer(TextFieldRenderer):
|
||||
"""
|
||||
Renderer for :class:`rattail.gpc.GPC` fields.
|
||||
"""
|
||||
|
||||
@property
|
||||
def length(self):
|
||||
# Hm, should maybe consider hard-coding this...?
|
||||
return len(str(GPC(0)))
|
||||
|
||||
|
||||
class ProductFieldRenderer(TextFieldRenderer):
|
||||
"""
|
||||
Renderer for fields which represent :class:`rattail.db.Product` instances.
|
||||
"""
|
||||
|
||||
def render_readonly(self, **kwargs):
|
||||
product = self.raw_value
|
||||
if product is None:
|
||||
return ''
|
||||
return product.full_description
|
44
tailbone/forms/renderers/users.py
Normal file
44
tailbone/forms/renderers/users.py
Normal file
|
@ -0,0 +1,44 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2012 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
# Rattail is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
User Field Renderers
|
||||
"""
|
||||
|
||||
from formalchemy.fields import TextFieldRenderer
|
||||
|
||||
|
||||
__all__ = ['UserFieldRenderer']
|
||||
|
||||
|
||||
class UserFieldRenderer(TextFieldRenderer):
|
||||
"""
|
||||
Renderer for fields which represent ``User`` instances.
|
||||
"""
|
||||
|
||||
def render_readonly(self, **kwargs):
|
||||
user = self.raw_value
|
||||
if user is None:
|
||||
return u''
|
||||
return unicode(user.display_name)
|
61
tailbone/forms/simpleform.py
Normal file
61
tailbone/forms/simpleform.py
Normal file
|
@ -0,0 +1,61 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2012 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
# Rattail is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
``pyramid_simpleform`` Forms
|
||||
"""
|
||||
|
||||
from pyramid_simpleform import renderers
|
||||
|
||||
from webhelpers.html import tags
|
||||
from webhelpers.html import HTML
|
||||
|
||||
from edbob.util import prettify
|
||||
|
||||
|
||||
__all__ = ['FormRenderer']
|
||||
|
||||
|
||||
class FormRenderer(renderers.FormRenderer):
|
||||
"""
|
||||
Customized form renderer. Provides some extra methods for convenience.
|
||||
"""
|
||||
|
||||
def field_div(self, name, field, label=None):
|
||||
errors = self.errors_for(name)
|
||||
if errors:
|
||||
errors = [HTML.tag('div', class_='field-error', c=x) for x in errors]
|
||||
errors = tags.literal('').join(errors)
|
||||
|
||||
label = HTML.tag('label', for_=name, c=label or prettify(name))
|
||||
inner = HTML.tag('div', class_='field', c=field)
|
||||
|
||||
outer_class = 'field-wrapper'
|
||||
if errors:
|
||||
outer_class += ' error'
|
||||
outer = HTML.tag('div', class_=outer_class, c=(errors or '') + label + inner)
|
||||
return outer
|
||||
|
||||
def referrer_field(self):
|
||||
return self.hidden('referrer', value=self.form.request.get_referrer())
|
32
tailbone/grids/__init__.py
Normal file
32
tailbone/grids/__init__.py
Normal file
|
@ -0,0 +1,32 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2012 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
# Rattail is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
Grids
|
||||
"""
|
||||
|
||||
from .core import *
|
||||
from .alchemy import *
|
||||
from . import util
|
||||
from . import search
|
122
tailbone/grids/alchemy.py
Normal file
122
tailbone/grids/alchemy.py
Normal file
|
@ -0,0 +1,122 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2012 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
# Rattail is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
FormAlchemy Grid Classes
|
||||
"""
|
||||
|
||||
from webhelpers.html import tags
|
||||
from webhelpers.html import HTML
|
||||
|
||||
import formalchemy
|
||||
|
||||
import edbob
|
||||
from edbob.util import prettify
|
||||
|
||||
from .core import Grid
|
||||
from .. import Session
|
||||
from sqlalchemy.orm import object_session
|
||||
|
||||
__all__ = ['AlchemyGrid']
|
||||
|
||||
|
||||
class AlchemyGrid(Grid):
|
||||
|
||||
sort_map = {}
|
||||
|
||||
pager = None
|
||||
pager_format = '$link_first $link_previous ~1~ $link_next $link_last'
|
||||
|
||||
def __init__(self, request, cls, instances, **kwargs):
|
||||
super(AlchemyGrid, self).__init__(request, **kwargs)
|
||||
self._formalchemy_grid = formalchemy.Grid(
|
||||
cls, instances, session=Session(), request=request)
|
||||
self._formalchemy_grid.prettify = prettify
|
||||
|
||||
def __delattr__(self, attr):
|
||||
delattr(self._formalchemy_grid, attr)
|
||||
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self._formalchemy_grid, attr)
|
||||
|
||||
def cell_class(self, field):
|
||||
classes = [field.name]
|
||||
return ' '.join(classes)
|
||||
|
||||
def checkbox(self, row):
|
||||
return tags.checkbox('check-'+row.uuid)
|
||||
|
||||
def column_header(self, field):
|
||||
class_ = None
|
||||
label = field.label()
|
||||
if field.key in self.sort_map:
|
||||
class_ = 'sortable'
|
||||
if field.key == self.config['sort']:
|
||||
class_ += ' sorted ' + self.config['dir']
|
||||
label = tags.link_to(label, '#')
|
||||
return HTML.tag('th', class_=class_, field=field.key,
|
||||
title=self.column_titles.get(field.key), c=label)
|
||||
|
||||
def view_route_kwargs(self, row):
|
||||
return {'uuid': row.uuid}
|
||||
|
||||
def edit_route_kwargs(self, row):
|
||||
return {'uuid': row.uuid}
|
||||
|
||||
def delete_route_kwargs(self, row):
|
||||
return {'uuid': row.uuid}
|
||||
|
||||
def iter_fields(self):
|
||||
return self._formalchemy_grid.render_fields.itervalues()
|
||||
|
||||
def iter_rows(self):
|
||||
for row in self._formalchemy_grid.rows:
|
||||
self._formalchemy_grid._set_active(row, object_session(row))
|
||||
yield row
|
||||
|
||||
def page_count_options(self):
|
||||
options = edbob.config.get('edbob.pyramid', 'grid.page_count_options')
|
||||
if options:
|
||||
options = options.split(',')
|
||||
options = [int(x.strip()) for x in options]
|
||||
else:
|
||||
options = [5, 10, 20, 50, 100]
|
||||
return options
|
||||
|
||||
def page_links(self):
|
||||
return self.pager.pager(self.pager_format,
|
||||
symbol_next='next',
|
||||
symbol_previous='prev',
|
||||
onclick="grid_navigate_page(this, '$partial_url'); return false;")
|
||||
|
||||
def render_field(self, field):
|
||||
if self._formalchemy_grid.readonly:
|
||||
return field.render_readonly()
|
||||
return field.render()
|
||||
|
||||
def row_attrs(self, row, i):
|
||||
attrs = super(AlchemyGrid, self).row_attrs(row, i)
|
||||
if hasattr(row, 'uuid'):
|
||||
attrs['uuid'] = row.uuid
|
||||
return attrs
|
136
tailbone/grids/core.py
Normal file
136
tailbone/grids/core.py
Normal file
|
@ -0,0 +1,136 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2012 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
# Rattail is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
Core Grid Classes
|
||||
"""
|
||||
|
||||
try:
|
||||
from collections import OrderedDict
|
||||
except ImportError:
|
||||
from ordereddict import OrderedDict
|
||||
|
||||
from webhelpers.html import HTML
|
||||
from webhelpers.html.builder import format_attrs
|
||||
|
||||
from pyramid.renderers import render
|
||||
|
||||
from edbob.core import Object
|
||||
|
||||
|
||||
__all__ = ['Grid']
|
||||
|
||||
|
||||
class Grid(Object):
|
||||
|
||||
full = False
|
||||
hoverable = True
|
||||
checkboxes = False
|
||||
partial_only = False
|
||||
|
||||
viewable = False
|
||||
view_route_name = None
|
||||
view_route_kwargs = None
|
||||
|
||||
editable = False
|
||||
edit_route_name = None
|
||||
edit_route_kwargs = None
|
||||
|
||||
deletable = False
|
||||
delete_route_name = None
|
||||
delete_route_kwargs = None
|
||||
|
||||
def __init__(self, request, **kwargs):
|
||||
kwargs.setdefault('fields', OrderedDict())
|
||||
kwargs.setdefault('column_titles', {})
|
||||
kwargs.setdefault('extra_columns', [])
|
||||
super(Grid, self).__init__(**kwargs)
|
||||
self.request = request
|
||||
|
||||
def add_column(self, name, label, callback):
|
||||
self.extra_columns.append(
|
||||
Object(name=name, label=label, callback=callback))
|
||||
|
||||
def column_header(self, field):
|
||||
return HTML.tag('th', field=field.name,
|
||||
title=self.column_titles.get(field.name),
|
||||
c=field.label)
|
||||
|
||||
def div_attrs(self):
|
||||
classes = ['grid']
|
||||
if self.full:
|
||||
classes.append('full')
|
||||
if self.hoverable:
|
||||
classes.append('hoverable')
|
||||
return format_attrs(
|
||||
class_=' '.join(classes),
|
||||
url=self.request.current_route_url())
|
||||
|
||||
def get_view_url(self, row):
|
||||
kwargs = {}
|
||||
if self.view_route_kwargs:
|
||||
if callable(self.view_route_kwargs):
|
||||
kwargs = self.view_route_kwargs(row)
|
||||
else:
|
||||
kwargs = self.view_route_kwargs
|
||||
return self.request.route_url(self.view_route_name, **kwargs)
|
||||
|
||||
def get_edit_url(self, row):
|
||||
kwargs = {}
|
||||
if self.edit_route_kwargs:
|
||||
if callable(self.edit_route_kwargs):
|
||||
kwargs = self.edit_route_kwargs(row)
|
||||
else:
|
||||
kwargs = self.edit_route_kwargs
|
||||
return self.request.route_url(self.edit_route_name, **kwargs)
|
||||
|
||||
def get_delete_url(self, row):
|
||||
kwargs = {}
|
||||
if self.delete_route_kwargs:
|
||||
if callable(self.delete_route_kwargs):
|
||||
kwargs = self.delete_route_kwargs(row)
|
||||
else:
|
||||
kwargs = self.delete_route_kwargs
|
||||
return self.request.route_url(self.delete_route_name, **kwargs)
|
||||
|
||||
def get_row_attrs(self, row, i):
|
||||
attrs = self.row_attrs(row, i)
|
||||
return format_attrs(**attrs)
|
||||
|
||||
def iter_fields(self):
|
||||
return self.fields.itervalues()
|
||||
|
||||
def iter_rows(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def render(self, template='/grids/grid.mako', **kwargs):
|
||||
kwargs.setdefault('grid', self)
|
||||
return render(template, kwargs)
|
||||
|
||||
def render_field(self, field):
|
||||
raise NotImplementedError
|
||||
|
||||
def row_attrs(self, row, i):
|
||||
attrs = {'class_': 'odd' if i % 2 else 'even'}
|
||||
return attrs
|
285
tailbone/grids/search.py
Normal file
285
tailbone/grids/search.py
Normal file
|
@ -0,0 +1,285 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2012 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
# Rattail is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
Grid Search Filters
|
||||
"""
|
||||
|
||||
from sqlalchemy import or_
|
||||
|
||||
from webhelpers.html import tags
|
||||
from webhelpers.html import literal
|
||||
|
||||
from pyramid.renderers import render
|
||||
from pyramid_simpleform import Form
|
||||
from pyramid_simpleform.renderers import FormRenderer
|
||||
|
||||
from edbob.core import Object
|
||||
from edbob.util import prettify
|
||||
|
||||
|
||||
class SearchFilter(Object):
|
||||
"""
|
||||
Base class and default implementation for search filters.
|
||||
"""
|
||||
|
||||
def __init__(self, name, label=None, **kwargs):
|
||||
Object.__init__(self, **kwargs)
|
||||
self.name = name
|
||||
self.label = label or prettify(name)
|
||||
|
||||
def types_select(self):
|
||||
types = [
|
||||
('is', "is"),
|
||||
('nt', "is not"),
|
||||
('lk', "contains"),
|
||||
('nl', "doesn't contain"),
|
||||
]
|
||||
options = []
|
||||
filter_map = self.search.filter_map[self.name]
|
||||
for value, label in types:
|
||||
if value in filter_map:
|
||||
options.append((value, label))
|
||||
return tags.select('filter_type_'+self.name,
|
||||
self.search.config.get('filter_type_'+self.name),
|
||||
options, class_='filter-type')
|
||||
|
||||
def value_control(self):
|
||||
return tags.text(self.name, self.search.config.get(self.name))
|
||||
|
||||
|
||||
class BooleanSearchFilter(SearchFilter):
|
||||
"""
|
||||
Boolean search filter.
|
||||
"""
|
||||
|
||||
def value_control(self):
|
||||
return tags.select(self.name, self.search.config.get(self.name),
|
||||
["True", "False"])
|
||||
|
||||
|
||||
def EnumSearchFilter(enum):
|
||||
options = enum.items()
|
||||
|
||||
class EnumSearchFilter(SearchFilter):
|
||||
def value_control(self):
|
||||
return tags.select(self.name, self.search.config.get(self.name), options)
|
||||
|
||||
return EnumSearchFilter
|
||||
|
||||
|
||||
class SearchForm(Form):
|
||||
"""
|
||||
Generic form class which aggregates :class:`SearchFilter` instances.
|
||||
"""
|
||||
|
||||
def __init__(self, request, filter_map, config, *args, **kwargs):
|
||||
super(SearchForm, self).__init__(request, *args, **kwargs)
|
||||
self.filter_map = filter_map
|
||||
self.config = config
|
||||
self.filters = {}
|
||||
|
||||
def add_filter(self, filter_):
|
||||
filter_.search = self
|
||||
self.filters[filter_.name] = filter_
|
||||
|
||||
|
||||
class SearchFormRenderer(FormRenderer):
|
||||
"""
|
||||
Renderer for :class:`SearchForm` instances.
|
||||
"""
|
||||
|
||||
def __init__(self, form, *args, **kwargs):
|
||||
super(SearchFormRenderer, self).__init__(form, *args, **kwargs)
|
||||
self.request = form.request
|
||||
self.filters = form.filters
|
||||
self.config = form.config
|
||||
|
||||
def add_filter(self, visible):
|
||||
options = ['add a filter']
|
||||
for f in self.sorted_filters():
|
||||
options.append((f.name, f.label))
|
||||
return self.select('add-filter', options,
|
||||
style='display: none;' if len(visible) == len(self.filters) else None)
|
||||
|
||||
def checkbox(self, name, checked=None, **kwargs):
|
||||
if name.startswith('include_filter_'):
|
||||
if checked is None:
|
||||
checked = self.config[name]
|
||||
return tags.checkbox(name, checked=checked, **kwargs)
|
||||
if checked is None:
|
||||
checked = False
|
||||
return super(SearchFormRenderer, self).checkbox(name, checked=checked, **kwargs)
|
||||
|
||||
def render(self, **kwargs):
|
||||
kwargs['search'] = self
|
||||
return literal(render('/grids/search.mako', kwargs))
|
||||
|
||||
def sorted_filters(self):
|
||||
return sorted(self.filters.values(), key=lambda x: x.label)
|
||||
|
||||
def text(self, name, **kwargs):
|
||||
return tags.text(name, value=self.config.get(name), **kwargs)
|
||||
|
||||
|
||||
def filter_exact(field):
|
||||
"""
|
||||
Convenience function which returns a filter map entry, with typical logic
|
||||
built in for "exact match" queries applied to ``field``.
|
||||
"""
|
||||
|
||||
return {
|
||||
'is':
|
||||
lambda q, v: q.filter(field == v) if v else q,
|
||||
'nt':
|
||||
lambda q, v: q.filter(field != v) if v else q,
|
||||
}
|
||||
|
||||
|
||||
def filter_ilike(field):
|
||||
"""
|
||||
Convenience function which returns a filter map entry, with typical logic
|
||||
built in for "ILIKE" queries applied to ``field``.
|
||||
"""
|
||||
|
||||
def ilike(query, value):
|
||||
if value:
|
||||
query = query.filter(field.ilike('%%%s%%' % value))
|
||||
return query
|
||||
|
||||
def not_ilike(query, value):
|
||||
if value:
|
||||
query = query.filter(or_(
|
||||
field == None,
|
||||
~field.ilike('%%%s%%' % value),
|
||||
))
|
||||
return query
|
||||
|
||||
return {'lk': ilike, 'nl': not_ilike}
|
||||
|
||||
|
||||
def get_filter_config(prefix, request, filter_map, **kwargs):
|
||||
"""
|
||||
Returns a configuration dictionary for a search form.
|
||||
"""
|
||||
|
||||
config = {}
|
||||
|
||||
def update_config(dict_, prefix='', exclude_by_default=False):
|
||||
"""
|
||||
Updates the ``config`` dictionary based on the contents of ``dict_``.
|
||||
"""
|
||||
|
||||
for field in filter_map:
|
||||
if prefix+'include_filter_'+field in dict_:
|
||||
include = dict_[prefix+'include_filter_'+field]
|
||||
include = bool(include) and include != '0'
|
||||
config['include_filter_'+field] = include
|
||||
elif exclude_by_default:
|
||||
config['include_filter_'+field] = False
|
||||
if prefix+'filter_type_'+field in dict_:
|
||||
config['filter_type_'+field] = dict_[prefix+'filter_type_'+field]
|
||||
if prefix+field in dict_:
|
||||
config[field] = dict_[prefix+field]
|
||||
|
||||
# Update config to exclude all filters by default.
|
||||
for field in filter_map:
|
||||
config['include_filter_'+field] = False
|
||||
|
||||
# Update config to honor default settings.
|
||||
config.update(kwargs)
|
||||
|
||||
# Update config with data cached in session.
|
||||
update_config(request.session, prefix=prefix+'.')
|
||||
|
||||
# Update config with data from GET/POST request.
|
||||
if request.params.get('filters') == 'true':
|
||||
update_config(request.params, exclude_by_default=True)
|
||||
|
||||
# Cache filter data in session.
|
||||
for key in config:
|
||||
if (not key.startswith('filter_factory_')
|
||||
and not key.startswith('filter_label_')):
|
||||
request.session[prefix+'.'+key] = config[key]
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def get_filter_map(cls, exact=[], ilike=[], **kwargs):
|
||||
"""
|
||||
Convenience function which returns a "filter map" for ``cls``.
|
||||
|
||||
``exact``, if provided, should be a list of field names for which "exact"
|
||||
filtering is to be allowed.
|
||||
|
||||
``ilike``, if provided, should be a list of field names for which "ILIKE"
|
||||
filtering is to be allowed.
|
||||
|
||||
Any remaining ``kwargs`` are assumed to be filter map entries themselves,
|
||||
and are added directly to the map.
|
||||
"""
|
||||
|
||||
fmap = {}
|
||||
for name in exact:
|
||||
fmap[name] = filter_exact(getattr(cls, name))
|
||||
for name in ilike:
|
||||
fmap[name] = filter_ilike(getattr(cls, name))
|
||||
fmap.update(kwargs)
|
||||
return fmap
|
||||
|
||||
|
||||
def get_search_form(request, filter_map, config):
|
||||
"""
|
||||
Returns a :class:`SearchForm` instance with a :class:`SearchFilter` for
|
||||
each filter in ``filter_map``, using configuration from ``config``.
|
||||
"""
|
||||
|
||||
search = SearchForm(request, filter_map, config)
|
||||
for field in filter_map:
|
||||
factory = config.get('filter_factory_%s' % field, SearchFilter)
|
||||
label = config.get('filter_label_%s' % field)
|
||||
search.add_filter(factory(field, label=label))
|
||||
return search
|
||||
|
||||
|
||||
def filter_query(query, config, filter_map, join_map):
|
||||
"""
|
||||
Filters ``query`` according to ``config`` and ``filter_map``. ``join_map``
|
||||
is used, if necessary, to join additional tables to the base query. The
|
||||
filtered query is returned.
|
||||
"""
|
||||
|
||||
joins = config.setdefault('joins', [])
|
||||
for key in config:
|
||||
if key.startswith('include_filter_') and config[key]:
|
||||
field = key[15:]
|
||||
if field in join_map and field not in joins:
|
||||
query = join_map[field](query)
|
||||
joins.append(field)
|
||||
value = config.get(field)
|
||||
if value:
|
||||
fmap = filter_map[field]
|
||||
filt = fmap[config['filter_type_'+field]]
|
||||
query = filt(query, value)
|
||||
return query
|
138
tailbone/grids/util.py
Normal file
138
tailbone/grids/util.py
Normal file
|
@ -0,0 +1,138 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2012 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
# Rattail is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
Grid Utilities
|
||||
"""
|
||||
|
||||
from sqlalchemy.orm.attributes import InstrumentedAttribute
|
||||
|
||||
from webhelpers.html import literal
|
||||
|
||||
from pyramid.response import Response
|
||||
|
||||
from .search import SearchFormRenderer
|
||||
|
||||
|
||||
def get_sort_config(name, request, **kwargs):
|
||||
"""
|
||||
Returns a configuration dictionary for grid sorting.
|
||||
"""
|
||||
|
||||
# Initial config uses some default values.
|
||||
config = {
|
||||
'dir': 'asc',
|
||||
'per_page': 20,
|
||||
'page': 1,
|
||||
}
|
||||
|
||||
# Override with defaults provided by caller.
|
||||
config.update(kwargs)
|
||||
|
||||
# Override with values from GET/POST request and/or session.
|
||||
for key in config:
|
||||
full_key = name+'_'+key
|
||||
if request.params.get(key):
|
||||
value = request.params[key]
|
||||
config[key] = value
|
||||
request.session[full_key] = value
|
||||
elif request.session.get(full_key):
|
||||
value = request.session[full_key]
|
||||
config[key] = value
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def get_sort_map(cls, names=None, **kwargs):
|
||||
"""
|
||||
Convenience function which returns a sort map for ``cls``.
|
||||
|
||||
If ``names`` is not specified, the map will include all "standard" fields
|
||||
present on the mapped class. Otherwise, the map will be limited to only
|
||||
the fields which are named.
|
||||
|
||||
All remaining ``kwargs`` are assumed to be sort map entries, and will be
|
||||
added to the map directly.
|
||||
"""
|
||||
|
||||
smap = {}
|
||||
if names is None:
|
||||
names = []
|
||||
for attr in cls.__dict__:
|
||||
obj = getattr(cls, attr)
|
||||
if isinstance(obj, InstrumentedAttribute):
|
||||
if obj.key != 'uuid':
|
||||
names.append(obj.key)
|
||||
for name in names:
|
||||
smap[name] = sorter(getattr(cls, name))
|
||||
smap.update(kwargs)
|
||||
return smap
|
||||
|
||||
|
||||
def render_grid(grid, search_form=None, **kwargs):
|
||||
"""
|
||||
Convenience function to render ``grid`` (which should be a
|
||||
:class:`tailbone.grids.Grid` instance).
|
||||
|
||||
This "usually" will return a dictionary to be used as context for rendering
|
||||
the final view template.
|
||||
|
||||
However, if a partial grid is requested (or mandated), then the grid body
|
||||
will be rendered and a :class:`pyramid.response.Response` object will be
|
||||
returned instead.
|
||||
"""
|
||||
|
||||
if grid.partial_only or grid.request.params.get('partial'):
|
||||
return Response(body=grid.render(), content_type='text/html')
|
||||
kwargs['grid'] = literal(grid.render())
|
||||
if search_form:
|
||||
kwargs['search'] = SearchFormRenderer(search_form)
|
||||
return kwargs
|
||||
|
||||
|
||||
def sort_query(query, config, sort_map, join_map={}):
|
||||
"""
|
||||
Sorts ``query`` according to ``config`` and ``sort_map``. ``join_map`` is
|
||||
used, if necessary, to join additional tables to the base query. The
|
||||
sorted query is returned.
|
||||
"""
|
||||
|
||||
field = config.get('sort')
|
||||
if not field:
|
||||
return query
|
||||
joins = config.setdefault('joins', [])
|
||||
if field in join_map and field not in joins:
|
||||
query = join_map[field](query)
|
||||
joins.append(field)
|
||||
sort = sort_map[field]
|
||||
return sort(query, config['dir'])
|
||||
|
||||
|
||||
def sorter(field):
|
||||
"""
|
||||
Returns a function suitable for a sort map callable, with typical logic
|
||||
built in for sorting applied to ``field``.
|
||||
"""
|
||||
|
||||
return lambda q, d: q.order_by(getattr(field, d)())
|
45
tailbone/helpers.py
Normal file
45
tailbone/helpers.py
Normal file
|
@ -0,0 +1,45 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2012 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
# Rattail is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
Template Context Helpers
|
||||
"""
|
||||
|
||||
import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
from webhelpers.html import *
|
||||
from webhelpers.html.tags import *
|
||||
|
||||
from edbob.pyramid.forms import pretty_datetime
|
||||
|
||||
|
||||
def pretty_date(date):
|
||||
"""
|
||||
Render a human-friendly date string.
|
||||
"""
|
||||
|
||||
if not date:
|
||||
return ''
|
||||
return date.strftime('%a %d %b %Y')
|
96
tailbone/reports/inventory_worksheet.mako
Normal file
96
tailbone/reports/inventory_worksheet.mako
Normal file
|
@ -0,0 +1,96 @@
|
|||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html style="direction: ltr;" xmlns="http://www.w3.org/1999/xhtml" lang="en-us">
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
|
||||
<title>Inventory Worksheet : ${department.name}</title>
|
||||
<style type="text/css">
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
margin: 10px 0px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 20px;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
table {
|
||||
border-bottom: 1px solid #000000;
|
||||
border-left: 1px solid #000000;
|
||||
border-collapse: collapse;
|
||||
empty-cells: show;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
th {
|
||||
border-right: 1px solid #000000;
|
||||
border-top: 1px solid #000000;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
th.subdepartment {
|
||||
border-left: none;
|
||||
font-size: 1.2em;
|
||||
padding: 15px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
td {
|
||||
border-right: 1px solid #000000;
|
||||
border-top: 1px solid #000000;
|
||||
padding: 2px 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
td.upc {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
td.count {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
td.spacer {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Inventory Worksheet</h1>
|
||||
<h2>Department: ${department.name} (${department.number})</h2>
|
||||
<h3>generated on ${date} at ${time}</h3>
|
||||
<br clear="all" />
|
||||
|
||||
<table>
|
||||
% for subdepartment in department.subdepartments:
|
||||
<% products = get_products(subdepartment) %>
|
||||
% if products:
|
||||
<tr>
|
||||
<th class="subdepartment" colspan="4">Subdepartment: ${subdepartment.name} (${subdepartment.number})</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>UPC</th>
|
||||
<th>Brand</th>
|
||||
<th>Description</th>
|
||||
<th>Count</th>
|
||||
</tr>
|
||||
% for product in products:
|
||||
<tr>
|
||||
<td class="upc">${get_upc(product)}</td>
|
||||
<td class="brand">${product.brand or ''}</td>
|
||||
<td class="description">${product.description}</td>
|
||||
<td class="count"> </td>
|
||||
</tr>
|
||||
% endfor
|
||||
<tr>
|
||||
<td class="spacer" colspan="19">
|
||||
</tr>
|
||||
% endif
|
||||
% endfor
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
128
tailbone/reports/ordering_worksheet.mako
Normal file
128
tailbone/reports/ordering_worksheet.mako
Normal file
|
@ -0,0 +1,128 @@
|
|||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html style="direction: ltr;" xmlns="http://www.w3.org/1999/xhtml" lang="en-us">
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
|
||||
<title>Ordering Worksheet : ${vendor.name}</title>
|
||||
<style type="text/css">
|
||||
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
margin: 10px 0px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 20px;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
table {
|
||||
border-bottom: 1px solid #000000;
|
||||
border-left: 1px solid #000000;
|
||||
border-collapse: collapse;
|
||||
empty-cells: show;
|
||||
}
|
||||
|
||||
th {
|
||||
border-right: 1px solid #000000;
|
||||
border-top: 1px solid #000000;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
th.department {
|
||||
border-left: none;
|
||||
font-size: 1.2em;
|
||||
padding: 15px;
|
||||
text-align: left;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
th.subdepartment {
|
||||
border-left: none;
|
||||
padding: 15px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
td {
|
||||
border-right: 1px solid #000000;
|
||||
border-top: 1px solid #000000;
|
||||
padding: 2px 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
td.upc,
|
||||
td.case-qty,
|
||||
td.code,
|
||||
td.preferred {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
td.code {
|
||||
font-family: monospace;
|
||||
font-size: 120%;
|
||||
}
|
||||
|
||||
td.scratch_pad {
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
td.spacer {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Ordering Worksheet</h1>
|
||||
<h2>Vendor: ${vendor.name} (${vendor.id})</h2>
|
||||
<h2>Phone: ${vendor.phone or ''}</h2>
|
||||
<h2>Contact: ${vendor.contact or ''}</h2>
|
||||
<h3>generated on ${date} at ${time}</h3>
|
||||
<br clear="all" />
|
||||
|
||||
<table>
|
||||
% for dept in sorted(costs, key=lambda x: x.name):
|
||||
<tr>
|
||||
<th class="department" colspan="21">Department: ${dept.name} (${dept.number})</th>
|
||||
</tr>
|
||||
% for subdept in sorted(costs[dept], key=lambda x: x.name):
|
||||
<tr>
|
||||
<th class="subdepartment" colspan="21">Subdepartment: ${subdept.name} (${subdept.number})</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>UPC</th>
|
||||
<th>Brand</th>
|
||||
<th>Description</th>
|
||||
<th>Size</th>
|
||||
<th>Case</th>
|
||||
<th>Vend. Code</th>
|
||||
<th>Pref.</th>
|
||||
<th colspan="14">Order Scratch Pad</th>
|
||||
</tr>
|
||||
% for cost in sorted(costs[dept][subdept], key=cost_sort_key):
|
||||
<tr>
|
||||
<td class="upc">${get_upc(cost.product)}</td>
|
||||
<td class="brand">${cost.product.brand or ''}</td>
|
||||
<td class="desc">${cost.product.description}</td>
|
||||
<td class="size">${cost.product.size or ''}</td>
|
||||
<td class="case-qty">${cost.case_size} ${rattail.UNIT_OF_MEASURE.get(cost.product.unit_of_measure, '')}</td>
|
||||
<td class="code">${cost.code or ''}</td>
|
||||
<td class="preferred">${'X' if cost.preference == 1 else ''}</td>
|
||||
% for i in range(14):
|
||||
<td class="scratch_pad"> </td>
|
||||
% endfor
|
||||
</tr>
|
||||
% endfor
|
||||
<tr>
|
||||
<td class="spacer" colspan="21">
|
||||
</tr>
|
||||
% endfor
|
||||
% endfor
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
31
tailbone/static/__init__.py
Normal file
31
tailbone/static/__init__.py
Normal file
|
@ -0,0 +1,31 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2012 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
# Rattail is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
Static Assets
|
||||
"""
|
||||
|
||||
|
||||
def includeme(config):
|
||||
config.add_static_view('tailbone', 'tailbone:static')
|
168
tailbone/static/css/grids.css
Normal file
168
tailbone/static/css/grids.css
Normal file
|
@ -0,0 +1,168 @@
|
|||
|
||||
/******************************
|
||||
* Grid Header
|
||||
******************************/
|
||||
|
||||
table.grid-header {
|
||||
padding-bottom: 5px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
/******************************
|
||||
* Form (Filters etc.)
|
||||
******************************/
|
||||
|
||||
table.grid-header td.form {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
|
||||
/******************************
|
||||
* Context Menu
|
||||
******************************/
|
||||
|
||||
table.grid-header td.context-menu {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
table.grid-header td.context-menu ul {
|
||||
list-style-type: none;
|
||||
margin: 0px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
table.grid-header td.context-menu ul li {
|
||||
line-height: 2em;
|
||||
}
|
||||
|
||||
/******************************
|
||||
* Tools
|
||||
******************************/
|
||||
|
||||
table.grid-header td.tools {
|
||||
padding-bottom: 10px;
|
||||
text-align: right;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
table.grid-header td.tools div.buttons button {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
|
||||
/******************************
|
||||
* Grid
|
||||
******************************/
|
||||
|
||||
div.grid {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
div.grid table {
|
||||
border-top: 1px solid black;
|
||||
border-left: 1px solid black;
|
||||
border-collapse: collapse;
|
||||
font-size: 9pt;
|
||||
line-height: normal;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
div.grid.full table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
div.grid table th,
|
||||
div.grid table td {
|
||||
border-right: 1px solid black;
|
||||
border-bottom: 1px solid black;
|
||||
padding: 2px 3px;
|
||||
}
|
||||
|
||||
div.grid table th.sortable a {
|
||||
display: block;
|
||||
padding-right: 18px;
|
||||
}
|
||||
|
||||
div.grid table th.sorted {
|
||||
background-position: right center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
div.grid table th.sorted.asc {
|
||||
background-image: url(../img/sort_arrow_up.png);
|
||||
}
|
||||
|
||||
div.grid table th.sorted.desc {
|
||||
background-image: url(../img/sort_arrow_down.png);
|
||||
}
|
||||
|
||||
div.grid table tbody td {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
div.grid table tbody td.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
div.grid table tbody td.right {
|
||||
float: none;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
div.grid table tr.odd {
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
|
||||
div.grid table tbody tr.hovering {
|
||||
background-color: #bbbbbb;
|
||||
}
|
||||
|
||||
div.grid table tbody tr td.checkbox {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
div.grid table tbody tr td.view,
|
||||
div.grid table tbody tr td.edit,
|
||||
div.grid table tbody tr td.delete {
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
cursor: pointer;
|
||||
min-width: 18px;
|
||||
text-align: center;
|
||||
width: 18px;
|
||||
}
|
||||
|
||||
div.grid table tbody tr td.view {
|
||||
background-image: url(../img/view.png);
|
||||
}
|
||||
|
||||
div.grid table tbody tr td.edit {
|
||||
background-image: url(../img/edit.png);
|
||||
}
|
||||
|
||||
div.grid table tbody tr td.delete {
|
||||
background-image: url(../img/delete.png);
|
||||
}
|
||||
|
||||
div.pager {
|
||||
margin-bottom: 20px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
div.pager p {
|
||||
font-size: 10pt;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
div.pager p.showing {
|
||||
float: left;
|
||||
}
|
||||
|
||||
div.pager #grid-page-count {
|
||||
font-size: 8pt;
|
||||
}
|
||||
|
||||
div.pager p.page-links {
|
||||
float: right;
|
||||
}
|
BIN
tailbone/static/img/delete.png
Normal file
BIN
tailbone/static/img/delete.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 641 B |
BIN
tailbone/static/img/edit.png
Normal file
BIN
tailbone/static/img/edit.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 533 B |
BIN
tailbone/static/img/sort_arrow_down.png
Normal file
BIN
tailbone/static/img/sort_arrow_down.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 158 B |
BIN
tailbone/static/img/sort_arrow_up.png
Normal file
BIN
tailbone/static/img/sort_arrow_up.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 169 B |
BIN
tailbone/static/img/view.png
Normal file
BIN
tailbone/static/img/view.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 616 B |
57
tailbone/subscribers.py
Normal file
57
tailbone/subscribers.py
Normal file
|
@ -0,0 +1,57 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2012 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
# Rattail is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
Event Subscribers
|
||||
"""
|
||||
|
||||
from pyramid import threadlocal
|
||||
|
||||
import rattail
|
||||
from . import helpers
|
||||
|
||||
|
||||
def before_render(event):
|
||||
"""
|
||||
Adds goodies to the global template renderer context:
|
||||
|
||||
* ``rattail``
|
||||
"""
|
||||
|
||||
# Import labels module so it's available if/when needed.
|
||||
import rattail.labels
|
||||
|
||||
# Import SIL module so it's available if/when needed.
|
||||
import rattail.sil
|
||||
|
||||
request = event.get('request') or threadlocal.get_current_request()
|
||||
|
||||
renderer_globals = event
|
||||
renderer_globals['h'] = helpers
|
||||
renderer_globals['rattail'] = rattail
|
||||
|
||||
|
||||
def includeme(config):
|
||||
config.add_subscriber('tailbone.subscribers:before_render',
|
||||
'pyramid.events.BeforeRender')
|
44
tailbone/templates/autocomplete.mako
Normal file
44
tailbone/templates/autocomplete.mako
Normal file
|
@ -0,0 +1,44 @@
|
|||
<%def name="autocomplete(field_name, service_url, field_value=None, field_display=None, width='300px', selected=None, cleared=None)">
|
||||
<div id="${field_name}-container" class="autocomplete-container">
|
||||
${h.hidden(field_name, id=field_name, value=field_value)}
|
||||
${h.text(field_name+'-textbox', id=field_name+'-textbox', value=field_display,
|
||||
class_='autocomplete-textbox', style='display: none;' if field_value else '')}
|
||||
<div id="${field_name}-display" class="autocomplete-display"${'' if field_value else ' style="display: none;"'|n}>
|
||||
<span>${field_display or ''}</span>
|
||||
<button type="button" id="${field_name}-change" class="autocomplete-change">Change</button>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
$(function() {
|
||||
$('#${field_name}-textbox').autocomplete({
|
||||
source: '${service_url}',
|
||||
autoFocus: true,
|
||||
focus: function(event, ui) {
|
||||
return false;
|
||||
},
|
||||
select: function(event, ui) {
|
||||
$('#${field_name}').val(ui.item.value);
|
||||
$('#${field_name}-display span:first').text(ui.item.label);
|
||||
$('#${field_name}-textbox').hide();
|
||||
$('#${field_name}-display').show();
|
||||
% if selected:
|
||||
${selected}(ui.item.value, ui.item.label);
|
||||
% endif
|
||||
return false;
|
||||
}
|
||||
});
|
||||
$('#${field_name}-change').click(function() {
|
||||
$('#${field_name}').val('');
|
||||
$('#${field_name}-display').hide();
|
||||
with ($('#${field_name}-textbox')) {
|
||||
val('');
|
||||
show();
|
||||
focus();
|
||||
}
|
||||
% if cleared:
|
||||
${cleared}();
|
||||
% endif
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</%def>
|
11
tailbone/templates/batches/crud.mako
Normal file
11
tailbone/templates/batches/crud.mako
Normal file
|
@ -0,0 +1,11 @@
|
|||
<%inherit file="/crud.mako" />
|
||||
|
||||
<%def name="context_menu_items()">
|
||||
<li>${h.link_to("Back to Batches", url('batches'))}</li>
|
||||
<li>${h.link_to("View Batch Rows", url('batch.rows', uuid=form.fieldset.model.uuid))}</li>
|
||||
% if not form.readonly:
|
||||
<li>${h.link_to("View this Batch", url('batch.read', uuid=form.fieldset.model.uuid))}</li>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
${parent.body()}
|
5
tailbone/templates/batches/index.mako
Normal file
5
tailbone/templates/batches/index.mako
Normal file
|
@ -0,0 +1,5 @@
|
|||
<%inherit file="/grid.mako" />
|
||||
|
||||
<%def name="title()">Batches</%def>
|
||||
|
||||
${parent.body()}
|
42
tailbone/templates/batches/params.mako
Normal file
42
tailbone/templates/batches/params.mako
Normal file
|
@ -0,0 +1,42 @@
|
|||
<%inherit file="/base.mako" />
|
||||
|
||||
<%def name="title()">Batch Parameters</%def>
|
||||
|
||||
<%def name="head_tags()">
|
||||
${parent.head_tags()}
|
||||
<script language="javascript" type="text/javascript">
|
||||
|
||||
$(function() {
|
||||
|
||||
$('#create-batch').click(function() {
|
||||
disable_button(this, "Creating batch");
|
||||
disable_button('#cancel');
|
||||
$('form').submit();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
</%def>
|
||||
|
||||
<%def name="batch_params()"></%def>
|
||||
|
||||
<p>Please provide the following values for your new batch:</p>
|
||||
<br />
|
||||
|
||||
<div class="form">
|
||||
|
||||
${h.form(request.get_referrer())}
|
||||
${h.hidden('provider', value=provider)}
|
||||
${h.hidden('params', value='True')}
|
||||
|
||||
${self.batch_params()}
|
||||
|
||||
<div class="buttons">
|
||||
<button type="button" id="create-batch">Create Batch</button>
|
||||
<button type="button" id="cancel" onclick="location.href = '${request.get_referrer()}';">Cancel</button>
|
||||
</div>
|
||||
|
||||
${h.end_form()}
|
||||
|
||||
</div>
|
19
tailbone/templates/batches/params/print_labels.mako
Normal file
19
tailbone/templates/batches/params/print_labels.mako
Normal file
|
@ -0,0 +1,19 @@
|
|||
<%inherit file="/batches/params.mako" />
|
||||
|
||||
<%def name="batch_params()">
|
||||
|
||||
<div class="field-wrapper">
|
||||
<label for="profile">Label Type</label>
|
||||
<div class="field">
|
||||
${h.select('profile', None, label_profiles)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-wrapper">
|
||||
<label for="quantity">Quantity</label>
|
||||
<div class="field">${h.text('quantity', value=1)}</div>
|
||||
</div>
|
||||
|
||||
</%def>
|
||||
|
||||
${parent.body()}
|
40
tailbone/templates/batches/read.mako
Normal file
40
tailbone/templates/batches/read.mako
Normal file
|
@ -0,0 +1,40 @@
|
|||
<%inherit file="/batches/crud.mako" />
|
||||
|
||||
<%def name="context_menu_items()">
|
||||
${parent.context_menu_items()}
|
||||
<li>${h.link_to("Edit this Batch", url('batch.update', uuid=form.fieldset.model.uuid))}</li>
|
||||
<li>${h.link_to("Delete this Batch", url('batch.delete', uuid=form.fieldset.model.uuid))}</li>
|
||||
</%def>
|
||||
|
||||
${parent.body()}
|
||||
|
||||
<% batch = form.fieldset.model %>
|
||||
|
||||
<h2>Columns</h2>
|
||||
|
||||
<div class="grid full hoverable">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>SIL Name</th>
|
||||
<th>Display Name</th>
|
||||
<th>Description</th>
|
||||
<th>Data Type</th>
|
||||
<th>Visible</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
% for i, column in enumerate(batch.columns, 1):
|
||||
<tr class="${'odd' if i % 2 else 'even'}">
|
||||
<td>${column.name}</td>
|
||||
<td>${column.sil_name}</td>
|
||||
<td>${column.display_name}</td>
|
||||
<td>${column.description}</td>
|
||||
<td>${column.data_type}</td>
|
||||
<td>${column.visible}</td>
|
||||
</tr>
|
||||
% endfor
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
8
tailbone/templates/batches/rows/crud.mako
Normal file
8
tailbone/templates/batches/rows/crud.mako
Normal file
|
@ -0,0 +1,8 @@
|
|||
<%inherit file="/crud.mako" />
|
||||
|
||||
<%def name="context_menu_items()">
|
||||
<li>${h.link_to("Back to Batch", url('batch.read', uuid=form.fieldset.model.batch.uuid))}</li>
|
||||
<li>${h.link_to("Back to Batch Rows", url('batch.rows', uuid=form.fieldset.model.batch.uuid))}</li>
|
||||
</%def>
|
||||
|
||||
${parent.body()}
|
46
tailbone/templates/batches/rows/index.mako
Normal file
46
tailbone/templates/batches/rows/index.mako
Normal file
|
@ -0,0 +1,46 @@
|
|||
<%inherit file="/grid.mako" />
|
||||
|
||||
<%def name="title()">Batch Rows : ${batch.description}</%def>
|
||||
|
||||
<%def name="head_tags()">
|
||||
${parent.head_tags()}
|
||||
<script language="javascript" type="text/javascript">
|
||||
|
||||
$(function() {
|
||||
|
||||
$('#delete-results').click(function() {
|
||||
var msg = "This will delete all rows matching the current search.\n\n"
|
||||
+ "PLEASE NOTE that this may include some rows which are not visible "
|
||||
+ "on your screen.\n(I.e., if there is more than one \"page\" of results.)\n\n"
|
||||
+ "Are you sure you wish to delete these rows?";
|
||||
if (confirm(msg)) {
|
||||
disable_button(this, "Deleting rows");
|
||||
location.href = '${url('batch.rows.delete', uuid=batch.uuid)}';
|
||||
}
|
||||
});
|
||||
|
||||
$('#execute-batch').click(function() {
|
||||
if (confirm("Are you sure you wish to execute this batch?")) {
|
||||
disable_button(this, "Executing batch");
|
||||
location.href = '${url('batch.execute', uuid=batch.uuid)}';
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
</%def>
|
||||
|
||||
<%def name="context_menu_items()">
|
||||
<li>${h.link_to("Back to Batches", url('batches'))}</li>
|
||||
<li>${h.link_to("Back to Batch", url('batch.read', uuid=batch.uuid))}</li>
|
||||
</%def>
|
||||
|
||||
<%def name="tools()">
|
||||
<div class="buttons">
|
||||
<button type="button" id="delete-results">Delete Results</button>
|
||||
<button type="button" id="execute-batch">Execute Batch</button>
|
||||
</div>
|
||||
</%def>
|
||||
|
||||
${parent.body()}
|
12
tailbone/templates/brands/crud.mako
Normal file
12
tailbone/templates/brands/crud.mako
Normal file
|
@ -0,0 +1,12 @@
|
|||
<%inherit file="/crud.mako" />
|
||||
|
||||
<%def name="context_menu_items()">
|
||||
<li>${h.link_to("Back to Brands", url('brands'))}</li>
|
||||
% if form.readonly:
|
||||
<li>${h.link_to("Edit this Brand", url('brand.update', uuid=form.fieldset.model.uuid))}</li>
|
||||
% elif form.updating:
|
||||
<li>${h.link_to("View this Brand", url('brand.read', uuid=form.fieldset.model.uuid))}</li>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
${parent.body()}
|
11
tailbone/templates/brands/index.mako
Normal file
11
tailbone/templates/brands/index.mako
Normal file
|
@ -0,0 +1,11 @@
|
|||
<%inherit file="/grid.mako" />
|
||||
|
||||
<%def name="title()">Brands</%def>
|
||||
|
||||
<%def name="context_menu_items()">
|
||||
% if request.has_perm('brands.create'):
|
||||
<li>${h.link_to("Create a new Brand", url('brand.create'))}</li>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
${parent.body()}
|
12
tailbone/templates/categories/crud.mako
Normal file
12
tailbone/templates/categories/crud.mako
Normal file
|
@ -0,0 +1,12 @@
|
|||
<%inherit file="/crud.mako" />
|
||||
|
||||
<%def name="context_menu_items()">
|
||||
<li>${h.link_to("Back to Categories", url('categories'))}</li>
|
||||
% if form.readonly:
|
||||
<li>${h.link_to("Edit this Category", url('category.update', uuid=form.fieldset.model.uuid))}</li>
|
||||
% elif form.updating:
|
||||
<li>${h.link_to("View this Category", url('category.read', uuid=form.fieldset.model.uuid))}</li>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
${parent.body()}
|
11
tailbone/templates/categories/index.mako
Normal file
11
tailbone/templates/categories/index.mako
Normal file
|
@ -0,0 +1,11 @@
|
|||
<%inherit file="/grid.mako" />
|
||||
|
||||
<%def name="title()">Categories</%def>
|
||||
|
||||
<%def name="context_menu_items()">
|
||||
% if request.has_perm('categories.create'):
|
||||
<li>${h.link_to("Create a new Category", url('category.create'))}</li>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
${parent.body()}
|
20
tailbone/templates/crud.mako
Normal file
20
tailbone/templates/crud.mako
Normal file
|
@ -0,0 +1,20 @@
|
|||
<%inherit file="/form.mako" />
|
||||
|
||||
<%def name="title()">${"New "+form.pretty_name if form.creating else form.pretty_name+' : '+capture(self.model_title)}</%def>
|
||||
|
||||
<%def name="model_title()">${h.literal(str(form.fieldset.model))}</%def>
|
||||
|
||||
<%def name="head_tags()">
|
||||
${parent.head_tags()}
|
||||
<script type="text/javascript">
|
||||
$(function() {
|
||||
$('a.delete').click(function() {
|
||||
if (! confirm("Do you really wish to delete this object?")) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</%def>
|
||||
|
||||
${parent.body()}
|
12
tailbone/templates/customergroups/crud.mako
Normal file
12
tailbone/templates/customergroups/crud.mako
Normal file
|
@ -0,0 +1,12 @@
|
|||
<%inherit file="/crud.mako" />
|
||||
|
||||
<%def name="context_menu_items()">
|
||||
<li>${h.link_to("Back to Customer Groups", url('customer_groups'))}</li>
|
||||
% if form.readonly and request.has_perm('customer_groups.update'):
|
||||
<li>${h.link_to("Edit this Customer Group", url('customer_group.update', uuid=form.fieldset.model.uuid))}</li>
|
||||
% elif form.updating:
|
||||
<li>${h.link_to("View this Customer Group", url('customer_group.read', uuid=form.fieldset.model.uuid))}</li>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
${parent.body()}
|
11
tailbone/templates/customergroups/index.mako
Normal file
11
tailbone/templates/customergroups/index.mako
Normal file
|
@ -0,0 +1,11 @@
|
|||
<%inherit file="/grid.mako" />
|
||||
|
||||
<%def name="title()">Customer Groups</%def>
|
||||
|
||||
<%def name="context_menu_items()">
|
||||
% if request.has_perm('customer_groups.create'):
|
||||
<li>${h.link_to("Create a new Customer Group", url('customer_group.create'))}</li>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
${parent.body()}
|
12
tailbone/templates/customers/crud.mako
Normal file
12
tailbone/templates/customers/crud.mako
Normal file
|
@ -0,0 +1,12 @@
|
|||
<%inherit file="/crud.mako" />
|
||||
|
||||
<%def name="context_menu_items()">
|
||||
<li>${h.link_to("Back to Customers", url('customers'))}</li>
|
||||
% if form.readonly and request.has_perm('customers.update'):
|
||||
<li>${h.link_to("Edit this Customer", url('customer.update', uuid=form.fieldset.model.uuid))}</li>
|
||||
% elif form.updating:
|
||||
<li>${h.link_to("View this Customer", url('customer.read', uuid=form.fieldset.model.uuid))}</li>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
${parent.body()}
|
5
tailbone/templates/customers/index.mako
Normal file
5
tailbone/templates/customers/index.mako
Normal file
|
@ -0,0 +1,5 @@
|
|||
<%inherit file="/grid.mako" />
|
||||
|
||||
<%def name="title()">Customers</%def>
|
||||
|
||||
${parent.body()}
|
51
tailbone/templates/customers/read.mako
Normal file
51
tailbone/templates/customers/read.mako
Normal file
|
@ -0,0 +1,51 @@
|
|||
<%inherit file="/customers/crud.mako" />
|
||||
|
||||
${parent.body()}
|
||||
|
||||
<% customer = form.fieldset.model %>
|
||||
|
||||
<h2>People</h2>
|
||||
% if customer.people:
|
||||
<p>Customer account is associated with the following people:</p>
|
||||
<div class="grid clickable">
|
||||
<table>
|
||||
<thead>
|
||||
<th>First Name</th>
|
||||
<th>Last Name</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
% for i, person in enumerate(customer.people, 1):
|
||||
<tr class="${'odd' if i % 2 else 'even'}" url="${url('person.read', uuid=person.uuid)}">
|
||||
<td>${person.first_name or ''}</td>
|
||||
<td>${person.last_name or ''}</td>
|
||||
</tr>
|
||||
% endfor
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
% else:
|
||||
<p>Customer account is not associated with any people.</p>
|
||||
% endif
|
||||
|
||||
<h2>Groups</h2>
|
||||
% if customer.groups:
|
||||
<p>Customer account belongs to the following groups:</p>
|
||||
<div class="grid clickable">
|
||||
<table>
|
||||
<thead>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
% for i, group in enumerate(customer.groups, 1):
|
||||
<tr class="${'odd' if i % 2 else 'even'}" url="${url('customer_group.read', uuid=group.uuid)}">
|
||||
<td>${group.id}</td>
|
||||
<td>${group.name or ''}</td>
|
||||
</tr>
|
||||
% endfor
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
% else:
|
||||
<p>Customer account doesn't belong to any groups.</p>
|
||||
% endif
|
12
tailbone/templates/departments/crud.mako
Normal file
12
tailbone/templates/departments/crud.mako
Normal file
|
@ -0,0 +1,12 @@
|
|||
<%inherit file="/crud.mako" />
|
||||
|
||||
<%def name="context_menu_items()">
|
||||
<li>${h.link_to("Back to Departments", url('departments'))}</li>
|
||||
% if form.readonly:
|
||||
<li>${h.link_to("Edit this Department", url('department.update', uuid=form.fieldset.model.uuid))}</li>
|
||||
% elif form.updating:
|
||||
<li>${h.link_to("View this Department", url('department.read', uuid=form.fieldset.model.uuid))}</li>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
${parent.body()}
|
11
tailbone/templates/departments/index.mako
Normal file
11
tailbone/templates/departments/index.mako
Normal file
|
@ -0,0 +1,11 @@
|
|||
<%inherit file="/grid.mako" />
|
||||
|
||||
<%def name="title()">Departments</%def>
|
||||
|
||||
<%def name="context_menu_items()">
|
||||
% if request.has_perm('departments.create'):
|
||||
<li>${h.link_to("Create a new Department", url('department.create'))}</li>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
${parent.body()}
|
12
tailbone/templates/employees/crud.mako
Normal file
12
tailbone/templates/employees/crud.mako
Normal file
|
@ -0,0 +1,12 @@
|
|||
<%inherit file="/crud.mako" />
|
||||
|
||||
<%def name="context_menu_items()">
|
||||
<li>${h.link_to("Back to Employees", url('employees'))}</li>
|
||||
% if form.readonly:
|
||||
<li>${h.link_to("Edit this Employee", url('employee.update', uuid=form.fieldset.model.uuid))}</li>
|
||||
% elif form.updating:
|
||||
<li>${h.link_to("View this Employee", url('employee.read', uuid=form.fieldset.model.uuid))}</li>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
${parent.body()}
|
5
tailbone/templates/employees/index.mako
Normal file
5
tailbone/templates/employees/index.mako
Normal file
|
@ -0,0 +1,5 @@
|
|||
<%inherit file="/grid.mako" />
|
||||
|
||||
<%def name="title()">Employees</%def>
|
||||
|
||||
${parent.body()}
|
3
tailbone/templates/forms/field_autocomplete.mako
Normal file
3
tailbone/templates/forms/field_autocomplete.mako
Normal file
|
@ -0,0 +1,3 @@
|
|||
<%namespace file="/autocomplete.mako" import="autocomplete" />
|
||||
|
||||
${autocomplete(field_name, service_url, field_value, field_display, width=width, selected=selected, cleared=cleared)}
|
63
tailbone/templates/grids/grid.mako
Normal file
63
tailbone/templates/grids/grid.mako
Normal file
|
@ -0,0 +1,63 @@
|
|||
<div ${grid.div_attrs()}>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
% if grid.checkboxes:
|
||||
<th class="checkbox">${h.checkbox('check-all')}</th>
|
||||
% endif
|
||||
% for field in grid.iter_fields():
|
||||
${grid.column_header(field)}
|
||||
% endfor
|
||||
% for col in grid.extra_columns:
|
||||
<th>${col.label}</td>
|
||||
% endfor
|
||||
% if grid.viewable:
|
||||
<th> </th>
|
||||
% endif
|
||||
% if grid.editable:
|
||||
<th> </th>
|
||||
% endif
|
||||
% if grid.deletable:
|
||||
<th> </th>
|
||||
% endif
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
% for i, row in enumerate(grid.iter_rows(), 1):
|
||||
<tr ${grid.get_row_attrs(row, i)}>
|
||||
% if grid.checkboxes:
|
||||
<td class="checkbox">${grid.checkbox(row)}</td>
|
||||
% endif
|
||||
% for field in grid.iter_fields():
|
||||
<td class="${grid.cell_class(field)}">${grid.render_field(field)}</td>
|
||||
% endfor
|
||||
% for col in grid.extra_columns:
|
||||
<td class="${col.name}">${col.callback(row)}</td>
|
||||
% endfor
|
||||
% if grid.viewable:
|
||||
<td class="view" url="${grid.get_view_url(row)}"> </td>
|
||||
% endif
|
||||
% if grid.editable:
|
||||
<td class="edit" url="${grid.get_edit_url(row)}"> </td>
|
||||
% endif
|
||||
% if grid.deletable:
|
||||
<td class="delete" url="${grid.get_delete_url(row)}"> </td>
|
||||
% endif
|
||||
</tr>
|
||||
% endfor
|
||||
</tbody>
|
||||
</table>
|
||||
% if grid.pager:
|
||||
<div class="pager">
|
||||
<p class="showing">
|
||||
showing ${grid.pager.first_item} thru ${grid.pager.last_item} of ${grid.pager.item_count}
|
||||
(page ${grid.pager.page} of ${grid.pager.page_count})
|
||||
</p>
|
||||
<p class="page-links">
|
||||
${h.select('grid-page-count', grid.pager.items_per_page, grid.page_count_options())}
|
||||
per page
|
||||
${grid.page_links()}
|
||||
</p>
|
||||
</div>
|
||||
% endif
|
||||
</div>
|
26
tailbone/templates/labels/profiles/crud.mako
Normal file
26
tailbone/templates/labels/profiles/crud.mako
Normal file
|
@ -0,0 +1,26 @@
|
|||
<%inherit file="/crud.mako" />
|
||||
|
||||
<%def name="head_tags()">
|
||||
${parent.head_tags()}
|
||||
<style type="text/css">
|
||||
|
||||
div.form div.field-wrapper.format textarea {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
</style>
|
||||
</%def>
|
||||
|
||||
<%def name="context_menu_items()">
|
||||
<li>${h.link_to("Back to Label Profiles", url('label_profiles'))}</li>
|
||||
% if form.updating:
|
||||
<% profile = form.fieldset.model %>
|
||||
<% printer = profile.get_printer() %>
|
||||
% if printer and printer.required_settings:
|
||||
<li>${h.link_to("Edit Printer Settings", url('label_profile.printer_settings', uuid=profile.uuid))}</li>
|
||||
% endif
|
||||
<li>${h.link_to("View this Label Profile", url('label_profile.read', uuid=profile.uuid))}</li>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
${parent.body()}
|
11
tailbone/templates/labels/profiles/index.mako
Normal file
11
tailbone/templates/labels/profiles/index.mako
Normal file
|
@ -0,0 +1,11 @@
|
|||
<%inherit file="/grid.mako" />
|
||||
|
||||
<%def name="title()">Label Profiles</%def>
|
||||
|
||||
<%def name="context_menu_items()">
|
||||
% if request.has_perm('label_profiles.create'):
|
||||
<li>${h.link_to("Create a new Label Profile", url('label_profile.create'))}</li>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
${parent.body()}
|
48
tailbone/templates/labels/profiles/printer.mako
Normal file
48
tailbone/templates/labels/profiles/printer.mako
Normal file
|
@ -0,0 +1,48 @@
|
|||
<%inherit file="/base.mako" />
|
||||
|
||||
<%def name="title()">Printer Settings</%def>
|
||||
|
||||
<%def name="context_menu_items()">
|
||||
<li>${h.link_to("Back to Label Profiles", url('label_profiles'))}</li>
|
||||
<li>${h.link_to("View this Label Profile", url('label_profile.read', uuid=profile.uuid))}</li>
|
||||
<li>${h.link_to("Edit this Label Profile", url('label_profile.update', uuid=profile.uuid))}</li>
|
||||
</%def>
|
||||
|
||||
<div class="form-wrapper">
|
||||
|
||||
<ul class="context-menu">
|
||||
${self.context_menu_items()}
|
||||
</ul>
|
||||
|
||||
<div class="form">
|
||||
|
||||
<div class="field-wrapper">
|
||||
<label>Label Profile</label>
|
||||
<div class="field">${profile.description}</div>
|
||||
</div>
|
||||
|
||||
<div class="field-wrapper">
|
||||
<label>Printer Spec</label>
|
||||
<div class="field">${profile.printer_spec}</div>
|
||||
</div>
|
||||
|
||||
${h.form(request.current_route_url())}
|
||||
|
||||
% for name, display in printer.required_settings.iteritems():
|
||||
<div class="field-wrapper">
|
||||
<label for="${name}">${display}</label>
|
||||
<div class="field">
|
||||
${h.text(name, value=profile.get_printer_setting(name))}
|
||||
</div>
|
||||
</div>
|
||||
% endfor
|
||||
|
||||
<div class="buttons">
|
||||
${h.submit('update', "Update")}
|
||||
<button type="button" onclick="location.href = '${url('label_profile.read', uuid=profile.uuid)}';">Cancel</button>
|
||||
</div>
|
||||
|
||||
${h.end_form()}
|
||||
</div>
|
||||
|
||||
</div>
|
32
tailbone/templates/labels/profiles/read.mako
Normal file
32
tailbone/templates/labels/profiles/read.mako
Normal file
|
@ -0,0 +1,32 @@
|
|||
<%inherit file="/labels/profiles/crud.mako" />
|
||||
|
||||
<%def name="context_menu_items()">
|
||||
<li>${h.link_to("Back to Label Profiles", url('label_profiles'))}</li>
|
||||
% if form.readonly and request.has_perm('label_profiles.update'):
|
||||
<% profile = form.fieldset.model %>
|
||||
<% printer = profile.get_printer() %>
|
||||
<li>${h.link_to("Edit this Label Profile", url('label_profile.update', uuid=form.fieldset.model.uuid))}</li>
|
||||
% if printer and printer.required_settings:
|
||||
<li>${h.link_to("Edit Printer Settings", url('label_profile.printer_settings', uuid=profile.uuid))}</li>
|
||||
% endif
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
${parent.body()}
|
||||
|
||||
<% profile = form.fieldset.model %>
|
||||
<% printer = profile.get_printer() %>
|
||||
|
||||
% if printer and printer.required_settings:
|
||||
<h2>Printer Settings</h2>
|
||||
|
||||
<div class="form">
|
||||
% for name, display in printer.required_settings.iteritems():
|
||||
<div class="field-wrapper">
|
||||
<label>${display}</label>
|
||||
<div class="field">${profile.get_printer_setting(name) or ''}</div>
|
||||
</div>
|
||||
% endfor
|
||||
</div>
|
||||
|
||||
% endif
|
12
tailbone/templates/people/crud.mako
Normal file
12
tailbone/templates/people/crud.mako
Normal file
|
@ -0,0 +1,12 @@
|
|||
<%inherit file="/crud.mako" />
|
||||
|
||||
<%def name="context_menu_items()">
|
||||
<li>${h.link_to("Back to People", url('people'))}</li>
|
||||
% if form.readonly:
|
||||
<li>${h.link_to("Edit this Person", url('person.update', uuid=form.fieldset.model.uuid))}</li>
|
||||
% elif form.updating:
|
||||
<li>${h.link_to("View this Person", url('person.read', uuid=form.fieldset.model.uuid))}</li>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
${parent.body()}
|
27
tailbone/templates/products/batch.mako
Normal file
27
tailbone/templates/products/batch.mako
Normal file
|
@ -0,0 +1,27 @@
|
|||
<%inherit file="/base.mako" />
|
||||
|
||||
<%def name="title()">Create Products Batch</%def>
|
||||
|
||||
<%def name="context_menu_items()">
|
||||
<li>${h.link_to("Back to Products", url('products'))}</li>
|
||||
</%def>
|
||||
|
||||
<div class="form">
|
||||
|
||||
${h.form(request.current_route_url())}
|
||||
|
||||
<div class="field-wrapper">
|
||||
<label for="provider">Batch Type</label>
|
||||
<div class="field">
|
||||
${h.select('provider', None, providers)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="buttons">
|
||||
${h.submit('create', "Create Batch")}
|
||||
<button type="button" onclick="location.href = '${url('products')}';">Cancel</button>
|
||||
</div>
|
||||
|
||||
${h.end_form()}
|
||||
|
||||
</div>
|
12
tailbone/templates/products/crud.mako
Normal file
12
tailbone/templates/products/crud.mako
Normal file
|
@ -0,0 +1,12 @@
|
|||
<%inherit file="/crud.mako" />
|
||||
|
||||
<%def name="context_menu_items()">
|
||||
<li>${h.link_to("Back to Products", url('products'))}</li>
|
||||
% if form.readonly and request.has_perm('products.update'):
|
||||
<li>${h.link_to("Edit this Product", url('product.update', uuid=form.fieldset.model.uuid))}</li>
|
||||
% elif form.updating:
|
||||
<li>${h.link_to("View this Product", url('product.read', uuid=form.fieldset.model.uuid))}</li>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
${parent.body()}
|
107
tailbone/templates/products/index.mako
Normal file
107
tailbone/templates/products/index.mako
Normal file
|
@ -0,0 +1,107 @@
|
|||
<%inherit file="/grid.mako" />
|
||||
|
||||
<%def name="title()">Products</%def>
|
||||
|
||||
<%def name="head_tags()">
|
||||
${parent.head_tags()}
|
||||
<style type="text/css">
|
||||
|
||||
table.grid-header td.tools table {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
table.grid-header td.tools table th,
|
||||
table.grid-header td.tools table td {
|
||||
padding: 0px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
table.grid-header td.tools table #label-quantity {
|
||||
text-align: right;
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
div.grid table tbody td.size,
|
||||
div.grid table tbody td.regular_price_uuid,
|
||||
div.grid table tbody td.current_price_uuid {
|
||||
padding-right: 6px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
div.grid table tbody td.labels {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
</style>
|
||||
% if label_profiles and request.has_perm('products.print_labels'):
|
||||
<script language="javascript" type="text/javascript">
|
||||
|
||||
$(function() {
|
||||
$('div.grid a.print-label').live('click', function() {
|
||||
var quantity = $('#label-quantity').val();
|
||||
if (isNaN(quantity)) {
|
||||
alert("You must provide a valid label quantity.");
|
||||
$('#label-quantity').select();
|
||||
$('#label-quantity').focus();
|
||||
} else {
|
||||
$.ajax({
|
||||
url: '${url('products.print_labels')}',
|
||||
data: {
|
||||
'product': get_uuid(this),
|
||||
'profile': $('#label-profile').val(),
|
||||
'quantity': quantity,
|
||||
},
|
||||
success: function(data) {
|
||||
if (data.error) {
|
||||
alert("An error occurred while attempting to print:\n\n" + data.error);
|
||||
} else if (quantity == '1') {
|
||||
alert("1 label has been printed.");
|
||||
} else {
|
||||
alert(quantity + " labels have been printed.");
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
<%def name="tools()">
|
||||
% if label_profiles and request.has_perm('products.print_labels'):
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Label</td>
|
||||
<td>Qty.</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<td>
|
||||
<select name="label-profile" id="label-profile">
|
||||
% for profile in label_profiles:
|
||||
<option value="${profile.uuid}">${profile.description}</option>
|
||||
% endfor
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" name="label-quantity" id="label-quantity" value="1" />
|
||||
</td>
|
||||
</tbody>
|
||||
</table>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
<%def name="context_menu_items()">
|
||||
% if request.has_perm('products.create'):
|
||||
<li>${h.link_to("Create a new Product", url('product.create'))}</li>
|
||||
% endif
|
||||
% if request.has_perm('batches.create'):
|
||||
<li>${h.link_to("Create Batch from Results", url('products.create_batch'))}</li>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
${parent.body()}
|
59
tailbone/templates/products/read.mako
Normal file
59
tailbone/templates/products/read.mako
Normal file
|
@ -0,0 +1,59 @@
|
|||
<%inherit file="/products/crud.mako" />
|
||||
|
||||
${parent.body()}
|
||||
|
||||
<% product = form.fieldset.model %>
|
||||
|
||||
<div id="product-codes">
|
||||
<h2>Product Codes:</h2>
|
||||
% if product.codes:
|
||||
<div class="grid hoverable">
|
||||
<table>
|
||||
<thead>
|
||||
<th>Code</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
% for i, code in enumerate(product.codes, 1):
|
||||
<tr class="${'odd' if i % 2 else 'even'}">
|
||||
<td>${code}</td>
|
||||
</tr>
|
||||
% endfor
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
% else:
|
||||
<p>None on file.</p>
|
||||
% endif
|
||||
</div>
|
||||
|
||||
<div id="product-costs">
|
||||
<h2>Product Costs:</h2>
|
||||
% if product.costs:
|
||||
<div class="grid hoverable">
|
||||
<table>
|
||||
<thead>
|
||||
<th>Pref.</th>
|
||||
<th>Vendor</th>
|
||||
<th>Code</th>
|
||||
<th>Case Size</th>
|
||||
<th>Case Cost</th>
|
||||
<th>Unit Cost</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
% for i, cost in enumerate(product.costs, 1):
|
||||
<tr class="${'odd' if i % 2 else 'even'}">
|
||||
<td class="center">${'X' if cost.preference == 1 else ''}</td>
|
||||
<td>${cost.vendor}</td>
|
||||
<td class="center">${cost.code}</td>
|
||||
<td class="center">${cost.case_size}</td>
|
||||
<td class="right">${'$ %0.2f' % cost.case_cost if cost.case_cost is not None else ''}</td>
|
||||
<td class="right">${'$ %0.4f' % cost.unit_cost if cost.unit_cost is not None else ''}</td>
|
||||
</tr>
|
||||
% endfor
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
% else:
|
||||
<p>None on file.</p>
|
||||
% endif
|
||||
</div>
|
2
tailbone/templates/reports/base.mako
Normal file
2
tailbone/templates/reports/base.mako
Normal file
|
@ -0,0 +1,2 @@
|
|||
<%inherit file="/base.mako" />
|
||||
${parent.body()}
|
29
tailbone/templates/reports/inventory.mako
Normal file
29
tailbone/templates/reports/inventory.mako
Normal file
|
@ -0,0 +1,29 @@
|
|||
<%inherit file="/reports/base.mako" />
|
||||
|
||||
<%def name="title()">Report : Inventory Worksheet</%def>
|
||||
|
||||
<p>Please provide the following criteria to generate your report:</p>
|
||||
<br />
|
||||
|
||||
${h.form(request.current_route_url())}
|
||||
|
||||
<div class="field-wrapper">
|
||||
<label for="department">Department</label>
|
||||
<div class="field">
|
||||
<select name="department">
|
||||
% for department in departments:
|
||||
<option value="${department.uuid}">${department.name}</option>
|
||||
% endfor
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
${h.checkbox('weighted-only', label=h.literal("Include items sold by weight <strong>only</strong>."))}
|
||||
</div>
|
||||
|
||||
<div class="buttons">
|
||||
${h.submit('submit', "Generate Report")}
|
||||
</div>
|
||||
|
||||
${h.end_form()}
|
88
tailbone/templates/reports/ordering.mako
Normal file
88
tailbone/templates/reports/ordering.mako
Normal file
|
@ -0,0 +1,88 @@
|
|||
<%inherit file="/reports/base.mako" />
|
||||
|
||||
<%def name="title()">Report : Ordering Worksheet</%def>
|
||||
|
||||
<%def name="head_tags()">
|
||||
${parent.head_tags()}
|
||||
<style type="text/css">
|
||||
|
||||
div.grid {
|
||||
clear: none;
|
||||
}
|
||||
|
||||
</style>
|
||||
</%def>
|
||||
|
||||
<p>Please provide the following criteria to generate your report:</p>
|
||||
<br />
|
||||
|
||||
${h.form(request.current_route_url())}
|
||||
${h.hidden('departments', value='')}
|
||||
|
||||
<div class="field-wrapper">
|
||||
${h.hidden('vendor', value='')}
|
||||
<label for="vendor-name">Vendor:</label>
|
||||
${h.text('vendor-name', size='40', value='')}
|
||||
<div id="vendor-display" style="display: none;">
|
||||
<span>(no vendor)</span>
|
||||
<button type="button" id="change-vendor">Change</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-wrapper">
|
||||
<label>Departments:</label>
|
||||
<div class="grid"></div>
|
||||
</div>
|
||||
|
||||
<div class="field-wrapper">
|
||||
${h.checkbox('preferred_only', label="Include only those products for which this vendor is preferred.", checked=True)}
|
||||
</div>
|
||||
|
||||
<div class="buttons">
|
||||
${h.submit('submit', "Generate Report")}
|
||||
</div>
|
||||
|
||||
${h.end_form()}
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
$(function() {
|
||||
|
||||
var autocompleter = $('#vendor-name').autocomplete({
|
||||
serviceUrl: '${url('vendors.autocomplete')}',
|
||||
width: 300,
|
||||
onSelect: function(value, data) {
|
||||
$('#vendor').val(data);
|
||||
$('#vendor-name').hide();
|
||||
$('#vendor-name').val('');
|
||||
$('#vendor-display span').html(value);
|
||||
$('#vendor-display').show();
|
||||
loading($('div.grid'));
|
||||
$('div.grid').load('${url('departments.by_vendor')}', {'uuid': data});
|
||||
},
|
||||
});
|
||||
|
||||
$('#vendor-name').focus();
|
||||
|
||||
$('#change-vendor').click(function() {
|
||||
$('#vendor').val('');
|
||||
$('#vendor-display').hide();
|
||||
$('#vendor-name').show();
|
||||
$('#vendor-name').focus();
|
||||
$('div.grid').empty();
|
||||
});
|
||||
|
||||
$('form').submit(function() {
|
||||
var depts = [];
|
||||
$('div.grid table tbody tr').each(function() {
|
||||
if ($(this).find('td.checkbox input[type=checkbox]').is(':checked')) {
|
||||
depts.push(get_uuid(this));
|
||||
}
|
||||
$('#departments').val(depts.toString());
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
</script>
|
12
tailbone/templates/stores/crud.mako
Normal file
12
tailbone/templates/stores/crud.mako
Normal file
|
@ -0,0 +1,12 @@
|
|||
<%inherit file="/crud.mako" />
|
||||
|
||||
<%def name="context_menu_items()">
|
||||
<li>${h.link_to("Back to Stores", url('stores'))}</li>
|
||||
% if form.readonly and request.has_perm('stores.update'):
|
||||
<li>${h.link_to("Edit this Store", url('store.update', uuid=form.fieldset.model.uuid))}</li>
|
||||
% elif form.updating:
|
||||
<li>${h.link_to("View this Store", url('store.read', uuid=form.fieldset.model.uuid))}</li>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
${parent.body()}
|
11
tailbone/templates/stores/index.mako
Normal file
11
tailbone/templates/stores/index.mako
Normal file
|
@ -0,0 +1,11 @@
|
|||
<%inherit file="/grid.mako" />
|
||||
|
||||
<%def name="title()">Stores</%def>
|
||||
|
||||
<%def name="context_menu_items()">
|
||||
% if request.has_perm('stores.create'):
|
||||
<li>${h.link_to("Create a new Store", url('store.create'))}</li>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
${parent.body()}
|
12
tailbone/templates/subdepartments/crud.mako
Normal file
12
tailbone/templates/subdepartments/crud.mako
Normal file
|
@ -0,0 +1,12 @@
|
|||
<%inherit file="/crud.mako" />
|
||||
|
||||
<%def name="context_menu_items()">
|
||||
<li>${h.link_to("Back to Subdepartments", url('subdepartments'))}</li>
|
||||
% if form.readonly:
|
||||
<li>${h.link_to("Edit this Subdepartment", url('subdepartment.update', uuid=form.fieldset.model.uuid))}</li>
|
||||
% elif form.updating:
|
||||
<li>${h.link_to("View this Subdepartment", url('subdepartment.read', uuid=form.fieldset.model.uuid))}</li>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
${parent.body()}
|
11
tailbone/templates/subdepartments/index.mako
Normal file
11
tailbone/templates/subdepartments/index.mako
Normal file
|
@ -0,0 +1,11 @@
|
|||
<%inherit file="/grid.mako" />
|
||||
|
||||
<%def name="title()">Subdepartments</%def>
|
||||
|
||||
<%def name="context_menu_items()">
|
||||
% if request.has_perm('subdepartments.create'):
|
||||
<li>${h.link_to("Create a new Subdepartment", url('subdepartment.create'))}</li>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
${parent.body()}
|
12
tailbone/templates/users/crud.mako
Normal file
12
tailbone/templates/users/crud.mako
Normal file
|
@ -0,0 +1,12 @@
|
|||
<%inherit file="/crud.mako" />
|
||||
|
||||
<%def name="context_menu_items()">
|
||||
<li>${h.link_to("Back to Users", url('users'))}</li>
|
||||
% if form.readonly:
|
||||
<li>${h.link_to("Edit this User", url('user.update', uuid=form.fieldset.model.uuid))}</li>
|
||||
% elif form.updating:
|
||||
<li>${h.link_to("View this User", url('user.read', uuid=form.fieldset.model.uuid))}</li>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
${parent.body()}
|
12
tailbone/templates/vendors/crud.mako
vendored
Normal file
12
tailbone/templates/vendors/crud.mako
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
<%inherit file="/crud.mako" />
|
||||
|
||||
<%def name="context_menu_items()">
|
||||
<li>${h.link_to("Back to Vendors", url('vendors'))}</li>
|
||||
% if form.readonly:
|
||||
<li>${h.link_to("Edit this Vendor", url('vendor.update', uuid=form.fieldset.model.uuid))}</li>
|
||||
% elif form.updating:
|
||||
<li>${h.link_to("View this Vendor", url('vendor.read', uuid=form.fieldset.model.uuid))}</li>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
${parent.body()}
|
11
tailbone/templates/vendors/index.mako
vendored
Normal file
11
tailbone/templates/vendors/index.mako
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
<%inherit file="/grid.mako" />
|
||||
|
||||
<%def name="title()">Vendors</%def>
|
||||
|
||||
<%def name="context_menu_items()">
|
||||
% if request.has_perm('vendors.create'):
|
||||
<li>${h.link_to("Create a new Vendor", url('vendor.create'))}</li>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
${parent.body()}
|
47
tailbone/views/__init__.py
Normal file
47
tailbone/views/__init__.py
Normal 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')
|
61
tailbone/views/autocomplete.py
Normal file
61
tailbone/views/autocomplete.py
Normal 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]
|
35
tailbone/views/batches/__init__.py
Normal file
35
tailbone/views/batches/__init__.py
Normal 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')
|
203
tailbone/views/batches/core.py
Normal file
203
tailbone/views/batches/core.py
Normal 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')
|
52
tailbone/views/batches/params/__init__.py
Normal file
52
tailbone/views/batches/params/__init__.py
Normal 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')
|
51
tailbone/views/batches/params/labels.py
Normal file
51
tailbone/views/batches/params/labels.py
Normal 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')
|
222
tailbone/views/batches/rows.py
Normal file
222
tailbone/views/batches/rows.py
Normal 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
124
tailbone/views/brands.py
Normal 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')
|
110
tailbone/views/categories.py
Normal file
110
tailbone/views/categories.py
Normal 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
35
tailbone/views/core.py
Normal 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
212
tailbone/views/crud.py
Normal 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)
|
119
tailbone/views/customergroups.py
Normal file
119
tailbone/views/customergroups.py
Normal 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
165
tailbone/views/customers.py
Normal 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')
|
160
tailbone/views/departments.py
Normal file
160
tailbone/views/departments.py
Normal 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
178
tailbone/views/employees.py
Normal 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')
|
30
tailbone/views/grids/__init__.py
Normal file
30
tailbone/views/grids/__init__.py
Normal 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 *
|
181
tailbone/views/grids/alchemy.py
Normal file
181
tailbone/views/grids/alchemy.py
Normal 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)
|
68
tailbone/views/grids/core.py
Normal file
68
tailbone/views/grids/core.py
Normal 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
192
tailbone/views/labels.py
Normal 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
156
tailbone/views/people.py
Normal 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
368
tailbone/views/products.py
Normal 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
197
tailbone/views/reports.py
Normal 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
218
tailbone/views/roles.py
Normal 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
134
tailbone/views/stores.py
Normal 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')
|
116
tailbone/views/subdepartments.py
Normal file
116
tailbone/views/subdepartments.py
Normal 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
146
tailbone/views/users.py
Normal 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
131
tailbone/views/vendors.py
Normal 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')
|
Loading…
Add table
Add a link
Reference in a new issue