From 1575cad4478e387ff0c090882f3afd5c46cdca90 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 29 Jan 2022 08:57:03 -0600 Subject: [PATCH 0001/1083] Improve profile link helper for buefy themes --- tailbone/templates/util.mako | 43 +++++++++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/tailbone/templates/util.mako b/tailbone/templates/util.mako index 5d3100ad..2d4653aa 100644 --- a/tailbone/templates/util.mako +++ b/tailbone/templates/util.mako @@ -2,20 +2,43 @@ <%def name="view_profile_button(person)">
- ${h.link_to(person, url('people.view_profile', uuid=person.uuid), class_='button is-primary')} + % if use_buefy: + + ${person} + + % else: + ${h.link_to(person, url('people.view_profile', uuid=person.uuid), class_='button is-primary')} + % endif
<%def name="view_profiles_helper(people)"> % if request.has_perm('people.view_profile'): -
-

Profiles

-
-

View full profile for:

- % for person in people: - ${view_profile_button(person)} - % endfor -
-
+ % if use_buefy: + + % else: +
+

Profiles

+
+

View full profile for:

+ % for person in people: + ${view_profile_button(person)} + % endfor +
+
+ % endif % endif From 999bb29499c40a189b2bb6d71250b17f644a29e2 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 29 Jan 2022 12:36:54 -0600 Subject: [PATCH 0002/1083] Add support for rattail-integration project generator --- tailbone/templates/generate_project.mako | 77 ++++++++++++++++++++++++ tailbone/views/projects.py | 29 ++++++++- 2 files changed, 103 insertions(+), 3 deletions(-) diff --git a/tailbone/templates/generate_project.mako b/tailbone/templates/generate_project.mako index 51f404ee..fa39ec08 100644 --- a/tailbone/templates/generate_project.mako +++ b/tailbone/templates/generate_project.mako @@ -9,6 +9,7 @@ + ## @@ -181,6 +182,73 @@ ${h.end_form()} +
+ ${h.form(request.current_route_url(), ref='rattail_integrationForm')} + ${h.csrf_token(request)} + ${h.hidden('project_type', value='rattail_integration')} +
+
+
+

Naming

+
+
+
+ + + + + + + + + + + + + + ${h.hidden('slug', **{'v-model': 'rattail_integration.python_project_name'})} + + + + + +
+
+
+
+
+
+

Options

