diff --git a/tailbone/newgrids/alchemy.py b/tailbone/newgrids/alchemy.py index 0663ec73..80f727c1 100644 --- a/tailbone/newgrids/alchemy.py +++ b/tailbone/newgrids/alchemy.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2015 Lance Edgar +# Copyright © 2010-2016 Lance Edgar # # This file is part of Rattail. # @@ -24,18 +24,20 @@ FormAlchemy Grid Classes """ -from __future__ import unicode_literals +from __future__ import unicode_literals, absolute_import import logging import sqlalchemy as sa from sqlalchemy import orm +from edbob.util import prettify + +from rattail.db.types import GPCType + import formalchemy from webhelpers import paginate -from edbob.util import prettify - from tailbone.db import Session from tailbone.newgrids import Grid, GridColumn, filters @@ -64,7 +66,8 @@ class AlchemyGrid(Grid): def __init__(self, *args, **kwargs): super(AlchemyGrid, self).__init__(*args, **kwargs) fa_grid = formalchemy.Grid(self.model_class, instances=self.data, - session=Session(), request=self.request) + session=kwargs.get('session', Session()), + request=self.request) fa_grid.prettify = prettify self._fa_grid = fa_grid @@ -99,6 +102,8 @@ class AlchemyGrid(Grid): factory = filters.AlchemyBooleanFilter elif isinstance(column.type, (sa.Date, sa.DateTime)): factory = filters.AlchemyDateFilter + elif isinstance(column.type, GPCType): + factory = filters.AlchemyGPCFilter return factory(key, column=column, **kwargs) def iter_filters(self): @@ -107,6 +112,24 @@ class AlchemyGrid(Grid): """ return self.filters.itervalues() + def filter_data(self, query): + """ + Filter and return the given data set, according to current settings. + """ + # This overrides the core version only slightly, in that it will only + # invoke a join if any particular filter(s) actually modifies the + # query. The main motivation for this is on the products page, where + # the tricky "vendor (any)" filter has a weird join and causes + # unpredictable results. Now we can skip the join for that unless the + # user actually enters some criteria for it. + for filtr in self.iter_active_filters(): + original = query + query = filtr.filter(query) + if query is not original and filtr.key in self.joiners and filtr.key not in self.joined: + query = self.joiners[filtr.key](query) + self.joined.add(filtr.key) + return query + def make_sorters(self, sorters): """ Returns a mapping of sort options for the grid. Keyword args override diff --git a/tailbone/newgrids/core.py b/tailbone/newgrids/core.py index 45ad3393..38dc019a 100644 --- a/tailbone/newgrids/core.py +++ b/tailbone/newgrids/core.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2015 Lance Edgar +# Copyright © 2010-2016 Lance Edgar # # This file is part of Rattail. # @@ -24,7 +24,7 @@ Core Grid Classes """ -from __future__ import unicode_literals +from __future__ import unicode_literals, absolute_import from edbob.util import prettify @@ -246,11 +246,17 @@ class Grid(object): """ Check to see if the current user has default settings on file for this grid. """ - if not self.request.user: + user = self.request.user + if not user: return False + + session = getattr(self, 'session', Session()) + if user not in session: + user = session.merge(user) + # User defaults should have all or nothing, so just check one key. - key = 'tailbone.{0}.grid.{1}.sortkey'.format(self.request.user.uuid, self.key) - return get_setting(Session(), key) is not None + key = 'tailbone.{}.grid.{}.sortkey'.format(user.uuid, self.key) + return get_setting(session, key) is not None def apply_user_defaults(self, settings): """ @@ -600,10 +606,11 @@ class Grid(object): """ url = action.get_url(row) if url: + kwargs = {'class_': action.key} if action.icon: - icon = HTML.tag('span', class_='ui-icon ui-icon-{0}'.format(action.icon)) - return tags.link_to(icon + action.label, url) - return tags.link_to(action.label, url) + icon = HTML.tag('span', class_='ui-icon ui-icon-{}'.format(action.icon)) + return tags.link_to(icon + action.label, url, **kwargs) + return tags.link_to(action.label, url, **kwargs) def iter_rows(self): return self.make_visible_data() diff --git a/tailbone/newgrids/filters.py b/tailbone/newgrids/filters.py index fa5b71a7..2916abab 100644 --- a/tailbone/newgrids/filters.py +++ b/tailbone/newgrids/filters.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2015 Lance Edgar +# Copyright © 2010-2016 Lance Edgar # # This file is part of Rattail. # @@ -24,12 +24,13 @@ Grid Filters """ -from __future__ import unicode_literals +from __future__ import unicode_literals, absolute_import import sqlalchemy as sa from edbob.util import prettify +from rattail.gpc import GPC from rattail.util import OrderedDict from rattail.core import UNSPECIFIED @@ -367,6 +368,44 @@ class AlchemyDateFilter(AlchemyGridFilter): return self.filter_is_null(query, value) +class AlchemyGPCFilter(AlchemyGridFilter): + """ + GPC filter for SQLAlchemy. + """ + default_verbs = ['equal', 'not_equal'] + + def filter_equal(self, query, value): + """ + Filter data with an equal ('=') query. + """ + if value is None or value == '': + return query + try: + return query.filter(self.column.in_(( + GPC(value), + GPC(value, calc_check_digit='upc')))) + except ValueError: + return query + + def filter_not_equal(self, query, value): + """ + Filter data with a not eqaul ('!=') query. + """ + if value is None or value == '': + return query + + # When saying something is 'not equal' to something else, we must also + # include things which are nothing at all, in our result set. + try: + return query.filter(sa.or_( + ~self.column.in_(( + GPC(value), + GPC(value, calc_check_digit='upc'))), + self.column == None)) + except ValueError: + return query + + class GridFilterSet(OrderedDict): """ Collection class for :class:`GridFilter` instances. diff --git a/tailbone/static/css/newgrids.css b/tailbone/static/css/newgrids.css index 54c96a2d..59327e24 100644 --- a/tailbone/static/css/newgrids.css +++ b/tailbone/static/css/newgrids.css @@ -17,6 +17,7 @@ .newgrid-wrapper .newfilters fieldset { margin: -8px 0 5px 0; padding: 1px 5px 5px 5px; + width: 80%; } .newgrid-wrapper .newfilters .filter { diff --git a/tailbone/templates/products/batch.mako b/tailbone/templates/products/batch.mako index a0aaeaa4..ee44d607 100644 --- a/tailbone/templates/products/batch.mako +++ b/tailbone/templates/products/batch.mako @@ -20,7 +20,7 @@
${h.submit('create', "Create Batch")} - + ${h.link_to("Cancel", url('products'), class_='button')}
${h.end_form()} diff --git a/tailbone/templates/products/crud.mako b/tailbone/templates/products/crud.mako deleted file mode 100644 index 75a0421c..00000000 --- a/tailbone/templates/products/crud.mako +++ /dev/null @@ -1,16 +0,0 @@ -## -*- coding: utf-8 -*- -<%inherit file="/crud.mako" /> - -<%def name="context_menu_items()"> -
  • ${h.link_to("Back to Products", url('products'))}
  • - % if form.readonly and request.has_perm('products.update'): -
  • ${h.link_to("Edit this Product", url('product.update', uuid=form.fieldset.model.uuid))}
  • - % elif form.updating: -
  • ${h.link_to("View this Product", url('product.read', uuid=form.fieldset.model.uuid))}
  • - % endif - % if version_count is not Undefined and request.has_perm('product.versions.view'): -
  • ${h.link_to("View Change History ({0})".format(version_count), url('product.versions', uuid=form.fieldset.model.uuid))}
  • - % endif - - -${parent.body()} diff --git a/tailbone/templates/products/edit.mako b/tailbone/templates/products/edit.mako new file mode 100644 index 00000000..981d42ab --- /dev/null +++ b/tailbone/templates/products/edit.mako @@ -0,0 +1,11 @@ +## -*- coding: utf-8 -*- +<%inherit file="/master/edit.mako" /> + +<%def name="context_menu_items()"> + ${parent.context_menu_items()} + % if version_count is not Undefined and request.has_perm('product.versions.view'): +
  • ${h.link_to("View Change History ({})".format(version_count), url('product.versions', uuid=instance.uuid))}
  • + % endif + + +${parent.body()} diff --git a/tailbone/templates/products/index.mako b/tailbone/templates/products/index.mako index fec57c50..b4b2c61b 100644 --- a/tailbone/templates/products/index.mako +++ b/tailbone/templates/products/index.mako @@ -1,7 +1,5 @@ ## -*- coding: utf-8 -*- -<%inherit file="/grid.mako" /> - -<%def name="title()">Products +<%inherit file="/master/index.mako" /> <%def name="head_tags()"> ${parent.head_tags()} @@ -35,49 +33,48 @@ % if label_profiles and request.has_perm('products.print_labels'): - % endif -<%def name="tools()"> +<%def name="grid_tools()"> % if label_profiles and request.has_perm('products.print_labels'): - +
    - - + + @@ -97,9 +94,7 @@ <%def name="context_menu_items()"> - % if request.has_perm('products.create'): -
  • ${h.link_to("Create a new Product", url('product.create'))}
  • - % endif + ${parent.context_menu_items()} % if request.has_perm('batches.create'):
  • ${h.link_to("Create Batch from Results", url('products.create_batch'))}
  • % endif diff --git a/tailbone/templates/products/read.mako b/tailbone/templates/products/view.mako similarity index 93% rename from tailbone/templates/products/read.mako rename to tailbone/templates/products/view.mako index e084def3..b8414941 100644 --- a/tailbone/templates/products/read.mako +++ b/tailbone/templates/products/view.mako @@ -1,7 +1,9 @@ ## -*- coding: utf-8 -*- -<%inherit file="/products/crud.mako" /> +<%inherit file="/master/view.mako" /> <%namespace file="/forms/lib.mako" import="render_field_readonly" /> +<% product = instance %> + <%def name="head_tags()"> ${parent.head_tags()} -<% product = form.fieldset.model %> +<%def name="context_menu_items()"> + ${parent.context_menu_items()} + % if version_count is not Undefined and request.has_perm('product.versions.view'): +
  • ${h.link_to("View Change History ({})".format(version_count), url('product.versions', uuid=product.uuid))}
  • + % endif + <%def name="render_organization_fields(form)"> ${render_field_readonly(form.fieldset.department)} diff --git a/tailbone/views/master.py b/tailbone/views/master.py index ab847764..b216b543 100644 --- a/tailbone/views/master.py +++ b/tailbone/views/master.py @@ -330,20 +330,18 @@ class MasterView(View): """ return getattr(cls, 'grid_key', '{0}s'.format(cls.get_normalized_model_name())) - def make_grid_kwargs(self): + def make_grid_kwargs(self, **kwargs): """ Return a dictionary of kwargs to be passed to the factory when creating new grid instances. """ - return { + defaults = { 'width': 'full', 'filterable': self.filterable, 'sortable': True, 'default_sortkey': getattr(self, 'default_sortkey', None), 'sortdir': getattr(self, 'sortdir', 'asc'), 'pageable': self.pageable, - 'main_actions': self.get_main_actions(), - 'more_actions': self.get_more_actions(), 'checkboxes': self.checkboxes, 'checked': self.checked, 'row_attrs': self.get_row_attrs, @@ -353,6 +351,15 @@ class MasterView(View): 'permission_prefix': self.get_permission_prefix(), 'route_prefix': self.get_route_prefix(), } + if 'main_actions' not in kwargs and 'more_actions' not in kwargs: + main, more = self.get_grid_actions() + defaults['main_actions'] = main + defaults['more_actions'] = more + defaults.update(kwargs) + return defaults + + def get_grid_actions(self): + return self.get_main_actions(), self.get_more_actions() def get_row_attrs(self, row, i): """ @@ -379,7 +386,8 @@ class MasterView(View): Return a list of 'main' actions for the grid. """ actions = [] - if self.viewable: + prefix = self.get_permission_prefix() + if self.viewable and self.request.has_perm('{}.view'.format(prefix)): actions.append(self.make_action('view', icon='zoomin')) return actions @@ -388,9 +396,10 @@ class MasterView(View): Return a list of 'more' actions for the grid. """ actions = [] - if self.editable: + prefix = self.get_permission_prefix() + if self.editable and self.request.has_perm('{}.edit'.format(prefix)): actions.append(self.make_action('edit', icon='pencil')) - if self.deletable: + if self.deletable and self.request.has_perm('{}.delete'.format(prefix)): actions.append(self.make_action('delete', icon='trash', url=self.default_delete_url)) return actions @@ -421,14 +430,14 @@ class MasterView(View): values = [getattr(row, k) for k in keys] return dict(zip(keys, values)) - def make_grid(self): + def make_grid(self, **kwargs): """ Make and return a new (configured) grid instance. """ factory = self.get_grid_factory() key = self.get_grid_key() - data = self.get_data() - kwargs = self.make_grid_kwargs() + data = self.get_data(session=kwargs.get('session')) + kwargs = self.make_grid_kwargs(**kwargs) grid = factory(key, self.request, data=data, model_class=self.get_model_class(error=False), **kwargs) self.configure_grid(grid) grid.load_settings() diff --git a/tailbone/views/messages.py b/tailbone/views/messages.py index c1b3dbba..3b357902 100644 --- a/tailbone/views/messages.py +++ b/tailbone/views/messages.py @@ -106,8 +106,12 @@ class TerseRecipientsFieldRenderer(formalchemy.FieldRenderer): recipients = self.raw_value if not recipients: return '' - recips = filter(lambda r: r.recipient is not self.request.user, recipients) + message = self.field.parent.model + recips = [r for r in recipients if r.recipient is not self.request.user] recips = sorted([r.recipient.display_name for r in recips]) + if len(recips) < len(recipients) and ( + message.sender is not self.request.user or not recips): + recips.insert(0, 'you') if len(recips) < 5: return ', '.join(recips) return "{}, ...".format(', '.join(recips[:4])) diff --git a/tailbone/views/products.py b/tailbone/views/products.py index 2dc17a7f..72f3378c 100644 --- a/tailbone/views/products.py +++ b/tailbone/views/products.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2015 Lance Edgar +# Copyright © 2010-2016 Lance Edgar # # This file is part of Rattail. # @@ -32,158 +32,126 @@ import re import sqlalchemy as sa from sqlalchemy import orm +from rattail import enum, pod, sil, batches +from rattail.db import model, api, auth, Session as RattailSession +from rattail.gpc import GPC +from rattail.threads import Thread +from rattail.exceptions import LabelPrintingError + import formalchemy -from pyramid.httpexceptions import HTTPFound +from pyramid import httpexceptions from pyramid.renderers import render_to_response from webhelpers.html import tags -import rattail.labels -from rattail import enum -from rattail import sil -from rattail import batches -from rattail.threads import Thread -from rattail.exceptions import LabelPrintingError -from rattail.db import model -from rattail.db.model import ( - Product, ProductPrice, ProductCost, ProductCode, - Brand, Vendor, Department, Subdepartment, LabelProfile) -from rattail.gpc import GPC -from rattail.db.api import get_product_by_upc -from rattail.db.util import configure_session -from rattail.pod import get_image_url, get_image_path - -from tailbone.views import SearchableAlchemyGridView, CrudView, AutocompleteView -from tailbone.views.continuum import VersionView, version_defaults -from tailbone.forms import EnumFieldRenderer, DateTimeFieldRenderer +from tailbone import forms from tailbone.db import Session -from tailbone.forms import GPCFieldRenderer, BrandFieldRenderer, PriceFieldRenderer -from tailbone.forms.renderers import products as forms +from tailbone.views import MasterView, SearchableAlchemyGridView, AutocompleteView +from tailbone.views.continuum import VersionView, version_defaults +from tailbone.forms.renderers import products as products_forms +from tailbone.newgrids import GridAction from tailbone.progress import SessionProgress -class ProductsGrid(SearchableAlchemyGridView): +class ProductsView(MasterView): + """ + Master view for the Product class. + """ + model_class = model.Product - mapped_class = Product - config_prefix = 'products' - sort = 'description' + # child_version_classes = [ + # (model.ProductCode, 'product_uuid'), + # (model.ProductCost, 'product_uuid'), + # (model.ProductPrice, 'product_uuid'), + # ] # These aliases enable the grid queries to filter products which may be # purchased from *any* vendor, and yet sort by only the "preferred" vendor # (since that's what shows up in the grid column). - ProductCostAny = orm.aliased(ProductCost) - VendorAny = orm.aliased(Vendor) + ProductCostAny = orm.aliased(model.ProductCost) + VendorAny = orm.aliased(model.Vendor) - def join_map(self): + def __init__(self, request): + self.request = request + self.print_labels = request.rattail_config.getbool('tailbone', 'products.print_labels', default=False) + + def query(self, session): + user = self.request.user + if user and user not in session: + user = session.merge(user) + + query = session.query(model.Product) + if not auth.has_permission(session, user, 'products.view_deleted'): + query = query.filter(model.Product.deleted == False) + + # TODO: This used to be a good idea I thought...but in dev it didn't + # seem to make much difference, except with a larger (50K) data set it + # totally bogged things down instead of helping... + # query = query\ + # .options(orm.joinedload(model.Product.brand))\ + # .options(orm.joinedload(model.Product.department))\ + # .options(orm.joinedload(model.Product.subdepartment))\ + # .options(orm.joinedload(model.Product.regular_price))\ + # .options(orm.joinedload(model.Product.current_price))\ + # .options(orm.joinedload(model.Product.vendor)) + + return query + + def configure_grid(self, g): def join_vendor(q): - q = q.outerjoin( - ProductCost, - sa.and_( - ProductCost.product_uuid == Product.uuid, - ProductCost.preference == 1, - )) - q = q.outerjoin(Vendor) - return q + return q.outerjoin(model.ProductCost, + sa.and_( + model.ProductCost.product_uuid == model.Product.uuid, + model.ProductCost.preference == 1))\ + .outerjoin(model.Vendor) def join_vendor_any(q): - q = q.outerjoin( - self.ProductCostAny, - self.ProductCostAny.product_uuid == Product.uuid) - q = q.outerjoin( - self.VendorAny, - self.VendorAny.uuid == self.ProductCostAny.vendor_uuid) - return q + return q.outerjoin(self.ProductCostAny, + self.ProductCostAny.product_uuid == model.Product.uuid)\ + .outerjoin(self.VendorAny, + self.VendorAny.uuid == self.ProductCostAny.vendor_uuid) - return { - 'brand': - lambda q: q.outerjoin(Brand), - 'family': - lambda q: q.outerjoin(model.Family), - 'department': - lambda q: q.outerjoin(Department, - Department.uuid == Product.department_uuid), - 'subdepartment': - lambda q: q.outerjoin(Subdepartment, - Subdepartment.uuid == Product.subdepartment_uuid), - u'report_code': - lambda q: q.outerjoin(model.ReportCode), - '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, - 'vendor_any': - join_vendor_any, - 'code': - lambda q: q.outerjoin(ProductCode), - } + g.joiners['brand'] = lambda q: q.outerjoin(model.Brand) + g.joiners['family'] = lambda q: q.outerjoin(model.Family) + g.joiners['department'] = lambda q: q.outerjoin(model.Department, + model.Department.uuid == model.Product.department_uuid) + g.joiners['subdepartment'] = lambda q: q.outerjoin(model.Subdepartment, + model.Subdepartment.uuid == model.Product.subdepartment_uuid) + g.joiners['report_code'] = lambda q: q.outerjoin(model.ReportCode) + g.joiners['regular_price'] = lambda q: q.outerjoin(model.ProductPrice, + model.ProductPrice.uuid == model.Product.regular_price_uuid) + g.joiners['current_price'] = lambda q: q.outerjoin(model.ProductPrice, + model.ProductPrice.uuid == model.Product.current_price_uuid) + g.joiners['vendor'] = join_vendor + g.joiners['vendor_any'] = join_vendor_any + g.joiners['code'] = lambda q: q.outerjoin(model.ProductCode) - def filter_map(self): - return self.make_filter_map( - ilike=['description', 'size'], - upc=self.filter_gpc(model.Product.upc), - brand=self.filter_ilike(Brand.name), - family=self.filter_ilike(model.Family.name), - department=self.filter_ilike(Department.name), - report_code=self.filter_ilike(model.ReportCode.name), - subdepartment=self.filter_ilike(Subdepartment.name), - vendor=self.filter_ilike(Vendor.name), - vendor_any=self.filter_ilike(self.VendorAny.name), - code=self.filter_ilike(ProductCode.code)) + g.sorters['brand'] = g.make_sorter(model.Brand.name) + g.sorters['department'] = g.make_sorter(model.Department.name) + g.sorters['subdepartment'] = g.make_sorter(model.Subdepartment.name) + g.sorters['vendor'] = g.make_sorter(model.Vendor.name) - def filter_config(self): - return self.make_filter_config( - include_filter_upc=True, - filter_type_upc='is', - 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', - filter_label_vendor="Vendor (preferred)", - include_filter_vendor_any=True, - filter_label_vendor_any="Vendor (any)", - filter_type_vendor_any='lk') + g.filters['upc'].default_active = True + g.filters['upc'].default_verb = 'equal' + g.filters['upc'].label = "UPC" + g.filters['description'].default_active = True + g.filters['description'].default_verb = 'contains' + g.filters['brand'] = g.make_filter('brand', model.Brand.name, + default_active=True, default_verb='contains') + g.filters['family'] = g.make_filter('family', model.Family.name) + g.filters['department'] = g.make_filter('department', model.Department.name, + default_active=True, default_verb='contains') + g.filters['subdepartment'] = g.make_filter('subdepartment', model.Subdepartment.name) + g.filters['report_code'] = g.make_filter('report_code', model.ReportCode.name) + g.filters['vendor'] = g.make_filter('vendor', model.Vendor.name, label="Vendor (preferred)") + g.filters['vendor_any'] = g.make_filter('vendor_any', self.VendorAny.name, label="Vendor (any)") + g.filters['code'] = g.make_filter('code', model.ProductCode.code) - 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)) + g.default_sortkey = 'description' - def query(self): - q = self.make_query() - if not self.request.has_perm('products.view_deleted'): - q = q.filter(model.Product.deleted == False) - q = q.options(orm.joinedload(Product.brand)) - q = q.options(orm.joinedload(Product.department)) - q = q.options(orm.joinedload(Product.subdepartment)) - q = q.options(orm.joinedload(Product.regular_price)) - q = q.options(orm.joinedload(Product.current_price)) - q = q.options(orm.joinedload(Product.vendor)) - return q - - def grid(self): - def extra_row_class(row, i): - cls = [] - if row.not_for_sale: - cls.append('not-for-sale') - if row.deleted: - cls.append('deleted') - return ' '.join(cls) if cls else None - g = self.make_grid(extra_row_class=extra_row_class) - g.upc.set(renderer=GPCFieldRenderer) - g.regular_price.set(renderer=PriceFieldRenderer) - g.current_price.set(renderer=PriceFieldRenderer) + g.upc.set(renderer=forms.renderers.GPCFieldRenderer) + g.regular_price.set(renderer=forms.renderers.PriceFieldRenderer) + g.current_price.set(renderer=forms.renderers.PriceFieldRenderer) g.configure( include=[ g.upc.label("UPC"), @@ -197,80 +165,68 @@ class ProductsGrid(SearchableAlchemyGridView): ], 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' + # TODO: need to check for 'print labels' permission here also + if self.print_labels: + g.more_actions.append(GridAction('print_label', icon='print')) - # Maybe add Print Label column. - if self.rattail_config.getbool('tailbone', 'products.print_labels', default=True): - q = Session.query(LabelProfile) - if q.count(): - def labels(row): - return tags.link_to("Print", '#', class_='print-label') - g.add_column('labels', "Labels", labels) + def template_kwargs_index(self, **kwargs): + if self.print_labels: + kwargs['label_profiles'] = Session.query(model.LabelProfile)\ + .filter(model.LabelProfile.visible == True)\ + .order_by(model.LabelProfile.ordinal)\ + .all() + return kwargs - return g + def row_attrs(self, row, i): - 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()} + attrs = {'uuid': row.uuid} + classes = [] + if row.not_for_sale: + classes.append('not-for-sale') + if row.deleted: + classes.append('deleted') + if classes: + attrs['class_'] = ' '.join(classes) -class ProductCrud(CrudView): - """ - Product CRUD view class. - """ - mapped_class = Product - home_route = 'products' - child_version_classes = [ - (model.ProductCode, 'product_uuid'), - (model.ProductCost, 'product_uuid'), - (model.ProductPrice, 'product_uuid'), - ] + return attrs - 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 get_instance(self): + key = self.request.matchdict['uuid'] + product = Session.query(model.Product).get(key) + if product: + return product + price = Session.query(model.ProductPrice).get(key) + if price: + return price.product + raise httpexceptions.HTTPNotFound() - def fieldset(self, model): - fs = self.make_fieldset(model) - fs.upc.set(renderer=GPCFieldRenderer) + def configure_fieldset(self, fs): + + fs.upc.set(renderer=forms.renderers.GPCFieldRenderer) fs.brand.set(options=[]) - fs.unit_of_measure.set(renderer=EnumFieldRenderer(enum.UNIT_OF_MEASURE)) - fs.regular_price.set(renderer=PriceFieldRenderer) - fs.current_price.set(renderer=PriceFieldRenderer) + fs.unit_of_measure.set(renderer=forms.renderers.EnumFieldRenderer(enum.UNIT_OF_MEASURE)) + fs.regular_price.set(renderer=forms.renderers.PriceFieldRenderer) + fs.current_price.set(renderer=forms.renderers.PriceFieldRenderer) + fs.last_sold.set(renderer=forms.renderers.DateTimeFieldRenderer(self.rattail_config)) - fs.append(formalchemy.Field('current_price_ends')) - fs.current_price_ends.set(value=lambda p: p.current_price.ends if p.current_price else None) - fs.current_price_ends.set(renderer=DateTimeFieldRenderer(self.request.rattail_config)) + fs.append(formalchemy.Field('current_price_ends', + value=lambda p: p.current_price.ends if p.current_price else None, + renderer=forms.renderers.DateTimeFieldRenderer(self.rattail_config))) - fs.last_sold.set(renderer=DateTimeFieldRenderer(self.request.rattail_config)) fs.configure( include=[ fs.upc.label("UPC"), - fs.brand.with_renderer(BrandFieldRenderer), + fs.brand.with_renderer(forms.renderers.BrandFieldRenderer), fs.description, fs.unit_size, fs.unit_of_measure.label("Unit of Measure"), fs.size, fs.weighed, fs.case_pack, - fs.department.with_renderer(forms.DepartmentFieldRenderer), - fs.subdepartment.with_renderer(forms.SubdepartmentFieldRenderer), - fs.category.with_renderer(forms.CategoryFieldRenderer), + fs.department.with_renderer(products_forms.DepartmentFieldRenderer), + fs.subdepartment.with_renderer(products_forms.SubdepartmentFieldRenderer), + fs.category.with_renderer(products_forms.CategoryFieldRenderer), fs.family, fs.report_code, fs.regular_price, @@ -285,33 +241,119 @@ class ProductCrud(CrudView): fs.deleted, fs.last_sold, ]) - if not self.readonly: + if not self.viewing: del fs.regular_price del fs.current_price if not self.request.has_perm('products.view_deleted'): del fs.deleted - return fs - def pre_crud(self, product): - self.product_deleted = not self.creating and product.deleted - - def post_crud(self, product, form): - if self.product_deleted: - self.request.session.flash("This product is marked as deleted.", 'error') - - def template_kwargs(self, form): - kwargs = super(ProductCrud, self).template_kwargs(form) + def template_kwargs_view(self, **kwargs): kwargs['image'] = False - product = form.fieldset.model + product = kwargs['instance'] if product.upc: - kwargs['image_url'] = get_image_url( - self.request.rattail_config, product.upc) - kwargs['image_path'] = get_image_path( - self.request.rattail_config, product.upc) + kwargs['image_url'] = pod.get_image_url(self.rattail_config, product.upc) + kwargs['image_path'] = pod.get_image_path(self.rattail_config, product.upc) if os.path.exists(kwargs['image_path']): kwargs['image'] = True return kwargs + def edit(self): + # TODO: Should add some more/better hooks, so don't have to duplicate + # so much code here. + self.editing = True + instance = self.get_instance() + form = self.make_form(instance) + product_deleted = instance.deleted + if self.request.method == 'POST': + if form.validate(): + self.save_form(form) + self.after_edit(instance) + self.request.session.flash("{} {} has been updated.".format( + self.get_model_title(), self.get_instance_title(instance))) + return self.redirect(self.get_action_url('view', instance)) + if product_deleted: + self.request.session.flash("This product is marked as deleted.", 'error') + return self.render_to_response('edit', {'instance': instance, + 'instance_title': self.get_instance_title(instance), + 'form': form}) + + def make_batch(self): + if self.request.method == 'POST': + provider = self.request.POST.get('provider') + if provider: + provider = batches.get_provider(self.rattail_config, provider) + if provider: + + if self.request.POST.get('params') == 'True': + provider.set_params(Session(), **self.request.POST) + + else: + try: + url = self.request.route_url('batch_params.{}'.format(provider.name)) + except KeyError: + pass + else: + self.request.session['referer'] = self.request.current_route_url() + return httpexceptions.HTTPFound(location=url) + + progress = SessionProgress(self.request, 'products.batch') + thread = Thread(target=self.make_batch_thread, args=(provider, progress)) + thread.start() + kwargs = { + 'key': 'products.batch', + 'cancel_url': self.request.route_url('products'), + 'cancel_msg': "Batch creation was canceled.", + } + return render_to_response('/progress.mako', kwargs, request=self.request) + + enabled = self.rattail_config.get('rattail.pyramid', 'batches.providers') + if enabled: + enabled = enabled.split() + + providers = [] + for provider in batches.iter_providers(): + if not enabled or provider.name in enabled: + providers.append((provider.name, provider.description)) + + return {'providers': providers} + + def make_batch_thread(self, provider, progress): + """ + Threat target for making a batch from current products query. + """ + session = RattailSession() + + grid = self.make_grid(session=session, pageable=False, + main_actions=[], more_actions=[]) + products = grid._fa_grid.rows + + batch = provider.make_batch(session, products, progress) + if not batch: + session.rollback() + session.close() + return + + session.commit() + session.refresh(batch) + session.close() + + progress.session.load() + progress.session['complete'] = True + progress.session['success_url'] = self.request.route_url('batch.read', uuid=batch.uuid) + progress.session['success_msg'] = 'Batch "{}" has been created.'.format(batch.description) + progress.session.save() + + @classmethod + def defaults(cls, config): + + # make batch from product query + config.add_route('products.create_batch', '/products/batch') + config.add_view(cls, attr='make_batch', route_name='products.create_batch', + renderer='/products/batch.mako', + permission='batches.create') + + cls._defaults(config) + class ProductVersionView(VersionView): """ @@ -331,7 +373,7 @@ class ProductVersionView(VersionView): """ uuid = self.request.matchdict['uuid'] product = Session.query(model.Product).get(uuid) - assert product, "No product found for UUID: {0}".format(repr(uuid)) + assert product, "No product found for UUID: {}".format(repr(uuid)) if product.deleted: self.request.session.flash("This product is marked as deleted.", 'error') @@ -354,8 +396,8 @@ class ProductsAutocomplete(AutocompleteView): def query(self, term): q = Session.query(model.Product).outerjoin(model.Brand) q = q.filter(sa.or_( - model.Brand.name.ilike('%{0}%'.format(term)), - model.Product.description.ilike('%{0}%'.format(term)))) + model.Brand.name.ilike('%{}%'.format(term)), + model.Product.description.ilike('%{}%'.format(term)))) if not self.request.has_perm('products.view_deleted'): q = q.filter(model.Product.deleted == False) q = q.order_by(model.Brand.name, model.Product.description) @@ -377,11 +419,11 @@ def products_search(request): upc = request.GET.get('upc', '').strip() upc = re.sub(r'\D', '', upc) if upc: - product = get_product_by_upc(Session, upc) + product = api.get_product_by_upc(Session(), upc) if not product: # Try again, assuming caller did not include check digit. upc = GPC(upc, calc_check_digit='upc') - product = get_product_by_upc(Session, upc) + product = api.get_product_by_upc(Session(), upc) if product: if product.deleted and not request.has_perm('products.view_deleted'): product = None @@ -396,12 +438,12 @@ def products_search(request): def print_labels(request): profile = request.params.get('profile') - profile = Session.query(LabelProfile).get(profile) if profile else None + profile = Session.query(model.LabelProfile).get(profile) if profile else None if not profile: return {'error': "Label profile not found"} product = request.params.get('product') - product = Session.query(Product).get(product) if product else None + product = Session.query(model.Product).get(product) if product else None if not product: return {'error': "Product not found"} @@ -421,114 +463,19 @@ def print_labels(request): return {} -class CreateProductsBatch(ProductsGrid): - - def make_batch(self, provider, progress): - from rattail.db import Session - session = Session() - configure_session(self.request.rattail_config, session) - - self._filter_config = self.filter_config() - self._sort_config = self.sort_config() - products = self.make_query(session) - - batch = provider.make_batch(session, products, progress) - if not batch: - session.rollback() - session.close() - return - - session.commit() - session.refresh(batch) - session.close() - - progress.session.load() - progress.session['complete'] = True - progress.session['success_url'] = self.request.route_url('batch.read', uuid=batch.uuid) - progress.session['success_msg'] = "Batch \"%s\" has been created." % batch.description - progress.session.save() - - def __call__(self): - if self.request.POST: - provider = self.request.POST.get('provider') - if provider: - provider = batches.get_provider(self.request.rattail_config, provider) - if provider: - - if self.request.POST.get('params') == 'True': - provider.set_params(Session(), **self.request.POST) - - else: - try: - url = self.request.route_url('batch_params.%s' % provider.name) - except KeyError: - pass - else: - self.request.session['referer'] = self.request.current_route_url() - return HTTPFound(location=url) - - progress = SessionProgress(self.request, 'products.batch') - thread = Thread(target=self.make_batch, args=(provider, progress)) - thread.start() - kwargs = { - 'key': 'products.batch', - 'cancel_url': self.request.route_url('products'), - 'cancel_msg': "Batch creation was canceled.", - } - return render_to_response('/progress.mako', kwargs, request=self.request) - - enabled = self.request.rattail_config.get('rattail.pyramid', 'batches.providers') - if enabled: - enabled = enabled.split() - - providers = [] - for provider in batches.iter_providers(): - if not enabled or provider.name in enabled: - providers.append((provider.name, provider.description)) - - return {'providers': providers} - - -def add_routes(config): - config.add_route('products', '/products') - config.add_route('products.autocomplete', '/products/autocomplete') - config.add_route('products.search', '/products/search') - config.add_route('products.print_labels', '/products/labels') - config.add_route('products.create_batch', '/products/batch') - config.add_route('product.create', '/products/new') - config.add_route('product.read', '/products/{uuid}') - config.add_route('product.update', '/products/{uuid}/edit') - config.add_route('product.delete', '/products/{uuid}/delete') - - def includeme(config): - add_routes(config) - - config.add_view(ProductsGrid, route_name='products', - renderer='/products/index.mako', - permission='products.list') + config.add_route('products.autocomplete', '/products/autocomplete') config.add_view(ProductsAutocomplete, route_name='products.autocomplete', - renderer='json', - permission='products.list') + renderer='json', permission='products.list') + config.add_route('products.print_labels', '/products/labels') 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') + + config.add_route('products.search', '/products/search') config.add_view(products_search, route_name='products.search', renderer='json', permission='products.list') + ProductsView.defaults(config) version_defaults(config, ProductVersionView, 'product')
    LabelQty.LabelQty.