Refactor products view(s) per new master pattern.
Finally!
This commit is contained in:
		
							parent
							
								
									254c68034a
								
							
						
					
					
						commit
						583548cad5
					
				
					 12 changed files with 421 additions and 394 deletions
				
			
		|  | @ -2,7 +2,7 @@ | ||||||
| ################################################################################ | ################################################################################ | ||||||
| # | # | ||||||
| #  Rattail -- Retail Software Framework | #  Rattail -- Retail Software Framework | ||||||
| #  Copyright © 2010-2015 Lance Edgar | #  Copyright © 2010-2016 Lance Edgar | ||||||
| # | # | ||||||
| #  This file is part of Rattail. | #  This file is part of Rattail. | ||||||
| # | # | ||||||
|  | @ -24,18 +24,20 @@ | ||||||
| FormAlchemy Grid Classes | FormAlchemy Grid Classes | ||||||
| """ | """ | ||||||
| 
 | 
 | ||||||
| from __future__ import unicode_literals | from __future__ import unicode_literals, absolute_import | ||||||
| 
 | 
 | ||||||
| import logging | import logging | ||||||
| 
 | 
 | ||||||
| import sqlalchemy as sa | import sqlalchemy as sa | ||||||
| from sqlalchemy import orm | from sqlalchemy import orm | ||||||
| 
 | 
 | ||||||
|  | from edbob.util import prettify | ||||||
|  | 
 | ||||||
|  | from rattail.db.types import GPCType | ||||||
|  | 
 | ||||||
| import formalchemy | import formalchemy | ||||||
| from webhelpers import paginate | from webhelpers import paginate | ||||||
| 
 | 
 | ||||||
| from edbob.util import prettify |  | ||||||
| 
 |  | ||||||
| from tailbone.db import Session | from tailbone.db import Session | ||||||
| from tailbone.newgrids import Grid, GridColumn, filters | from tailbone.newgrids import Grid, GridColumn, filters | ||||||
| 
 | 
 | ||||||
|  | @ -64,7 +66,8 @@ class AlchemyGrid(Grid): | ||||||
|     def __init__(self, *args, **kwargs): |     def __init__(self, *args, **kwargs): | ||||||
|         super(AlchemyGrid, self).__init__(*args, **kwargs) |         super(AlchemyGrid, self).__init__(*args, **kwargs) | ||||||
|         fa_grid = formalchemy.Grid(self.model_class, instances=self.data, |         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 |         fa_grid.prettify = prettify | ||||||
|         self._fa_grid = fa_grid |         self._fa_grid = fa_grid | ||||||
| 
 | 
 | ||||||
|  | @ -99,6 +102,8 @@ class AlchemyGrid(Grid): | ||||||
|             factory = filters.AlchemyBooleanFilter |             factory = filters.AlchemyBooleanFilter | ||||||
|         elif isinstance(column.type, (sa.Date, sa.DateTime)): |         elif isinstance(column.type, (sa.Date, sa.DateTime)): | ||||||
|             factory = filters.AlchemyDateFilter |             factory = filters.AlchemyDateFilter | ||||||
|  |         elif isinstance(column.type, GPCType): | ||||||
|  |             factory = filters.AlchemyGPCFilter | ||||||
|         return factory(key, column=column, **kwargs) |         return factory(key, column=column, **kwargs) | ||||||
| 
 | 
 | ||||||
|     def iter_filters(self): |     def iter_filters(self): | ||||||
|  | @ -107,6 +112,24 @@ class AlchemyGrid(Grid): | ||||||
|         """ |         """ | ||||||
|         return self.filters.itervalues() |         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): |     def make_sorters(self, sorters): | ||||||
|         """ |         """ | ||||||
|         Returns a mapping of sort options for the grid.  Keyword args override |         Returns a mapping of sort options for the grid.  Keyword args override | ||||||
|  |  | ||||||
|  | @ -2,7 +2,7 @@ | ||||||
| ################################################################################ | ################################################################################ | ||||||
| # | # | ||||||
| #  Rattail -- Retail Software Framework | #  Rattail -- Retail Software Framework | ||||||
| #  Copyright © 2010-2015 Lance Edgar | #  Copyright © 2010-2016 Lance Edgar | ||||||
| # | # | ||||||
| #  This file is part of Rattail. | #  This file is part of Rattail. | ||||||
| # | # | ||||||
|  | @ -24,7 +24,7 @@ | ||||||
| Core Grid Classes | Core Grid Classes | ||||||
| """ | """ | ||||||
| 
 | 
 | ||||||
| from __future__ import unicode_literals | from __future__ import unicode_literals, absolute_import | ||||||
| 
 | 
 | ||||||
| from edbob.util import prettify | 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. |         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 |             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. |         # 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) |         key = 'tailbone.{}.grid.{}.sortkey'.format(user.uuid, self.key) | ||||||
|         return get_setting(Session(), key) is not None |         return get_setting(session, key) is not None | ||||||
| 
 | 
 | ||||||