+
+
+
+ + + + + + + + + + + +
+
+
+ ${h.end_form()} +
+
${h.form(request.current_route_url(), ref='byjoveForm')} ${h.csrf_token(request)} @@ -310,6 +378,15 @@ uses_fabric: true, } + ThisPageData.rattail_integration = { + integration_name: "Foo", + integration_url: "https://www.example.com/", + python_project_name: "rattail-foo", + python_package_name: "rattail_foo", + extends_config: true, + extends_db: true, + } + ThisPageData.byjove = { name: "Okay-Then-Mobile", slug: "okay-then-mobile", diff --git a/tailbone/views/projects.py b/tailbone/views/projects.py index 1770e021..489cb4f4 100644 --- a/tailbone/views/projects.py +++ b/tailbone/views/projects.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2020 Lance Edgar +# Copyright © 2010-2022 Lance Edgar # # This file is part of Rattail. # @@ -81,6 +81,25 @@ class GenerateProject(colander.MappingSchema): uses_fabric = colander.SchemaNode(colander.Boolean()) +class GenerateRattailIntegrationProject(colander.MappingSchema): + """ + Schema to generate new rattail-integration project + """ + integration_name = colander.SchemaNode(colander.String()) + + integration_url = colander.SchemaNode(colander.String()) + + slug = colander.SchemaNode(colander.String()) + + python_project_name = colander.SchemaNode(colander.String()) + + python_name = colander.SchemaNode(colander.String()) + + extends_config = colander.SchemaNode(colander.Boolean()) + + extends_db = colander.SchemaNode(colander.Boolean()) + + class GenerateByjoveProject(colander.MappingSchema): """ Schema for generating a new 'byjove' project @@ -115,7 +134,9 @@ class GenerateProjectView(View): def __init__(self, request): super(GenerateProjectView, self).__init__(request) - self.handler = self.get_handler() + self.project_handler = self.get_handler() + # TODO: deprecate / remove this + self.handler = self.project_handler def get_handler(self): from rattail.projects.handler import RattailProjectHandler @@ -132,13 +153,15 @@ class GenerateProjectView(View): project_type = 'rattail' if self.request.method == 'POST': project_type = self.request.POST.get('project_type', 'rattail') - if project_type not in ('rattail', 'byjove', 'fabric'): + if project_type not in self.project_handler.get_supported_project_types(): raise ValueError("Unknown project type: {}".format(project_type)) if project_type == 'byjove': schema = GenerateByjoveProject elif project_type == 'fabric': schema = GenerateFabricProject + elif project_type == 'rattail_integration': + schema = GenerateRattailIntegrationProject else: schema = GenerateProject form = forms.Form(schema=schema(), request=self.request, From 8a08b3f7c75367249822b24e2141af5330fdf02b Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 29 Jan 2022 14:42:52 -0600 Subject: [PATCH 0003/1083] Add support for tailbone-integration project generator --- tailbone/templates/generate_project.mako | 48 ++++++++++++++++++++++++ tailbone/views/projects.py | 17 +++++++++ 2 files changed, 65 insertions(+) diff --git a/tailbone/templates/generate_project.mako b/tailbone/templates/generate_project.mako index fa39ec08..72caa83c 100644 --- a/tailbone/templates/generate_project.mako +++ b/tailbone/templates/generate_project.mako @@ -10,6 +10,7 @@ + ## @@ -249,6 +250,46 @@ ${h.end_form()}
+
+ ${h.form(request.current_route_url(), ref='tailbone_integrationForm')} + ${h.csrf_token(request)} + ${h.hidden('project_type', value='tailbone_integration')} +
+
+
+

Naming

+
+
+
+ + + + + + + + + + + + + + ${h.hidden('slug', **{'v-model': 'tailbone_integration.python_project_name'})} + + + + + +
+
+
+ ${h.end_form()} +
+
${h.form(request.current_route_url(), ref='byjoveForm')} ${h.csrf_token(request)} @@ -387,6 +428,13 @@ extends_db: true, } + ThisPageData.tailbone_integration = { + integration_name: "Foo", + integration_url: "https://www.example.com/", + python_project_name: "tailbone-foo", + python_package_name: "tailbone_foo", + } + ThisPageData.byjove = { name: "Okay-Then-Mobile", slug: "okay-then-mobile", diff --git a/tailbone/views/projects.py b/tailbone/views/projects.py index 489cb4f4..746a3c47 100644 --- a/tailbone/views/projects.py +++ b/tailbone/views/projects.py @@ -100,6 +100,21 @@ class GenerateRattailIntegrationProject(colander.MappingSchema): extends_db = colander.SchemaNode(colander.Boolean()) +class GenerateTailboneIntegrationProject(colander.MappingSchema): + """ + Schema to generate new tailbone-integration project + """ + integration_name = colander.SchemaNode(colander.String()) + + integration_url = colander.SchemaNode(colander.String()) + + slug = colander.SchemaNode(colander.String()) + + python_project_name = colander.SchemaNode(colander.String()) + + python_name = colander.SchemaNode(colander.String()) + + class GenerateByjoveProject(colander.MappingSchema): """ Schema for generating a new 'byjove' project @@ -162,6 +177,8 @@ class GenerateProjectView(View): schema = GenerateFabricProject elif project_type == 'rattail_integration': schema = GenerateRattailIntegrationProject + elif project_type == 'tailbone_integration': + schema = GenerateTailboneIntegrationProject else: schema = GenerateProject form = forms.Form(schema=schema(), request=self.request, From 16a4fe1a4f129b699d959855bab3011084d97cb5 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 31 Jan 2022 14:52:55 -0600 Subject: [PATCH 0004/1083] Update changelog --- CHANGES.rst | 8 ++++++++ tailbone/_version.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index a311d8ed..67ca37fd 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,14 @@ CHANGELOG ========= +0.8.200 (2022-01-31) +-------------------- + +* Improve profile link helper for buefy themes. + +* Add project generator support for rattail-integration, tailbone-integration. + + 0.8.199 (2022-01-26) -------------------- diff --git a/tailbone/_version.py b/tailbone/_version.py index ea7cb4cc..e78d61a4 100644 --- a/tailbone/_version.py +++ b/tailbone/_version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8; -*- -__version__ = '0.8.199' +__version__ = '0.8.200' From 4716545b7ea27480a1016158bffa21ce7140ca66 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 31 Jan 2022 16:52:16 -0600 Subject: [PATCH 0005/1083] Show helptext for params when generating new report --- tailbone/templates/forms/util.mako | 3 +++ tailbone/views/reports.py | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/tailbone/templates/forms/util.mako b/tailbone/templates/forms/util.mako index cc3a5d2d..0b4f4012 100644 --- a/tailbone/templates/forms/util.mako +++ b/tailbone/templates/forms/util.mako @@ -9,6 +9,9 @@ % if isinstance(field.schema.typ, deform.FileData): class="file" % endif + % if form.has_helptext(field.name): + message="${form.render_helptext(field.name)}" + % endif % if error_messages: type="is-danger" :message='${form.messages_json(error_messages)|n}' diff --git a/tailbone/views/reports.py b/tailbone/views/reports.py index 2839c5b5..c9a14be7 100644 --- a/tailbone/views/reports.py +++ b/tailbone/views/reports.py @@ -401,6 +401,7 @@ class GenerateReport(View): } schema = colander.Schema() + helptext = {} for param in report_params: # make a new node of appropriate schema type @@ -420,10 +421,13 @@ class GenerateReport(View): if hasattr(param, 'default'): node.default = param.default + # set docstring + helptext[param.name] = param.helptext + schema.add(node) form = forms.Form(schema=schema, request=self.request, - use_buefy=use_buefy) + use_buefy=use_buefy, helptext=helptext) form.submit_label = "Generate this Report" form.cancel_url = self.request.route_url('generate_report') From 15fc82fc34edec82c0df47155186ac45acfeb5cf Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 31 Jan 2022 17:51:03 -0600 Subject: [PATCH 0006/1083] Tweak handling of empty params when generating report not sure there was a compelling reason to use `colander.null` other than that is what pyramid generally does? but `None` seems to work fine for me so far.. (used w/ optional date param) --- tailbone/views/reports.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tailbone/views/reports.py b/tailbone/views/reports.py index c9a14be7..b9850ed6 100644 --- a/tailbone/views/reports.py +++ b/tailbone/views/reports.py @@ -415,7 +415,7 @@ class GenerateReport(View): # allow empty value if param is optional if not param.required: - node.missing = colander.null + node.missing = None # maybe set default value if hasattr(param, 'default'): From d677cb1bc8b1253d78e9c4128832cfa684de6350 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 31 Jan 2022 17:53:37 -0600 Subject: [PATCH 0007/1083] Update changelog --- CHANGES.rst | 8 ++++++++ tailbone/_version.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 67ca37fd..fbd74f40 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,14 @@ CHANGELOG ========= +0.8.201 (2022-01-31) +-------------------- + +* Show helptext for params when generating new report. + +* Tweak handling of empty params when generating report. + + 0.8.200 (2022-01-31) -------------------- diff --git a/tailbone/_version.py b/tailbone/_version.py index e78d61a4..24b92de9 100644 --- a/tailbone/_version.py +++ b/tailbone/_version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8; -*- -__version__ = '0.8.200' +__version__ = '0.8.201' From b22e7fd07778cf8150cb63d566695956eea84814 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 31 Jan 2022 19:34:24 -0600 Subject: [PATCH 0008/1083] Make "generate report" the same as "create new generated report" no reason to reinvent that wheel --- .../reports/{ => generated}/choose.mako | 12 +- .../reports/{ => generated}/generate.mako | 10 +- .../templates/reports/generated/index.mako | 11 -- .../templates/reports/generated/view.mako | 7 - tailbone/views/exports.py | 8 +- tailbone/views/reports.py | 128 ++++++++---------- 6 files changed, 58 insertions(+), 118 deletions(-) rename tailbone/templates/reports/{ => generated}/choose.mako (90%) rename tailbone/templates/reports/{ => generated}/generate.mako (63%) delete mode 100644 tailbone/templates/reports/generated/index.mako diff --git a/tailbone/templates/reports/choose.mako b/tailbone/templates/reports/generated/choose.mako similarity index 90% rename from tailbone/templates/reports/choose.mako rename to tailbone/templates/reports/generated/choose.mako index 58c9ee22..a6cb8977 100644 --- a/tailbone/templates/reports/choose.mako +++ b/tailbone/templates/reports/generated/choose.mako @@ -1,9 +1,5 @@ ## -*- coding: utf-8; -*- -<%inherit file="/form.mako" /> - -<%def name="title()">${index_title} - -<%def name="content_title()"> +<%inherit file="/master/create.mako" /> <%def name="extra_javascript()"> ${parent.extra_javascript()} @@ -60,12 +56,6 @@ -<%def name="context_menu_items()"> - % if request.has_perm('report_output.list'): - ${h.link_to("View Generated Reports", url('report_output'))} - % endif - - <%def name="render_buefy_form()">

Please select the type of report you wish to generate.

diff --git a/tailbone/templates/reports/generate.mako b/tailbone/templates/reports/generated/generate.mako similarity index 63% rename from tailbone/templates/reports/generate.mako rename to tailbone/templates/reports/generated/generate.mako index 57e72385..38adfe34 100644 --- a/tailbone/templates/reports/generate.mako +++ b/tailbone/templates/reports/generated/generate.mako @@ -1,15 +1,9 @@ ## -*- coding: utf-8; -*- -<%inherit file="/form.mako" /> +<%inherit file="/master/form.mako" /> <%def name="title()">${index_title} » ${report.name} -<%def name="content_title()">${report.name} - -<%def name="context_menu_items()"> - % if request.has_perm('report_output.list'): - ${h.link_to("View Generated Reports", url('report_output'))} - % endif - +<%def name="content_title()">New Report:  ${report.name} <%def name="render_buefy_form()">
diff --git a/tailbone/templates/reports/generated/index.mako b/tailbone/templates/reports/generated/index.mako deleted file mode 100644 index 63a5b9b5..00000000 --- a/tailbone/templates/reports/generated/index.mako +++ /dev/null @@ -1,11 +0,0 @@ -## -*- coding: utf-8; -*- -<%inherit file="/master/index.mako" /> - -<%def name="context_menu_items()"> - ${parent.context_menu_items()} - % if request.has_perm('{}.generate'.format(permission_prefix)): -
  • ${h.link_to("Generate new Report", url('generate_report'))}
  • - % endif - - -${parent.body()} diff --git a/tailbone/templates/reports/generated/view.mako b/tailbone/templates/reports/generated/view.mako index ce8ef38d..496857c5 100644 --- a/tailbone/templates/reports/generated/view.mako +++ b/tailbone/templates/reports/generated/view.mako @@ -1,13 +1,6 @@ ## -*- coding: utf-8; -*- <%inherit file="/master/view.mako" /> -<%def name="context_menu_items()"> - ${parent.context_menu_items()} - % if request.has_perm('{}.generate'.format(permission_prefix)): -
  • ${h.link_to("Generate new Report", url('generate_report'))}
  • - % endif - - <%def name="modify_this_page_vars()"> ${parent.modify_this_page_vars()} diff --git a/tailbone/templates/deform/autocomplete_jquery.pt b/tailbone/templates/deform/autocomplete_jquery.pt index 6e1a1e61..c0e79c29 100644 --- a/tailbone/templates/deform/autocomplete_jquery.pt +++ b/tailbone/templates/deform/autocomplete_jquery.pt @@ -108,10 +108,13 @@ tal:define="vmodel vmodel|'field_model_' + name;" tal:omit-tag=""> + tal:attributes=":assigned-label assigned_label or 'null'; + @input input_callback or 'null'; + @new-label new_label_callback or 'null';">
    diff --git a/tailbone/views/batch/vendorcatalog.py b/tailbone/views/batch/vendorcatalog.py index b463a5f7..8173cac5 100644 --- a/tailbone/views/batch/vendorcatalog.py +++ b/tailbone/views/batch/vendorcatalog.py @@ -203,9 +203,14 @@ class VendorCatalogView(FileBatchMasterView): if vendor: vendor_display = six.text_type(vendor) vendors_url = self.request.route_url('vendors.autocomplete') - f.set_widget('vendor_uuid', forms.widgets.JQueryAutocompleteWidget( - field_display=vendor_display, service_url=vendors_url, - assigned_label='vendorName')) + f.set_widget('vendor_uuid', + forms.widgets.JQueryAutocompleteWidget( + field_display=vendor_display, + service_url=vendors_url, + ref='vendorAutocomplete', + assigned_label='vendorName', + input_callback='vendorChanged', + new_label_callback='vendorLabelChanging')) else: f.set_readonly('vendor') From 025cabd1ad477e2a6139481f8c778b20fcfc6649 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 5 Feb 2022 21:52:30 -0600 Subject: [PATCH 0016/1083] Update changelog --- CHANGES.rst | 8 ++++++++ tailbone/_version.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 2496d970..83a9df22 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,14 @@ CHANGELOG ========= +0.8.205 (2022-02-05) +-------------------- + +* Tweak how product key field is handled for product views. + +* Add some autocomplete workarounds for new vendor catalog batch. + + 0.8.204 (2022-02-04) -------------------- diff --git a/tailbone/_version.py b/tailbone/_version.py index 7bcfd6af..152e2331 100644 --- a/tailbone/_version.py +++ b/tailbone/_version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8; -*- -__version__ = '0.8.204' +__version__ = '0.8.205' From 072f5da69d8af620a420b5cc37a61879ad99ec5b Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 8 Feb 2022 12:21:24 -0600 Subject: [PATCH 0017/1083] Add "full lookup" product search modal for new custorder page --- .../static/js/tailbone.buefy.autocomplete.js | 6 + tailbone/templates/custorders/create.mako | 39 ++- tailbone/templates/products/lookup.mako | 257 ++++++++++++++++++ tailbone/views/products.py | 114 +++++++- 4 files changed, 403 insertions(+), 13 deletions(-) create mode 100644 tailbone/templates/products/lookup.mako diff --git a/tailbone/static/js/tailbone.buefy.autocomplete.js b/tailbone/static/js/tailbone.buefy.autocomplete.js index f615c2a9..b4070fab 100644 --- a/tailbone/static/js/tailbone.buefy.autocomplete.js +++ b/tailbone/static/js/tailbone.buefy.autocomplete.js @@ -223,6 +223,12 @@ const TailboneAutocomplete = { // we have nothing to go on here.. return "" }, + + // returns the "raw" user input from the underlying buefy + // autocomplete component + getUserInput() { + return this.buefyValue + }, }, } diff --git a/tailbone/templates/custorders/create.mako b/tailbone/templates/custorders/create.mako index c3aed270..ddabfc4d 100644 --- a/tailbone/templates/custorders/create.mako +++ b/tailbone/templates/custorders/create.mako @@ -1,5 +1,6 @@ ## -*- coding: utf-8; -*- <%inherit file="/master/create.mako" /> +<%namespace name="product_lookup" file="/products/lookup.mako" /> <%def name="extra_styles()"> ${parent.extra_styles()} @@ -54,6 +55,7 @@ <%def name="render_this_page_template()"> ${parent.render_this_page_template()} + ${product_lookup.tailbone_product_lookup_template()} + + +<%def name="tailbone_product_lookup_component()"> + + diff --git a/tailbone/views/products.py b/tailbone/views/products.py index 15b2083f..752a996d 100644 --- a/tailbone/views/products.py +++ b/tailbone/views/products.py @@ -179,9 +179,10 @@ class ProductView(MasterView): 'tailbone', 'products.print_labels', default=False) app = self.get_rattail_app() - self.product_handler = app.get_products_handler() - # TODO: deprecate / remove this - self.handler = self.product_handler + self.products_handler = app.get_products_handler() + # TODO: deprecate / remove these + self.product_handler = self.products_handler + self.handler = self.products_handler def query(self, session): user = self.request.user @@ -535,7 +536,7 @@ class ProductView(MasterView): if not product.not_for_sale: price = product[field] if price: - return self.product_handler.render_price(price) + return self.products_handler.render_price(price) def render_current_price_for_grid(self, product, field): text = self.render_price(product, field) or "" @@ -1173,7 +1174,7 @@ class ProductView(MasterView): key = self.rattail_config.product_key() kwargs['product_key_field'] = self.product_key_fields.get(key, key) - kwargs['image_url'] = self.product_handler.get_image_url(product) + kwargs['image_url'] = self.products_handler.get_image_url(product) # add price history, if user has access if self.rattail_config.versioning_enabled() and self.has_perm('versions'): @@ -1743,6 +1744,105 @@ class ProductView(MasterView): return {'ok': True} def search(self): + """ + Perform a product search across multiple fields, and return + the results as JSON suitable for row data for a Buefy + ```` component. + """ + if 'term' not in self.request.GET: + # TODO: deprecate / remove this? not sure if/where it is used + return self.search_v1() + + term = self.request.GET.get('term') + if not term: + return {'ok': True, 'results': []} + + supported_fields = [ + 'product_key', + 'vendor_code', + 'alt_code', + 'brand_name', + 'description', + ] + + search_fields = [] + for field in supported_fields: + key = 'search_{}'.format(field) + if self.request.GET.get(key) == 'true': + search_fields.append(field) + + final_results = [] + session = self.Session() + model = self.model + + lookup_fields = [] + if 'product_key' in search_fields: + lookup_fields.append('_product_key_') + if 'vendor_code' in search_fields: + lookup_fields.append('vendor_code') + if 'alt_code' in search_fields: + lookup_fields.append('alt_code') + if lookup_fields: + product = self.products_handler.locate_product_for_entry( + session, term, lookup_fields=lookup_fields) + if product: + final_results.append(self.search_normalize_result(product)) + + # base wildcard query + query = session.query(model.Product) + if 'brand_name' in search_fields: + query = query.outerjoin(model.Brand) + + # now figure out wildcard criteria + criteria = [] + for word in term.split(): + if 'brand_name' in search_fields and 'description' in search_fields: + criteria.append(sa.or_( + model.Brand.name.ilike('%{}%'.format(word)), + model.Product.description.ilike('%{}%'.format(word)))) + elif 'brand_name' in search_fields: + criteria.append(model.Brand.name.ilike('%{}%'.format(word))) + elif 'description' in search_fields: + criteria.append(model.Product.description.ilike('%{}%'.format(word))) + + # execute wildcard query if applicable + max_results = 30 # TODO: make conifgurable? + elided = 0 + if criteria: + query = query.filter(sa.and_(*criteria)) + count = query.count() + if count > max_results: + elided = count - max_results + for product in query[:max_results]: + final_results.append(self.search_normalize_result(product)) + + return {'ok': True, 'results': final_results, 'elided': elided} + + def search_normalize_result(self, product, **kwargs): + return self.products_handler.normalize_product(product, fields=[ + 'product_key', + 'url', + 'image_url', + 'brand_name', + 'description', + 'size', + 'full_description', + 'department_name', + 'unit_price', + 'unit_price_display', + 'sale_price', + 'sale_price_display', + 'sale_ends_display', + 'vendor_name', + # TODO: should be case_size + 'case_quantity', + 'case_price', + 'case_price_display', + 'uom_choices', + ]) + + # TODO: deprecate / remove this? not sure if/where it is used + def search_v1(self): """ Locate a product(s) by UPC. @@ -2027,10 +2127,10 @@ class ProductView(MasterView): renderer='{}/batch.mako'.format(template_prefix), permission='{}.make_batch'.format(permission_prefix)) - # search (by upc) + # search config.add_route('products.search', '/products/search') config.add_view(cls, attr='search', route_name='products.search', - renderer='json', permission='products.view') + renderer='json', permission='products.list') # product image config.add_route('products.image', '/products/{uuid}/image') From 8cc54b6106a052e09992c0ee84c4f6d2309e4dea Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 8 Feb 2022 12:23:12 -0600 Subject: [PATCH 0018/1083] Update changelog --- CHANGES.rst | 6 ++++++ tailbone/_version.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 83a9df22..b9cdb3d7 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,12 @@ CHANGELOG ========= +0.8.206 (2022-02-08) +-------------------- + +* Add "full lookup" product search modal for new custorder page. + + 0.8.205 (2022-02-05) -------------------- diff --git a/tailbone/_version.py b/tailbone/_version.py index 152e2331..dddd2841 100644 --- a/tailbone/_version.py +++ b/tailbone/_version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8; -*- -__version__ = '0.8.205' +__version__ = '0.8.206' From f1c2fd399ea6f2fe285aad080f61950d7d195e49 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Wed, 9 Feb 2022 18:02:09 -0600 Subject: [PATCH 0019/1083] Try out new config defaults function for user views pretty sure this is a good idea but we'll see --- tailbone/views/users.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tailbone/views/users.py b/tailbone/views/users.py index 124d355a..157f42ee 100644 --- a/tailbone/views/users.py +++ b/tailbone/views/users.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2021 Lance Edgar +# Copyright © 2010-2022 Lance Edgar # # This file is part of Rattail. # @@ -516,6 +516,11 @@ class UserEventView(MasterView): UserEventsView = UserEventView +def defaults(config, **kwargs): + base = globals() + kwargs.get('UserView', base['UserView']).defaults(config) + kwargs.get('UserEventView', base['UserEventView']).defaults(config) + + def includeme(config): - UserView.defaults(config) - UserEventView.defaults(config) + defaults(config) From 065ad9e422e69cd88f6c9b382e9ae45f0e132c85 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 10 Feb 2022 10:55:41 -0600 Subject: [PATCH 0020/1083] Add highlight for non-active users in grid --- tailbone/views/users.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tailbone/views/users.py b/tailbone/views/users.py index 157f42ee..52064346 100644 --- a/tailbone/views/users.py +++ b/tailbone/views/users.py @@ -135,6 +135,10 @@ class UserView(PrincipalMasterView): g.set_link('last_name') g.set_link('display_name') + def grid_extra_class(self, user, i): + if not user.active: + return 'warning' + def editable_instance(self, user): """ If the given user is "protected" then we only allow edit if current From e8526135672cb014b8a09e9ca3c21163e970f32e Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 10 Feb 2022 11:16:39 -0600 Subject: [PATCH 0021/1083] Add highlight for non-active customers in grid --- tailbone/views/customers.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tailbone/views/customers.py b/tailbone/views/customers.py index c9bfacb9..b4dd77c6 100644 --- a/tailbone/views/customers.py +++ b/tailbone/views/customers.py @@ -131,12 +131,20 @@ class CustomerView(MasterView): g.set_sorter('person', model.Person.display_name) g.set_renderer('person', self.grid_render_person) + # active_in_pos + g.filters['active_in_pos'].default_active = True + g.filters['active_in_pos'].default_verb = 'is_true' + g.set_link('id') g.set_link('number') g.set_link('name') g.set_link('person') g.set_link('email') + def grid_extra_class(self, customer, i): + if not customer.active_in_pos: + return 'warning' + def get_instance(self): try: instance = super(CustomerView, self).get_instance() From 9584fb57b0cf0fcb4dbdabaa91936bf8cc5a7d04 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 10 Feb 2022 20:31:03 -0600 Subject: [PATCH 0022/1083] Only prevent cache for index pages if so configured there is a performance hit for this, depending on your perspective, so let's make it opt-in only for now --- tailbone/views/master.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/tailbone/views/master.py b/tailbone/views/master.py index 3b7bed2a..bcbb58d3 100644 --- a/tailbone/views/master.py +++ b/tailbone/views/master.py @@ -4490,14 +4490,19 @@ class MasterView(View): config.add_tailbone_permission(permission_prefix, '{}.list'.format(permission_prefix), "List / search {}".format(model_title_plural)) config.add_route(route_prefix, '{}/'.format(url_prefix)) + kwargs = {} + if rattail_config.getbool('tailbone', + 'prevent_cache_for_index_views', + default=False): + # hopefully, instruct browser to never cache this page. + # on windows/chrome we are seeing some caching when e.g. + # user applies some filters, then views a record, then + # clicks back button, filters no longer are applied + # cf. https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/viewconfig.html#non-predicate-arguments + kwargs['http_cache'] = 0 config.add_view(cls, attr='index', route_name=route_prefix, permission='{}.list'.format(permission_prefix), - # hopefully, instruct browser to never cache this page. - # on windows/chrome we are seeing some caching when e.g. - # user applies some filters, then views a record, then - # clicks back button, filters no longer are applied - # cf. https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/viewconfig.html#non-predicate-arguments - http_cache=0) + **kwargs) # download results # this is the "new" more flexible approach, but we only want to From 86a42064ea9dd7957e605406a8c84e39be1d6f97 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 11 Feb 2022 15:35:12 -0600 Subject: [PATCH 0023/1083] Cleanup labels for Vendor/Code "preferred" vs. "any" in products grid --- tailbone/views/products.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/tailbone/views/products.py b/tailbone/views/products.py index 752a996d..33615ef4 100644 --- a/tailbone/views/products.py +++ b/tailbone/views/products.py @@ -283,8 +283,6 @@ class ProductView(MasterView): g.joiners['code'] = lambda q: q.outerjoin(model.ProductCode) g.joiners['vendor'] = join_vendor g.joiners['vendor_any'] = join_vendor_any - g.joiners['vendor_code'] = join_vendor_code - g.joiners['vendor_code_any'] = join_vendor_code_any g.sorters['brand'] = g.make_sorter(model.Brand.name) g.sorters['subdepartment'] = g.make_sorter(model.Subdepartment.name) @@ -322,8 +320,19 @@ class ProductView(MasterView): g.filters['vendor'] = g.make_filter('vendor', model.Vendor.name) g.filters['vendor_any'] = g.make_filter('vendor_any', self.VendorAny.name) # factory=VendorAnyFilter, joiner=join_vendor_any) - g.filters['vendor_code'] = g.make_filter('vendor_code', ProductCostCode.code) - g.filters['vendor_code_any'] = g.make_filter('vendor_code_any', ProductCostCodeAny.code) + + # g.joiners['vendor_code_any'] = join_vendor_code_any + # g.filters['vendor_code_any'] = g.make_filter('vendor_code_any', ProductCostCodeAny.code) + # g.joiners['vendor_code'] = join_vendor_code + # g.filters['vendor_code'] = g.make_filter('vendor_code', ProductCostCode.code) + + # vendor_code* + g.set_joiner('vendor_code', join_vendor_code) + g.set_filter('vendor_code', ProductCostCode.code) + g.set_label('vendor_code', "Vendor Code (preferred)") + g.set_joiner('vendor_code_any', join_vendor_code_any) + g.set_filter('vendor_code_any', ProductCostCodeAny.code) + g.set_label('vendor_code_any', "Vendor Code (any)") # category g.set_joiner('category', lambda q: q.outerjoin(model.Category)) @@ -390,7 +399,7 @@ class ProductView(MasterView): g.set_label('vendor', "Vendor (preferred)") g.set_label('vendor_any', "Vendor (any)") - g.set_label('vendor', "Pref. Vendor") + g.set_label('vendor', "Vendor (preferred)") def configure_common_form(self, f): super(ProductView, self).configure_common_form(f) From 0ead06106c856849e34cb6ec51960161d5d0d64b Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 11 Feb 2022 16:48:46 -0600 Subject: [PATCH 0024/1083] Add config for showing ordered vs. shipped amounts when receiving --- tailbone/templates/receiving/configure.mako | 23 ++++++++++++++++++++ tailbone/views/purchasing/receiving.py | 24 +++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/tailbone/templates/receiving/configure.mako b/tailbone/templates/receiving/configure.mako index 349dc621..dff280bb 100644 --- a/tailbone/templates/receiving/configure.mako +++ b/tailbone/templates/receiving/configure.mako @@ -53,6 +53,29 @@
    +

    Display

    +
    + + + + Show "ordered" quantities in row grid + + + + + + Show "shipped" quantities in row grid + + + +
    +

    Product Handling

    diff --git a/tailbone/views/purchasing/receiving.py b/tailbone/views/purchasing/receiving.py index e481db82..12979d0b 100644 --- a/tailbone/views/purchasing/receiving.py +++ b/tailbone/views/purchasing/receiving.py @@ -159,6 +159,8 @@ class ReceivingBatchView(PurchasingBatchView): 'description', 'size', 'department_name', + 'cases_ordered', + 'units_ordered', 'cases_shipped', 'units_shipped', 'cases_received', @@ -904,6 +906,20 @@ class ReceivingBatchView(PurchasingBatchView): g.set_joiner('credits', lambda q: q.outerjoin(Credits)) g.sorters['credits'] = lambda q, d: q.order_by(getattr(Credits.c.credit_count, d)()) + show_ordered = self.rattail_config.getbool( + 'rattail.batch', 'purchase.receiving.show_ordered_column_in_grid', + default=False) + if not show_ordered: + g.remove('cases_ordered', + 'units_ordered') + + show_shipped = self.rattail_config.getbool( + 'rattail.batch', 'purchase.receiving.show_shipped_column_in_grid', + default=False) + if not show_shipped: + g.remove('cases_shipped', + 'units_shipped') + # hide 'ordered' columns for truck dump parent, if its "children first" # flag is set, since that batch type is only concerned with receiving if batch.is_truck_dump_parent() and not batch.truck_dump_children_first: @@ -1851,6 +1867,14 @@ class ReceivingBatchView(PurchasingBatchView): 'option': 'purchase.allow_truck_dump_receiving', 'type': bool}, + # display + {'section': 'rattail.batch', + 'option': 'purchase.receiving.show_ordered_column_in_grid', + 'type': bool}, + {'section': 'rattail.batch', + 'option': 'purchase.receiving.show_shipped_column_in_grid', + 'type': bool}, + # product handling {'section': 'rattail.batch', 'option': 'purchase.allow_cases', From 85ef73dcb975882f6c9a35fbc54f0c115e6c6a33 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 11 Feb 2022 16:55:25 -0600 Subject: [PATCH 0025/1083] Tell browser not to cache certain pages, by default main grid/index pages, and any view page which contains a row grid --- tailbone/views/master.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/tailbone/views/master.py b/tailbone/views/master.py index bcbb58d3..77a844a4 100644 --- a/tailbone/views/master.py +++ b/tailbone/views/master.py @@ -4485,21 +4485,22 @@ class MasterView(View): config.add_tailbone_permission_group(permission_prefix, model_title_plural, overwrite=False) + # on windows/chrome we are seeing some caching when e.g. user + # applies some filters, then views a record, then clicks back + # button, filters no longer are applied. so by default we + # instruct browser to never cache certain pages which contain + # a grid. at this point only /index and /view + # cf. https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/viewconfig.html#non-predicate-arguments + prevent_cache = rattail_config.getbool('tailbone', + 'prevent_cache_for_index_views', + default=True) + # list/search if cls.listable: config.add_tailbone_permission(permission_prefix, '{}.list'.format(permission_prefix), "List / search {}".format(model_title_plural)) config.add_route(route_prefix, '{}/'.format(url_prefix)) - kwargs = {} - if rattail_config.getbool('tailbone', - 'prevent_cache_for_index_views', - default=False): - # hopefully, instruct browser to never cache this page. - # on windows/chrome we are seeing some caching when e.g. - # user applies some filters, then views a record, then - # clicks back button, filters no longer are applied - # cf. https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/viewconfig.html#non-predicate-arguments - kwargs['http_cache'] = 0 + kwargs = {'http_cache': 0} if prevent_cache else {} config.add_view(cls, attr='index', route_name=route_prefix, permission='{}.list'.format(permission_prefix), **kwargs) @@ -4667,8 +4668,10 @@ class MasterView(View): # view by record key config.add_route('{}.view'.format(route_prefix), instance_url_prefix) + kwargs = {'http_cache': 0} if prevent_cache and cls.has_rows else {} config.add_view(cls, attr='view', route_name='{}.view'.format(route_prefix), - permission='{}.view'.format(permission_prefix)) + permission='{}.view'.format(permission_prefix), + **kwargs) # version history if cls.has_versions and rattail_config and rattail_config.versioning_enabled(): From a6d97538aff3786400e0d1d4f5ef5bd83b57c027 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 11 Feb 2022 19:15:39 -0600 Subject: [PATCH 0026/1083] Use new-style config defaults for customer views --- tailbone/views/customers.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/tailbone/views/customers.py b/tailbone/views/customers.py index b4dd77c6..310bddb5 100644 --- a/tailbone/views/customers.py +++ b/tailbone/views/customers.py @@ -614,12 +614,23 @@ def customer_info(request): } -def includeme(config): +def defaults(config, **kwargs): + base = globals() - # info + # TODO: deprecate / remove this config.add_route('customer.info', '/customers/info') + customer_info = kwargs.get('customer_info', base['customer_info']) config.add_view(customer_info, route_name='customer.info', renderer='json', permission='customers.view') + CustomerView = kwargs.get('CustomerView', + base['CustomerView']) CustomerView.defaults(config) + + PendingCustomerView = kwargs.get('PendingCustomerView', + base['PendingCustomerView']) PendingCustomerView.defaults(config) + + +def includeme(config): + defaults(config) From 4e3aa1af8318eaa3a3488621386ab160548b8e9a Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 12 Feb 2022 19:16:16 -0600 Subject: [PATCH 0027/1083] Tweak how "duration" fields are rendered for grids, forms --- tailbone/forms/core.py | 10 +++++----- tailbone/grids/core.py | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/tailbone/forms/core.py b/tailbone/forms/core.py index 17921c72..850768cf 100644 --- a/tailbone/forms/core.py +++ b/tailbone/forms/core.py @@ -27,7 +27,6 @@ Forms Core from __future__ import unicode_literals, absolute_import import json -import datetime import logging import six @@ -36,7 +35,7 @@ from sqlalchemy import orm from sqlalchemy.ext.associationproxy import AssociationProxy, ASSOCIATION_PROXY from rattail.time import localtime -from rattail.util import prettify, pretty_boolean, pretty_hours, pretty_quantity +from rattail.util import prettify, pretty_boolean, pretty_quantity from rattail.core import UNSPECIFIED import colander @@ -902,10 +901,11 @@ class Form(object): return raw_datetime(self.request.rattail_config, value) def render_duration(self, record, field_name): - value = self.obtain_value(record, field_name) - if value is None: + seconds = self.obtain_value(record, field_name) + if seconds is None: return "" - return pretty_hours(datetime.timedelta(seconds=value)) + app = self.request.rattail_config.get_app() + return app.render_duration(seconds=seconds) def render_boolean(self, record, field_name): value = self.obtain_value(record, field_name) diff --git a/tailbone/grids/core.py b/tailbone/grids/core.py index d825e4b4..cb71e144 100644 --- a/tailbone/grids/core.py +++ b/tailbone/grids/core.py @@ -26,7 +26,6 @@ Core Grid Classes from __future__ import unicode_literals, absolute_import -import datetime import warnings import logging @@ -358,10 +357,11 @@ class Grid(object): return pretty_quantity(value) def render_duration(self, obj, column_name): - value = self.obtain_value(obj, column_name) - if value is None: + seconds = self.obtain_value(obj, column_name) + if seconds is None: return "" - return pretty_hours(datetime.timedelta(seconds=value)) + app = self.request.rattail_config.get_app() + return app.render_duration(seconds=seconds) def render_duration_hours(self, obj, field): value = self.obtain_value(obj, field) From 09227fa30aec42bf5af8c3f2cbc2d9785f01f55c Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 13 Feb 2022 16:27:24 -0600 Subject: [PATCH 0028/1083] New upgrades should be enabled by default --- tailbone/views/upgrades.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/tailbone/views/upgrades.py b/tailbone/views/upgrades.py index 0484dabc..11c2930c 100644 --- a/tailbone/views/upgrades.py +++ b/tailbone/views/upgrades.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2020 Lance Edgar +# Copyright © 2010-2022 Lance Edgar # # This file is part of Rattail. # @@ -155,6 +155,7 @@ class UpgradeView(MasterView): def configure_form(self, f): super(UpgradeView, self).configure_form(f) + upgrade = f.model_instance # status_code if self.creating: @@ -168,7 +169,6 @@ class UpgradeView(MasterView): f.remove('executing') f.set_type('created', 'datetime') - f.set_type('enabled', 'boolean') f.set_type('executed', 'datetime') # f.set_widget('not_until', dfwidget.DateInputWidget()) f.set_widget('notes', dfwidget.TextAreaWidget(cols=80, rows=8)) @@ -179,7 +179,6 @@ class UpgradeView(MasterView): # f.set_readonly('created_by') f.set_readonly('executed') f.set_readonly('executed_by') - upgrade = f.model_instance if self.creating or self.editing: f.remove_field('created') f.remove_field('created_by') @@ -188,11 +187,6 @@ class UpgradeView(MasterView): if self.creating or not upgrade.executed: f.remove_field('executed') f.remove_field('executed_by') - if self.editing and upgrade.executed: - f.remove_field('enabled') - - elif f.model_instance.executed: - f.remove_field('enabled') else: f.remove_field('executed') @@ -200,6 +194,13 @@ class UpgradeView(MasterView): f.remove_field('stdout_file') f.remove_field('stderr_file') + # enabled + if not self.creating and upgrade.executed: + f.remove('enabled') + else: + f.set_type('enabled', 'boolean') + f.set_default('enabled', True) + if not self.viewing or not upgrade.executed: f.remove_field('package_diff') f.remove_field('exit_code') @@ -462,5 +463,12 @@ class UpgradeView(MasterView): cls._defaults(config) -def includeme(config): +def defaults(config, **kwargs): + base = globals() + + UpgradeView = kwargs.get('UpgradeView', base['UpgradeView']) UpgradeView.defaults(config) + + +def includeme(config): + defaults(config) From 753daa55e8c06f65d59b633d8cb2485a7f2adff8 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 13 Feb 2022 21:41:47 -0600 Subject: [PATCH 0029/1083] Update changelog --- CHANGES.rst | 18 ++++++++++++++++++ tailbone/_version.py | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index b9cdb3d7..186dcd7f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,24 @@ CHANGELOG ========= +0.8.207 (2022-02-13) +-------------------- + +* Try out new config defaults function for some views (user, customer). + +* Add highlight for non-active users, customers in grid. + +* Prevent cache for index pages by default, unless configured not to. + +* Cleanup labels for Vendor/Code "preferred" vs. "any" in products grid. + +* Add config for showing ordered vs. shipped amounts when receiving. + +* Tweak how "duration" fields are rendered for grids, forms. + +* New upgrades should be enabled by default. + + 0.8.206 (2022-02-08) -------------------- diff --git a/tailbone/_version.py b/tailbone/_version.py index dddd2841..0b0bbd54 100644 --- a/tailbone/_version.py +++ b/tailbone/_version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8; -*- -__version__ = '0.8.206' +__version__ = '0.8.207' From 6093be43c9904980106478511cec3d4bb5fbdd6d Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 13 Feb 2022 21:51:42 -0600 Subject: [PATCH 0030/1083] Allow override of navbar-end element in falafel theme header --- tailbone/templates/themes/falafel/base.mako | 62 +++++++++++---------- 1 file changed, 34 insertions(+), 28 deletions(-) diff --git a/tailbone/templates/themes/falafel/base.mako b/tailbone/templates/themes/falafel/base.mako index 5ab12a03..94e20f3e 100644 --- a/tailbone/templates/themes/falafel/base.mako +++ b/tailbone/templates/themes/falafel/base.mako @@ -236,34 +236,7 @@ % endfor
    - + ${self.render_navbar_end()}
    @@ -552,6 +525,38 @@ ${tailbone_autocomplete_template()} +<%def name="render_navbar_end()"> + + + +<%def name="render_user_menu()"> + % if request.user: + + % else: + ${h.link_to("Login", url('login'), class_='navbar-item')} + % endif + + <%def name="render_instance_header_buttons()"> ${self.render_crud_header_buttons()} ${self.render_prevnext_header_buttons()} @@ -665,6 +670,7 @@ let WholePage = { template: '#whole-page-template', + computed: {}, methods: { changeContentTitle(newTitle) { From 962d31c4c2d7fc6b2c7c5fd2a0a4ee9e747b7c2b Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 14 Feb 2022 19:19:33 -0600 Subject: [PATCH 0031/1083] Add initial support for editing user preferences by default this exposes just one setting which has only one possible value, so not very useful. but can override as needed --- tailbone/templates/configure.mako | 16 ++- tailbone/templates/themes/falafel/base.mako | 1 + tailbone/templates/users/preferences.mako | 55 +++++++ tailbone/templates/users/view.mako | 8 ++ tailbone/views/master.py | 32 +++-- tailbone/views/users.py | 151 ++++++++++++++++++-- 6 files changed, 239 insertions(+), 24 deletions(-) create mode 100644 tailbone/templates/users/preferences.mako diff --git a/tailbone/templates/configure.mako b/tailbone/templates/configure.mako index de2b4e78..f05b24c0 100644 --- a/tailbone/templates/configure.mako +++ b/tailbone/templates/configure.mako @@ -30,14 +30,24 @@ +<%def name="intro_message()"> +

    + This page lets you modify the + % if config_preferences is not Undefined and config_preferences: + preferences + % else: + configuration + % endif + for ${config_title}. +

    + + <%def name="buttons_row()">
    -

    - This page lets you modify the configuration for ${config_title}. -

    + ${self.intro_message()}
    diff --git a/tailbone/templates/themes/falafel/base.mako b/tailbone/templates/themes/falafel/base.mako index 94e20f3e..db669c78 100644 --- a/tailbone/templates/themes/falafel/base.mako +++ b/tailbone/templates/themes/falafel/base.mako @@ -549,6 +549,7 @@ ${h.link_to("Messages{}".format(" ({})".format(inbox_count) if inbox_count else ''), url('messages.inbox'), class_='navbar-item')} % endif ${h.link_to("Change Password", url('change_password'), class_='navbar-item')} + ${h.link_to("Edit Preferences", url('my.preferences'), class_='navbar-item')} ${h.link_to("Logout", url('logout'), class_='navbar-item')}
    diff --git a/tailbone/templates/users/preferences.mako b/tailbone/templates/users/preferences.mako new file mode 100644 index 00000000..a44534dc --- /dev/null +++ b/tailbone/templates/users/preferences.mako @@ -0,0 +1,55 @@ +## -*- coding: utf-8; -*- +<%inherit file="/configure.mako" /> + +<%def name="title()"> + % if current_user: + Edit Preferences + % else: + ${index_title} » ${instance_title} » Preferences + % endif + + +<%def name="content_title()">Preferences + +<%def name="intro_message()"> +

    + % if current_user: + This page lets you modify your preferences. + % else: + This page lets you modify the preferences for ${config_title}. + % endif +

    + + +<%def name="form_content()"> + +

    Display

    +
    + + + + + + + + +
    + + +<%def name="modify_this_page_vars()"> + ${parent.modify_this_page_vars()} + + + + +${parent.body()} diff --git a/tailbone/templates/users/view.mako b/tailbone/templates/users/view.mako index 8477ebfa..b34902a1 100644 --- a/tailbone/templates/users/view.mako +++ b/tailbone/templates/users/view.mako @@ -14,4 +14,12 @@ % endif +<%def name="context_menu_items()"> + ${parent.context_menu_items()} + % if master.has_perm('preferences'): +
  • ${h.link_to("Edit User Preferences", action_url('preferences', instance))}
  • + % endif + + + ${parent.body()} diff --git a/tailbone/views/master.py b/tailbone/views/master.py index 77a844a4..89def384 100644 --- a/tailbone/views/master.py +++ b/tailbone/views/master.py @@ -4290,6 +4290,7 @@ class MasterView(View): 'type': bool, 'value': config.getbool('rattail.batch', 'purchase.allow_cases'), + 'save_if_empty': False, } Note that some of the above is optional, in particular it @@ -4316,9 +4317,11 @@ class MasterView(View): return '{}.{}'.format(simple['section'], simple['option']) - def configure_get_context(self): + def configure_get_context(self, simple_settings=None, + input_file_templates=True): context = {} - simple_settings = self.configure_get_simple_settings() + if simple_settings is None: + simple_settings = self.configure_get_simple_settings() if simple_settings: config = self.rattail_config @@ -4342,7 +4345,7 @@ class MasterView(View): context['simple_settings'] = settings # add settings for downloadable input file templates, if any - if self.has_input_file_templates: + if input_file_templates and self.has_input_file_templates: settings = {} file_options = {} file_option_dirs = {} @@ -4359,11 +4362,13 @@ class MasterView(View): return context - def configure_gather_settings(self, data): + def configure_gather_settings(self, data, simple_settings=None, + input_file_templates=True): settings = [] # maybe collect "simple" settings - simple_settings = self.configure_get_simple_settings() + if simple_settings is None: + simple_settings = self.configure_get_simple_settings() if simple_settings: for simple in simple_settings: @@ -4377,11 +4382,14 @@ class MasterView(View): else: value = six.text_type(value) - settings.append({'name': name, - 'value': value}) + # only want to save this setting if we received a + # value, or if empty values are okay to save + if value or simple.get('save_if_empty'): + settings.append({'name': name, + 'value': value}) # maybe also collect input file template settings - if self.has_input_file_templates: + if input_file_templates and self.has_input_file_templates: for template in self.normalize_input_file_templates(): # mode @@ -4401,16 +4409,18 @@ class MasterView(View): return settings - def configure_remove_settings(self): + def configure_remove_settings(self, simple_settings=None, + input_file_templates=True): model = self.model names = [] - simple_settings = self.configure_get_simple_settings() + if simple_settings is None: + simple_settings = self.configure_get_simple_settings() if simple_settings: names.extend([self.configure_get_name_for_simple_setting(simple) for simple in simple_settings]) - if self.has_input_file_templates: + if input_file_templates and self.has_input_file_templates: for template in self.normalize_input_file_templates(): names.extend([ template['setting_mode'], diff --git a/tailbone/views/users.py b/tailbone/views/users.py index 52064346..ecff3bb9 100644 --- a/tailbone/views/users.py +++ b/tailbone/views/users.py @@ -26,12 +26,10 @@ User Views from __future__ import unicode_literals, absolute_import -import copy - import six from sqlalchemy import orm -from rattail.db import model +from rattail.db.model import User, UserEvent from rattail.db.auth import (administrator_role, guest_role, authenticated_role, set_user_password) @@ -40,8 +38,7 @@ from deform import widget as dfwidget from webhelpers2.html import HTML, tags from tailbone import forms -from tailbone.db import Session -from tailbone.views import MasterView +from tailbone.views import MasterView, View from tailbone.views.principal import PrincipalMasterView, PermissionsRenderer @@ -49,9 +46,9 @@ class UserView(PrincipalMasterView): """ Master view for the User model. """ - model_class = model.User + model_class = User has_rows = True - model_row_class = model.UserEvent + model_row_class = UserEvent has_versions = True touchable = True @@ -99,6 +96,7 @@ class UserView(PrincipalMasterView): def query(self, session): query = super(UserView, self).query(session) + model = self.model # bring in the related Person(s) query = query.outerjoin(model.Person)\ @@ -108,6 +106,7 @@ class UserView(PrincipalMasterView): def configure_grid(self, g): super(UserView, self).configure_grid(g) + model = self.model del g.filters['salt'] g.filters['username'].default_active = True @@ -160,6 +159,7 @@ class UserView(PrincipalMasterView): return not self.user_is_protected(user) def unique_username(self, node, value): + model = self.model query = self.Session.query(model.User)\ .filter(model.User.username == value) if self.editing: @@ -181,6 +181,7 @@ class UserView(PrincipalMasterView): def configure_form(self, f): super(UserView, self).configure_form(f) + model = self.model user = f.model_instance # username @@ -265,6 +266,7 @@ class UserView(PrincipalMasterView): f.remove('set_password') def get_possible_roles(self): + model = self.model # some roles should never have users "belong" to them excluded = [ @@ -281,6 +283,7 @@ class UserView(PrincipalMasterView): .order_by(model.Role.name) def objectify(self, form, data=None): + model = self.model # create/update user as per normal if data is None: @@ -328,6 +331,7 @@ class UserView(PrincipalMasterView): if 'roles' not in data: return + model = self.model old_roles = set([r.uuid for r in user.roles]) new_roles = data['roles'] admin = administrator_role(self.Session()) @@ -389,6 +393,7 @@ class UserView(PrincipalMasterView): return HTML.tag('ul', c=items) def get_row_data(self, user): + model = self.model return self.Session.query(model.UserEvent)\ .filter(model.UserEvent.user == user) @@ -402,6 +407,7 @@ class UserView(PrincipalMasterView): g.main_actions = [] def get_version_child_classes(self): + model = self.model return [ (model.UserRole, 'user_uuid'), ] @@ -409,6 +415,7 @@ class UserView(PrincipalMasterView): def find_principals_with_permission(self, session, permission): app = self.get_rattail_app() auth = app.get_auth_handler() + model = self.model # TODO: this should search Permission table instead, and work backward to User? all_users = session.query(model.User)\ @@ -448,6 +455,105 @@ class UserView(PrincipalMasterView): assert not removing._roles self.Session.delete(removing) + def preferences(self, user=None): + """ + View to modify preferences for a particular user. + """ + current_user = True + if not user: + current_user = False + user = self.get_instance() + + # TODO: this is of course largely copy/pasted from the + # MasterView.configure() method..should refactor? + if self.request.method == 'POST': + if self.request.POST.get('remove_settings'): + self.preferences_remove_settings(user) + self.request.session.flash("Settings have been removed.") + return self.redirect(self.request.current_route_url()) + else: + data = self.request.POST + + # then gather/save settings + settings = self.preferences_gather_settings(data, user) + self.preferences_remove_settings(user) + self.configure_save_settings(settings) + self.request.session.flash("Settings have been saved.") + return self.redirect(self.request.current_route_url()) + + context = self.preferences_get_context(user, current_user) + return self.render_to_response('preferences', context) + + def my_preferences(self): + """ + View to modify preferences for the current user. + """ + user = self.request.user + if not user: + raise self.forbidden() + return self.preferences(user=user) + + def preferences_get_context(self, user, current_user): + simple_settings = self.preferences_get_simple_settings(user) + context = self.configure_get_context(simple_settings=simple_settings, + input_file_templates=False) + + instance_title = self.get_instance_title(user) + context.update({ + 'user': user, + 'instance': user, + 'instance_title': instance_title, + 'instance_url': self.get_action_url('view', user), + 'config_title': instance_title, + 'config_preferences': True, + 'current_user': current_user, + }) + + if current_user: + context.update({ + 'index_url': None, + 'index_title': instance_title, + }) + + # theme style options + options = [{'value': None, 'label': "default"}] + styles = self.rattail_config.getlist('tailbone', 'themes.styles', + default=[]) + for name in styles: + css = self.rattail_config.get('tailbone', + 'themes.style.{}'.format(name)) + if css: + options.append({'value': css, 'label': name}) + context['buefy_css_options'] = options + + return context + + def preferences_get_simple_settings(self, user): + """ + This method is conceptually the same as for + :meth:`~tailbone.views.master.MasterView.configure_get_simple_settings()`. + See its docs for more info. + + The only difference here is that we are given a user account, + so the settings involved should only pertain to that user. + """ + return [ + + # display + {'section': 'tailbone.{}'.format(user.uuid), + 'option': 'buefy_css'}, + ] + + def preferences_gather_settings(self, data, user): + simple_settings = self.preferences_get_simple_settings(user) + return self.configure_gather_settings( + data, simple_settings=simple_settings, input_file_templates=False) + + def preferences_remove_settings(self, user): + simple_settings = self.preferences_get_simple_settings(user) + self.configure_remove_settings(simple_settings=simple_settings, + input_file_templates=False) + @classmethod def defaults(cls, config): cls._user_defaults(config) @@ -459,7 +565,9 @@ class UserView(PrincipalMasterView): """ Provide extra default configuration for the User master view. """ + route_prefix = cls.get_route_prefix() permission_prefix = cls.get_permission_prefix() + instance_url_prefix = cls.get_instance_url_prefix() model_title = cls.get_model_title() # view/edit roles @@ -468,6 +576,23 @@ class UserView(PrincipalMasterView): config.add_tailbone_permission(permission_prefix, '{}.edit_roles'.format(permission_prefix), "Edit the Roles to which a {} belongs".format(model_title)) + # edit preferences for any user + config.add_tailbone_permission(permission_prefix, + '{}.preferences'.format(permission_prefix), + "Edit preferences for any {}".format(model_title)) + config.add_route('{}.preferences'.format(route_prefix), + '{}/preferences'.format(instance_url_prefix)) + config.add_view(cls, attr='preferences', + route_name='{}.preferences'.format(route_prefix), + permission='{}.preferences'.format(permission_prefix)) + + # edit "my" preferences (for current user) + config.add_route('my.preferences', + '/my/preferences') + config.add_view(cls, attr='my_preferences', + route_name='my.preferences') + + # TODO: deprecate / remove this UsersView = UserView @@ -476,7 +601,7 @@ class UserEventView(MasterView): """ Master view for all user events """ - model_class = model.UserEvent + model_class = UserEvent url_prefix = '/user-events' viewable = False creatable = False @@ -492,10 +617,12 @@ class UserEventView(MasterView): def get_data(self, session=None): query = super(UserEventView, self).get_data(session=session) + model = self.model return query.join(model.User) def configure_grid(self, g): super(UserEventView, self).configure_grid(g) + model = self.model g.set_joiner('person', lambda q: q.outerjoin(model.Person)) g.set_sorter('user', model.User.username) g.set_sorter('person', model.Person.display_name) @@ -522,8 +649,12 @@ UserEventsView = UserEventView def defaults(config, **kwargs): base = globals() - kwargs.get('UserView', base['UserView']).defaults(config) - kwargs.get('UserEventView', base['UserEventView']).defaults(config) + + UserView = kwargs.get('UserView', base['UserView']) + UserView.defaults(config) + + UserEventView = kwargs.get('UserEventView', base['UserEventView']) + UserEventView.defaults(config) def includeme(config): From 47bfcc23cb1699ed396b1466d5b580e14ae2f6c6 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 15 Feb 2022 10:15:08 -0600 Subject: [PATCH 0032/1083] Add FormPosterMixin to WholePage class --- tailbone/templates/themes/falafel/base.mako | 1 + 1 file changed, 1 insertion(+) diff --git a/tailbone/templates/themes/falafel/base.mako b/tailbone/templates/themes/falafel/base.mako index db669c78..4378c4cf 100644 --- a/tailbone/templates/themes/falafel/base.mako +++ b/tailbone/templates/themes/falafel/base.mako @@ -671,6 +671,7 @@ let WholePage = { template: '#whole-page-template', + mixins: [FormPosterMixin], computed: {}, methods: { From 8744ee74b3b5934838deb595db2a5d78036e7eb6 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 15 Feb 2022 17:34:28 -0600 Subject: [PATCH 0033/1083] Update changelog --- CHANGES.rst | 10 ++++++++++ tailbone/_version.py | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 186dcd7f..3fabd6b7 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,16 @@ CHANGELOG ========= +0.8.208 (2022-02-15) +-------------------- + +* Allow override of navbar-end element in falafel theme header. + +* Add initial support for editing user preferences. + +* Add FormPosterMixin to WholePage class. + + 0.8.207 (2022-02-13) -------------------- diff --git a/tailbone/_version.py b/tailbone/_version.py index 0b0bbd54..1528a0ff 100644 --- a/tailbone/_version.py +++ b/tailbone/_version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8; -*- -__version__ = '0.8.207' +__version__ = '0.8.208' From 778578292f73cf850a4679798dcb75caf239e897 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Wed, 16 Feb 2022 16:16:40 -0600 Subject: [PATCH 0034/1083] Fix progress bar when running problem report --- tailbone/views/reports.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tailbone/views/reports.py b/tailbone/views/reports.py index 8985e204..12957934 100644 --- a/tailbone/views/reports.py +++ b/tailbone/views/reports.py @@ -644,7 +644,7 @@ class ProblemReportView(MasterView): def execute_instance(self, report_info, user, progress=None, **kwargs): report = report_info['_report'] - problems = self.handler.run_problem_report(report) + problems = self.handler.run_problem_report(report, progress=progress) return "Report found {} problems".format(len(problems)) From b6bd095d8e50f4d256ac5dd23085b811b8391437 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Wed, 16 Feb 2022 16:33:49 -0600 Subject: [PATCH 0035/1083] Update changelog --- CHANGES.rst | 6 ++++++ tailbone/_version.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 3fabd6b7..e2173067 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,12 @@ CHANGELOG ========= +0.8.209 (2022-02-16) +-------------------- + +* Fix progress bar when running problem report. + + 0.8.208 (2022-02-15) -------------------- diff --git a/tailbone/_version.py b/tailbone/_version.py index 1528a0ff..734c777e 100644 --- a/tailbone/_version.py +++ b/tailbone/_version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8; -*- -__version__ = '0.8.208' +__version__ = '0.8.209' From 57e22c9ff5eb121c40cc78dc72b8fad46d0fac41 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 18 Feb 2022 15:39:12 -0600 Subject: [PATCH 0036/1083] Only show DB picker for permissioned users --- tailbone/views/common.py | 6 ++++-- tailbone/views/master.py | 25 ++++++++++++++----------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/tailbone/views/common.py b/tailbone/views/common.py index 37b2c4a4..c3e40547 100644 --- a/tailbone/views/common.py +++ b/tailbone/views/common.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2021 Lance Edgar +# Copyright © 2010-2022 Lance Edgar # # This file is part of Rattail. # @@ -222,7 +222,9 @@ class CommonView(View): config.add_tailbone_permission('common', 'common.change_db_engine', "Change which Database Engine is active (for user)") config.add_route('change_db_engine', '/change-db-engine', request_method='POST') - config.add_view(cls, attr='change_db_engine', route_name='change_db_engine') + config.add_view(cls, attr='change_db_engine', + route_name='change_db_engine', + permission='common.change_db_engine') # change theme config.add_tailbone_permission('common', 'common.change_app_theme', diff --git a/tailbone/views/master.py b/tailbone/views/master.py index 89def384..6c174f06 100644 --- a/tailbone/views/master.py +++ b/tailbone/views/master.py @@ -2276,18 +2276,21 @@ class MasterView(View): kwargs['expose_db_picker'] = False if self.supports_multiple_engines: - # view declares support for multiple engines, but we only want to - # show the picker if we have more than one engine configured - engines = self.get_db_engines() - if len(engines) > 1: + # DB picker is only shown for permissioned users + if self.request.has_perm('common.change_db_engine'): - # user session determines "current" db engine *of this type* - # (note that many master views may declare the same type, and - # would therefore share the "current" engine) - selected = self.get_current_engine_dbkey() - kwargs['expose_db_picker'] = True - kwargs['db_picker_options'] = [tags.Option(k) for k in engines] - kwargs['db_picker_selected'] = selected + # view declares support for multiple engines, but we only want to + # show the picker if we have more than one engine configured + engines = self.get_db_engines() + if len(engines) > 1: + + # user session determines "current" db engine *of this type* + # (note that many master views may declare the same type, and + # would therefore share the "current" engine) + selected = self.get_current_engine_dbkey() + kwargs['expose_db_picker'] = True + kwargs['db_picker_options'] = [tags.Option(k) for k in engines] + kwargs['db_picker_selected'] = selected # add info for downloadable input file templates, if any if self.has_input_file_templates: From e990be3570235b120c3a2993107b2c3ded6b1869 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 19 Feb 2022 14:39:40 -0600 Subject: [PATCH 0037/1083] Expose some new trainwreck fields --- tailbone/views/trainwreck/base.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tailbone/views/trainwreck/base.py b/tailbone/views/trainwreck/base.py index 20e7701d..4f6c9e15 100644 --- a/tailbone/views/trainwreck/base.py +++ b/tailbone/views/trainwreck/base.py @@ -94,6 +94,9 @@ class TransactionView(MasterView): 'tax', 'cashback', 'total', + 'patronage', + 'equity_current', + 'self_updated', 'void', ] @@ -156,6 +159,7 @@ class TransactionView(MasterView): g.set_enum('system', self.enum.TRAINWRECK_SYSTEM) g.set_type('total', 'currency') + g.set_type('patronage', 'currency') g.set_label('terminal_id', "Terminal") g.set_label('receipt_number', "Receipt No.") g.set_label('customer_id', "Customer ID") @@ -184,6 +188,7 @@ class TransactionView(MasterView): f.set_type('tax', 'currency') f.set_type('cashback', 'currency') f.set_type('total', 'currency') + f.set_type('patronage', 'currency') # label overrides f.set_label('system_id', "System ID") From 7f06b3e53bcc07c3c3b9c709bc5979b4ea6045e3 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 19 Feb 2022 17:31:14 -0600 Subject: [PATCH 0038/1083] Expose per-item discounts for Trainwreck --- .../trainwreck/transactions/view_row.mako | 16 +++++++ tailbone/views/trainwreck/base.py | 46 +++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 tailbone/templates/trainwreck/transactions/view_row.mako diff --git a/tailbone/templates/trainwreck/transactions/view_row.mako b/tailbone/templates/trainwreck/transactions/view_row.mako new file mode 100644 index 00000000..9abcb8ba --- /dev/null +++ b/tailbone/templates/trainwreck/transactions/view_row.mako @@ -0,0 +1,16 @@ +## -*- coding: utf-8; -*- +<%inherit file="/master/view_row.mako" /> + +<%def name="modify_this_page_vars()"> + ${parent.modify_this_page_vars()} + + + + +${parent.body()} diff --git a/tailbone/views/trainwreck/base.py b/tailbone/views/trainwreck/base.py index 4f6c9e15..6fac7605 100644 --- a/tailbone/views/trainwreck/base.py +++ b/tailbone/views/trainwreck/base.py @@ -30,6 +30,8 @@ import six from rattail.time import localtime +from webhelpers2.html import HTML + from tailbone.db import Session, TrainwreckSession, ExtraTrainwreckSessions from tailbone.views import MasterView @@ -136,9 +138,13 @@ class TransactionView(MasterView): 'description', 'unit_quantity', 'subtotal', + 'discounts', + 'discounted_subtotal', 'tax', 'total', 'exempt_from_gross_sales', + 'net_sales', + 'gross_sales', 'void', ] @@ -231,6 +237,46 @@ class TransactionView(MasterView): f.set_type('tax', 'currency') f.set_type('total', 'currency') + # discounts + f.set_renderer('discounts', self.render_discounts) + + def render_discounts(self, item, field): + if not item.discounts: + return + + route_prefix = self.get_route_prefix() + factory = self.get_grid_factory() + use_buefy = self.get_use_buefy() + + g = factory( + key='{}.discounts'.format(route_prefix), + data=[] if use_buefy else item.discounts, + columns=['description', 'amount'], + request=self.request) + + if use_buefy: + return HTML.literal( + g.render_buefy_table_element(data_prop='discountsData')) + else: + g.set_type('amount', 'currency') + return HTML.literal(g.render_grid()) + + def template_kwargs_view_row(self, **kwargs): + use_buefy = self.get_use_buefy() + if use_buefy: + + app = self.get_rattail_app() + item = kwargs['instance'] + discounts_data = [] + for discount in item.discounts: + discounts_data.append({ + 'description': discount.description, + 'amount': app.render_currency(discount.amount), + }) + kwargs['discounts_data'] = discounts_data + + return kwargs + def rollover(self): """ View for performing yearly rollover functions. From 66fd2ff5e674b0cec9251fd58283faa8f758e753 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 19 Feb 2022 21:00:54 -0600 Subject: [PATCH 0039/1083] Show SRP as currency for vendor catalog batch --- tailbone/views/batch/vendorcatalog.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tailbone/views/batch/vendorcatalog.py b/tailbone/views/batch/vendorcatalog.py index 8173cac5..86d8404b 100644 --- a/tailbone/views/batch/vendorcatalog.py +++ b/tailbone/views/batch/vendorcatalog.py @@ -321,6 +321,7 @@ class VendorCatalogView(FileBatchMasterView): f.set_renderer('product', self.render_product) f.set_type('upc', 'gpc') f.set_type('discount_percent', 'percent') + f.set_type('suggested_retail', 'currency') def template_kwargs_view_row(self, **kwargs): row = kwargs['instance'] From ceceb3f03084d8bcd7bcb77b171dd23aa042ead4 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 20 Feb 2022 15:25:17 -0600 Subject: [PATCH 0040/1083] Update changelog --- CHANGES.rst | 10 ++++++++++ tailbone/_version.py | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index e2173067..3232924d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,16 @@ CHANGELOG ========= +0.8.210 (2022-02-20) +-------------------- + +* Only show DB picker for permissioned users. + +* Expose some new trainwreck fields; per-item discounts. + +* Show SRP as currency for vendor catalog batch. + + 0.8.209 (2022-02-16) -------------------- diff --git a/tailbone/_version.py b/tailbone/_version.py index 734c777e..aec2e7bb 100644 --- a/tailbone/_version.py +++ b/tailbone/_version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8; -*- -__version__ = '0.8.209' +__version__ = '0.8.210' From 5b697cdf26176ace21f6d5a9061cf7c4db20b4dd Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 20 Feb 2022 17:06:51 -0600 Subject: [PATCH 0041/1083] Add view template stub for trainwreck transaction --- tailbone/templates/trainwreck/transactions/view.mako | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 tailbone/templates/trainwreck/transactions/view.mako diff --git a/tailbone/templates/trainwreck/transactions/view.mako b/tailbone/templates/trainwreck/transactions/view.mako new file mode 100644 index 00000000..601fa053 --- /dev/null +++ b/tailbone/templates/trainwreck/transactions/view.mako @@ -0,0 +1,6 @@ +## -*- coding: utf-8; -*- +<%inherit file="/master/view.mako" /> + +## nb. this exists just so everyone can inherit from it + +${parent.body()} From 4d404cb20ba33586e1a1e0ad80245c6e9ec25ba7 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 20 Feb 2022 19:40:32 -0600 Subject: [PATCH 0042/1083] Add auto-filter hyperlinks for batch row status breakdown --- tailbone/grids/core.py | 13 +++++++++- tailbone/templates/batch/view.mako | 26 ++++++++++++++++---- tailbone/templates/grids/b-table.mako | 7 ++++++ tailbone/templates/grids/buefy.mako | 35 +++++++++++++++++++++++++++ tailbone/templates/master/view.mako | 2 +- tailbone/views/batch/core.py | 27 +++++++++++++-------- 6 files changed, 93 insertions(+), 17 deletions(-) diff --git a/tailbone/grids/core.py b/tailbone/grids/core.py index cb71e144..bc7c6684 100644 --- a/tailbone/grids/core.py +++ b/tailbone/grids/core.py @@ -79,7 +79,7 @@ class Grid(object): sortable=False, sorters={}, default_sortkey=None, default_sortdir='asc', pageable=False, default_pagesize=20, default_page=1, checkboxes=False, checked=None, check_handler=None, check_all_handler=None, - clicking_row_checks_box=False, + clicking_row_checks_box=False, click_handlers=None, main_actions=[], more_actions=[], delete_speedbump=False, ajax_data_url=None, component='tailbone-grid', **kwargs): @@ -136,6 +136,8 @@ class Grid(object): self.check_all_handler = check_all_handler self.clicking_row_checks_box = clicking_row_checks_box + self.click_handlers = click_handlers or {} + self.main_actions = main_actions or [] self.more_actions = more_actions or [] self.delete_speedbump = delete_speedbump @@ -261,6 +263,15 @@ class Grid(object): if self.linked_columns and key in self.linked_columns: self.linked_columns.remove(key) + def set_click_handler(self, key, handler): + if handler: + self.click_handlers[key] = handler + else: + self.click_handlers.pop(key, None) + + def has_click_handler(self, key): + return key in self.click_handlers + def set_renderer(self, key, renderer): self.renderers[key] = renderer diff --git a/tailbone/templates/batch/view.mako b/tailbone/templates/batch/view.mako index 1b7787bb..1faa0f86 100644 --- a/tailbone/templates/batch/view.mako +++ b/tailbone/templates/batch/view.mako @@ -154,13 +154,18 @@ <%def name="render_status_breakdown()"> - % if status_breakdown is not Undefined and status_breakdown is not None: + % if use_buefy:

    Row Status Breakdown

    - % if use_buefy: - ${status_breakdown_grid.render_buefy_table_element(data_prop='statusBreakdownData', empty_labels=True)|n} - % elif status_breakdown: + ${status_breakdown_grid|n} +
    +
    + % elif status_breakdown is not Undefined and status_breakdown is not None: +
    +

    Row Status Breakdown

    +
    + % if status_breakdown:
    % for i, (status, count) in enumerate(status_breakdown): @@ -407,7 +412,18 @@ ${parent.modify_this_page_vars()} diff --git a/tailbone/views/purchasing/receiving.py b/tailbone/views/purchasing/receiving.py index 12979d0b..e8123406 100644 --- a/tailbone/views/purchasing/receiving.py +++ b/tailbone/views/purchasing/receiving.py @@ -674,6 +674,7 @@ class ReceivingBatchView(PurchasingBatchView): for key, label in labels.items(): if key in grouped: breakdown.append({ + 'key': key, 'title': label, 'count': len(grouped[key]), }) @@ -683,15 +684,26 @@ class ReceivingBatchView(PurchasingBatchView): def template_kwargs_view(self, **kwargs): kwargs = super(ReceivingBatchView, self).template_kwargs_view(**kwargs) batch = kwargs['instance'] + use_buefy = self.get_use_buefy() if self.handler.has_purchase_order(batch) and self.handler.has_invoice_file(batch): breakdown = self.make_po_vs_invoice_breakdown(batch) - factory = self.get_grid_factory() - kwargs['po_vs_invoice_breakdown_grid'] = factory( - 'batch_po_vs_invoice_breakdown', - data=breakdown, - columns=['title', 'count']) + if use_buefy: + + g = factory('batch_po_vs_invoice_breakdown', [], + columns=['title', 'count']) + g.set_click_handler('title', "autoFilterPoVsInvoice(props.row)") + kwargs['po_vs_invoice_breakdown_data'] = breakdown + kwargs['po_vs_invoice_breakdown_grid'] = HTML.literal( + g.render_buefy_table_element(data_prop='poVsInvoiceBreakdownData', + empty_labels=True)) + + else: + kwargs['po_vs_invoice_breakdown_grid'] = factory( + 'batch_po_vs_invoice_breakdown', + data=breakdown, + columns=['title', 'count']) return kwargs From 0c5992ad756abf0407fb15e126d58d0b80900f51 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 22 Feb 2022 20:39:06 -0600 Subject: [PATCH 0044/1083] Add grid hyperlinks for trainwreck transaction line items --- tailbone/views/trainwreck/base.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tailbone/views/trainwreck/base.py b/tailbone/views/trainwreck/base.py index 6fac7605..167848cd 100644 --- a/tailbone/views/trainwreck/base.py +++ b/tailbone/views/trainwreck/base.py @@ -220,6 +220,9 @@ class TransactionView(MasterView): g.set_type('tax', 'currency') g.set_type('total', 'currency') + g.set_link('item_scancode') + g.set_link('description') + def row_grid_extra_class(self, row, i): if row.void: return 'warning' From 3553f23eab25a5999082cec80dc09693c7cb776f Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Wed, 23 Feb 2022 00:26:14 -0600 Subject: [PATCH 0045/1083] Use dict instead of custom object to represent menus as prep for editing menu config directly in app --- tailbone/menus.py | 86 ++++++++++----------- tailbone/templates/menu.mako | 12 +-- tailbone/templates/themes/falafel/base.mako | 28 +++---- 3 files changed, 63 insertions(+), 63 deletions(-) diff --git a/tailbone/menus.py b/tailbone/menus.py index 2402e768..e2c025c1 100644 --- a/tailbone/menus.py +++ b/tailbone/menus.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2021 Lance Edgar +# Copyright © 2010-2022 Lance Edgar # # This file is part of Rattail. # @@ -26,38 +26,11 @@ App Menus from __future__ import unicode_literals, absolute_import -from rattail.core import Object +import re + from rattail.util import import_module_path -class MenuGroup(Object): - title = None - items = None - is_menu = True - is_link = False - - -class MenuItem(Object): - title = None - url = None - target = None - is_link = True - is_menu = False - is_sep = False - - -class MenuItemMenu(Object): - title = None - items = None - is_menu = True - is_sep = False - - -class MenuSeparator(object): - is_menu = False - is_sep = True - - def make_simple_menus(request): """ Build the main menu list for the app. @@ -93,30 +66,48 @@ def make_simple_menus(request): for subitem in item['items']: if subitem['allowed']: submenu_items.append(make_menu_entry(subitem)) - menu_items.append(MenuItemMenu( - title=item['title'], - items=submenu_items)) + menu_items.append({ + 'type': 'submenu', + 'title': item['title'], + 'items': submenu_items, + 'is_menu': True, + 'is_sep': False, + }) elif item.get('type') == 'sep': # we only want to add a sep, *if* we already have some # menu items (i.e. there is something to separate) # *and* the last menu item is not a sep (avoid doubles) - if menu_items and not menu_items[-1].is_sep: + if menu_items and not menu_items[-1]['is_sep']: menu_items.append(make_menu_entry(item)) else: # standard menu item menu_items.append(make_menu_entry(item)) # remove final separator if present - if menu_items and menu_items[-1].is_sep: + if menu_items and menu_items[-1]['is_sep']: menu_items.pop() # only add if we wound up with something assert menu_items if menu_items: - final_menus.append(MenuGroup( - title=topitem['title'], - items=menu_items)) + group = { + 'type': 'menu', + 'key': topitem.get('key'), + 'title': topitem['title'], + 'items': menu_items, + 'is_menu': True, + 'is_link': False, + } + + # topitem w/ no key likely means it did not come + # from config but rather explicit definition in + # code. so we are free to "invent" a (safe) key + # for it, since that is only for editing config + if not group['key']: + group['key'] = re.sub(r'\W', '', topitem['title'].lower()) + + final_menus.append(group) return final_menus @@ -128,13 +119,22 @@ def make_menu_entry(item): """ # separator if item.get('type') == 'sep': - return MenuSeparator() + return { + 'type': 'sep', + 'is_menu': False, + 'is_sep': True, + } # standard menu item - return MenuItem( - title=item['title'], - url=item['url'], - target=item.get('target')) + return { + 'type': 'item', + 'title': item['title'], + 'url': item['url'], + 'target': item.get('target'), + 'is_link': True, + 'is_menu': False, + 'is_sep': False, + } def is_allowed(request, item): diff --git a/tailbone/templates/menu.mako b/tailbone/templates/menu.mako index 7549e763..65acd0dd 100644 --- a/tailbone/templates/menu.mako +++ b/tailbone/templates/menu.mako @@ -4,16 +4,16 @@ % for topitem in menus:
  • - % if topitem.is_link: - ${h.link_to(topitem.title, topitem.url, target=topitem.target)} + % if topitem['is_link']: + ${h.link_to(topitem['title'], topitem['url'], target=topitem['target'])} % else: - ${topitem.title} + ${topitem['title']}
      - % for subitem in topitem.items: - % if subitem.is_sep: + % for subitem in topitem['items']: + % if subitem['is_sep']:
    • -
    • % else: -
    • ${h.link_to(subitem.title, subitem.url, target=subitem.target)}
    • +
    • ${h.link_to(subitem['title'], subitem['url'], target=subitem['target'])}
    • % endif % endfor
    diff --git a/tailbone/templates/themes/falafel/base.mako b/tailbone/templates/themes/falafel/base.mako index 4378c4cf..0bc0aca8 100644 --- a/tailbone/templates/themes/falafel/base.mako +++ b/tailbone/templates/themes/falafel/base.mako @@ -200,33 +200,33 @@