Compare commits
	
		
			37 commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 2500805c54 | |||
| 8a3c147858 | |||
| 5ae236f228 | |||
| b62e41966c | |||
| c230536e49 | |||
| 3cc37bea30 | |||
| 207125bdb3 | |||
| 5a78b0740d | |||
| 8e7169fb4a | |||
| 6f14bb0e88 | |||
| fcc90d25ac | |||
| e150453801 | |||
| e2582ffec5 | |||
| a6508154cb | |||
| 7348eec671 | |||
| 4221fa50dd | |||
| e0ebd43e7a | |||
| c7ee9de9eb | |||
| 950db697a0 | |||
| 358b3b75a5 | |||
| 7e559a01b3 | |||
| 23bdde245a | |||
| 2c269b640b | |||
|   | f1c8ffedda | ||
|   | aace6033c5 | ||
|   | 7171c7fb06 | ||
|   | 993f066f2c | ||
|   | 980031f524 | ||
|   | bcaf0d08bc | ||
|   | ac439c949b | ||
|   | 20b3f87dbe | ||
|   | 9e55717041 | ||
|   | 772b6610cb | ||
|   | 3f27f626df | ||
|   | 29743e70b7 | ||
|   | 54220601ed | ||
|   | 9a6f8970ae | 
					 18 changed files with 270 additions and 77 deletions
				
			
		
							
								
								
									
										87
									
								
								CHANGELOG.md
									
										
									
									
									
								
							
							
						
						
									
										87
									
								
								CHANGELOG.md
									
										
									
									
									
								
							|  | @ -5,6 +5,93 @@ All notable changes to Tailbone will be documented in this file. | |||
| The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) | ||||
| and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). | ||||
| 
 | ||||
| ## v0.23.0 (2025-10-19) | ||||
| 
 | ||||
| ### Feat | ||||
| 
 | ||||
| - require latest rattail; drop passlib dependency | ||||
| 
 | ||||
| ### Fix | ||||
| 
 | ||||
| - depend on latest rattail | ||||
| 
 | ||||
| ## v0.22.11 (2025-09-20) | ||||
| 
 | ||||
| ### Fix | ||||
| 
 | ||||
| - avoid error when row object missing field | ||||
| 
 | ||||
| ## v0.22.10 (2025-09-20) | ||||
| 
 | ||||
| ### Fix | ||||
| 
 | ||||
| - avoid error if 'default' theme not included | ||||
| - fix config extension entry point | ||||
| 
 | ||||
| ## v0.22.9 (2025-09-20) | ||||
| 
 | ||||
| ### Fix | ||||
| 
 | ||||
| - small bugfixes per upstream changes | ||||
| 
 | ||||
| ## v0.22.8 (2025-05-20) | ||||
| 
 | ||||
| ### Fix | ||||
| 
 | ||||
| - add startup hack for tempmon DB model | ||||
| 
 | ||||
| ## v0.22.7 (2025-02-19) | ||||
| 
 | ||||
| ### Fix | ||||
| 
 | ||||
| - stop using old config for logo image url on login page | ||||
| - fix warning msg for deprecated Grid param | ||||
| 
 | ||||
| ## v0.22.6 (2025-02-01) | ||||
| 
 | ||||
| ### Fix | ||||
| 
 | ||||
| - register vue3 form component for products -> make batch | ||||
| 
 | ||||
| ## v0.22.5 (2024-12-16) | ||||
| 
 | ||||
| ### Fix | ||||
| 
 | ||||
| - whoops this is latest rattail | ||||
| - require newer rattail lib | ||||
| - require newer wuttaweb | ||||
| - let caller request safe HTML literal for rendered grid table | ||||
| 
 | ||||
| ## v0.22.4 (2024-11-22) | ||||
| 
 | ||||
| ### Fix | ||||
| 
 | ||||
| - avoid error in product search for duplicated key | ||||
| - use vmodel for confirm password widget input | ||||
| 
 | ||||
| ## v0.22.3 (2024-11-19) | ||||
| 
 | ||||
| ### Fix | ||||
| 
 | ||||
| - avoid error for trainwreck query when not a customer | ||||
| 
 | ||||
| ## v0.22.2 (2024-11-18) | ||||
| 
 | ||||
| ### Fix | ||||
| 
 | ||||
| - use local/custom enum for continuum operations | ||||
| - add basic master view for Product Costs | ||||
| - show continuum operation type when viewing version history | ||||
| - always define `app` attr for ViewSupplement | ||||
| - avoid deprecated import | ||||
| 
 | ||||