|     def apply_user_defaults(self, settings): |     def apply_user_defaults(self, settings): | ||||||
|         """ |         """ | ||||||
|  | @ -600,10 +606,11 @@ class Grid(object): | ||||||
|         """ |         """ | ||||||
|         url = action.get_url(row) |         url = action.get_url(row) | ||||||
|         if url: |         if url: | ||||||
|  |             kwargs = {'class_': action.key} | ||||||
|             if action.icon: |             if action.icon: | ||||||
|                 icon = HTML.tag('span', class_='ui-icon ui-icon-{0}'.format(action.icon)) |                 icon = HTML.tag('span', class_='ui-icon ui-icon-{}'.format(action.icon)) | ||||||
|                 return tags.link_to(icon + action.label, url) |                 return tags.link_to(icon + action.label, url, **kwargs) | ||||||
|             return tags.link_to(action.label, url) |             return tags.link_to(action.label, url, **kwargs) | ||||||
| 
 | 
 | ||||||
|     def iter_rows(self): |     def iter_rows(self): | ||||||
|         return self.make_visible_data() |         return self.make_visible_data() | ||||||
|  |  | ||||||
|  | @ -2,7 +2,7 @@ | ||||||
| ################################################################################ | ################################################################################ | ||||||
| # | # | ||||||
| #  Rattail -- Retail Software Framework | #  Rattail -- Retail Software Framework | ||||||
| #  Copyright © 2010-2015 Lance Edgar | #  Copyright © 2010-2016 Lance Edgar | ||||||
| # | # | ||||||
| #  This file is part of Rattail. | #  This file is part of Rattail. | ||||||
| # | # | ||||||
|  | @ -24,12 +24,13 @@ | ||||||
| Grid Filters | Grid Filters | ||||||
| """ | """ | ||||||
| 
 | 
 | ||||||
| from __future__ import unicode_literals | from __future__ import unicode_literals, absolute_import | ||||||
| 
 | 
 | ||||||
| import sqlalchemy as sa | import sqlalchemy as sa | ||||||
| 
 | 
 | ||||||
| from edbob.util import prettify | from edbob.util import prettify | ||||||
| 
 | 
 | ||||||
|  | from rattail.gpc import GPC | ||||||
| from rattail.util import OrderedDict | from rattail.util import OrderedDict | ||||||
| from rattail.core import UNSPECIFIED | from rattail.core import UNSPECIFIED | ||||||
| 
 | 
 | ||||||