| ## v0.22.1 (2024-11-02) | ||||
| 
 | ||||
| ### Fix | ||||
| 
 | ||||
| - fix submit button for running problem report | ||||
| - avoid deprecated grid method | ||||
| 
 | ||||
| ## v0.22.0 (2024-10-22) | ||||
| 
 | ||||
| ### Feat | ||||
|  |  | |||
|  | @ -27,10 +27,10 @@ templates_path = ['_templates'] | |||
| exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] | ||||
| 
 | ||||
| intersphinx_mapping = { | ||||
|     'rattail': ('https://rattailproject.org/docs/rattail/', None), | ||||
|     'rattail': ('https://docs.wuttaproject.org/rattail/', None), | ||||
|     'webhelpers2': ('https://webhelpers2.readthedocs.io/en/latest/', None), | ||||
|     'wuttaweb': ('https://rattailproject.org/docs/wuttaweb/', None), | ||||
|     'wuttjamaican': ('https://rattailproject.org/docs/wuttjamaican/', None), | ||||
|     'wuttaweb': ('https://docs.wuttaproject.org/wuttaweb/', None), | ||||
|     'wuttjamaican': ('https://docs.wuttaproject.org/wuttjamaican/', None), | ||||
| } | ||||
| 
 | ||||
| # allow todo entries to show up | ||||
|  |  | |||
|  | @ -6,7 +6,7 @@ build-backend = "hatchling.build" | |||
| 
 | ||||