|  | @ -367,6 +368,44 @@ class AlchemyDateFilter(AlchemyGridFilter): | ||||||
|         return self.filter_is_null(query, value) |         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): | class GridFilterSet(OrderedDict): | ||||||
|     """ |     """ | ||||||
|     Collection class for :class:`GridFilter` instances. |     Collection class for :class:`GridFilter` instances. | ||||||
|  |  | ||||||
|  | @ -17,6 +17,7 @@ | ||||||
| .newgrid-wrapper .newfilters fieldset { | .newgrid-wrapper .newfilters fieldset { | ||||||
|     margin: -8px 0 5px 0; |     margin: -8px 0 5px 0; | ||||||
|     padding: 1px 5px 5px 5px; |     padding: 1px 5px 5px 5px; | ||||||
|  |     width: 80%; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .newgrid-wrapper .newfilters .filter { | .newgrid-wrapper .newfilters .filter { | ||||||
|  |  | ||||||
|  | @ -20,7 +20,7 @@ | ||||||
| 
 | 
 | ||||||
|   <div class="buttons"> |   <div class="buttons"> | ||||||
|     ${h.submit('create', "Create Batch")} |     ${h.submit('create', "Create Batch")} | ||||||
|     <button type="button" onclick="location.href = '${url('products')}';">Cancel</button> |     ${h.link_to("Cancel", url('products'), class_='button')} | ||||||
|   </div> |   </div> | ||||||
| 
 | 
 | ||||||
|   ${h.end_form()} |   ${h.end_form()} | ||||||
|  |  | ||||||
|  | @ -1,16 +0,0 @@ | ||||||
| ## -*- coding: utf-8 -*- |  | ||||||
| <%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 |  | ||||||
|   % if version_count is not Undefined and request.has_perm('product.versions.view'): |  | ||||||
|       <li>${h.link_to("View Change History ({0})".format(version_count), url('product.versions', uuid=form.fieldset.model.uuid))}</li> |  | ||||||
|   % endif |  | ||||||
| </%def> |  | ||||||
| 
 |  | ||||||
| ${parent.body()} |  | ||||||
							
								
								
									
										11
									
								
								tailbone/templates/products/edit.mako
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								tailbone/templates/products/edit.mako
									
										
									
									
									
										Normal file
									
								
							|  | @ -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'): | ||||||
|  |       <li>${h.link_to("View Change History ({})".format(version_count), url('product.versions', uuid=instance.uuid))}</li> | ||||||
|  |   % endif | ||||||
|  | </%def> | ||||||
|  | 
 | ||||||
|  | ${parent.body()} | ||||||
|  | @ -1,7 +1,5 @@ | ||||||
| ## -*- coding: utf-8 -*- | ## -*- coding: utf-8 -*- | ||||||
| <%inherit file="/grid.mako" /> | <%inherit file="/master/index.mako" /> | ||||||
| 
 |  | ||||||
| <%def name="title()">Products</%def> |  | ||||||
| 
 | 
 | ||||||
| <%def name="head_tags()"> | <%def name="head_tags()"> | ||||||
|   ${parent.head_tags()} |   ${parent.head_tags()} | ||||||
|  | @ -35,49 +33,48 @@ | ||||||
|      |      | ||||||
|   </style> |   </style> | ||||||
|   % if label_profiles and request.has_perm('products.print_labels'): |   % if label_profiles and request.has_perm('products.print_labels'): | ||||||
|       <script language="javascript" type="text/javascript"> |       <script type="text/javascript"> | ||||||
| 
 | 
 | ||||||
|         $(function() { |       $(function() { | ||||||
|             $('div.grid a.print-label').live('click', function() { |           $('.newgrid-wrapper').on('click', 'a.print_label', function() { | ||||||
|                 var quantity = $('#label-quantity').val(); |               var quantity = $('table.label-printing #label-quantity'); | ||||||
|                 if (isNaN(quantity)) { |               if (isNaN(quantity.val())) { | ||||||
|                     alert("You must provide a valid label quantity."); |                   alert("You must provide a valid label quantity."); | ||||||
|                     $('#label-quantity').select(); |                   quantity.select(); | ||||||
|                     $('#label-quantity').focus(); |                   quantity.focus(); | ||||||
|                 } else { |               } else { | ||||||
|                     $.ajax({ |                   quantity = quantity.val(); | ||||||
|                         url: '${url('products.print_labels')}', |                   var data = { | ||||||
|                         data: { |                       product: get_uuid(this), | ||||||
|                             'product': get_uuid(this), |                       profile: $('#label-profile').val(), | ||||||
|                             'profile': $('#label-profile').val(), |                       quantity: quantity | ||||||
|                             'quantity': quantity, |                   }; | ||||||
|                         }, |                   console.log(data); | ||||||
|                         success: function(data) { |                   $.get('${url('products.print_labels')}', data, function(data) { | ||||||
|                             if (data.error) { |                       if (data.error) { | ||||||
|                                 alert("An error occurred while attempting to print:\n\n" + data.error); |                           alert("An error occurred while attempting to print:\n\n" + data.error); | ||||||
|                             } else if (quantity == '1') { |                       } else if (quantity == '1') { | ||||||
|                                 alert("1 label has been printed."); |                           alert("1 label has been printed."); | ||||||
|                             } else { |                       } else { | ||||||
|                                 alert(quantity + " labels have been printed."); |                           alert(quantity + " labels have been printed."); | ||||||
|                             } |                       } | ||||||
|                         }, |                   }); | ||||||
|                     }); |               } | ||||||
|                 } |               return false; | ||||||
|                 return false; |           }); | ||||||
|             }); |       }); | ||||||
|         }); |  | ||||||
| 
 | 
 | ||||||
|       </script> |       </script> | ||||||
|   % endif |   % endif | ||||||
| </%def> | </%def> | ||||||
| 
 | 
 | ||||||
| <%def name="tools()"> | <%def name="grid_tools()"> | ||||||
|   % if label_profiles and request.has_perm('products.print_labels'): |   % if label_profiles and request.has_perm('products.print_labels'): | ||||||
|       <table> |       <table class="label-printing"> | ||||||
|         <thead> |         <thead> | ||||||
|           <tr> |           <tr> | ||||||
|             <td>Label</td> |             <th>Label</th> | ||||||
|             <td>Qty.</td> |             <th>Qty.</th> | ||||||
|           </tr> |           </tr> | ||||||
|         </thead> |         </thead> | ||||||
|         <tbody> |         <tbody> | ||||||
|  | @ -97,9 +94,7 @@ | ||||||
| </%def> | </%def> | ||||||
| 
 | 
 | ||||||
| <%def name="context_menu_items()"> | <%def name="context_menu_items()"> | ||||||
|   % if request.has_perm('products.create'): |   ${parent.context_menu_items()} | ||||||
|       <li>${h.link_to("Create a new Product", url('product.create'))}</li> |  | ||||||
|   % endif |  | ||||||
|   % if request.has_perm('batches.create'): |   % if request.has_perm('batches.create'): | ||||||
|       <li>${h.link_to("Create Batch from Results", url('products.create_batch'))}</li> |       <li>${h.link_to("Create Batch from Results", url('products.create_batch'))}</li> | ||||||
|   % endif |   % endif | ||||||
|  |  | ||||||
|  | @ -1,7 +1,9 @@ | ||||||
| ## -*- coding: utf-8 -*- | ## -*- coding: utf-8 -*- | ||||||
| <%inherit file="/products/crud.mako" /> | <%inherit file="/master/view.mako" /> | ||||||
| <%namespace file="/forms/lib.mako" import="render_field_readonly" /> | <%namespace file="/forms/lib.mako" import="render_field_readonly" /> | ||||||
| 
 | 
 | ||||||
|  | <% product = instance %> | ||||||
|  | 
 | ||||||
| <%def name="head_tags()"> | <%def name="head_tags()"> | ||||||
|   ${parent.head_tags()} |   ${parent.head_tags()} | ||||||
|   <style type="text/css"> |   <style type="text/css"> | ||||||
|  | @ -20,7 +22,12 @@ | ||||||
|   </style> |   </style> | ||||||
| </%def> | </%def> | ||||||
| 
 | 
 | ||||||
| <% 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'): | ||||||
|  |       <li>${h.link_to("View Change History ({})".format(version_count), url('product.versions', uuid=product.uuid))}</li> | ||||||
|  |   % endif | ||||||
|  | </%def> | ||||||
| 
 | 
 | ||||||
| <%def name="render_organization_fields(form)"> | <%def name="render_organization_fields(form)"> | ||||||
|     ${render_field_readonly(form.fieldset.department)} |     ${render_field_readonly(form.fieldset.department)} | ||||||
|  | @ -330,20 +330,18 @@ class MasterView(View): | ||||||
|         """ |         """ | ||||||
|         return getattr(cls, 'grid_key', '{0}s'.format(cls.get_normalized_model_name())) |         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 |         Return a dictionary of kwargs to be passed to the factory when creating | ||||||
|         new grid instances. |         new grid instances. | ||||||
|         """ |         """ | ||||||
|         return { |         defaults = { | ||||||
|             'width': 'full', |             'width': 'full', | ||||||
|             'filterable': self.filterable, |             'filterable': self.filterable, | ||||||
|             'sortable': True, |             'sortable': True, | ||||||
|             'default_sortkey': getattr(self, 'default_sortkey', None), |             'default_sortkey': getattr(self, 'default_sortkey', None), | ||||||
|             'sortdir': getattr(self, 'sortdir', 'asc'), |             'sortdir': getattr(self, 'sortdir', 'asc'), | ||||||
|             'pageable': self.pageable, |             'pageable': self.pageable, | ||||||
|             'main_actions': self.get_main_actions(), |  | ||||||
|             'more_actions': self.get_more_actions(), |  | ||||||
|             'checkboxes': self.checkboxes, |             'checkboxes': self.checkboxes, | ||||||
|             'checked': self.checked, |             'checked': self.checked, | ||||||
|             'row_attrs': self.get_row_attrs, |             'row_attrs': self.get_row_attrs, | ||||||
|  | @ -353,6 +351,15 @@ class MasterView(View): | ||||||
|             'permission_prefix': self.get_permission_prefix(), |             'permission_prefix': self.get_permission_prefix(), | ||||||
|             'route_prefix': self.get_route_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): |     def get_row_attrs(self, row, i): | ||||||
|         """ |         """ | ||||||
|  | @ -379,7 +386,8 @@ class MasterView(View): | ||||||
|         Return a list of 'main' actions for the grid. |         Return a list of 'main' actions for the grid. | ||||||
|         """ |         """ | ||||||
|         actions = [] |         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')) |             actions.append(self.make_action('view', icon='zoomin')) | ||||||
|         return actions |         return actions | ||||||
| 
 | 
 | ||||||
|  | @ -388,9 +396,10 @@ class MasterView(View): | ||||||
|         Return a list of 'more' actions for the grid. |         Return a list of 'more' actions for the grid. | ||||||
|         """ |         """ | ||||||
|         actions = [] |         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')) |             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)) |             actions.append(self.make_action('delete', icon='trash', url=self.default_delete_url)) | ||||||
|         return actions |         return actions | ||||||
| 
 | 
 | ||||||
|  | @ -421,14 +430,14 @@ class MasterView(View): | ||||||
|             values = [getattr(row, k) for k in keys] |             values = [getattr(row, k) for k in keys] | ||||||
|             return dict(zip(keys, values)) |             return dict(zip(keys, values)) | ||||||
| 
 | 
 | ||||||
|     def make_grid(self): |     def make_grid(self, **kwargs): | ||||||
|         """ |         """ | ||||||
|         Make and return a new (configured) grid instance. |         Make and return a new (configured) grid instance. | ||||||
|         """ |         """ | ||||||
|         factory = self.get_grid_factory() |         factory = self.get_grid_factory() | ||||||
|         key = self.get_grid_key() |         key = self.get_grid_key() | ||||||
|         data = self.get_data() |         data = self.get_data(session=kwargs.get('session')) | ||||||
|         kwargs = self.make_grid_kwargs() |         kwargs = self.make_grid_kwargs(**kwargs) | ||||||
|         grid = factory(key, self.request, data=data, model_class=self.get_model_class(error=False), **kwargs) |         grid = factory(key, self.request, data=data, model_class=self.get_model_class(error=False), **kwargs) | ||||||
|         self.configure_grid(grid) |         self.configure_grid(grid) | ||||||
|         grid.load_settings() |         grid.load_settings() | ||||||
|  |  | ||||||
|  | @ -106,8 +106,12 @@ class TerseRecipientsFieldRenderer(formalchemy.FieldRenderer): | ||||||
|         recipients = self.raw_value |         recipients = self.raw_value | ||||||
|         if not recipients: |         if not recipients: | ||||||
|             return '' |             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]) |         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: |         if len(recips) < 5: | ||||||
|             return ', '.join(recips) |             return ', '.join(recips) | ||||||
|         return "{}, ...".format(', '.join(recips[:4])) |         return "{}, ...".format(', '.join(recips[:4])) | ||||||
|  |  | ||||||
|  | @ -2,7 +2,7 @@ | ||||||
| ################################################################################ | ################################################################################ | ||||||
| # | # | ||||||
| #  Rattail -- Retail Software Framework | #  Rattail -- Retail Software Framework | ||||||
| #  Copyright © 2010-2015 Lance Edgar | #  Copyright © 2010-2016 Lance Edgar | ||||||
| # | # | ||||||
| #  This file is part of Rattail. | #  This file is part of Rattail. | ||||||
| # | # | ||||||
|  | @ -32,158 +32,126 @@ import re | ||||||
| import sqlalchemy as sa | import sqlalchemy as sa | ||||||
| from sqlalchemy import orm | 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 | import formalchemy | ||||||
| from pyramid.httpexceptions import HTTPFound | from pyramid import httpexceptions | ||||||
| from pyramid.renderers import render_to_response | from pyramid.renderers import render_to_response | ||||||
| from webhelpers.html import tags | from webhelpers.html import tags | ||||||
| 
 | 
 | ||||||
| import rattail.labels | from tailbone import forms | ||||||
| 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.db import Session | from tailbone.db import Session | ||||||
| from tailbone.forms import GPCFieldRenderer, BrandFieldRenderer, PriceFieldRenderer | from tailbone.views import MasterView, SearchableAlchemyGridView, AutocompleteView | ||||||
| from tailbone.forms.renderers import products as forms | 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 | from tailbone.progress import SessionProgress | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class ProductsGrid(SearchableAlchemyGridView): | class ProductsView(MasterView): | ||||||
|  |     """ | ||||||
|  |     Master view for the Product class. | ||||||
|  |     """ | ||||||
|  |     model_class = model.Product | ||||||
| 
 | 
 | ||||||
|     mapped_class = Product |     # child_version_classes = [ | ||||||
|     config_prefix = 'products' |     #     (model.ProductCode, 'product_uuid'), | ||||||
|     sort = 'description' |     #     (model.ProductCost, 'product_uuid'), | ||||||
|  |     #     (model.ProductPrice, 'product_uuid'), | ||||||
|  |     #     ] | ||||||
| 
 | 
 | ||||||
|     # These aliases enable the grid queries to filter products which may be |     # These aliases enable the grid queries to filter products which may be | ||||||
|     # purchased from *any* vendor, and yet sort by only the "preferred" vendor |     # purchased from *any* vendor, and yet sort by only the "preferred" vendor | ||||||
|     # (since that's what shows up in the grid column). |     # (since that's what shows up in the grid column). | ||||||
|     ProductCostAny = orm.aliased(ProductCost) |     ProductCostAny = orm.aliased(model.ProductCost) | ||||||
|     VendorAny = orm.aliased(Vendor) |     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): |         def join_vendor(q): | ||||||
|             q = q.outerjoin( |             return q.outerjoin(model.ProductCost, | ||||||
|                 ProductCost, |                                sa.and_( | ||||||
|                 sa.and_( |                                    model.ProductCost.product_uuid == model.Product.uuid, | ||||||
|                     ProductCost.product_uuid == Product.uuid, |                                    model.ProductCost.preference == 1))\ | ||||||
|                     ProductCost.preference == 1, |                     .outerjoin(model.Vendor) | ||||||
|                     )) |  | ||||||
|             q = q.outerjoin(Vendor) |  | ||||||
|             return q |  | ||||||
| 
 | 
 | ||||||
|         def join_vendor_any(q): |         def join_vendor_any(q): | ||||||
|             q = q.outerjoin( |             return q.outerjoin(self.ProductCostAny, | ||||||
|                 self.ProductCostAny, |                                self.ProductCostAny.product_uuid == model.Product.uuid)\ | ||||||
|                 self.ProductCostAny.product_uuid == Product.uuid) |                     .outerjoin(self.VendorAny, | ||||||
|             q = q.outerjoin( |                                self.VendorAny.uuid == self.ProductCostAny.vendor_uuid) | ||||||
|                 self.VendorAny, |  | ||||||
|                 self.VendorAny.uuid == self.ProductCostAny.vendor_uuid) |  | ||||||
|             return q |  | ||||||
| 
 | 
 | ||||||
|         return { |         g.joiners['brand'] = lambda q: q.outerjoin(model.Brand) | ||||||
|             'brand': |         g.joiners['family'] = lambda q: q.outerjoin(model.Family) | ||||||
|                 lambda q: q.outerjoin(Brand), |         g.joiners['department'] = lambda q: q.outerjoin(model.Department, | ||||||
|             'family': |                                                         model.Department.uuid == model.Product.department_uuid) | ||||||
|                 lambda q: q.outerjoin(model.Family), |         g.joiners['subdepartment'] = lambda q: q.outerjoin(model.Subdepartment, | ||||||
|             'department': |                                                            model.Subdepartment.uuid == model.Product.subdepartment_uuid) | ||||||
|                 lambda q: q.outerjoin(Department, |         g.joiners['report_code'] = lambda q: q.outerjoin(model.ReportCode) | ||||||
|                                       Department.uuid == Product.department_uuid), |         g.joiners['regular_price'] = lambda q: q.outerjoin(model.ProductPrice, | ||||||
|             'subdepartment': |                                                            model.ProductPrice.uuid == model.Product.regular_price_uuid) | ||||||
|                 lambda q: q.outerjoin(Subdepartment, |         g.joiners['current_price'] = lambda q: q.outerjoin(model.ProductPrice, | ||||||
|                                       Subdepartment.uuid == Product.subdepartment_uuid), |                                                            model.ProductPrice.uuid == model.Product.current_price_uuid) | ||||||
|             u'report_code': |         g.joiners['vendor'] = join_vendor | ||||||
|                 lambda q: q.outerjoin(model.ReportCode), |         g.joiners['vendor_any'] = join_vendor_any | ||||||
|             'regular_price': |         g.joiners['code'] = lambda q: q.outerjoin(model.ProductCode) | ||||||
|                 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), |  | ||||||
|             } |  | ||||||
| 
 | 
 | ||||||
|     def filter_map(self): |         g.sorters['brand'] = g.make_sorter(model.Brand.name) | ||||||
|         return self.make_filter_map( |         g.sorters['department'] = g.make_sorter(model.Department.name) | ||||||
|             ilike=['description', 'size'], |         g.sorters['subdepartment'] = g.make_sorter(model.Subdepartment.name) | ||||||
|             upc=self.filter_gpc(model.Product.upc), |         g.sorters['vendor'] = g.make_sorter(model.Vendor.name) | ||||||
|             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)) |  | ||||||
| 
 | 
 | ||||||
|     def filter_config(self): |         g.filters['upc'].default_active = True | ||||||
|         return self.make_filter_config( |         g.filters['upc'].default_verb = 'equal' | ||||||
|             include_filter_upc=True, |         g.filters['upc'].label = "UPC" | ||||||
|             filter_type_upc='is', |         g.filters['description'].default_active = True | ||||||
|             filter_label_upc="UPC", |         g.filters['description'].default_verb = 'contains' | ||||||
|             include_filter_brand=True, |         g.filters['brand'] = g.make_filter('brand', model.Brand.name, | ||||||
|             filter_type_brand='lk', |                                            default_active=True, default_verb='contains') | ||||||
|             include_filter_description=True, |         g.filters['family'] = g.make_filter('family', model.Family.name) | ||||||
|             filter_type_description='lk', |         g.filters['department'] = g.make_filter('department', model.Department.name, | ||||||
|             include_filter_department=True, |                                                 default_active=True, default_verb='contains') | ||||||
|             filter_type_department='lk', |         g.filters['subdepartment'] = g.make_filter('subdepartment', model.Subdepartment.name) | ||||||
|             filter_label_vendor="Vendor (preferred)", |         g.filters['report_code'] = g.make_filter('report_code', model.ReportCode.name) | ||||||
|             include_filter_vendor_any=True, |         g.filters['vendor'] = g.make_filter('vendor', model.Vendor.name, label="Vendor (preferred)") | ||||||
|             filter_label_vendor_any="Vendor (any)", |         g.filters['vendor_any'] = g.make_filter('vendor_any', self.VendorAny.name, label="Vendor (any)") | ||||||
|             filter_type_vendor_any='lk') |         g.filters['code'] = g.make_filter('code', model.ProductCode.code) | ||||||
| 
 | 
 | ||||||
|     def sort_map(self): |         g.default_sortkey = 'description' | ||||||
|         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): |         g.upc.set(renderer=forms.renderers.GPCFieldRenderer) | ||||||
|         q = self.make_query() |         g.regular_price.set(renderer=forms.renderers.PriceFieldRenderer) | ||||||
|         if not self.request.has_perm('products.view_deleted'): |         g.current_price.set(renderer=forms.renderers.PriceFieldRenderer) | ||||||
|             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.configure( |         g.configure( | ||||||
|             include=[ |             include=[ | ||||||
|                 g.upc.label("UPC"), |                 g.upc.label("UPC"), | ||||||
|  | @ -197,80 +165,68 @@ class ProductsGrid(SearchableAlchemyGridView): | ||||||
|                 ], |                 ], | ||||||
|             readonly=True) |             readonly=True) | ||||||
| 
 | 
 | ||||||
|         if self.request.has_perm('products.read'): |         # TODO: need to check for 'print labels' permission here also | ||||||
|             g.viewable = True |         if self.print_labels: | ||||||
|             g.view_route_name = 'product.read' |             g.more_actions.append(GridAction('print_label', icon='print')) | ||||||
|         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' |  | ||||||
| 
 | 
 | ||||||
|         # Maybe add Print Label column. |     def template_kwargs_index(self, **kwargs): | ||||||
|         if self.rattail_config.getbool('tailbone', 'products.print_labels', default=True): |         if self.print_labels: | ||||||
|             q = Session.query(LabelProfile) |             kwargs['label_profiles'] = Session.query(model.LabelProfile)\ | ||||||
|             if q.count(): |                                               .filter(model.LabelProfile.visible == True)\ | ||||||
|                 def labels(row): |                                               .order_by(model.LabelProfile.ordinal)\ | ||||||
|                     return tags.link_to("Print", '#', class_='print-label') |                                               .all() | ||||||
|                 g.add_column('labels', "Labels", labels) |         return kwargs | ||||||
| 
 | 
 | ||||||
|         return g |     def row_attrs(self, row, i): | ||||||
| 
 | 
 | ||||||
|     def render_kwargs(self): |         attrs = {'uuid': row.uuid} | ||||||
|         q = Session.query(LabelProfile) |  | ||||||
|         q = q.filter(LabelProfile.visible == True) |  | ||||||
|         q = q.order_by(LabelProfile.ordinal) |  | ||||||
|         return {'label_profiles': q.all()} |  | ||||||
| 
 | 
 | ||||||
|  |         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): |         return attrs | ||||||
|     """ |  | ||||||
|     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'), |  | ||||||
|         ] |  | ||||||
| 
 | 
 | ||||||
|     def get_model(self, key): |     def get_instance(self): | ||||||
|         model = super(ProductCrud, self).get_model(key) |         key = self.request.matchdict['uuid'] | ||||||
|         if model: |         product = Session.query(model.Product).get(key) | ||||||
|             return model |         if product: | ||||||
|         model = Session.query(ProductPrice).get(key) |             return product | ||||||
|         if model: |         price = Session.query(model.ProductPrice).get(key) | ||||||
|             return model.product |         if price: | ||||||
|         return None |             return price.product | ||||||
|  |         raise httpexceptions.HTTPNotFound() | ||||||
| 
 | 
 | ||||||
|     def fieldset(self, model): |     def configure_fieldset(self, fs): | ||||||
|         fs = self.make_fieldset(model) | 
 | ||||||
|         fs.upc.set(renderer=GPCFieldRenderer) |         fs.upc.set(renderer=forms.renderers.GPCFieldRenderer) | ||||||
|         fs.brand.set(options=[]) |         fs.brand.set(options=[]) | ||||||
|         fs.unit_of_measure.set(renderer=EnumFieldRenderer(enum.UNIT_OF_MEASURE)) |         fs.unit_of_measure.set(renderer=forms.renderers.EnumFieldRenderer(enum.UNIT_OF_MEASURE)) | ||||||
|         fs.regular_price.set(renderer=PriceFieldRenderer) |         fs.regular_price.set(renderer=forms.renderers.PriceFieldRenderer) | ||||||
|         fs.current_price.set(renderer=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.append(formalchemy.Field('current_price_ends', | ||||||
|         fs.current_price_ends.set(value=lambda p: p.current_price.ends if p.current_price else None) |                                     value=lambda p: p.current_price.ends if p.current_price else None, | ||||||
|         fs.current_price_ends.set(renderer=DateTimeFieldRenderer(self.request.rattail_config)) |                                     renderer=forms.renderers.DateTimeFieldRenderer(self.rattail_config))) | ||||||
| 
 | 
 | ||||||
|         fs.last_sold.set(renderer=DateTimeFieldRenderer(self.request.rattail_config)) |  | ||||||
|         fs.configure( |         fs.configure( | ||||||
|             include=[ |             include=[ | ||||||
|                 fs.upc.label("UPC"), |                 fs.upc.label("UPC"), | ||||||
|                 fs.brand.with_renderer(BrandFieldRenderer), |                 fs.brand.with_renderer(forms.renderers.BrandFieldRenderer), | ||||||
|                 fs.description, |                 fs.description, | ||||||
|                 fs.unit_size, |                 fs.unit_size, | ||||||
|                 fs.unit_of_measure.label("Unit of Measure"), |                 fs.unit_of_measure.label("Unit of Measure"), | ||||||
|                 fs.size, |                 fs.size, | ||||||
|                 fs.weighed, |                 fs.weighed, | ||||||
|                 fs.case_pack, |                 fs.case_pack, | ||||||
|                 fs.department.with_renderer(forms.DepartmentFieldRenderer), |                 fs.department.with_renderer(products_forms.DepartmentFieldRenderer), | ||||||
|                 fs.subdepartment.with_renderer(forms.SubdepartmentFieldRenderer), |                 fs.subdepartment.with_renderer(products_forms.SubdepartmentFieldRenderer), | ||||||
|                 fs.category.with_renderer(forms.CategoryFieldRenderer), |                 fs.category.with_renderer(products_forms.CategoryFieldRenderer), | ||||||
|                 fs.family, |                 fs.family, | ||||||
|                 fs.report_code, |                 fs.report_code, | ||||||
|                 fs.regular_price, |                 fs.regular_price, | ||||||
|  | @ -285,33 +241,119 @@ class ProductCrud(CrudView): | ||||||
|                 fs.deleted, |                 fs.deleted, | ||||||
|                 fs.last_sold, |                 fs.last_sold, | ||||||
|                 ]) |                 ]) | ||||||
|         if not self.readonly: |         if not self.viewing: | ||||||
|             del fs.regular_price |             del fs.regular_price | ||||||
|             del fs.current_price |             del fs.current_price | ||||||
|         if not self.request.has_perm('products.view_deleted'): |         if not self.request.has_perm('products.view_deleted'): | ||||||
|             del fs.deleted |             del fs.deleted | ||||||
|         return fs |  | ||||||
| 
 | 
 | ||||||
|     def pre_crud(self, product): |     def template_kwargs_view(self, **kwargs): | ||||||
|         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) |  | ||||||
|         kwargs['image'] = False |         kwargs['image'] = False | ||||||
|         product = form.fieldset.model |         product = kwargs['instance'] | ||||||
|         if product.upc: |         if product.upc: | ||||||
|             kwargs['image_url'] = get_image_url( |             kwargs['image_url'] = pod.get_image_url(self.rattail_config, product.upc) | ||||||
|                 self.request.rattail_config, product.upc) |             kwargs['image_path'] = pod.get_image_path(self.rattail_config, product.upc) | ||||||
|             kwargs['image_path'] = get_image_path( |  | ||||||
|                 self.request.rattail_config, product.upc) |  | ||||||
|             if os.path.exists(kwargs['image_path']): |             if os.path.exists(kwargs['image_path']): | ||||||
|                 kwargs['image'] = True |                 kwargs['image'] = True | ||||||
|         return kwargs |         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): | class ProductVersionView(VersionView): | ||||||
|     """ |     """ | ||||||
|  | @ -331,7 +373,7 @@ class ProductVersionView(VersionView): | ||||||
|         """ |         """ | ||||||
|         uuid = self.request.matchdict['uuid'] |         uuid = self.request.matchdict['uuid'] | ||||||
|         product = Session.query(model.Product).get(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: |         if product.deleted: | ||||||
|             self.request.session.flash("This product is marked as deleted.", 'error') |             self.request.session.flash("This product is marked as deleted.", 'error') | ||||||
| 
 | 
 | ||||||
|  | @ -354,8 +396,8 @@ class ProductsAutocomplete(AutocompleteView): | ||||||
|     def query(self, term): |     def query(self, term): | ||||||
|         q = Session.query(model.Product).outerjoin(model.Brand) |         q = Session.query(model.Product).outerjoin(model.Brand) | ||||||
|         q = q.filter(sa.or_( |         q = q.filter(sa.or_( | ||||||
|                 model.Brand.name.ilike('%{0}%'.format(term)), |                 model.Brand.name.ilike('%{}%'.format(term)), | ||||||
|                 model.Product.description.ilike('%{0}%'.format(term)))) |                 model.Product.description.ilike('%{}%'.format(term)))) | ||||||
|         if not self.request.has_perm('products.view_deleted'): |         if not self.request.has_perm('products.view_deleted'): | ||||||
|             q = q.filter(model.Product.deleted == False) |             q = q.filter(model.Product.deleted == False) | ||||||
|         q = q.order_by(model.Brand.name, model.Product.description) |         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 = request.GET.get('upc', '').strip() | ||||||
|     upc = re.sub(r'\D', '', upc) |     upc = re.sub(r'\D', '', upc) | ||||||
|     if upc: |     if upc: | ||||||
|         product = get_product_by_upc(Session, upc) |         product = api.get_product_by_upc(Session(), upc) | ||||||
|         if not product: |         if not product: | ||||||
|             # Try again, assuming caller did not include check digit. |             # Try again, assuming caller did not include check digit. | ||||||
|             upc = GPC(upc, calc_check_digit='upc') |             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: | ||||||
|             if product.deleted and not request.has_perm('products.view_deleted'): |             if product.deleted and not request.has_perm('products.view_deleted'): | ||||||
|                 product = None |                 product = None | ||||||
|  | @ -396,12 +438,12 @@ def products_search(request): | ||||||
| 
 | 
 | ||||||
| def print_labels(request): | def print_labels(request): | ||||||
|     profile = request.params.get('profile') |     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: |     if not profile: | ||||||
|         return {'error': "Label profile not found"} |         return {'error': "Label profile not found"} | ||||||
| 
 | 
 | ||||||
|     product = request.params.get('product') |     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: |     if not product: | ||||||
|         return {'error': "Product not found"} |         return {'error': "Product not found"} | ||||||
| 
 | 
 | ||||||
|  | @ -421,114 +463,19 @@ def print_labels(request): | ||||||
|     return {} |     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): | 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', |     config.add_view(ProductsAutocomplete, route_name='products.autocomplete', | ||||||
|                     renderer='json', |                     renderer='json', permission='products.list') | ||||||
|                     permission='products.list') |  | ||||||
| 
 | 
 | ||||||
|  |     config.add_route('products.print_labels', '/products/labels') | ||||||
|     config.add_view(print_labels, route_name='products.print_labels', |     config.add_view(print_labels, route_name='products.print_labels', | ||||||
|                     renderer='json', permission='products.print_labels') |                     renderer='json', permission='products.print_labels') | ||||||
|     config.add_view(CreateProductsBatch, route_name='products.create_batch', | 
 | ||||||
|                     renderer='/products/batch.mako', |     config.add_route('products.search', '/products/search') | ||||||
|                     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_view(products_search, route_name='products.search', |     config.add_view(products_search, route_name='products.search', | ||||||
|                     renderer='json', permission='products.list') |                     renderer='json', permission='products.list') | ||||||
| 
 | 
 | ||||||
|  |     ProductsView.defaults(config) | ||||||
|     version_defaults(config, ProductVersionView, 'product') |     version_defaults(config, ProductVersionView, 'product') | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Lance Edgar
						Lance Edgar