| [project] | ||||
| name = "Tailbone" | ||||
| version = "0.22.0" | ||||
| version = "0.23.0" | ||||
| description = "Backoffice Web Application for Rattail" | ||||
| readme = "README.md" | ||||
| authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] | ||||
|  | @ -43,7 +43,6 @@ dependencies = [ | |||
|         "openpyxl", | ||||
|         "paginate", | ||||
|         "paginate_sqlalchemy", | ||||
|         "passlib", | ||||
|         "Pillow", | ||||
|         "pyramid>=2", | ||||
|         "pyramid_beaker", | ||||
|  | @ -53,13 +52,13 @@ dependencies = [ | |||
|         "pyramid_mako", | ||||
|         "pyramid_retry", | ||||
|         "pyramid_tm", | ||||
|         "rattail[db,bouncer]>=0.18.5", | ||||
|         "rattail[db,bouncer]>=0.21.0", | ||||
|         "sa-filters", | ||||
|         "simplejson", | ||||
|         "transaction", | ||||
|         "waitress", | ||||
|         "WebHelpers2", | ||||
|         "WuttaWeb>=0.14.0", | ||||
|         "WuttaWeb>=0.21.0", | ||||
|         "zope.sqlalchemy>=1.5", | ||||
| ] | ||||
| 
 | ||||
|  | @ -78,7 +77,7 @@ webapi = "tailbone.webapi:main" | |||
| beaker = "tailbone.cleanup:BeakerCleaner" | ||||
| 
 | ||||
| 
 | ||||
| [project.entry-points."rattail.config.extensions"] | ||||
| [project.entry-points."wutta.config.extensions"] | ||||
| tailbone = "tailbone.config:ConfigExtension" | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -26,7 +26,6 @@ Tailbone Web API - Master View | |||
| 
 | ||||
| import json | ||||
| 
 | ||||
| from rattail.config import parse_bool | ||||
| from rattail.db.util import get_fieldnames | ||||
| 
 | ||||
| from cornice import resource, Service | ||||
|  | @ -185,7 +184,7 @@ class APIMasterView(APIView): | |||
|             if sortcol: | ||||
|                 spec = { | ||||
|                     'field': sortcol.field_name, | ||||
|                     'direction': 'asc' if parse_bool(self.request.params['ascending']) else 'desc', | ||||
|                     'direction': 'asc' if self.config.parse_bool(self.request.params['ascending']) else 'desc', | ||||
|                 } | ||||
|                 if sortcol.model_name: | ||||
|                     spec['model'] = sortcol.model_name | ||||
|  |  | |||
|  | @ -62,6 +62,17 @@ def make_rattail_config(settings): | |||
|     # nb. this is for compaibility with wuttaweb | ||||
|     settings['wutta_config'] = rattail_config | ||||
| 
 | ||||
|     # must import all sqlalchemy models before things get rolling, | ||||
|     # otherwise can have errors about continuum TransactionMeta class | ||||
|     # not yet mapped, when relevant pages are first requested... | ||||
|     # cf. https://docs.pylonsproject.org/projects/pyramid_cookbook/en/latest/database/sqlalchemy.html#importing-all-sqlalchemy-models | ||||
|     # hat tip to https://stackoverflow.com/a/59241485 | ||||
|     if getattr(rattail_config, 'tempmon_engine', None): | ||||
|         from rattail_tempmon.db import model as tempmon_model, Session as TempmonSession | ||||
|         tempmon_session = TempmonSession() | ||||
|         tempmon_session.query(tempmon_model.Appliance).first() | ||||
|         tempmon_session.close() | ||||
| 
 | ||||
|     # configure database sessions | ||||
|     if hasattr(rattail_config, 'appdb_engine'): | ||||
|         tailbone.db.Session.configure(bind=rattail_config.appdb_engine) | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ | |||
| ################################################################################ | ||||
| # | ||||
| #  Rattail -- Retail Software Framework | ||||
| #  Copyright © 2010-2023 Lance Edgar | ||||
| #  Copyright © 2010-2024 Lance Edgar | ||||
| # | ||||
| #  This file is part of Rattail. | ||||
| # | ||||
|  | @ -270,9 +270,21 @@ class VersionDiff(Diff): | |||
|         for field in self.fields: | ||||
|             values[field] = {'before': self.render_old_value(field), | ||||
|                              'after': self.render_new_value(field)} | ||||
| 
 | ||||
|         operation = None | ||||
|         if self.version.operation_type == continuum.Operation.INSERT: | ||||
|             operation = 'INSERT' | ||||
|         elif self.version.operation_type == continuum.Operation.UPDATE: | ||||
|             operation = 'UPDATE' | ||||
|         elif self.version.operation_type == continuum.Operation.DELETE: | ||||
|             operation = 'DELETE' | ||||
|         else: | ||||
|             operation = self.version.operation_type | ||||
| 
 | ||||
|         return { | ||||
|             'key': id(self.version), | ||||
|             'model_title': self.title, | ||||
|             'operation': operation, | ||||
|             'diff_class': self.nature, | ||||
|             'fields': self.fields, | ||||
|             'values': values, | ||||
|  |  | |||
|  | @ -235,7 +235,7 @@ class Grid(WuttaGrid): | |||
| 
 | ||||
|         if 'pageable' in kwargs: | ||||
|             warnings.warn("pageable param is deprecated for Grid(); " | ||||
|                           "please use vue_tagname param instead", | ||||
|                           "please use paginated param instead", | ||||
|                           DeprecationWarning, stacklevel=2) | ||||
|             kwargs.setdefault('paginated', kwargs.pop('pageable')) | ||||
| 
 | ||||
|  | @ -578,7 +578,7 @@ class Grid(WuttaGrid): | |||
| 
 | ||||
|         try: | ||||
|             return obj[column_name] | ||||
|         except TypeError: | ||||
|         except (TypeError, KeyError): | ||||
|             pass | ||||
| 
 | ||||
|     def render_currency(self, obj, column_name): | ||||
|  | @ -1223,6 +1223,7 @@ class Grid(WuttaGrid): | |||
| 
 | ||||
|     def render_table_element(self, template='/grids/b-table.mako', | ||||
|                              data_prop='gridData', empty_labels=False, | ||||
|                              literal=False, | ||||
|                              **kwargs): | ||||
|         """ | ||||
|         This is intended for ad-hoc "small" grids with static data.  Renders | ||||
|  | @ -1239,7 +1240,10 @@ class Grid(WuttaGrid): | |||
|         if context['paginated']: | ||||
|             context.setdefault('per_page', 20) | ||||
|         context['view_click_handler'] = self.get_view_click_handler() | ||||
|         return render(template, context) | ||||
|         result = render(template, context) | ||||
|         if literal: | ||||
|             result = HTML.literal(result) | ||||
|         return result | ||||
| 
 | ||||
|     def get_view_click_handler(self): | ||||
|         """ """ | ||||
|  |  | |||
|  | @ -394,6 +394,11 @@ class TailboneMenuHandler(WuttaMenuHandler): | |||
|                     'route': 'products', | ||||
|                     'perm': 'products.list', | ||||
|                 }, | ||||
|                 { | ||||
|                     'title': "Product Costs", | ||||
|                     'route': 'product_costs', | ||||
|                     'perm': 'product_costs.list', | ||||
|                 }, | ||||
|                 { | ||||
|                     'title': "Departments", | ||||
|                     'route': 'departments', | ||||
|  | @ -451,6 +456,11 @@ class TailboneMenuHandler(WuttaMenuHandler): | |||
|                     'route': 'vendors', | ||||
|                     'perm': 'vendors.list', | ||||
|                 }, | ||||
|                 { | ||||
|                     'title': "Product Costs", | ||||
|                     'route': 'product_costs', | ||||
|                     'perm': 'product_costs.list', | ||||
|                 }, | ||||
|                 {'type': 'sep'}, | ||||
|                 { | ||||
|                     'title': "Ordering", | ||||
|  |  | |||
|  | @ -1,6 +1,7 @@ | |||
| <div i18n:domain="deform" tal:omit-tag="" | ||||
|       tal:define="oid oid|field.oid; | ||||
|                   name name|field.name; | ||||
|                   vmodel vmodel|'field_model_' + name; | ||||
|                   css_class css_class|field.widget.css_class; | ||||
|                   style style|field.widget.style;"> | ||||
| 
 | ||||
|  | @ -8,7 +9,7 @@ | |||
|     ${field.start_mapping()} | ||||
|     <b-input type="password" | ||||
|              name="${name}" | ||||
|              value="${field.widget.redisplay and cstruct or ''}" | ||||
|              v-model="${vmodel}" | ||||
|              tal:attributes="class string: form-control ${css_class or ''}; | ||||
|                              style style; | ||||
|                              attributes|field.widget.attributes|{};" | ||||
|  | @ -18,7 +19,6 @@ | |||
|     </b-input> | ||||
|     <b-input type="password" | ||||
|              name="${name}-confirm" | ||||
|              value="${field.widget.redisplay and confirm or ''}" | ||||
|              tal:attributes="class string: form-control ${css_class or ''}; | ||||
|                              style style; | ||||
|                              confirm_attributes|field.widget.confirm_attributes|{};" | ||||
|  |  | |||
|  | @ -196,6 +196,7 @@ | |||
| 
 | ||||
|                   <p class="block has-text-weight-bold"> | ||||
|                     {{ version.model_title }} | ||||
|                     ({{ version.operation }}) | ||||
|                   </p> | ||||
| 
 | ||||
|                   <table class="diff monospace is-size-7" | ||||
|  |  | |||
|  | @ -55,19 +55,20 @@ | |||
| </%def> | ||||
| 
 | ||||
| <%def name="render_form_template()"> | ||||
|   <script type="text/x-template" id="${form.component}-template"> | ||||
|   <script type="text/x-template" id="${form.vue_tagname}-template"> | ||||
|     ${self.render_form_innards()} | ||||
|   </script> | ||||
| </%def> | ||||
| 
 | ||||
| <%def name="modify_vue_vars()"> | ||||
|   ${parent.modify_vue_vars()} | ||||
|   <% request.register_component(form.vue_tagname, form.vue_component) %> | ||||
|   <script> | ||||
| 
 | ||||
|     ## TODO: ugh, an awful lot of duplicated code here (from /forms/deform.mako) | ||||
| 
 | ||||
|     let ${form.vue_component} = { | ||||
|         template: '#${form.component}-template', | ||||
|         template: '#${form.vue_tagname}-template', | ||||
|         methods: { | ||||
| 
 | ||||
|             ## TODO: deprecate / remove the latter option here | ||||
|  |  | |||
|  | @ -45,11 +45,10 @@ | |||
|             <b-button @click="runReportShowDialog = false"> | ||||
|               Cancel | ||||
|             </b-button> | ||||
|             ${h.form(master.get_action_url('execute', instance))} | ||||
|             ${h.form(master.get_action_url('execute', instance), **{'@submit': 'runReportSubmitting = true'})} | ||||
|             ${h.csrf_token(request)} | ||||
|             <b-button type="is-primary" | ||||
|                       native-type="submit" | ||||
|                       @click="runReportSubmitting = true" | ||||
|                       :disabled="runReportSubmitting" | ||||
|                       icon-pack="fas" | ||||
|                       icon-left="arrow-circle-right"> | ||||
|  |  | |||
|  | @ -300,8 +300,8 @@ def get_available_themes(rattail_config, include=None): | |||
|     available.sort() | ||||
| 
 | ||||
|     # make default theme the first option | ||||
|     i = available.index('default') | ||||
|     if i >= 0: | ||||
|     if 'default' in available: | ||||
|         i = available.index('default') | ||||
|         available.pop(i) | ||||
|     available.insert(0, 'default') | ||||
| 
 | ||||
|  |  | |||
|  | @ -44,28 +44,6 @@ class UserLogin(colander.MappingSchema): | |||
|                                    widget=dfwidget.PasswordWidget()) | ||||
| 
 | ||||
| 
 | ||||
| @colander.deferred | ||||
| def current_password_correct(node, kw): | ||||
|     request = kw['request'] | ||||
|     app = request.rattail_config.get_app() | ||||
|     auth = app.get_auth_handler() | ||||
|     user = kw['user'] | ||||
|     def validate(node, value): | ||||
|         if not auth.authenticate_user(Session(), user.username, value): | ||||
|             raise colander.Invalid(node, "The password is incorrect") | ||||
|     return validate | ||||
| 
 | ||||
| 
 | ||||
| class ChangePassword(colander.MappingSchema): | ||||
| 
 | ||||
|     current_password = colander.SchemaNode(colander.String(), | ||||
|                                            widget=dfwidget.PasswordWidget(), | ||||
|                                            validator=current_password_correct) | ||||
| 
 | ||||
|     new_password = colander.SchemaNode(colander.String(), | ||||
|                                        widget=dfwidget.CheckedPasswordWidget()) | ||||
| 
 | ||||
| 
 | ||||
| class AuthenticationView(View): | ||||
| 
 | ||||
|     def forbidden(self): | ||||
|  | @ -116,10 +94,6 @@ class AuthenticationView(View): | |||
|             else: | ||||
|                 self.request.session.flash("Invalid username or password", 'error') | ||||
| 
 | ||||
|         image_url = self.rattail_config.get( | ||||
|             'tailbone', 'main_image_url', | ||||
|             default=self.request.static_url('tailbone:static/img/home_logo.png')) | ||||
| 
 | ||||
|         # nb. hacky..but necessary, to add the refs, for autofocus | ||||
|         # (also add key handler, so ENTER acts like TAB) | ||||
|         dform = form.make_deform_form() | ||||
|  | @ -132,7 +106,6 @@ class AuthenticationView(View): | |||
|         return { | ||||
|             'form': form, | ||||
|             'referrer': referrer, | ||||
|             'image_url': image_url, | ||||
|             'index_title': app.get_node_title(), | ||||
|             'help_url': global_help_url(self.rattail_config), | ||||
|         } | ||||
|  | @ -181,7 +154,23 @@ class AuthenticationView(View): | |||
|                 self.request.user)) | ||||
|             return self.redirect(self.request.get_referrer()) | ||||
| 
 | ||||
|         schema = ChangePassword().bind(user=self.request.user, request=self.request) | ||||
|         def check_user_password(node, value): | ||||
|             auth = self.app.get_auth_handler() | ||||
|             user = self.request.user | ||||
|             if not auth.check_user_password(user, value): | ||||
|                 node.raise_invalid("The password is incorrect") | ||||
| 
 | ||||
|         schema = colander.Schema() | ||||
| 
 | ||||
|         schema.add(colander.SchemaNode(colander.String(), | ||||
|                                        name='current_password', | ||||
|                                        widget=dfwidget.PasswordWidget(), | ||||
|                                        validator=check_user_password)) | ||||
| 
 | ||||
|         schema.add(colander.SchemaNode(colander.String(), | ||||
|                                        name='new_password', | ||||
|                                        widget=dfwidget.CheckedPasswordWidget())) | ||||
| 
 | ||||
|         form = forms.Form(schema=schema, request=self.request) | ||||
|         if form.validate(): | ||||
|             auth = self.app.get_auth_handler() | ||||
|  |  | |||
|  | @ -343,7 +343,7 @@ class MasterView(View): | |||
|             return self.redirect(self.request.current_route_url(**kw)) | ||||
| 
 | ||||
|         # Stash some grid stats, for possible use when generating URLs. | ||||
|         if grid.paginated and hasattr(grid, 'pager'): | ||||
|         if grid.paginated and grid.pager is not None: | ||||
|             self.first_visible_grid_index = grid.pager.first_item | ||||
| 
 | ||||
|         # return grid data only, if partial page was requested | ||||
|  | @ -412,7 +412,7 @@ class MasterView(View): | |||
|             session = self.Session() | ||||
|         kwargs.setdefault('paginated', False) | ||||
|         grid = self.make_grid(session=session, **kwargs) | ||||
|         return grid.make_visible_data() | ||||
|         return grid.get_visible_data() | ||||
| 
 | ||||
|     def get_grid_columns(self): | ||||
|         """ | ||||
|  | @ -903,7 +903,7 @@ class MasterView(View): | |||
| 
 | ||||
|     def valid_employee_uuid(self, node, value): | ||||
|         if value: | ||||
|             model = self.model | ||||
|             model = self.app.model | ||||
|             employee = self.Session.get(model.Employee, value) | ||||
|             if not employee: | ||||
|                 node.raise_invalid("Employee not found") | ||||
|  | @ -939,7 +939,7 @@ class MasterView(View): | |||
| 
 | ||||
|     def valid_vendor_uuid(self, node, value): | ||||
|         if value: | ||||
|             model = self.model | ||||
|             model = self.app.model | ||||
|             vendor = self.Session.get(model.Vendor, value) | ||||
|             if not vendor: | ||||
|                 node.raise_invalid("Vendor not found") | ||||
|  | @ -1382,7 +1382,7 @@ class MasterView(View): | |||
|         return classes | ||||
| 
 | ||||
|     def make_revisions_grid(self, obj, empty_data=False): | ||||
|         model = self.model | ||||
|         model = self.app.model | ||||
|         route_prefix = self.get_route_prefix() | ||||
|         row_url = lambda txn, i: self.request.route_url(f'{route_prefix}.version', | ||||
|                                                         uuid=obj.uuid, | ||||
|  | @ -1710,7 +1710,7 @@ class MasterView(View): | |||
|         kwargs.setdefault('paginated', False) | ||||
|         kwargs.setdefault('sortable', sort) | ||||
|         grid = self.make_row_grid(session=session, **kwargs) | ||||
|         return grid.make_visible_data() | ||||
|         return grid.get_visible_data() | ||||
| 
 | ||||
|     @classmethod | ||||
|     def get_row_url_prefix(cls): | ||||
|  | @ -2153,7 +2153,7 @@ class MasterView(View): | |||
|         Thread target for executing an object. | ||||
|         """ | ||||
|         app = self.get_rattail_app() | ||||
|         model = self.model | ||||
|         model = self.app.model | ||||
|         session = app.make_session() | ||||
|         obj = self.get_instance_for_key(key, session) | ||||
|         user = session.get(model.User, user_uuid) | ||||
|  | @ -2594,7 +2594,7 @@ class MasterView(View): | |||
|         """ | ||||
|         # nb. self.Session may differ, so use tailbone.db.Session | ||||
|         session = Session() | ||||
|         model = self.model | ||||
|         model = self.app.model | ||||
|         route_prefix = self.get_route_prefix() | ||||
| 
 | ||||
|         info = session.query(model.TailbonePageHelp)\ | ||||
|  | @ -2617,7 +2617,7 @@ class MasterView(View): | |||
|         """ | ||||
|         # nb. self.Session may differ, so use tailbone.db.Session | ||||
|         session = Session() | ||||
|         model = self.model | ||||
|         model = self.app.model | ||||
|         route_prefix = self.get_route_prefix() | ||||
| 
 | ||||
|         info = session.query(model.TailbonePageHelp)\ | ||||
|  | @ -2639,7 +2639,7 @@ class MasterView(View): | |||
| 
 | ||||
|         # nb. self.Session may differ, so use tailbone.db.Session | ||||
|         session = Session() | ||||
|         model = self.model | ||||
|         model = self.app.model | ||||
|         route_prefix = self.get_route_prefix() | ||||
|         schema = colander.Schema() | ||||
| 
 | ||||
|  | @ -2673,7 +2673,7 @@ class MasterView(View): | |||
| 
 | ||||
|         # nb. self.Session may differ, so use tailbone.db.Session | ||||
|         session = Session() | ||||
|         model = self.model | ||||
|         model = self.app.model | ||||
|         route_prefix = self.get_route_prefix() | ||||
|         schema = colander.Schema() | ||||
| 
 | ||||
|  | @ -5541,7 +5541,7 @@ class MasterView(View): | |||
|                                   input_file_templates=True, | ||||
|                                   output_file_templates=True): | ||||
|         app = self.get_rattail_app() | ||||
|         model = self.model | ||||
|         model = self.app.model | ||||
|         names = [] | ||||
| 
 | ||||
|         if simple_settings is None: | ||||
|  | @ -6100,7 +6100,7 @@ class MasterView(View): | |||
|                         renderer='json') | ||||
| 
 | ||||
| 
 | ||||
| class ViewSupplement(object): | ||||
| class ViewSupplement: | ||||
|     """ | ||||
|     Base class for view "supplements" - which are sort of like plugins | ||||
|     which can "supplement" certain aspects of the view. | ||||
|  | @ -6127,6 +6127,7 @@ class ViewSupplement(object): | |||
|     def __init__(self, master): | ||||
|         self.master = master | ||||
|         self.request = master.request | ||||
|         self.app = master.app | ||||
|         self.model = master.model | ||||
|         self.rattail_config = master.rattail_config | ||||
|         self.Session = master.Session | ||||
|  | @ -6160,7 +6161,7 @@ class ViewSupplement(object): | |||
|         This is accomplished by subjecting the current base query to a | ||||
|         join, e.g. something like:: | ||||
| 
 | ||||
|            model = self.model | ||||
|            model = self.app.model | ||||
|            query = query.outerjoin(model.MyExtension) | ||||
|            return query | ||||
|         """ | ||||
|  |  | |||
|  | @ -564,15 +564,19 @@ class PersonView(MasterView): | |||
|         Method which must return the base query for the profile's POS | ||||
|         Transactions grid data. | ||||
|         """ | ||||
|         app = self.get_rattail_app() | ||||
|         customer = app.get_customer(person) | ||||
|         customer = self.app.get_customer(person) | ||||
| 
 | ||||
|         key_field = app.get_customer_key_field() | ||||
|         customer_key = getattr(customer, key_field) | ||||
|         if customer_key is not None: | ||||
|             customer_key = str(customer_key) | ||||
|         if customer: | ||||
|             key_field = self.app.get_customer_key_field() | ||||
|             customer_key = getattr(customer, key_field) | ||||
|             if customer_key is not None: | ||||
|                 customer_key = str(customer_key) | ||||
|         else: | ||||
|             # nb. this should *not* match anything, so query returns | ||||
|             # no results.. | ||||
|             customer_key = person.uuid | ||||
| 
 | ||||
|         trainwreck = app.get_trainwreck_handler() | ||||
|         trainwreck = self.app.get_trainwreck_handler() | ||||
|         model = trainwreck.get_model() | ||||
|         query = TrainwreckSession.query(model.Transaction)\ | ||||
|                                  .filter(model.Transaction.customer_id == customer_key) | ||||
|  |  | |||
|  | @ -34,7 +34,7 @@ import sqlalchemy_continuum as continuum | |||
| 
 | ||||
| from rattail import enum, pod, sil | ||||
| from rattail.db import api, auth, Session as RattailSession | ||||
| from rattail.db.model import Product, PendingProduct, CustomerOrderItem | ||||
| from rattail.db.model import Product, PendingProduct, ProductCost, CustomerOrderItem | ||||
| from rattail.gpc import GPC | ||||
| from rattail.threads import Thread | ||||
| from rattail.exceptions import LabelPrintingError | ||||
|  | @ -1857,7 +1857,8 @@ class ProductView(MasterView): | |||
|             lookup_fields.append('alt_code') | ||||
|         if lookup_fields: | ||||
|             product = self.products_handler.locate_product_for_entry( | ||||
|                 session, term, lookup_fields=lookup_fields) | ||||
|                 session, term, lookup_fields=lookup_fields, | ||||
|                 first_if_multiple=True) | ||||
|             if product: | ||||
|                 final_results.append(self.search_normalize_result(product)) | ||||
| 
 | ||||
|  | @ -2668,6 +2669,78 @@ class PendingProductView(MasterView): | |||
|                         permission=f'{permission_prefix}.ignore_product') | ||||
| 
 | ||||
| 
 | ||||
| class ProductCostView(MasterView): | ||||
|     """ | ||||
|     Master view for Product Costs | ||||
|     """ | ||||
|     model_class = ProductCost | ||||
|     route_prefix = 'product_costs' | ||||
|     url_prefix = '/products/costs' | ||||
|     has_versions = True | ||||
| 
 | ||||
|     grid_columns = [ | ||||
|         '_product_key_', | ||||
|         'vendor', | ||||
|         'preference', | ||||
|         'code', | ||||
|         'case_size', | ||||
|         'case_cost', | ||||
|         'pack_size', | ||||
|         'pack_cost', | ||||
|         'unit_cost', | ||||
|     ] | ||||
| 
 | ||||
|     def query(self, session): | ||||
|         """ """ | ||||
|         query = super().query(session) | ||||
|         model = self.app.model | ||||
| 
 | ||||
|         # always join on Product | ||||
|         return query.join(model.Product) | ||||
| 
 | ||||
|     def configure_grid(self, g): | ||||
|         """ """ | ||||
|         super().configure_grid(g) | ||||
|         model = self.app.model | ||||
| 
 | ||||
|         # product key | ||||
|         field = self.get_product_key_field() | ||||
|         g.set_renderer(field, self.render_product_key) | ||||
|         g.set_sorter(field, getattr(model.Product, field)) | ||||
|         g.set_sort_defaults(field) | ||||
|         g.set_filter(field, getattr(model.Product, field)) | ||||
| 
 | ||||
|         # vendor | ||||
|         g.set_joiner('vendor', lambda q: q.join(model.Vendor)) | ||||
|         g.set_sorter('vendor', model.Vendor.name) | ||||
|         g.set_filter('vendor', model.Vendor.name, label="Vendor Name") | ||||
| 
 | ||||
|     def render_product_key(self, cost, field): | ||||
|         """ """ | ||||
|         handler = self.app.get_products_handler() | ||||
|         return handler.render_product_key(cost.product) | ||||
| 
 | ||||
|     def configure_form(self, f): | ||||
|         """ """ | ||||
|         super().configure_form(f) | ||||
| 
 | ||||
|         # product | ||||
|         f.set_renderer('product', self.render_product) | ||||
|         if 'product_uuid' in f and 'product' in f: | ||||
|             f.remove('product') | ||||
|             f.replace('product_uuid', 'product') | ||||
| 
 | ||||
|         # vendor | ||||
|         f.set_renderer('vendor', self.render_vendor) | ||||
|         if 'vendor_uuid' in f and 'vendor' in f: | ||||
|             f.remove('vendor') | ||||
|             f.replace('vendor_uuid', 'vendor') | ||||
| 
 | ||||
|         # futures | ||||
|         # TODO: should eventually show a subgrid here? | ||||
|         f.remove('futures') | ||||
| 
 | ||||
| 
 | ||||
| def defaults(config, **kwargs): | ||||
|     base = globals() | ||||
| 
 | ||||
|  | @ -2677,6 +2750,9 @@ def defaults(config, **kwargs): | |||
|     PendingProductView = kwargs.get('PendingProductView', base['PendingProductView']) | ||||
|     PendingProductView.defaults(config) | ||||
| 
 | ||||
|     ProductCostView = kwargs.get('ProductCostView', base['ProductCostView']) | ||||
|     ProductCostView.defaults(config) | ||||
| 
 | ||||
| 
 | ||||
| def includeme(config): | ||||
|     defaults(config) | ||||
|  |  | |||
|  | @ -341,7 +341,7 @@ class TestGrid(WebTestCase): | |||
| 
 | ||||
|         # settings are loaded, applied, saved | ||||
|         self.assertEqual(grid.sort_defaults, []) | ||||
|         self.assertFalse(hasattr(grid, 'active_sorters')) | ||||
|         self.assertIsNone(grid.active_sorters) | ||||
|         self.request.GET = {'sort1key': 'name', 'sort1dir': 'desc'} | ||||
|         grid.load_settings() | ||||
|         self.assertEqual(grid.active_sorters, [{'key': 'name', 'dir': 'desc'}]) | ||||
|  | @ -365,7 +365,7 @@ class TestGrid(WebTestCase): | |||
|         # with sort defaults | ||||
|         grid = self.make_grid(model_class=model.Setting, sortable=True, | ||||
|                               sort_on_backend=True, sort_defaults='name') | ||||
|         self.assertFalse(hasattr(grid, 'active_sorters')) | ||||
|         self.assertIsNone(grid.active_sorters) | ||||
|         grid.load_settings() | ||||
|         self.assertEqual(grid.active_sorters, [{'key': 'name', 'dir': 'asc'}]) | ||||
| 
 | ||||
|  | @ -376,7 +376,7 @@ class TestGrid(WebTestCase): | |||
|             mod.SortInfo('name', 'asc'), | ||||
|             mod.SortInfo('value', 'desc'), | ||||
|         ] | ||||
|         self.assertFalse(hasattr(grid, 'active_sorters')) | ||||
|         self.assertIsNone(grid.active_sorters) | ||||
|         grid.load_settings() | ||||
|         self.assertEqual(grid.active_sorters, [{'key': 'name', 'dir': 'asc'}]) | ||||
| 
 | ||||
|  | @ -390,7 +390,7 @@ class TestGrid(WebTestCase): | |||
|         grid = self.make_grid(key='settings', model_class=model.Setting, | ||||
|                               sortable=True, sort_on_backend=True, | ||||
|                               paginated=True, paginate_on_backend=True) | ||||
|         self.assertFalse(hasattr(grid, 'active_sorters')) | ||||
|         self.assertIsNone(grid.active_sorters) | ||||
|         grid.load_settings() | ||||
|         self.assertEqual(grid.active_sorters, [{'key': 'name', 'dir': 'desc'}]) | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue