From 33af2e6fa1037b6f221923e854b7aed50b4cc0e9 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 23 Dec 2021 14:46:28 -0600 Subject: [PATCH 0001/1119] Show create button on "most" pages for a master view --- tailbone/templates/themes/falafel/base.mako | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tailbone/templates/themes/falafel/base.mako b/tailbone/templates/themes/falafel/base.mako index b15a786d..5a873735 100644 --- a/tailbone/templates/themes/falafel/base.mako +++ b/tailbone/templates/themes/falafel/base.mako @@ -303,6 +303,13 @@ ${h.link_to(instance_title, instance_url)} + % elif master.creatable and master.show_create_link and master.has_perm('create'): + + % endif % if master.viewing and grid_index: ${grid_index_nav()} From 819ae22b0e51e52d26f380ec2d0577cabb132461 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 23 Dec 2021 15:18:30 -0600 Subject: [PATCH 0002/1119] Expose products setting for type 2 UPC lookup also expose Configure button for most master view pages --- tailbone/templates/master/index.mako | 2 +- tailbone/templates/products/configure.mako | 13 ++++++++++ tailbone/templates/themes/falafel/base.mako | 27 ++++++++++++++++----- tailbone/views/products.py | 5 ++++ 4 files changed, 40 insertions(+), 7 deletions(-) diff --git a/tailbone/templates/master/index.mako b/tailbone/templates/master/index.mako index ca0615ce..6be36948 100644 --- a/tailbone/templates/master/index.mako +++ b/tailbone/templates/master/index.mako @@ -162,7 +162,7 @@
  • ${h.link_to("Create a new {}".format(model_title), url('{}.create'.format(route_prefix)))}
  • % endif % endif - % if master.configurable and master.has_perm('configure'): + % if not use_buefy and master.configurable and master.has_perm('configure'):
  • ${h.link_to("Configure {}".format(config_title), url('{}.configure'.format(route_prefix)))}
  • % endif % if master.has_input_file_templates and master.has_perm('download_template'): diff --git a/tailbone/templates/products/configure.mako b/tailbone/templates/products/configure.mako index e3c21307..31b879c5 100644 --- a/tailbone/templates/products/configure.mako +++ b/tailbone/templates/products/configure.mako @@ -29,6 +29,19 @@ +

    Handling

    +
    + + + + Auto-convert Type 2 UPC for sake of lookup + + + +
    +

    Display

    diff --git a/tailbone/templates/themes/falafel/base.mako b/tailbone/templates/themes/falafel/base.mako index 5a873735..1e996e6b 100644 --- a/tailbone/templates/themes/falafel/base.mako +++ b/tailbone/templates/themes/falafel/base.mako @@ -304,12 +304,14 @@ ${h.link_to(instance_title, instance_url)} % elif master.creatable and master.show_create_link and master.has_perm('create'): - - + % if not request.matched_route.name.endswith('.create'): + + + % endif % endif % if master.viewing and grid_index: ${grid_index_nav()} @@ -367,6 +369,19 @@
    % endif + % if master.configurable and master.has_perm('configure'): + % if not request.matched_route.name.endswith('.configure'): +
    + + +
    + % endif + % endif + ## Theme Picker % if expose_theme_picker and request.has_perm('common.change_app_theme'):
    diff --git a/tailbone/views/products.py b/tailbone/views/products.py index 6459085b..7f40e1e3 100644 --- a/tailbone/views/products.py +++ b/tailbone/views/products.py @@ -1919,6 +1919,11 @@ class ProductView(MasterView): {'section': 'rattail', 'option': 'product.key_title'}, + # handling + {'section': 'rattail', + 'option': 'products.convert_type2_for_gpc_lookup', + 'type': bool}, + # display {'section': 'tailbone', 'option': 'products.show_pod_image', From 1b0d6581db543f584352a322c2ac267929244e61 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 23 Dec 2021 16:18:06 -0600 Subject: [PATCH 0003/1119] Bugfix --- tailbone/templates/themes/falafel/base.mako | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tailbone/templates/themes/falafel/base.mako b/tailbone/templates/themes/falafel/base.mako index 1e996e6b..b50cfef7 100644 --- a/tailbone/templates/themes/falafel/base.mako +++ b/tailbone/templates/themes/falafel/base.mako @@ -369,7 +369,7 @@
    % endif - % if master.configurable and master.has_perm('configure'): + % if master and master.configurable and master.has_perm('configure'): % if not request.matched_route.name.endswith('.configure'):
    Date: Thu, 23 Dec 2021 20:24:43 -0600 Subject: [PATCH 0004/1119] Add basic "resolve" support for person, product from new custorder --- .../templates/customers/pending/view.mako | 143 ++++++++++++++++++ tailbone/templates/custorders/create.mako | 15 +- tailbone/templates/products/pending/view.mako | 130 ++++++++++++++++ tailbone/views/customers.py | 48 ++++++ tailbone/views/custorders/items.py | 42 ++++- tailbone/views/custorders/orders.py | 72 +++++++-- tailbone/views/master.py | 9 +- tailbone/views/products.py | 47 ++++++ 8 files changed, 474 insertions(+), 32 deletions(-) create mode 100644 tailbone/templates/customers/pending/view.mako create mode 100644 tailbone/templates/products/pending/view.mako diff --git a/tailbone/templates/customers/pending/view.mako b/tailbone/templates/customers/pending/view.mako new file mode 100644 index 00000000..e9e54c99 --- /dev/null +++ b/tailbone/templates/customers/pending/view.mako @@ -0,0 +1,143 @@ +## -*- coding: utf-8; -*- +<%inherit file="/master/view.mako" /> +## <%namespace file="/util.mako" import="view_profiles_helper" /> + +<%def name="object_helpers()"> + ${parent.object_helpers()} + + % if instance.custorder_records: + + % endif + + ## % if instance.status_code == enum.PENDING_CUSTOMER_STATUS_PENDING and master.has_any_perm('resolve_person', 'resolve_customer'): + % if instance.status_code == enum.PENDING_CUSTOMER_STATUS_PENDING and master.has_perm('resolve_person'): + + + + + + % endif + + +<%def name="modify_this_page_vars()"> + ${parent.modify_this_page_vars()} + + + +${parent.body()} diff --git a/tailbone/templates/custorders/create.mako b/tailbone/templates/custorders/create.mako index ff41f765..db9af7ec 100644 --- a/tailbone/templates/custorders/create.mako +++ b/tailbone/templates/custorders/create.mako @@ -12,18 +12,6 @@ % endif -<%def name="render_instance_header_buttons()"> - ${parent.render_instance_header_buttons()} - % if use_buefy and master.configurable and master.has_perm('configure'): -
    - - -
    - % endif - - <%def name="page_content()">
    % if use_buefy: @@ -1968,6 +1956,9 @@ % if product_price_may_be_questionable: this.productPriceNeedsConfirmation = false % endif + + this.itemDialogTabIndex = 1 + }, response => { this.clearProduct() }) diff --git a/tailbone/templates/products/pending/view.mako b/tailbone/templates/products/pending/view.mako new file mode 100644 index 00000000..90d9c687 --- /dev/null +++ b/tailbone/templates/products/pending/view.mako @@ -0,0 +1,130 @@ +## -*- coding: utf-8; -*- +<%inherit file="/master/view.mako" /> + +<%def name="object_helpers()"> + ${parent.object_helpers()} + % if instance.custorder_item_records: + + % endif + + % if instance.status_code == enum.PENDING_PRODUCT_STATUS_PENDING and master.has_perm('resolve_product'): + + + + + + % endif + + +<%def name="modify_this_page_vars()"> + ${parent.modify_this_page_vars()} + + + + +${parent.body()} diff --git a/tailbone/views/customers.py b/tailbone/views/customers.py index ba05f475..bf8284c0 100644 --- a/tailbone/views/customers.py +++ b/tailbone/views/customers.py @@ -500,6 +500,9 @@ class PendingCustomerView(MasterView): super(PendingCustomerView, self).configure_grid(g) g.set_enum('status_code', self.enum.PENDING_CUSTOMER_STATUS) + g.filters['status_code'].default_active = True + g.filters['status_code'].default_verb = 'not_equal' + g.filters['status_code'].default_value = six.text_type(self.enum.PENDING_CUSTOMER_STATUS_RESOLVED) g.set_sort_defaults('display_name') g.set_link('id') @@ -523,6 +526,51 @@ class PendingCustomerView(MasterView): f.set_readonly('user') f.set_renderer('user', self.render_user) + def editable_instance(self, pending): + if pending.status_code == self.enum.PENDING_CUSTOMER_STATUS_RESOLVED: + return False + return True + + def resolve_person(self): + model = self.model + pending = self.get_instance() + redirect = self.redirect(self.get_action_url('view', pending)) + + uuid = self.request.POST['person_uuid'] + person = self.Session.query(model.Person).get(uuid) + if not person: + self.request.session.flash("Person not found!", 'error') + return redirect + + app = self.get_rattail_app() + people_handler = app.get_people_handler() + people_handler.resolve_person(pending, person, self.request.user) + self.Session.flush() + return redirect + + @classmethod + def defaults(cls, config): + cls._defaults(config) + cls._pending_customer_defaults(config) + + @classmethod + def _pending_customer_defaults(cls, config): + route_prefix = cls.get_route_prefix() + instance_url_prefix = cls.get_instance_url_prefix() + permission_prefix = cls.get_permission_prefix() + model_title = cls.get_model_title() + + # resolve person + config.add_tailbone_permission(permission_prefix, + '{}.resolve_person'.format(permission_prefix), + "Resolve a {} as a Person".format(model_title)) + config.add_route('{}.resolve_person'.format(route_prefix), + '{}/resolve-person'.format(instance_url_prefix), + request_method='POST') + config.add_view(cls, attr='resolve_person', + route_name='{}.resolve_person'.format(route_prefix), + permission='{}.resolve_person'.format(permission_prefix)) + # # TODO: this is referenced by some custom apps, but should be moved?? # def unique_id(value, field): diff --git a/tailbone/views/custorders/items.py b/tailbone/views/custorders/items.py index 4d62e505..823130ed 100644 --- a/tailbone/views/custorders/items.py +++ b/tailbone/views/custorders/items.py @@ -52,6 +52,7 @@ class CustomerOrderItemView(MasterView): deletable = False labels = { + 'order': "Customer Order", 'order_id': "Order ID", 'order_uom': "Order UOM", 'status_code': "Status", @@ -172,21 +173,37 @@ class CustomerOrderItemView(MasterView): def configure_form(self, f): super(CustomerOrderItemView, self).configure_form(f) use_buefy = self.get_use_buefy() + item = f.model_instance # order f.set_renderer('order', self.render_order) - # product + # (pending) product f.set_renderer('product', self.render_product) - - # pending_product f.set_renderer('pending_product', self.render_pending_product) + if self.viewing: + if item.product and not item.pending_product: + f.remove('pending_product') + elif item.pending_product and not item.product: + f.remove('product') # product uom f.set_enum('product_unit_of_measure', self.enum.UNIT_OF_MEASURE) + # highlight pending fields + f.set_renderer('product_brand', self.highlight_pending_field) + f.set_renderer('product_description', self.highlight_pending_field) + f.set_renderer('product_size', self.highlight_pending_field) + f.set_renderer('case_quantity', self.highlight_pending_field_quantity) + + 'unit_price', + 'total_price', + 'price_needs_confirmation', + 'paid_amount', + 'status_code', + 'notes', + # quantity fields - f.set_type('case_quantity', 'quantity') f.set_type('cases_ordered', 'quantity') f.set_type('units_ordered', 'quantity') f.set_type('order_quantity', 'quantity') @@ -210,10 +227,27 @@ class CustomerOrderItemView(MasterView): else: f.remove('notes') + def highlight_pending_field(self, item, field, value=None): + if value is None: + value = getattr(item, field) + if not item.product_uuid and item.pending_product_uuid: + return HTML.tag('span', c=[value], + class_='has-text-success') + return value + + def highlight_pending_field_quantity(self, item, field): + app = self.get_rattail_app() + value = getattr(item, field) + value = app.render_quantity(value) + return self.highlight_pending_field(item, field, value) + def render_price_with_confirmation(self, item, field): price = getattr(item, field) app = self.get_rattail_app() text = app.render_currency(price) + if not item.product_uuid and item.pending_product_uuid: + text = HTML.tag('span', c=[text], + class_='has-text-success') if item.price_needs_confirmation: return HTML.tag('span', class_='has-background-warning', c=[text]) diff --git a/tailbone/views/custorders/orders.py b/tailbone/views/custorders/orders.py index a97b9978..c60e859e 100644 --- a/tailbone/views/custorders/orders.py +++ b/tailbone/views/custorders/orders.py @@ -108,20 +108,23 @@ class CustomerOrderView(MasterView): def configure_grid(self, g): super(CustomerOrderView, self).configure_grid(g) - g.set_joiner('customer', lambda q: q.outerjoin(model.Customer)) - g.set_joiner('person', lambda q: q.outerjoin(model.Person)) - - g.filters['customer'] = g.make_filter('customer', model.Customer.name, - label="Customer Name", - default_active=True, - default_verb='contains') - g.filters['person'] = g.make_filter('person', model.Person.display_name, - label="Person Name", - default_active=True, - default_verb='contains') - - g.set_sorter('customer', model.Customer.name) - g.set_sorter('person', model.Person.display_name) + # customer or person + if self.batch_handler.new_order_requires_customer(): + g.remove('person') + g.set_joiner('customer', lambda q: q.outerjoin(model.Customer)) + g.set_sorter('customer', model.Customer.name) + g.filters['customer'] = g.make_filter('customer', model.Customer.name, + label="Customer Name", + default_active=True, + default_verb='contains') + else: + g.remove('customer') + g.set_joiner('person', lambda q: q.outerjoin(model.Person)) + g.set_sorter('person', model.Person.display_name) + g.filters['person'] = g.make_filter('person', model.Person.display_name, + label="Person Name", + default_active=True, + default_verb='contains') g.set_enum('status_code', self.enum.CUSTORDER_STATUS) @@ -133,13 +136,33 @@ class CustomerOrderView(MasterView): def configure_form(self, f): super(CustomerOrderView, self).configure_form(f) + order = f.model_instance f.set_readonly('id') f.set_renderer('store', self.render_store) + + # (pending) customer f.set_renderer('customer', self.render_customer) f.set_renderer('person', self.render_person) f.set_renderer('pending_customer', self.render_pending_customer) + if self.viewing: + if self.batch_handler.new_order_requires_customer(): + f.remove('person') + if order.customer and not order.pending_customer: + f.remove('pending_customer') + elif order.pending_customer and not order.customer: + f.remove('customer') + else: + f.remove('customer') + if order.person and not order.pending_customer: + f.remove('pending_customer') + elif order.pending_customer and not order.person: + f.remove('person') + + # contact info + f.set_renderer('phone_number', self.highlight_pending_field) + f.set_renderer('email_address', self.highlight_pending_field) f.set_type('total_price', 'currency') @@ -150,6 +173,20 @@ class CustomerOrderView(MasterView): f.set_readonly('created_by') f.set_renderer('created_by', self.render_user) + def highlight_pending_field(self, order, field): + value = getattr(order, field) + pending = False + if self.batch_handler.new_order_requires_customer(): + if not order.customer_uuid and order.pending_customer_uuid: + pending = True + else: + if not order.person_uuid and order.pending_customer_uuid: + pending = True + if pending: + return HTML.tag('span', c=[value], + class_='has-text-success') + return value + def render_person(self, order, field): person = order.person if not person: @@ -164,7 +201,8 @@ class CustomerOrderView(MasterView): return text = six.text_type(pending) url = self.request.route_url('pending_customers.view', uuid=pending.uuid) - return tags.link_to(text, url) + return tags.link_to(text, url, + class_='has-background-warning') def get_row_data(self, order): return self.Session.query(model.CustomerOrderItem)\ @@ -218,6 +256,10 @@ class CustomerOrderView(MasterView): g.set_link('product_brand') g.set_link('product_description') + def row_grid_extra_class(self, item, i): + if not item.product_uuid and item.pending_product_uuid: + return 'has-text-success' + def render_price_with_confirmation(self, item, field): price = getattr(item, field) app = self.get_rattail_app() diff --git a/tailbone/views/master.py b/tailbone/views/master.py index 75996653..f83ec52e 100644 --- a/tailbone/views/master.py +++ b/tailbone/views/master.py @@ -290,6 +290,12 @@ class MasterView(View): return self.request.has_perm('{}.{}'.format( self.get_permission_prefix(), name)) + def has_any_perm(self, *names): + for name in names: + if self.has_perm(name): + return True + return False + @classmethod def get_config_url(cls): if hasattr(cls, 'config_url'): @@ -801,7 +807,8 @@ class MasterView(View): return text = six.text_type(pending) url = self.request.route_url('pending_products.view', uuid=pending.uuid) - return tags.link_to(text, url) + return tags.link_to(text, url, + class_='has-background-warning') def render_vendor(self, obj, field): vendor = getattr(obj, field) diff --git a/tailbone/views/products.py b/tailbone/views/products.py index 7f40e1e3..30e5fe5f 100644 --- a/tailbone/views/products.py +++ b/tailbone/views/products.py @@ -2035,6 +2035,9 @@ class PendingProductView(MasterView): super(PendingProductView, self).configure_grid(g) g.set_enum('status_code', self.enum.PENDING_PRODUCT_STATUS) + g.filters['status_code'].default_active = True + g.filters['status_code'].default_verb = 'not_equal' + g.filters['status_code'].default_value = six.text_type(self.enum.PENDING_PRODUCT_STATUS_RESOLVED) g.set_sort_defaults('created', 'desc') @@ -2137,6 +2140,11 @@ class PendingProductView(MasterView): # f.set_readonly('status_code') f.set_enum('status_code', self.enum.PENDING_PRODUCT_STATUS) + def editable_instance(self, pending): + if pending.status_code == self.enum.PENDING_PRODUCT_STATUS_RESOLVED: + return False + return True + def objectify(self, form, data=None): if data is None: data = form.validated @@ -2182,6 +2190,45 @@ class PendingProductView(MasterView): 'error') return self.redirect(self.get_action_url('view', pending)) + def resolve_product(self): + model = self.model + pending = self.get_instance() + redirect = self.redirect(self.get_action_url('view', pending)) + + uuid = self.request.POST['product_uuid'] + product = self.Session.query(model.Product).get(uuid) + if not product: + self.request.session.flash("Product not found!", 'error') + return redirect + + app = self.get_rattail_app() + products_handler = app.get_products_handler() + products_handler.resolve_product(pending, product, self.request.user) + return redirect + + @classmethod + def defaults(cls, config): + cls._defaults(config) + cls._pending_product_defaults(config) + + @classmethod + def _pending_product_defaults(cls, config): + route_prefix = cls.get_route_prefix() + instance_url_prefix = cls.get_instance_url_prefix() + permission_prefix = cls.get_permission_prefix() + model_title = cls.get_model_title() + + # resolve product + config.add_tailbone_permission(permission_prefix, + '{}.resolve_product'.format(permission_prefix), + "Resolve a {} as a Product".format(model_title)) + config.add_route('{}.resolve_product'.format(route_prefix), + '{}/resolve-product'.format(instance_url_prefix), + request_method='POST') + config.add_view(cls, attr='resolve_product', + route_name='{}.resolve_product'.format(route_prefix), + permission='{}.resolve_product'.format(permission_prefix)) + def print_labels(request): profile = request.params.get('profile') From c2d76966a339008075c9bc98a4d35088bdc8fc34 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Wed, 29 Dec 2021 11:14:40 -0600 Subject: [PATCH 0005/1119] 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 0ee219b4..affe5ccd 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,16 @@ CHANGELOG ========= +0.8.190 (2021-12-29) +-------------------- + +* Show create button on "most" pages for a master view. + +* Expose products setting for type 2 UPC lookup. + +* Add basic "resolve" support for person, product from new custorder. + + 0.8.189 (2021-12-23) -------------------- diff --git a/tailbone/_version.py b/tailbone/_version.py index ce4233d3..b07a949a 100644 --- a/tailbone/_version.py +++ b/tailbone/_version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8; -*- -__version__ = '0.8.189' +__version__ = '0.8.190' From 7b7eee92cda66f205d0796f87f31690f51729ec6 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 30 Dec 2021 11:04:59 -0600 Subject: [PATCH 0006/1119] Fix permission check for input file template links --- tailbone/templates/master/index.mako | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tailbone/templates/master/index.mako b/tailbone/templates/master/index.mako index 6be36948..48e51286 100644 --- a/tailbone/templates/master/index.mako +++ b/tailbone/templates/master/index.mako @@ -165,7 +165,7 @@ % if not use_buefy and master.configurable and master.has_perm('configure'):
  • ${h.link_to("Configure {}".format(config_title), url('{}.configure'.format(route_prefix)))}
  • % endif - % if master.has_input_file_templates and master.has_perm('download_template'): + % if master.has_input_file_templates and master.has_perm('create'): % for template in six.itervalues(input_file_templates):
  • ${h.link_to("Download {} Template".format(template['label']), template['effective_url'])}
  • % endfor From 94883c1433c9ff6746a128f61cd14bc212208520 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 31 Dec 2021 13:46:36 -0600 Subject: [PATCH 0007/1119] Remove usage of `app.get_designated_import_handler()` --- tailbone/views/importing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tailbone/views/importing.py b/tailbone/views/importing.py index d93e4cfd..2a660b08 100644 --- a/tailbone/views/importing.py +++ b/tailbone/views/importing.py @@ -167,7 +167,7 @@ class ImportingView(MasterView): """ key = self.request.matchdict['key'] app = self.get_rattail_app() - handler = app.get_designated_import_handler(key, ignore_errors=True) + handler = app.get_import_handler(key, ignore_errors=True) if handler: return self.normalize(handler) raise self.notfound() From 3aac855fa1a16539fea3101ef2d12804a4047a32 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 1 Jan 2022 19:12:46 -0600 Subject: [PATCH 0008/1119] Add basic configure page for Trainwreck also the beginnings of a "yearly rollover" page which hopefully will prove useful for helping to automate that, once i figure out how best to go about it... --- tailbone/subscribers.py | 3 +- .../trainwreck/transactions/configure.mako | 31 +++++ .../trainwreck/transactions/index.mako | 12 ++ .../trainwreck/transactions/rollover.mako | 57 ++++++++ tailbone/views/master.py | 18 ++- tailbone/views/trainwreck/base.py | 130 ++++++++++++++++-- 6 files changed, 234 insertions(+), 17 deletions(-) create mode 100644 tailbone/templates/trainwreck/transactions/configure.mako create mode 100644 tailbone/templates/trainwreck/transactions/index.mako create mode 100644 tailbone/templates/trainwreck/transactions/rollover.mako diff --git a/tailbone/subscribers.py b/tailbone/subscribers.py index bce94a98..150aa6da 100644 --- a/tailbone/subscribers.py +++ b/tailbone/subscribers.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. # @@ -108,6 +108,7 @@ def before_render(event): request = event.get('request') or threadlocal.get_current_request() renderer_globals = event + renderer_globals['rattail_app'] = request.rattail_config.get_app() renderer_globals['h'] = helpers renderer_globals['url'] = request.route_url renderer_globals['rattail'] = rattail diff --git a/tailbone/templates/trainwreck/transactions/configure.mako b/tailbone/templates/trainwreck/transactions/configure.mako new file mode 100644 index 00000000..7cf03165 --- /dev/null +++ b/tailbone/templates/trainwreck/transactions/configure.mako @@ -0,0 +1,31 @@ +## -*- coding: utf-8; -*- +<%inherit file="/configure.mako" /> + +<%def name="form_content()"> + +

    Hidden Databases

    +
    + % for key, engine in six.iteritems(trainwreck_engines): + + + ${key} + + + % endfor +
    + + +<%def name="modify_this_page_vars()"> + ${parent.modify_this_page_vars()} + + + + +${parent.body()} diff --git a/tailbone/templates/trainwreck/transactions/index.mako b/tailbone/templates/trainwreck/transactions/index.mako new file mode 100644 index 00000000..31d956fc --- /dev/null +++ b/tailbone/templates/trainwreck/transactions/index.mako @@ -0,0 +1,12 @@ +## -*- coding: utf-8; -*- +<%inherit file="/master/index.mako" /> + +<%def name="context_menu_items()"> + ${parent.context_menu_items()} + % if master.has_perm('rollover'): +
  • ${h.link_to("Yearly Rollover", url('{}.rollover'.format(route_prefix)))}
  • + % endif + + + +${parent.body()} diff --git a/tailbone/templates/trainwreck/transactions/rollover.mako b/tailbone/templates/trainwreck/transactions/rollover.mako new file mode 100644 index 00000000..6d6e0b17 --- /dev/null +++ b/tailbone/templates/trainwreck/transactions/rollover.mako @@ -0,0 +1,57 @@ +## -*- coding: utf-8; -*- +<%inherit file="/page.mako" /> + +<%def name="title()">${index_title} » Yearly Rollover + +<%def name="content_title()">Yearly Rollover + +<%def name="page_content()"> +
    + + % if six.text_type(next_year) not in trainwreck_engines: + + You do not have a database configured for next year (${next_year}).  + You should be sure to configure it before next year rolls around. + + % endif + +

    + The following Trainwreck databases are configured: +

    + + + + + + +<%def name="modify_this_page_vars()"> + ${parent.modify_this_page_vars()} + + + + +${parent.body()} diff --git a/tailbone/views/master.py b/tailbone/views/master.py index f83ec52e..3807408b 100644 --- a/tailbone/views/master.py +++ b/tailbone/views/master.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. # @@ -60,6 +60,7 @@ from webob.compat import cgi_FieldStorage from tailbone import forms, grids, diffs from tailbone.views import View +from tailbone.db import Session from tailbone.config import global_help_url @@ -4412,15 +4413,20 @@ class MasterView(View): ]) if names: - self.Session.query(model.Setting)\ - .filter(model.Setting.name.in_(names))\ - .delete(synchronize_session=False) + # nb. we do not use self.Session b/c that may not point to + # the Rattail DB for the subclass + Session().query(model.Setting)\ + .filter(model.Setting.name.in_(names))\ + .delete(synchronize_session=False) def configure_save_settings(self, settings): model = self.model + # nb. we do not use self.Session b/c that may not point to the + # Rattail DB for the subclass + session = Session() for setting in settings: - self.Session.add(model.Setting(name=setting['name'], - value=setting['value'])) + session.add(model.Setting(name=setting['name'], + value=setting['value'])) ############################## # Pyramid View Config diff --git a/tailbone/views/trainwreck/base.py b/tailbone/views/trainwreck/base.py index 60a0f873..20e7701d 100644 --- a/tailbone/views/trainwreck/base.py +++ b/tailbone/views/trainwreck/base.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. # @@ -29,9 +29,8 @@ from __future__ import unicode_literals, absolute_import import six from rattail.time import localtime -from rattail.util import OrderedDict -from tailbone.db import TrainwreckSession, ExtraTrainwreckSessions +from tailbone.db import Session, TrainwreckSession, ExtraTrainwreckSessions from tailbone.views import MasterView @@ -53,6 +52,8 @@ class TransactionView(MasterView): SessionDefault = TrainwreckSession SessionExtras = ExtraTrainwreckSessions + configurable = True + labels = { 'store_id': "Store", 'cashback': "Cash Back", @@ -139,13 +140,9 @@ class TransactionView(MasterView): ] def get_db_engines(self): - engines = OrderedDict(self.rattail_config.trainwreck_engines) - hidden = self.rattail_config.getlist('tailbone', 'engines.trainwreck.hidden', - default=None) - if hidden: - for key in hidden: - engines.pop(key, None) - return engines + app = self.get_rattail_app() + trainwreck_handler = app.get_trainwreck_handler() + return trainwreck_handler.get_trainwreck_engines(include_hidden=False) def configure_grid(self, g): super(TransactionView, self).configure_grid(g) @@ -228,3 +225,116 @@ class TransactionView(MasterView): f.set_type('discounted_subtotal', 'currency') f.set_type('tax', 'currency') f.set_type('total', 'currency') + + def rollover(self): + """ + View for performing yearly rollover functions. + """ + app = self.get_rattail_app() + trainwreck_handler = app.get_trainwreck_handler() + trainwreck_engines = trainwreck_handler.get_trainwreck_engines() + current_year = app.localtime().year + + # find oldest and newest dates for each database + engines_data = [] + for key, engine in six.iteritems(trainwreck_engines): + + if key == 'default': + session = self.Session() + else: + session = ExtraTrainwreckSessions[key]() + + error = False + oldest = None + newest = None + try: + oldest = trainwreck_handler.get_oldest_transaction_date(session) + newest = trainwreck_handler.get_newest_transaction_date(session) + except: + error = True + + engines_data.append({ + 'key': key, + 'oldest_date': app.render_date(oldest) if oldest else None, + 'newest_date': app.render_date(newest) if newest else None, + 'error': error, + }) + + return self.render_to_response('rollover', { + 'instance_title': "Yearly Rollover", + 'trainwreck_handler': trainwreck_handler, + 'current_year': current_year, + 'next_year': current_year + 1, + 'trainwreck_engines': trainwreck_engines, + 'engines_data': engines_data, + }) + + def configure_get_context(self): + context = super(TransactionView, self).configure_get_context() + + app = self.get_rattail_app() + trainwreck_handler = app.get_trainwreck_handler() + trainwreck_engines = trainwreck_handler.get_trainwreck_engines() + + context['trainwreck_engines'] = trainwreck_engines + context['hidden_databases'] = dict([ + (key, trainwreck_handler.engine_is_hidden(key)) + for key in trainwreck_engines]) + + return context + + def configure_gather_settings(self, data): + settings = super(TransactionView, self).configure_gather_settings(data) + + app = self.get_rattail_app() + trainwreck_handler = app.get_trainwreck_handler() + trainwreck_engines = trainwreck_handler.get_trainwreck_engines() + + hidden = [] + for key in trainwreck_engines: + name = 'hidedb_{}'.format(key) + if data.get(name) == 'true': + hidden.append(key) + settings.append({'name': 'trainwreck.db.hide', + 'value': ', '.join(hidden)}) + + return settings + + def configure_remove_settings(self): + super(TransactionView, self).configure_remove_settings() + + model = self.model + names = [ + 'trainwreck.db.hide', + 'tailbone.engines.trainwreck.hidden', # deprecated + ] + # nb. we do not use self.Session b/c that points to trainwreck + Session.query(model.Setting)\ + .filter(model.Setting.name.in_(names))\ + .delete(synchronize_session=False) + + @classmethod + def defaults(cls, config): + cls._trainwreck_defaults(config) + cls._defaults(config) + + @classmethod + def _trainwreck_defaults(cls, config): + route_prefix = cls.get_route_prefix() + url_prefix = cls.get_url_prefix() + permission_prefix = cls.get_permission_prefix() + model_title_plural = cls.get_model_title_plural() + + # fix perm group title + config.add_tailbone_permission_group(permission_prefix, + model_title_plural) + + # rollover + config.add_tailbone_permission(permission_prefix, + '{}.rollover'.format(permission_prefix), + label="Perform yearly rollover for Trainwreck") + config.add_route('{}.rollover'.format(route_prefix), + '{}/rollover'.format(url_prefix)) + config.add_view(cls, attr='rollover', + route_name='{}.rollover'.format(route_prefix), + permission='{}.rollover'.format(permission_prefix)) From a0bb481a4399283042c8514c9478bb348c127370 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 3 Jan 2022 15:34:00 -0600 Subject: [PATCH 0009/1119] Use `AuthHandler.get_permissions()` instead of deprecated `cache_permissions()` --- tailbone/api/auth.py | 4 ++-- tailbone/subscribers.py | 9 +-------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/tailbone/api/auth.py b/tailbone/api/auth.py index 80f8fac0..c4d04b90 100644 --- a/tailbone/api/auth.py +++ b/tailbone/api/auth.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. # @@ -89,7 +89,7 @@ class AuthenticationView(APIView): return { 'ok': True, 'user': self.get_user_info(user), - 'permissions': list(auth.cache_permissions(Session(), user)), + 'permissions': list(auth.get_permissions(Session(), user)), } def authenticate_user(self, username, password): diff --git a/tailbone/subscribers.py b/tailbone/subscribers.py index 150aa6da..44b69247 100644 --- a/tailbone/subscribers.py +++ b/tailbone/subscribers.py @@ -89,15 +89,8 @@ def new_request(event): if rattail_config: app = rattail_config.get_app() auth = app.get_auth_handler() - request.tailbone_cached_permissions = auth.cache_permissions( + request.tailbone_cached_permissions = auth.get_permissions( Session(), request.user) - # TODO: until we know otherwise, let's assume this is not needed - # else: - # # TODO: not sure why this would really work, or even be - # # needed, if there was no rattail config? - # from rattail.db.auth import cache_permissions - # request.tailbone_cached_permissions = cache_permissions( - # Session(), request.user) def before_render(event): From 5e0ba81b21ec359872b574c1adb3a01a99943b53 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 3 Jan 2022 16:17:00 -0600 Subject: [PATCH 0010/1119] Update changelog --- CHANGES.rst | 12 ++++++++++++ tailbone/_version.py | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index affe5ccd..10c39d54 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,18 @@ CHANGELOG ========= +0.8.191 (2022-01-03) +-------------------- + +* Fix permission check for input file template links. + +* Remove usage of ``app.get_designated_import_handler()``. + +* Add basic configure page for Trainwreck. + +* Use ``AuthHandler.get_permissions()``. + + 0.8.190 (2021-12-29) -------------------- diff --git a/tailbone/_version.py b/tailbone/_version.py index b07a949a..b6faadaa 100644 --- a/tailbone/_version.py +++ b/tailbone/_version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8; -*- -__version__ = '0.8.190' +__version__ = '0.8.191' From ad110c2ce22b030e2f373bb5fb85bd1f4c9aa4d3 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 3 Jan 2022 21:10:34 -0600 Subject: [PATCH 0011/1119] Remove unused import --- tailbone/views/products.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tailbone/views/products.py b/tailbone/views/products.py index 30e5fe5f..0e192bca 100644 --- a/tailbone/views/products.py +++ b/tailbone/views/products.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. # @@ -41,7 +41,6 @@ from rattail.gpc import GPC from rattail.threads import Thread from rattail.exceptions import LabelPrintingError from rattail.util import load_object, pretty_quantity, OrderedDict, simple_error -from rattail.batch import get_batch_handler from rattail.time import localtime, make_utc import colander From f89dc88c0eb3276d4765ce3244efc47163807885 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 6 Jan 2022 12:53:01 -0600 Subject: [PATCH 0012/1119] Add configurable template file for vendor catalog batch --- .../static/files/vendor_catalog_template.xlsx | Bin 0 -> 7593 bytes .../batch/vendorcatalog/configure.mako | 9 +++++++++ .../templates/batch/vendorcatalog/index.mako | 3 --- tailbone/views/batch/vendorcatalog.py | 17 +++++++++++------ 4 files changed, 20 insertions(+), 9 deletions(-) create mode 100644 tailbone/static/files/vendor_catalog_template.xlsx create mode 100644 tailbone/templates/batch/vendorcatalog/configure.mako diff --git a/tailbone/static/files/vendor_catalog_template.xlsx b/tailbone/static/files/vendor_catalog_template.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..f68be31d228508d1bd3667c9873b3911f8bab9b3 GIT binary patch literal 7593 zcmeHMWmwev5+tzmTr&+>COcKr5mI~1O!Rx7D>USTe?A7x}`68&vWH? z&p99N=R5mh_t|G>_W!)|d*?Uvjo5uJS#V>5=!Mon!ogOJQq{x}M^;FsV9iA^3-QHT;;Y`BTd^E&D z=C>-~0}JTXEZy8-_!S%*lR=SiPUn~yE65)3FTI^}L!EO6dc>5K5RMWckq_h6_!zyi zCu1pN4}QorsZGXhA)F!qc5FD`)cQj_=xyVUauVd4%BleN_)YEv7pmnj9+i8?i<_Yn zyJ^zrC#yaZgg|YX=nmK?OxEAon1Ae4X2Ty;_&^jcl$5+{Lib}9JQ5a~>uM3#UQ_3Jw2{a;r9@k6w%KYU?>3k(+c_d_KCT-TmTNp&(VPKU17AU-b z1Lb0A>}+nP=IU%^Z{c!>Q{vY?MR;l~q`i72P_5p2iJ_G?iT={3@*0nd$a=bZcn;ev zp)-=EbR`NtK{g4p&!pWwqc87ijE1wFZFwYkK#^6 z^!39|+GEl-$ciJydFt68W8Ts{ECpIam{uxE{yMbTH3;n){~+=Ayg=0nM{cJ0(6!7I)lx z-uI)CxBthMR^}vPk(wpLa|aJ7aWY;G$0mwj?K?mI)n15k_MoLvtKsMU%8ihHg6;So zSV3BtB?iIMCP7)8*Q?6NA0z0Ay3H)n7Okwt!&n|66Z7KpPf|u<#EUV0u?fVz01_pe zGY6r83*jP5L9CXGAADXHe%;^T)2yv#RhtTEl#;cx5EICF$~0E?;6}N`jm0SoX2t8w@OF z#RD z-~5rP=^;NVbatS++)v;FB1)?R4S0})O_Q+d((6Rh3H&1UrDY^=6F9^3c322@UQas2 zlP?X>e9`6?6>S^o%Vr%AQi2jcj$JRuQPY+s2F3Z!OiHi_IzFWT1ieh6UvLk1era4RVbcITkp3{f zg6j<Uul}`Sud=^e{c3DO}aOy{5 z{Bp1|x@H&e*pE(kmn1`RL8_zepk{W9>X-3uh{AIV>SGBw#Yub}h7ryC$n?f_O6A zyhG#s#z4I${vhd5HxECnyM{(jc)5kSYD1GELR)`NMQ@lo;@YS%qJD_jIH;UXFGCZ* z)j*ISjciZ1!Y)AKEsVD}P9=(&#=2qRJXxZc zMl5|4FC5R0cshr zs?^P~sokjdtj?3^J{GPRFS_tnl`ABA)07rdmNz?Vr2Yx)lU}Fa&rH?COm$3}=p1s* zaf%TI=%31rO3mAa+@{Qb)fO zM++yk#Voylh@jy6Ok!*)Cf;#`gv(^za;-?&B}usw4q;kqL2|s_p82snTu=s&>PkjA zw1`go9ad;i*qB&Hr>snhSbF}*Wt4p4nXqh0O&=&tK|3ya-|7Sfe`W2{_0^Hhm*usG zM6-b|NaB;UELc}p*rmqIOV3o6{FQI&lRW`ZCS3iOB2xIc(jp?3x;@kYDs-5T&ZV=y53BvK3C~oa zbVNU(u&;Xj!|cGmg4sKPd{fFQ%8wDC%jhXHtiru9s@oh)9vG`=ofT94 zNsx1|%P`N7#7H8c96u(61v&V>H(!rcg?>~mw_rkFdHYnh-_&H}S`|Rb&Fa@vX@ztw z$~h~No}SFO%+65z{&4ilvQ(*v4gawtnL_Z@^rJF6a}y4fTxuuuzMW}Mo7u9dZdU?N zO{ipM=Q50TX?1L@M_cDkkqEglk~L>QjfUVZYLTs*$)KB2iaD8pCTB2)n^xeXZ-rZX z%mU9J3V3k})OAPCby4W@2qBQUMb}#(;-gKue7au(?UX2j-EL~QOUIYKVcr#D z8$d<@_HFCA^^dbJ;E&nY6=G}tbLidH(N~&g!@KP_%g|GTt6o)Q!%C5~8>O9aP&0m| z6OETmZEr2->ttW(G@YU&?5$^ge4sxID@UsirTds2G8>ww z%tSt>QgDEW9+nwSIhIPlBc-AfqQ{k%0Ye@hWe1Ub21i*DrYY0#X;LBryP|SUzgq7I z7%J(XO%nbpF$XS-Z8$qnSAW{Q_yIPePfS!ih=vnuk3HU}Rt5B#%TO$*&y9+0;{=kQ z{k$C=O(N6H!LgM1(pWKh8eKq7A5vdoQaXKdTAenWrG&Ir>KFNG22yD~<$PkRuvUpW z+eGNZ6F(n-xYxLa_>{Wf`ve4%V8l6x3q_7m7F!>Ap$~Y#j`N0Nbnt+NVH6H!n?B1h z8HK4`d*BM>av%kS&#cl7e$4e^H%CT}bwU&%Iz*|cRy_mOA7)UXIF&h)$*{_sF2p{N z8f{#lQTNU-nt4h8K+In%mD(%r#5mA0*cgF)&qmX~N!p1{!vrK%ZcnfDC{02xJzueF zC`VznAgdcG`KxIob+crpp+jMI{Q3@}$(iW3OL60R3b+j>hbaxCRxu8k2rIlw4X})f z!`&beyR8>Zusjz66C+7?>buLpQ@U z|8uuQYimPUN{VkH(+hP&8Sj)nNe!2(W5PG@ppxNbxh!_cF=tv5<(l0M9?b36qY z<7v#mfHm6O>F_tS&lVd;!>!t?Omx|clO>^iq&-L3uKe;j?K;fdv*xOmSMS=ikC*D7 z^p5xM>qSs3Jsi?saT8KXxSr`hY9oEwY&V@MB8FK{zz7qO!?peHsSU7?>m&EJ?DX)N z(6H~7@8wrP7u|uRua&P)_gtr0ww(rwzHMoG)9R%yJ=GR}={r3eVtD)-Nw;#_1e{OT z8(CBz=#x&DAGv!(5*YRAfur?1ExL~&=Uw`!pwFzynyGz=9gxuB{OZvKySWYls933y z=#>w&QtH{KgVp z-pg^em@WXY92p=u)vR)!|sfj z$v|IRPBCF_uZ5avZM&sRp23JJWy#Q4o;xE-naNHkZ6~qaEl;+7@a*B?c<0mD6IZWh zyX-#s_b#4Gc-6)b^ri}`z6uzGl4ex3aq+pYfRII~q*~x*&byK?ZQn`xDrUFfLzbrI zo>Q+hZN^6lzR{rT=qd7 zsZ0nTkq`)T;GYzn2FJAKTKO|u+8uk1L>-sFFjrT4=8snerOl}i2{f`vEx}P9pN5`LoP^^;8twi6p zdx6(-l){Ii$JpjHNhE+@BhKTFR#IF=aUD{ea5OzIov{t!6%yKyKbmka&*cMwr|Q$W zG!#%Bzp`v&2tgv=txOH0|&Ha$Wx1_$j9%zr)e%VSW zldaI`l~S!@zB<=R)R^1e410$YQLq~3_gfUaZn66NihEZ|^u!(PUCr%X4b>rz<}Uho zQ1xl_DFv|MHSd;EHIl6J;F_Y_B8f!TK_NVe!WkG-x!bEJ^B1U!OJ=M+pt!vN2wAtU&$L>}rOm-BGdw*<=Uq)Y%D zmNy*uE8Qu8vW~MykPm@u#qzH> zLd?GxR^eM@RFQv9To%}A#k9bt*`&}zwi`HMv3#?O9NPWx@M1eu3$Ptq_aYhT`K**9 z^0VGrZ%t!({ntm6A(;-{tOg0X5|&KgpUhDh>#@OK4iO8wkh)PyMr3z$yn@r(s1TDe ztA=g2gI!>2;oX53ettw^Vbz#=y0sF2BSV?x_s#S&M7V}_a?^5wL<8I@ez?Iyec<9*v3m8q|;R%r!_ic^DuBu`rShR6@)pAz#Kc#)1}#b`);kFWH<*>heH zsYYTizR`{MxXV~Z-CUT{ZMK$w&MUBRcrd>fQTI15cZ;Y$+uyIHl;!^%=l<^DZcp>e zm~Q|1HqL)-ZGMk%UxK~cQT#H8TSopdN&VNx;`b=`WsN^aDM0wkC_ly9-=qBfv!tT_ zWt5+S$nR0^clLMF;4d4$ZRdZR4S#pO-`LzW|G$j>wg%k+`kztYch~!u{axqt%S>-+ zd9M%p-SPe%bw}V|7Ji%4zvv8p4{*PR-d$zC44vS9fIlv_-`($*mb*gx%Zdr_x&Kji kfA_wB%>VrKqqisFFN%?}Ji;v_U|`ViUbokB$WH?N3&9)PivR!s literal 0 HcmV?d00001 diff --git a/tailbone/templates/batch/vendorcatalog/configure.mako b/tailbone/templates/batch/vendorcatalog/configure.mako new file mode 100644 index 00000000..e4fa346a --- /dev/null +++ b/tailbone/templates/batch/vendorcatalog/configure.mako @@ -0,0 +1,9 @@ +## -*- coding: utf-8; -*- +<%inherit file="/configure.mako" /> + +<%def name="form_content()"> + ${self.input_file_templates_section()} + + + +${parent.body()} diff --git a/tailbone/templates/batch/vendorcatalog/index.mako b/tailbone/templates/batch/vendorcatalog/index.mako index 1fac1170..fa6e4a5a 100644 --- a/tailbone/templates/batch/vendorcatalog/index.mako +++ b/tailbone/templates/batch/vendorcatalog/index.mako @@ -3,9 +3,6 @@ <%def name="context_menu_items()"> ${parent.context_menu_items()} - % if generic_template_url and master.has_perm('create'): -
  • ${h.link_to("Download Generic Template", generic_template_url)}
  • - % endif % if h.route_exists(request, 'vendors') and request.has_perm('vendors.list'):
  • ${h.link_to("View Vendors", url('vendors'))}
  • % endif diff --git a/tailbone/views/batch/vendorcatalog.py b/tailbone/views/batch/vendorcatalog.py index 7a1a4153..5436f7d7 100644 --- a/tailbone/views/batch/vendorcatalog.py +++ b/tailbone/views/batch/vendorcatalog.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. # @@ -57,6 +57,8 @@ class VendorCatalogView(FileBatchMasterView): template_prefix = '/batch/vendorcatalog' editable = False rows_bulk_deletable = True + has_input_file_templates = True + configurable = True labels = { 'vendor_id': "Vendor ID", @@ -133,6 +135,14 @@ class VendorCatalogView(FileBatchMasterView): 'status_text', ] + def get_input_file_templates(self): + return [ + {'key': 'default', + 'label': "Default", + 'default_url': self.request.static_url( + 'tailbone:static/files/vendor_catalog_template.xlsx')}, + ] + def get_parsers(self): if not hasattr(self, 'parsers'): parsers = sorted(iter_catalog_parsers(), key=lambda p: p.display) @@ -252,11 +262,6 @@ class VendorCatalogView(FileBatchMasterView): f.set_type('upc', 'gpc') f.set_type('discount_percent', 'percent') - def template_kwargs_index(self, **kwargs): - url = self.rattail_config.get('tailbone', 'batch.vendorcatalog.generic_template_url') - kwargs['generic_template_url'] = url - return kwargs - def template_kwargs_create(self, **kwargs): parsers = self.get_parsers() for parser in parsers: From ab61778d3547e38cb0faf46364e57ffba861272d Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 7 Jan 2022 15:03:56 -0600 Subject: [PATCH 0013/1119] Some aesthetic improvements for vendor catalog batch hopefully they're improvements... --- .../batch/vendorcatalog/view_row.mako | 13 ++++ tailbone/templates/batch/view.mako | 63 +++++++++++++++++++ tailbone/templates/master/view.mako | 22 ++++++- tailbone/views/batch/core.py | 4 +- tailbone/views/batch/vendorcatalog.py | 39 ++++++++---- 5 files changed, 124 insertions(+), 17 deletions(-) create mode 100644 tailbone/templates/batch/vendorcatalog/view_row.mako diff --git a/tailbone/templates/batch/vendorcatalog/view_row.mako b/tailbone/templates/batch/vendorcatalog/view_row.mako new file mode 100644 index 00000000..6aaf9bf4 --- /dev/null +++ b/tailbone/templates/batch/vendorcatalog/view_row.mako @@ -0,0 +1,13 @@ +## -*- coding: utf-8; -*- +<%inherit file="/master/view_row.mako" /> + +<%def name="render_buefy_form()"> +
    + +
    + ${catalog_entry_diff.render_html()} +
    + + + +${parent.body()} diff --git a/tailbone/templates/batch/view.mako b/tailbone/templates/batch/view.mako index 36b9b633..1b7787bb 100644 --- a/tailbone/templates/batch/view.mako +++ b/tailbone/templates/batch/view.mako @@ -351,6 +351,58 @@
    +<%def name="render_row_grid_tools()"> + ${parent.render_row_grid_tools()} + % if use_buefy and master.rows_bulk_deletable and not batch.executed and master.has_perm('delete_rows'): + + Delete Results + + + + + % endif + + <%def name="modify_this_page_vars()"> ${parent.modify_this_page_vars()} diff --git a/tailbone/templates/master/view.mako b/tailbone/templates/master/view.mako index 37d60c39..f361ad04 100644 --- a/tailbone/templates/master/view.mako +++ b/tailbone/templates/master/view.mako @@ -71,16 +71,32 @@ % if master.touchable and request.has_perm('{}.touch'.format(permission_prefix)):
  • ${h.link_to("\"Touch\" this {}".format(model_title), url('{}.touch'.format(route_prefix), uuid=instance.uuid))}
  • % endif - % if master.has_rows and master.rows_downloadable_csv and request.has_perm('{}.row_results_csv'.format(permission_prefix)): -
  • ${h.link_to("Download row results as CSV", url('{}.row_results_csv'.format(route_prefix), uuid=instance.uuid))}
  • + % if not use_buefy and master.has_rows and master.rows_downloadable_csv and master.has_perm('row_results_csv'): +
  • ${h.link_to("Download row results as CSV", master.get_action_url('row_results_csv', instance))}
  • % endif - % if master.has_rows and master.rows_downloadable_xlsx and master.has_perm('row_results_xlsx'): + % if not use_buefy and master.has_rows and master.rows_downloadable_xlsx and master.has_perm('row_results_xlsx'):
  • ${h.link_to("Download row results as XLSX", master.get_action_url('row_results_xlsx', instance))}
  • % endif <%def name="render_row_grid_tools()"> ${rows_grid_tools} + % if use_buefy: + % if master.rows_downloadable_xlsx and master.has_perm('row_results_xlsx'): + + Download Results XLSX + + % endif + % if master.rows_downloadable_csv and master.has_perm('row_results_csv'): + + Download Results CSV + + % endif + % endif <%def name="render_this_page()"> diff --git a/tailbone/views/batch/core.py b/tailbone/views/batch/core.py index 2f73de53..36ad341b 100644 --- a/tailbone/views/batch/core.py +++ b/tailbone/views/batch/core.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. # @@ -686,6 +686,8 @@ class BatchMasterView(MasterView): return HTML.tag('p', c=[link]) def make_batch_row_grid_tools(self, batch): + if self.get_use_buefy(): + return if self.rows_bulk_deletable and not batch.executed and self.request.has_perm('{}.delete_rows'.format(self.get_permission_prefix())): url = self.request.route_url('{}.delete_rows'.format(self.get_route_prefix()), uuid=batch.uuid) return HTML.tag('p', c=[tags.link_to("Delete all rows matching current search", url)]) diff --git a/tailbone/views/batch/vendorcatalog.py b/tailbone/views/batch/vendorcatalog.py index 5436f7d7..3668500a 100644 --- a/tailbone/views/batch/vendorcatalog.py +++ b/tailbone/views/batch/vendorcatalog.py @@ -40,6 +40,7 @@ from webhelpers2.html import tags from tailbone import forms from tailbone.db import Session from tailbone.views.batch import FileBatchMasterView +from tailbone.diffs import Diff log = logging.getLogger(__name__) @@ -76,11 +77,12 @@ class VendorCatalogView(FileBatchMasterView): form_fields = [ 'id', - 'description', - 'vendor', 'filename', + 'parser_key', + 'vendor', 'future', 'effective', + 'description', 'notes', 'created', 'created_by', @@ -114,16 +116,6 @@ class VendorCatalogView(FileBatchMasterView): 'description', 'size', 'is_preferred_vendor', - 'old_vendor_code', - 'vendor_code', - 'old_case_size', - 'case_size', - 'old_case_cost', - 'case_cost', - 'case_cost_diff', - 'old_unit_cost', - 'unit_cost', - 'unit_cost_diff', 'suggested_retail', 'starts', 'ends', @@ -131,6 +123,8 @@ class VendorCatalogView(FileBatchMasterView): 'discount_ends', 'discount_amount', 'discount_percent', + 'case_cost_diff', + 'unit_cost_diff', 'status_code', 'status_text', ] @@ -228,7 +222,7 @@ class VendorCatalogView(FileBatchMasterView): # starts if not batch.future: - g.hide_column('starts') + g.remove('starts') g.set_type('old_unit_cost', 'currency') g.set_type('unit_cost', 'currency') @@ -262,6 +256,25 @@ class VendorCatalogView(FileBatchMasterView): f.set_type('upc', 'gpc') f.set_type('discount_percent', 'percent') + def template_kwargs_view_row(self, **kwargs): + row = kwargs['instance'] + batch = row.batch + + fields = [ + 'vendor_code', + 'case_size', + 'case_cost', + 'unit_cost', + ] + old_data = dict([(field, getattr(row, 'old_{}'.format(field))) + for field in fields]) + new_data = dict([(field, getattr(row, field)) + for field in fields]) + kwargs['catalog_entry_diff'] = Diff(old_data, new_data, fields=fields, + monospace=True) + + return kwargs + def template_kwargs_create(self, **kwargs): parsers = self.get_parsers() for parser in parsers: From 88b3279e63a66768f16f68666b392e326210cad9 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 7 Jan 2022 19:27:10 -0600 Subject: [PATCH 0014/1119] Several disparate changes needed for vendor catalog improvements - invoke vendor handler where appropriate, e.g. for parsers - reverse "polarity" of dropdown chooser setting; rename it - tweak autocomplete behavior yet again, for dynamic values - auto-select vendor upon parser selection, when possible --- tailbone/forms/widgets.py | 4 +- .../static/js/tailbone.buefy.autocomplete.js | 16 +++ tailbone/templates/autocomplete.mako | 4 +- .../templates/batch/vendorcatalog/create.mako | 24 ++++ .../templates/deform/autocomplete_jquery.pt | 3 +- tailbone/templates/forms/deform_buefy.mako | 2 + tailbone/templates/vendors/configure.mako | 8 +- tailbone/views/batch/vendorcatalog.py | 114 +++++++++++------- tailbone/views/purchasing/batch.py | 24 ++-- tailbone/views/purchasing/costing.py | 27 +++-- tailbone/views/purchasing/receiving.py | 27 +++-- tailbone/views/vendors/core.py | 4 +- 12 files changed, 164 insertions(+), 93 deletions(-) diff --git a/tailbone/forms/widgets.py b/tailbone/forms/widgets.py index ad9d9c31..3dac0a6a 100644 --- a/tailbone/forms/widgets.py +++ b/tailbone/forms/widgets.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. # @@ -247,6 +247,7 @@ class JQueryAutocompleteWidget(dfwidget.AutocompleteInputWidget): template = 'autocomplete_jquery' requirements = None field_display = "" + assigned_label = None service_url = None cleared_callback = None selected_callback = None @@ -275,6 +276,7 @@ class JQueryAutocompleteWidget(dfwidget.AutocompleteInputWidget): kw['options'] = json.dumps(options) kw['field_display'] = self.field_display kw['cleared_callback'] = self.cleared_callback + kw['assigned_label'] = self.assigned_label kw.setdefault('selected_callback', self.selected_callback) tmpl_values = self.get_template_values(field, cstruct, kw) template = readonly and self.readonly_template or self.template diff --git a/tailbone/static/js/tailbone.buefy.autocomplete.js b/tailbone/static/js/tailbone.buefy.autocomplete.js index 53c41b40..ce0aece9 100644 --- a/tailbone/static/js/tailbone.buefy.autocomplete.js +++ b/tailbone/static/js/tailbone.buefy.autocomplete.js @@ -95,6 +95,22 @@ const TailboneAutocomplete = { } }, + watch: { + // TODO: yikes this feels hacky. what happens is, when the + // caller explicitly assigns a new UUID value to the tailbone + // autocomplate component, the underlying buefy autocomplete + // component was not getting the new value. so here we are + // explicitly making sure it is in sync. this issue was + // discovered on the "new vendor catalog batch" page + value(val) { + this.$nextTick(() => { + if (this.buefyValue != val) { + this.buefyValue = val + } + }) + }, + }, + methods: { // fetch new search results from the server. this is invoked diff --git a/tailbone/templates/autocomplete.mako b/tailbone/templates/autocomplete.mako index 7961d07c..8c84aedd 100644 --- a/tailbone/templates/autocomplete.mako +++ b/tailbone/templates/autocomplete.mako @@ -64,7 +64,7 @@ - {{ getDisplayText() }} (click to change) diff --git a/tailbone/templates/batch/vendorcatalog/create.mako b/tailbone/templates/batch/vendorcatalog/create.mako index 87d65c54..78b5b17d 100644 --- a/tailbone/templates/batch/vendorcatalog/create.mako +++ b/tailbone/templates/batch/vendorcatalog/create.mako @@ -3,6 +3,7 @@ <%def name="extra_javascript()"> ${parent.extra_javascript()} + % if not use_buefy: + % endif +<%def name="modify_this_page_vars()"> + ${parent.modify_this_page_vars()} + + + + ${parent.body()} diff --git a/tailbone/templates/deform/autocomplete_jquery.pt b/tailbone/templates/deform/autocomplete_jquery.pt index 1533cc2b..6e1a1e61 100644 --- a/tailbone/templates/deform/autocomplete_jquery.pt +++ b/tailbone/templates/deform/autocomplete_jquery.pt @@ -110,7 +110,8 @@ + initial-label="${field_display}" + tal:attributes=":assigned-label assigned_label or 'null';"> diff --git a/tailbone/templates/forms/deform_buefy.mako b/tailbone/templates/forms/deform_buefy.mako index 17ccf7d1..a26c946a 100644 --- a/tailbone/templates/forms/deform_buefy.mako +++ b/tailbone/templates/forms/deform_buefy.mako @@ -76,6 +76,8 @@ template: '#${form.component}-template', components: {}, props: {}, + watch: {}, + computed: {}, methods: { ## TODO: deprecate / remove the latter option here diff --git a/tailbone/templates/vendors/configure.mako b/tailbone/templates/vendors/configure.mako index 0bcb4a9e..121617b2 100644 --- a/tailbone/templates/vendors/configure.mako +++ b/tailbone/templates/vendors/configure.mako @@ -6,11 +6,11 @@

    Display

    - - + - Show vendor chooser as autocomplete field + Show vendor chooser as dropdown (select) element diff --git a/tailbone/views/batch/vendorcatalog.py b/tailbone/views/batch/vendorcatalog.py index 3668500a..dfd03ac9 100644 --- a/tailbone/views/batch/vendorcatalog.py +++ b/tailbone/views/batch/vendorcatalog.py @@ -30,15 +30,13 @@ import logging import six -from rattail.db import model, api -from rattail.vendors.catalogs import iter_catalog_parsers +from rattail.db import model import colander from deform import widget as dfwidget from webhelpers2.html import tags from tailbone import forms -from tailbone.db import Session from tailbone.views.batch import FileBatchMasterView from tailbone.diffs import Diff @@ -139,13 +137,9 @@ class VendorCatalogView(FileBatchMasterView): def get_parsers(self): if not hasattr(self, 'parsers'): - parsers = sorted(iter_catalog_parsers(), key=lambda p: p.display) - supported = self.rattail_config.getlist( - 'tailbone', 'batch.vendorcatalog.supported_parsers') - if supported: - parsers = [parser for parser in parsers - if parser.key in supported] - self.parsers = parsers + app = self.get_rattail_app() + vendor_handler = app.get_vendor_handler() + self.parsers = vendor_handler.get_supported_catalog_parsers() return self.parsers def configure_grid(self, g): @@ -160,24 +154,8 @@ class VendorCatalogView(FileBatchMasterView): def configure_form(self, f): super(VendorCatalogView, self).configure_form(f) - - # vendor - f.set_renderer('vendor', self.render_vendor) - if self.creating and 'vendor' in f: - f.replace('vendor', 'vendor_uuid') - f.set_node('vendor_uuid', colander.String()) - vendor_display = "" - if self.request.method == 'POST': - if self.request.POST.get('vendor_uuid'): - vendor = self.Session.query(model.Vendor).get(self.request.POST['vendor_uuid']) - 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)) - f.set_label('vendor_uuid', "Vendor") - else: - f.set_readonly('vendor') + app = self.get_rattail_app() + vendor_handler = app.get_vendor_handler() # filename f.set_label('filename', "Catalog File") @@ -196,12 +174,75 @@ class VendorCatalogView(FileBatchMasterView): f.set_widget('parser_key', dfwidget.SelectWidget(values=values)) f.set_label('parser_key', "File Type") + # vendor + f.set_renderer('vendor', self.render_vendor) + if self.creating and 'vendor' in f: + f.replace('vendor', 'vendor_uuid') + f.set_label('vendor_uuid', "Vendor") + use_dropdown = vendor_handler.choice_uses_dropdown() + if use_dropdown: + vendors = self.Session.query(model.Vendor)\ + .order_by(model.Vendor.id) + vendor_values = [(vendor.uuid, "({}) {}".format(vendor.id, + vendor.name)) + for vendor in vendors] + f.set_widget('vendor_uuid', + dfwidget.SelectWidget(values=vendor_values)) + else: + vendor_display = "" + if self.request.method == 'POST': + if self.request.POST.get('vendor_uuid'): + vendor = self.Session.query(model.Vendor).get( + self.request.POST['vendor_uuid']) + 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')) + else: + f.set_readonly('vendor') + # effective if self.creating: f.remove('effective') else: f.set_readonly('effective') + def template_kwargs_create(self, **kwargs): + use_buefy = self.get_use_buefy() + app = self.get_rattail_app() + vendor_handler = app.get_vendor_handler() + parsers = self.get_parsers() + parsers_data = {} + for parser in parsers: + if use_buefy: + pdata = {'key': parser.key, + 'vendor_key': parser.vendor_key} + if parser.vendor_key: + vendor = vendor_handler.get_vendor(self.Session(), + parser.vendor_key) + if vendor: + pdata['vendor_uuid'] = vendor.uuid + pdata['vendor_name'] = vendor.name + parsers_data[parser.key] = pdata + else: + if parser.vendor_key: + vendor = vendor_handler.get_vendor(self.Session(), + parser.vendor_key) + if vendor: + parser.vendormap_value = "{{uuid: '{}', name: '{}'}}".format( + vendor.uuid, vendor.name.replace("'", "\\'")) + else: + log.warning("vendor '{}' not found for parser: {}".format( + parser.vendor_key, parser.key)) + parser.vendormap_value = 'null' + else: + parser.vendormap_value = 'null' + kwargs['parsers'] = parsers + kwargs['parsers_data'] = parsers_data + return kwargs + def get_batch_kwargs(self, batch): kwargs = super(VendorCatalogView, self).get_batch_kwargs(batch) kwargs['parser_key'] = batch.parser_key @@ -275,23 +316,6 @@ class VendorCatalogView(FileBatchMasterView): return kwargs - def template_kwargs_create(self, **kwargs): - parsers = self.get_parsers() - for parser in parsers: - if parser.vendor_key: - vendor = api.get_vendor(Session(), parser.vendor_key) - if vendor: - parser.vendormap_value = "{{uuid: '{}', name: '{}'}}".format( - vendor.uuid, vendor.name.replace("'", "\\'")) - else: - log.warning("vendor '{}' not found for parser: {}".format( - parser.vendor_key, parser.key)) - parser.vendormap_value = 'null' - else: - parser.vendormap_value = 'null' - kwargs['parsers'] = parsers - return kwargs - # TODO: deprecate / remove this VendorCatalogsView = VendorCatalogView diff --git a/tailbone/views/purchasing/batch.py b/tailbone/views/purchasing/batch.py index a4dab2aa..8a015838 100644 --- a/tailbone/views/purchasing/batch.py +++ b/tailbone/views/purchasing/batch.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. # @@ -29,7 +29,6 @@ from __future__ import unicode_literals, absolute_import import six from rattail.db import model, api -from rattail.time import localtime import colander from deform import widget as dfwidget @@ -230,7 +229,8 @@ class PurchasingBatchView(BatchMasterView): super(PurchasingBatchView, self).configure_form(f) model = self.model batch = f.model_instance - today = localtime(self.rattail_config).date() + app = self.get_rattail_app() + today = app.localtime().date() use_buefy = self.get_use_buefy() # mode @@ -265,9 +265,15 @@ class PurchasingBatchView(BatchMasterView): if self.creating: f.replace('vendor', 'vendor_uuid') f.set_label('vendor_uuid', "Vendor") - use_autocomplete = self.rattail_config.getbool( - 'rattail', 'vendor.use_autocomplete', default=True) - if use_autocomplete: + vendor_handler = app.get_vendor_handler() + use_dropdown = vendor_handler.choice_uses_dropdown() + if use_dropdown: + vendors = self.Session.query(model.Vendor)\ + .order_by(model.Vendor.id) + vendor_values = [(vendor.uuid, "({}) {}".format(vendor.id, vendor.name)) + for vendor in vendors] + f.set_widget('vendor_uuid', dfwidget.SelectWidget(values=vendor_values)) + else: vendor_display = "" if self.request.method == 'POST': if self.request.POST.get('vendor_uuid'): @@ -277,12 +283,6 @@ class PurchasingBatchView(BatchMasterView): vendors_url = self.request.route_url('vendors.autocomplete') f.set_widget('vendor_uuid', forms.widgets.JQueryAutocompleteWidget( field_display=vendor_display, service_url=vendors_url)) - else: - vendors = self.Session.query(model.Vendor)\ - .order_by(model.Vendor.id) - vendor_values = [(vendor.uuid, "({}) {}".format(vendor.id, vendor.name)) - for vendor in vendors] - f.set_widget('vendor_uuid', dfwidget.SelectWidget(values=vendor_values)) elif self.editing: f.set_readonly('vendor') diff --git a/tailbone/views/purchasing/costing.py b/tailbone/views/purchasing/costing.py index d790fbc1..2f467feb 100644 --- a/tailbone/views/purchasing/costing.py +++ b/tailbone/views/purchasing/costing.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. # @@ -200,9 +200,19 @@ class CostingBatchView(PurchasingBatchView): form.set_default('workflow', valid_workflows[0]) # configure vendor field - use_autocomplete = self.rattail_config.getbool( - 'rattail', 'vendor.use_autocomplete', default=True) - if use_autocomplete: + app = self.get_rattail_app() + vendor_handler = app.get_vendor_handler() + use_dropdown = vendor_handler.choice_uses_dropdown() + if use_dropdown: + vendors = self.Session.query(model.Vendor)\ + .order_by(model.Vendor.id) + vendor_values = [(vendor.uuid, "({}) {}".format(vendor.id, vendor.name)) + for vendor in vendors] + if use_buefy: + form.set_widget('vendor', dfwidget.SelectWidget(values=vendor_values)) + else: + form.set_widget('vendor', forms.widgets.JQuerySelectWidget(values=vendor_values)) + else: vendor_display = "" if self.request.method == 'POST': if self.request.POST.get('vendor'): @@ -212,15 +222,6 @@ class CostingBatchView(PurchasingBatchView): vendors_url = self.request.route_url('vendors.autocomplete') form.set_widget('vendor', forms.widgets.JQueryAutocompleteWidget( field_display=vendor_display, service_url=vendors_url)) - else: - vendors = self.Session.query(model.Vendor)\ - .order_by(model.Vendor.id) - vendor_values = [(vendor.uuid, "({}) {}".format(vendor.id, vendor.name)) - for vendor in vendors] - if use_buefy: - form.set_widget('vendor', dfwidget.SelectWidget(values=vendor_values)) - else: - form.set_widget('vendor', forms.widgets.JQuerySelectWidget(values=vendor_values)) # configure workflow field values = [(workflow['workflow_key'], workflow['display']) diff --git a/tailbone/views/purchasing/receiving.py b/tailbone/views/purchasing/receiving.py index 3664cdef..e481db82 100644 --- a/tailbone/views/purchasing/receiving.py +++ b/tailbone/views/purchasing/receiving.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. # @@ -294,9 +294,19 @@ class ReceivingBatchView(PurchasingBatchView): use_buefy=use_buefy) # configure vendor field - use_autocomplete = self.rattail_config.getbool( - 'rattail', 'vendor.use_autocomplete', default=True) - if use_autocomplete: + app = self.get_rattail_app() + vendor_handler = app.get_vendor_handler() + use_dropdown = vendor_handler.choice_uses_dropdown() + if use_dropdown: + vendors = self.Session.query(model.Vendor)\ + .order_by(model.Vendor.id) + vendor_values = [(vendor.uuid, "({}) {}".format(vendor.id, vendor.name)) + for vendor in vendors] + if use_buefy: + form.set_widget('vendor', dfwidget.SelectWidget(values=vendor_values)) + else: + form.set_widget('vendor', forms.widgets.JQuerySelectWidget(values=vendor_values)) + else: vendor_display = "" if self.request.method == 'POST': if self.request.POST.get('vendor'): @@ -306,15 +316,6 @@ class ReceivingBatchView(PurchasingBatchView): vendors_url = self.request.route_url('vendors.autocomplete') form.set_widget('vendor', forms.widgets.JQueryAutocompleteWidget( field_display=vendor_display, service_url=vendors_url)) - else: - vendors = self.Session.query(model.Vendor)\ - .order_by(model.Vendor.id) - vendor_values = [(vendor.uuid, "({}) {}".format(vendor.id, vendor.name)) - for vendor in vendors] - if use_buefy: - form.set_widget('vendor', dfwidget.SelectWidget(values=vendor_values)) - else: - form.set_widget('vendor', forms.widgets.JQuerySelectWidget(values=vendor_values)) # configure workflow field values = [(workflow['workflow_key'], workflow['display']) diff --git a/tailbone/views/vendors/core.py b/tailbone/views/vendors/core.py index bf73e1b1..36280738 100644 --- a/tailbone/views/vendors/core.py +++ b/tailbone/views/vendors/core.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. # @@ -175,7 +175,7 @@ class VendorView(MasterView): # display {'section': 'rattail', - 'option': 'vendor.use_autocomplete', + 'option': 'vendors.choice_uses_dropdown', 'type': bool}, ] From 2ce7c93aeb79c92ffb855b1c5ae848e9ddb7263a Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 8 Jan 2022 12:19:35 -0600 Subject: [PATCH 0015/1119] Expose, honor "allow future" setting for vendor catalog batch --- .../batch/vendorcatalog/configure.mako | 13 ++++++ tailbone/views/batch/vendorcatalog.py | 46 +++++++++++++++---- 2 files changed, 51 insertions(+), 8 deletions(-) diff --git a/tailbone/templates/batch/vendorcatalog/configure.mako b/tailbone/templates/batch/vendorcatalog/configure.mako index e4fa346a..1e6309f4 100644 --- a/tailbone/templates/batch/vendorcatalog/configure.mako +++ b/tailbone/templates/batch/vendorcatalog/configure.mako @@ -3,6 +3,19 @@ <%def name="form_content()"> ${self.input_file_templates_section()} + +

    Options

    +
    + + + + Allow "future" cost changes + + + +
    diff --git a/tailbone/views/batch/vendorcatalog.py b/tailbone/views/batch/vendorcatalog.py index dfd03ac9..733f14b5 100644 --- a/tailbone/views/batch/vendorcatalog.py +++ b/tailbone/views/batch/vendorcatalog.py @@ -54,13 +54,15 @@ class VendorCatalogView(FileBatchMasterView): route_prefix = 'vendorcatalogs' url_prefix = '/vendors/catalogs' template_prefix = '/batch/vendorcatalog' - editable = False + bulk_deletable = True + results_executable = True rows_bulk_deletable = True has_input_file_templates = True configurable = True labels = { 'vendor_id': "Vendor ID", + 'parser_key': "Parser", } grid_columns = [ @@ -172,7 +174,9 @@ class VendorCatalogView(FileBatchMasterView): if not use_buefy: values.insert(0, ('', "(please choose)")) f.set_widget('parser_key', dfwidget.SelectWidget(values=values)) - f.set_label('parser_key', "File Type") + else: + f.set_readonly('parser_key') + f.set_renderer('parser_key', self.render_parser_key) # vendor f.set_renderer('vendor', self.render_vendor) @@ -203,11 +207,23 @@ class VendorCatalogView(FileBatchMasterView): else: f.set_readonly('vendor') - # effective - if self.creating: - f.remove('effective') - else: - f.set_readonly('effective') + if self.batch_handler.allow_future(): + + # effective + f.set_type('effective', 'date_jquery') + + else: # future not allowed + f.remove('future', + 'effective') + + def render_parser_key(self, batch, field): + key = getattr(batch, field) + if not key: + return + app = self.get_rattail_app() + vendor_handler = app.get_vendor_handler() + parser = vendor_handler.get_catalog_parser(key) + return parser.display def template_kwargs_create(self, **kwargs): use_buefy = self.get_use_buefy() @@ -254,7 +270,9 @@ class VendorCatalogView(FileBatchMasterView): kwargs['vendor_id'] = batch.vendor_id if batch.vendor_name: kwargs['vendor_name'] = batch.vendor_name - kwargs['future'] = batch.future + if self.batch_handler.allow_future(): + kwargs['future'] = batch.future + kwargs['effective'] = batch.effective return kwargs def configure_row_grid(self, g): @@ -316,6 +334,18 @@ class VendorCatalogView(FileBatchMasterView): return kwargs + def configure_get_simple_settings(self): + settings = super(VendorCatalogView, self).configure_get_simple_settings() or [] + settings.extend([ + + # key field + {'section': 'rattail.batch', + 'option': 'vendor_catalog.allow_future', + 'type': bool}, + + ]) + return settings + # TODO: deprecate / remove this VendorCatalogsView = VendorCatalogView From dc28b1337d1bffc83d074ffb381e1d70c80ab3dd Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 8 Jan 2022 13:35:59 -0600 Subject: [PATCH 0016/1119] Add config for supported vendor catalog parsers also explicitly set "native value" for all configuration checkbox fields, since apparently it will send `'false'` by default... --- .../batch/vendorcatalog/configure.mako | 30 +++++++++++++ tailbone/templates/custorders/configure.mako | 5 +++ tailbone/templates/products/configure.mako | 2 + tailbone/templates/receiving/configure.mako | 10 +++++ .../reports/generated/configure.mako | 1 + .../templates/settings/email/configure.mako | 1 + tailbone/templates/vendors/configure.mako | 1 + tailbone/views/batch/vendorcatalog.py | 42 +++++++++++++++++++ 8 files changed, 92 insertions(+) diff --git a/tailbone/templates/batch/vendorcatalog/configure.mako b/tailbone/templates/batch/vendorcatalog/configure.mako index 1e6309f4..0d57053e 100644 --- a/tailbone/templates/batch/vendorcatalog/configure.mako +++ b/tailbone/templates/batch/vendorcatalog/configure.mako @@ -10,12 +10,42 @@ Allow "future" cost changes
    + +

    Catalog Parsers

    +
    + +

    + Only the selected parsers will be exposed to users. +

    + + % for Parser in catalog_parsers: + + + ${Parser.display} + + + % endfor + +
    + + +<%def name="modify_this_page_vars()"> + ${parent.modify_this_page_vars()} + diff --git a/tailbone/templates/custorders/configure.mako b/tailbone/templates/custorders/configure.mako index e3e47054..976f1564 100644 --- a/tailbone/templates/custorders/configure.mako +++ b/tailbone/templates/custorders/configure.mako @@ -9,6 +9,7 @@ Require a Customer account @@ -17,6 +18,7 @@ Allow user to choose contact info @@ -25,6 +27,7 @@ Allow user to enter new contact info @@ -52,6 +55,7 @@ Allow creating orders for "unknown" products @@ -60,6 +64,7 @@ Allow prices to be flagged as "questionable" diff --git a/tailbone/templates/products/configure.mako b/tailbone/templates/products/configure.mako index 31b879c5..3b75bc7f 100644 --- a/tailbone/templates/products/configure.mako +++ b/tailbone/templates/products/configure.mako @@ -35,6 +35,7 @@ Auto-convert Type 2 UPC for sake of lookup @@ -48,6 +49,7 @@ Show "POD" Images as fallback diff --git a/tailbone/templates/receiving/configure.mako b/tailbone/templates/receiving/configure.mako index 06ab3769..349dc621 100644 --- a/tailbone/templates/receiving/configure.mako +++ b/tailbone/templates/receiving/configure.mako @@ -9,6 +9,7 @@ From Scratch @@ -17,6 +18,7 @@ From Invoice @@ -25,6 +27,7 @@ From Purchase Order @@ -33,6 +36,7 @@ From Purchase Order, with Invoice @@ -41,6 +45,7 @@ Truck Dump @@ -54,6 +59,7 @@ Allow Cases @@ -62,6 +68,7 @@ Allow "Expired" Credits @@ -75,6 +82,7 @@ Show Product Images @@ -83,6 +91,7 @@ Allow "Quick Receive" @@ -91,6 +100,7 @@ Allow "Quick Receive All" diff --git a/tailbone/templates/reports/generated/configure.mako b/tailbone/templates/reports/generated/configure.mako index e8224f28..50109702 100644 --- a/tailbone/templates/reports/generated/configure.mako +++ b/tailbone/templates/reports/generated/configure.mako @@ -9,6 +9,7 @@ Show report chooser as form, with dropdown diff --git a/tailbone/templates/settings/email/configure.mako b/tailbone/templates/settings/email/configure.mako index 228eb1a4..1e2e86a0 100644 --- a/tailbone/templates/settings/email/configure.mako +++ b/tailbone/templates/settings/email/configure.mako @@ -9,6 +9,7 @@ Make record of all attempts to send email diff --git a/tailbone/templates/vendors/configure.mako b/tailbone/templates/vendors/configure.mako index 121617b2..cb370e43 100644 --- a/tailbone/templates/vendors/configure.mako +++ b/tailbone/templates/vendors/configure.mako @@ -9,6 +9,7 @@ Show vendor chooser as dropdown (select) element diff --git a/tailbone/views/batch/vendorcatalog.py b/tailbone/views/batch/vendorcatalog.py index 733f14b5..80fde3fe 100644 --- a/tailbone/views/batch/vendorcatalog.py +++ b/tailbone/views/batch/vendorcatalog.py @@ -39,6 +39,7 @@ from webhelpers2.html import tags from tailbone import forms from tailbone.views.batch import FileBatchMasterView from tailbone.diffs import Diff +from tailbone.db import Session log = logging.getLogger(__name__) @@ -346,6 +347,47 @@ class VendorCatalogView(FileBatchMasterView): ]) return settings + def configure_get_context(self): + context = super(VendorCatalogView, self).configure_get_context() + app = self.get_rattail_app() + vendor_handler = app.get_vendor_handler() + + Parsers = vendor_handler.get_all_catalog_parsers() + Supported = vendor_handler.get_supported_catalog_parsers() + context['catalog_parsers'] = Parsers + context['catalog_parsers_data'] = dict([(Parser.key, Parser in Supported) + for Parser in Parsers]) + + return context + + def configure_gather_settings(self, data): + settings = super(VendorCatalogView, self).configure_gather_settings(data) + app = self.get_rattail_app() + vendor_handler = app.get_vendor_handler() + + supported = [] + for Parser in vendor_handler.get_all_catalog_parsers(): + name = 'catalog_parser_{}'.format(Parser.key) + if data.get(name) == 'true': + supported.append(Parser.key) + settings.append({'name': 'rattail.vendors.supported_catalog_parsers', + 'value': ', '.join(supported)}) + + return settings + + def configure_remove_settings(self): + super(VendorCatalogView, self).configure_remove_settings() + model = self.model + names = [ + 'rattail.vendors.supported_catalog_parsers', + 'tailbone.batch.vendorcatalog.supported_parsers', # deprecated + ] + + Session().query(model.Setting)\ + .filter(model.Setting.name.in_(names))\ + .delete(synchronize_session=False) + + # TODO: deprecate / remove this VendorCatalogsView = VendorCatalogView From 6af5157b4eb1537cd25a806bac22db6be0d75279 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 8 Jan 2022 19:48:14 -0600 Subject: [PATCH 0017/1119] Update some method calls to avoid deprecation warnings --- tailbone/views/custorders/orders.py | 7 ++++--- tailbone/views/employees.py | 4 ++-- tailbone/views/master.py | 2 +- tailbone/views/purchases/core.py | 10 +++++----- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/tailbone/views/custorders/orders.py b/tailbone/views/custorders/orders.py index c60e859e..12a0c339 100644 --- a/tailbone/views/custorders/orders.py +++ b/tailbone/views/custorders/orders.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. # @@ -277,8 +277,9 @@ class CustomerOrderView(MasterView): return text def get_batch_handler(self): - return get_batch_handler( - self.rattail_config, 'custorder', + app = self.get_rattail_app() + return app.get_batch_handler( + 'custorder', default='rattail.batch.custorder:CustomerOrderBatchHandler') def create(self, form=None, template='create'): diff --git a/tailbone/views/employees.py b/tailbone/views/employees.py index febe521e..c1f4b01c 100644 --- a/tailbone/views/employees.py +++ b/tailbone/views/employees.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. # @@ -112,7 +112,7 @@ class EmployeeView(MasterView): g.set_sorter('username', model.User.username) g.set_renderer('username', self.grid_render_username) else: - g.hide_column('username') + g.remove('username') # id if self.has_perm('edit'): diff --git a/tailbone/views/master.py b/tailbone/views/master.py index 3807408b..7eb9ebc8 100644 --- a/tailbone/views/master.py +++ b/tailbone/views/master.py @@ -455,7 +455,7 @@ class MasterView(View): # hide "local only" grid filter, unless global access allowed if self.secure_global_objects: if not self.has_perm('view_global'): - grid.hide_column('local_only') + grid.remove('local_only') grid.remove_filter('local_only') self.configure_column_product_key(grid) diff --git a/tailbone/views/purchases/core.py b/tailbone/views/purchases/core.py index 2cd28be8..eb32fa73 100644 --- a/tailbone/views/purchases/core.py +++ b/tailbone/views/purchases/core.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. # @@ -311,12 +311,12 @@ class PurchaseView(MasterView): purchase = self.get_instance() if purchase.status == self.enum.PURCHASE_STATUS_ORDERED: - g.hide_column('cases_received') - g.hide_column('units_received') - g.hide_column('invoice_total') + g.remove('cases_received', + 'units_received', + 'invoice_total') elif purchase.status in (self.enum.PURCHASE_STATUS_RECEIVED, self.enum.PURCHASE_STATUS_COSTED): - g.hide_column('po_total') + g.remove('po_total') def configure_row_form(self, f): super(PurchaseView, self).configure_row_form(f) From 94fc5c18593b0e7bbdfd65f64a34e690704a85d9 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 8 Jan 2022 20:08:32 -0600 Subject: [PATCH 0018/1119] Update changelog --- CHANGES.rst | 16 ++++++++++++++++ tailbone/_version.py | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 10c39d54..8216c8ad 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,22 @@ CHANGELOG ========= +0.8.192 (2022-01-08) +-------------------- + +* Add configurable template file for vendor catalog batch. + +* Some aesthetic improvements for vendor catalog batch. + +* Several disparate changes needed for vendor catalog improvements. + +* Expose, honor "allow future" setting for vendor catalog batch. + +* Add config for supported vendor catalog parsers. + +* Update some method calls to avoid deprecation warnings. + + 0.8.191 (2022-01-03) -------------------- diff --git a/tailbone/_version.py b/tailbone/_version.py index b6faadaa..4a1fd792 100644 --- a/tailbone/_version.py +++ b/tailbone/_version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8; -*- -__version__ = '0.8.191' +__version__ = '0.8.192' From 0545099a2b439e39067a680b466ac6455bed3c70 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 9 Jan 2022 15:20:35 -0600 Subject: [PATCH 0019/1119] Add buefy support for quick-printing product labels; also speed bump --- tailbone/grids/core.py | 6 +- tailbone/templates/formposter.mako | 3 +- tailbone/templates/master/index.mako | 3 - tailbone/templates/products/configure.mako | 31 +++++-- tailbone/templates/products/index.mako | 97 +++++++++++++++++++++- tailbone/views/master.py | 2 + tailbone/views/products.py | 61 +++++++++----- 7 files changed, 167 insertions(+), 36 deletions(-) diff --git a/tailbone/grids/core.py b/tailbone/grids/core.py index f918fad4..d825e4b4 100644 --- a/tailbone/grids/core.py +++ b/tailbone/grids/core.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. # @@ -197,7 +197,7 @@ class Grid(object): """ Mark the given column as "invisible" (but do not remove it). - Use :meth:`hide_column()` if you actually want to remove it. + Use :meth:`remove()` if you actually want to remove it. """ if invisible: if key not in self.invisible: @@ -217,7 +217,7 @@ class Grid(object): def replace(self, oldfield, newfield): self.insert_after(oldfield, newfield) - self.hide_column(oldfield) + self.remove(oldfield) def set_joiner(self, key, joiner): if joiner is None: diff --git a/tailbone/templates/formposter.mako b/tailbone/templates/formposter.mako index 6fc6eadc..885ac6c2 100644 --- a/tailbone/templates/formposter.mako +++ b/tailbone/templates/formposter.mako @@ -21,7 +21,8 @@ } else { this.$buefy.toast.open({ - message: "Submit failed: " + response.data.error, + message: "Submit failed: " + (response.data.error || + "(unknown error)"), type: 'is-danger', duration: 4000, // 4 seconds }) diff --git a/tailbone/templates/master/index.mako b/tailbone/templates/master/index.mako index 48e51286..5830519b 100644 --- a/tailbone/templates/master/index.mako +++ b/tailbone/templates/master/index.mako @@ -162,9 +162,6 @@
  • ${h.link_to("Create a new {}".format(model_title), url('{}.create'.format(route_prefix)))}
  • % endif % endif - % if not use_buefy and master.configurable and master.has_perm('configure'): -
  • ${h.link_to("Configure {}".format(config_title), url('{}.configure'.format(route_prefix)))}
  • - % endif % if master.has_input_file_templates and master.has_perm('create'): % for template in six.itervalues(input_file_templates):
  • ${h.link_to("Download {} Template".format(template['label']), template['effective_url'])}
  • diff --git a/tailbone/templates/products/configure.mako b/tailbone/templates/products/configure.mako index 3b75bc7f..612b8d36 100644 --- a/tailbone/templates/products/configure.mako +++ b/tailbone/templates/products/configure.mako @@ -3,7 +3,7 @@ <%def name="form_content()"> -

    Key Field

    +

    Display

    @@ -27,6 +27,15 @@ + + + Show "POD" Images as fallback + + +

    Handling

    @@ -43,18 +52,28 @@ -

    Display

    +

    Labels

    - - + - Show "POD" Images as fallback + Allow quick/direct label printing from Products page + + + + +
    diff --git a/tailbone/templates/products/index.mako b/tailbone/templates/products/index.mako index 3f65cd68..8eada2fc 100644 --- a/tailbone/templates/products/index.mako +++ b/tailbone/templates/products/index.mako @@ -1,8 +1,9 @@ -## -*- coding: utf-8 -*- +## -*- coding: utf-8; -*- <%inherit file="/master/index.mako" /> <%def name="extra_styles()"> ${parent.extra_styles()} + % if not use_buefy: + % endif <%def name="extra_javascript()"> ${parent.extra_javascript()} - % if label_profiles and request.has_perm('products.print_labels'): + % if not use_buefy and label_profiles and master.has_perm('print_labels'): + % endif + + + ${parent.body()} diff --git a/tailbone/views/master.py b/tailbone/views/master.py index 7eb9ebc8..7fd3cccf 100644 --- a/tailbone/views/master.py +++ b/tailbone/views/master.py @@ -4368,6 +4368,8 @@ class MasterView(View): if simple.get('type') is bool: value = six.text_type(bool(value)).lower() + elif simple.get('type') is int: + value = six.text_type(int(value or '0')) else: value = six.text_type(value) diff --git a/tailbone/views/products.py b/tailbone/views/products.py index 0e192bca..cf7be401 100644 --- a/tailbone/views/products.py +++ b/tailbone/views/products.py @@ -181,7 +181,9 @@ class ProductView(MasterView): self.print_labels = request.rattail_config.getbool('tailbone', 'products.print_labels', default=False) app = self.get_rattail_app() - self.handler = app.get_products_handler() + self.product_handler = app.get_products_handler() + # TODO: deprecate / remove this + self.handler = self.product_handler def query(self, session): user = self.request.user @@ -358,8 +360,13 @@ class ProductView(MasterView): g.set_sort_defaults('upc') - if self.print_labels and self.request.has_perm('products.print_labels'): - g.more_actions.append(grids.GridAction('print_label', icon='print')) + if self.print_labels and self.has_perm('print_labels'): + if use_buefy: + g.more_actions.append(self.make_action( + 'print_label', icon='print', url='#', + click_handler='quickLabelPrint(props.row)')) + else: + g.more_actions.append(grids.GridAction('print_label', icon='print')) g.set_type('upc', 'gpc') @@ -522,7 +529,7 @@ class ProductView(MasterView): if not product.not_for_sale: price = product[field] if price: - return self.handler.render_price(price) + return self.product_handler.render_price(price) def render_current_price_for_grid(self, product, field): text = self.render_price(product, field) or "" @@ -651,13 +658,20 @@ class ProductView(MasterView): return pretty_quantity(inventory.on_order) def template_kwargs_index(self, **kwargs): - if self.print_labels: - kwargs['label_profiles'] = Session.query(model.LabelProfile)\ - .filter(model.LabelProfile.visible == True)\ - .order_by(model.LabelProfile.ordinal)\ - .all() - return kwargs + kwargs = super(ProductView, self).template_kwargs_index(**kwargs) + model = self.model + if self.print_labels: + + kwargs['label_profiles'] = self.Session.query(model.LabelProfile)\ + .filter(model.LabelProfile.visible == True)\ + .order_by(model.LabelProfile.ordinal)\ + .all() + + kwargs['quick_label_speedbump_threshold'] = self.rattail_config.getint( + 'tailbone', 'products.quick_labels.speedbump_threshold') + + return kwargs def grid_extra_class(self, product, i): classes = [] @@ -794,10 +808,10 @@ class ProductView(MasterView): def get_instance(self): key = self.request.matchdict['uuid'] - product = Session.query(model.Product).get(key) + product = self.Session.query(model.Product).get(key) if product: return product - price = Session.query(model.ProductPrice).get(key) + price = self.Session.query(model.ProductPrice).get(key) if price: return price.product raise httpexceptions.HTTPNotFound() @@ -1151,7 +1165,7 @@ class ProductView(MasterView): product = kwargs['instance'] use_buefy = self.get_use_buefy() - kwargs['image_url'] = self.handler.get_image_url(product) + kwargs['image_url'] = self.product_handler.get_image_url(product) kwargs['product_key_field'] = self.rattail_config.product_key() # add price history, if user has access @@ -1701,11 +1715,11 @@ class ProductView(MasterView): upc = self.request.GET.get('upc', '').strip() upc = re.sub(r'\D', '', upc) if upc: - product = api.get_product_by_upc(Session(), upc) + product = api.get_product_by_upc(self.Session(), upc) if not product: # Try again, assuming caller did not include check digit. upc = GPC(upc, calc_check_digit='upc') - product = api.get_product_by_upc(Session(), upc) + product = api.get_product_by_upc(self.Session(), upc) if product and (not product.deleted or self.request.has_perm('products.view_deleted')): data = { 'uuid': product.uuid, @@ -1716,7 +1730,7 @@ class ProductView(MasterView): } uuid = self.request.GET.get('with_vendor_cost') if uuid: - vendor = Session.query(model.Vendor).get(uuid) + vendor = self.Session.query(model.Vendor).get(uuid) if not vendor: return {'error': "Vendor not found"} cost = product.cost_for_vendor(vendor) @@ -1912,21 +1926,28 @@ class ProductView(MasterView): def configure_get_simple_settings(self): return [ - # key field + # display {'section': 'rattail', 'option': 'product.key'}, {'section': 'rattail', 'option': 'product.key_title'}, + {'section': 'tailbone', + 'option': 'products.show_pod_image', + 'type': bool}, # handling {'section': 'rattail', 'option': 'products.convert_type2_for_gpc_lookup', 'type': bool}, - # display + # labels {'section': 'tailbone', - 'option': 'products.show_pod_image', + 'option': 'products.print_labels', 'type': bool}, + {'section': 'tailbone', + 'option': 'products.quick_labels.speedbump_threshold', + 'type': int}, + ] @classmethod @@ -2254,7 +2275,7 @@ def print_labels(request): except Exception as error: log.warning("error occurred while printing labels", exc_info=True) return {'error': six.text_type(error)} - return {} + return {'ok': True} def includeme(config): From 8579b890028e75a18d40802a9e7d44f0f229a0f6 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 9 Jan 2022 18:13:12 -0600 Subject: [PATCH 0020/1119] Add way to set form-wide schema validator was needed to enforce rule where one field is required only in some cases, depending on value of another field --- tailbone/forms/core.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/tailbone/forms/core.py b/tailbone/forms/core.py index f194e53e..17921c72 100644 --- a/tailbone/forms/core.py +++ b/tailbone/forms/core.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. # @@ -542,7 +542,10 @@ class Form(object): # apply any validators for key, validator in self.validators.items(): - if key in schema: + if key is None: + # this one is form-wide + schema.validator = validator + elif key in schema: schema[key].validator = validator # apply required flags @@ -671,6 +674,17 @@ class Form(object): self.schema[key].widget = widget def set_validator(self, key, validator): + """ + Set the validator for the schema node represented by the given + key. + + :param key: Normally this the name of one of the fields + contained in the form. It can also be ``None`` in which + case the validator pertains to the form at large instead of + one of the fields. + + :param validator: Callable validator for the node. + """ self.validators[key] = validator def set_required(self, key, required=True): From cabe4225080cdaaa7a20b9a434ded1cdd0b8fd0f Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 9 Jan 2022 19:25:18 -0600 Subject: [PATCH 0021/1119] Add progress support when deleting a batch b/c we must delete all rows individually, and some batches can be several thousand rows each --- tailbone/views/batch/core.py | 52 ++++++++++++++++++++++++++++++++++-- tailbone/views/master.py | 12 ++++++--- 2 files changed, 58 insertions(+), 6 deletions(-) diff --git a/tailbone/views/batch/core.py b/tailbone/views/batch/core.py index 36ad341b..88921f13 100644 --- a/tailbone/views/batch/core.py +++ b/tailbone/views/batch/core.py @@ -82,6 +82,7 @@ class BatchMasterView(MasterView): results_executable = False has_worksheet = False has_worksheet_file = False + delete_requires_progress = True input_file_template_config_section = 'rattail.batch' @@ -742,8 +743,55 @@ class BatchMasterView(MasterView): """ Delete all data (files etc.) for the batch. """ - self.handler.do_delete(batch) - super(BatchMasterView, self).delete_instance(batch) + app = self.get_rattail_app() + session = app.get_session(batch) + self.batch_handler.do_delete(batch) + session.flush() + + def delete_instance_with_progress(self, batch): + """ + Delete all data (files etc.) for the batch. + """ + return self.handler_action(batch, 'delete') + + def delete_thread(self, key, user_uuid, progress, **kwargs): + """ + Thread target for deleting a batch with progress indicator. + """ + app = self.get_rattail_app() + model = self.model + # nb. must make new session, separate from main thread + session = app.make_session() + batch = self.get_instance_for_key(key, session) + batch_str = six.text_type(batch) + + try: + # try to delete batch + self.handler.do_delete(batch, progress=progress, **kwargs) + + except Exception as error: + # error; log that and rollback + log.exception("delete failed for batch: %s", batch) + session.rollback() + session.close() + if progress: + progress.session.load() + progress.session['error'] = True + progress.session['error_msg'] = "Batch deletion failed: {}".format( + simple_error(error)) + progress.session.save() + + else: + # no error; finish up + session.commit() + session.close() + if progress: + progress.session.load() + progress.session['complete'] = True + progress.session['success_url'] = self.get_index_url() + progress.session['success_msg'] = "Batch has been deleted: {}".format( + batch_str) + progress.session.save() def get_fallback_templates(self, template, **kwargs): return [ diff --git a/tailbone/views/master.py b/tailbone/views/master.py index 7fd3cccf..3b7bed2a 100644 --- a/tailbone/views/master.py +++ b/tailbone/views/master.py @@ -100,6 +100,7 @@ class MasterView(View): viewable = True editable = True deletable = True + delete_requires_progress = False delete_confirm = 'full' bulk_deletable = False set_deletable = False @@ -1590,10 +1591,13 @@ class MasterView(View): if isinstance(result, httpexceptions.HTTPException): return result - self.delete_instance(instance) - self.request.session.flash("{} has been deleted: {}".format( - self.get_model_title(), instance_title)) - return self.redirect(self.get_after_delete_url(instance)) + if self.delete_requires_progress: + return self.delete_instance_with_progress(instance) + else: + self.delete_instance(instance) + self.request.session.flash("{} has been deleted: {}".format( + self.get_model_title(), instance_title)) + return self.redirect(self.get_after_delete_url(instance)) form.readonly = True return self.render_to_response('delete', { From eb221417e567312799e67fc96c00b168006d107d Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 10 Jan 2022 14:54:49 -0600 Subject: [PATCH 0022/1119] Expose the Sale, TPR, Current price fields for label batch still need to figure out how execution can print e.g. TPR prices... --- tailbone/views/batch/labels.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/tailbone/views/batch/labels.py b/tailbone/views/batch/labels.py index c52a5a67..79b14a76 100644 --- a/tailbone/views/batch/labels.py +++ b/tailbone/views/batch/labels.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. # @@ -84,6 +84,11 @@ class LabelBatchView(BatchMasterView): 'upc': "UPC", 'vendor_id': "Vendor ID", 'label_profile': "Label Type", + 'sale_start': "Sale Starts", + 'sale_stop': "Sale Ends", + 'tpr_price': "TPR Price", + 'tpr_starts': "TPR Starts", + 'tpr_ends': "TPR Ends", } row_form_fields = [ @@ -101,6 +106,12 @@ class LabelBatchView(BatchMasterView): 'sale_price', 'sale_start', 'sale_stop', + 'tpr_price', + 'tpr_starts', + 'tpr_ends', + 'current_price', + 'current_starts', + 'current_ends', 'vendor_id', 'vendor_name', 'vendor_item_code', From 9045505153cf0e8d98d872bd65ccd685453f532c Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 10 Jan 2022 16:34:06 -0600 Subject: [PATCH 0023/1119] Update changelog --- CHANGES.rst | 12 ++++++++++++ tailbone/_version.py | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 8216c8ad..f8e1196d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,18 @@ CHANGELOG ========= +0.8.193 (2022-01-10) +-------------------- + +* Add buefy support for quick-printing product labels; also speed bump. + +* Add way to set form-wide schema validator. + +* Add progress support when deleting a batch. + +* Expose the Sale, TPR, Current price fields for label batch. + + 0.8.192 (2022-01-08) -------------------- diff --git a/tailbone/_version.py b/tailbone/_version.py index 4a1fd792..9d4b02b3 100644 --- a/tailbone/_version.py +++ b/tailbone/_version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8; -*- -__version__ = '0.8.192' +__version__ = '0.8.193' From 9eeb921915534c29953eb32d0e8b9cafbf8ed217 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Wed, 12 Jan 2022 18:20:01 -0600 Subject: [PATCH 0024/1119] Include all static files in manifest --- MANIFEST.in | 2 ++ 1 file changed, 2 insertions(+) diff --git a/MANIFEST.in b/MANIFEST.in index 0114904a..a3d57f93 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -11,6 +11,8 @@ recursive-include tailbone/static *.jpg recursive-include tailbone/static *.gif recursive-include tailbone/static *.ico +recursive-include tailbone/static/files * + recursive-include tailbone/templates *.mako recursive-include tailbone/templates *.pt recursive-include tailbone/reports *.mako From 765b7b49577a2bccb58448ccd338717e4926946d Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Wed, 12 Jan 2022 18:20:25 -0600 Subject: [PATCH 0025/1119] Update usage of `app.get_email_handler()` to avoid warnings --- tailbone/views/email.py | 6 +++--- tailbone/views/reports.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tailbone/views/email.py b/tailbone/views/email.py index 7b46f490..42f05c90 100644 --- a/tailbone/views/email.py +++ b/tailbone/views/email.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. # @@ -85,7 +85,7 @@ class EmailSettingView(MasterView): def get_handler(self): app = self.get_rattail_app() - return app.get_mail_handler() + return app.get_email_handler() def get_data(self, session=None): data = [] @@ -292,7 +292,7 @@ class EmailPreview(View): def get_handler(self): app = self.get_rattail_app() - return app.get_mail_handler() + return app.get_email_handler() def __call__(self): diff --git a/tailbone/views/reports.py b/tailbone/views/reports.py index e2aa3db6..2839c5b5 100644 --- a/tailbone/views/reports.py +++ b/tailbone/views/reports.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. # @@ -585,7 +585,7 @@ class ProblemReportView(MasterView): } app = self.get_rattail_app() - handler = app.get_mail_handler() + handler = app.get_email_handler() email = handler.get_email(data['email_key']) data['email_recipients'] = email.get_recips('all') From 0b25469f33f74050f5128313c571155bfd09e854 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Wed, 12 Jan 2022 18:24:27 -0600 Subject: [PATCH 0026/1119] 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 f8e1196d..496639e9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,14 @@ CHANGELOG ========= +0.8.194 (2022-01-12) +-------------------- + +* Include all static files in manifest. + +* Update usage of ``app.get_email_handler()`` to avoid warnings. + + 0.8.193 (2022-01-10) -------------------- diff --git a/tailbone/_version.py b/tailbone/_version.py index 9d4b02b3..0a66a320 100644 --- a/tailbone/_version.py +++ b/tailbone/_version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8; -*- -__version__ = '0.8.193' +__version__ = '0.8.194' From e672e9670fb52aad9d6da2c7580880b2deb3620c Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 13 Jan 2022 14:21:40 -0600 Subject: [PATCH 0027/1119] Strip whitespace for new customer fields, in new custorder page --- tailbone/templates/custorders/create.mako | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tailbone/templates/custorders/create.mako b/tailbone/templates/custorders/create.mako index db9af7ec..c3aed270 100644 --- a/tailbone/templates/custorders/create.mako +++ b/tailbone/templates/custorders/create.mako @@ -415,21 +415,21 @@ From 517dd4ad9ed34b53550b4b69b228b0c784a54ace Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 13 Jan 2022 14:36:04 -0600 Subject: [PATCH 0028/1119] 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 496639e9..1910050e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,12 @@ CHANGELOG ========= +0.8.195 (2022-01-13) +-------------------- + +* Strip whitespace for new customer fields, in new custorder page. + + 0.8.194 (2022-01-12) -------------------- diff --git a/tailbone/_version.py b/tailbone/_version.py index 0a66a320..27909901 100644 --- a/tailbone/_version.py +++ b/tailbone/_version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8; -*- -__version__ = '0.8.194' +__version__ = '0.8.195' From fe7612c885ab88817cabbc2e1a00ba483abab214 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 13 Jan 2022 21:25:17 -0600 Subject: [PATCH 0029/1119] Use the new label handler also, move "print one-off labels" logic into product master view --- tailbone/views/labels/profiles.py | 22 +++++--- tailbone/views/products.py | 84 +++++++++++++++++-------------- 2 files changed, 59 insertions(+), 47 deletions(-) diff --git a/tailbone/views/labels/profiles.py b/tailbone/views/labels/profiles.py index 3dfe07ab..a91cdfb2 100644 --- a/tailbone/views/labels/profiles.py +++ b/tailbone/views/labels/profiles.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. # @@ -62,6 +62,11 @@ class LabelProfileView(MasterView): 'sync_me', ] + def __init__(self, request): + super(LabelProfileView, self).__init__(request) + app = self.get_rattail_app() + self.label_handler = app.get_label_handler() + def configure_grid(self, g): super(LabelProfileView, self).configure_grid(g) g.set_sort_defaults('ordinal') @@ -80,7 +85,7 @@ class LabelProfileView(MasterView): def after_edit(self, profile): if not profile.format: - formatter = profile.get_formatter(self.rattail_config) + formatter = self.label_handler.get_formatter(profile) if formatter: try: profile.format = formatter.default_format @@ -122,17 +127,17 @@ class LabelProfileView(MasterView): View for editing extended Printer Settings, for a given Label Profile. """ profile = self.get_instance() - read_profile = self.redirect(self.get_action_url('view', profile)) + redirect = self.redirect(self.get_action_url('view', profile)) - printer = profile.get_printer(self.rattail_config) + printer = self.label_handler.get_printer(profile) if not printer: msg = "Label profile \"{}\" does not have a functional printer spec.".format(profile) self.request.session.flash(msg) - return read_profile + return redirect if not printer.required_settings: msg = "Printer class for label profile \"{}\" does not require any settings.".format(profile) self.request.session.flash(msg) - return read_profile + return redirect form = self.make_printer_settings_form(profile, printer) @@ -140,8 +145,9 @@ class LabelProfileView(MasterView): if self.request.method == 'POST': for setting in printer.required_settings: if setting in self.request.POST: - profile.save_printer_setting(setting, self.request.POST[setting]) - return read_profile + self.label_handler.save_printer_setting( + profile, setting, self.request.POST[setting]) + return redirect return self.render_to_response('printer', { 'form': form, diff --git a/tailbone/views/products.py b/tailbone/views/products.py index cf7be401..57aeb8c7 100644 --- a/tailbone/views/products.py +++ b/tailbone/views/products.py @@ -49,7 +49,6 @@ from pyramid import httpexceptions from webhelpers2.html import tags, HTML from tailbone import forms, grids -from tailbone.db import Session from tailbone.views import MasterView from tailbone.util import raw_datetime @@ -178,7 +177,8 @@ class ProductView(MasterView): def __init__(self, request): super(ProductView, self).__init__(request) - self.print_labels = request.rattail_config.getbool('tailbone', 'products.print_labels', default=False) + self.expose_label_printing = self.rattail_config.getbool( + 'tailbone', 'products.print_labels', default=False) app = self.get_rattail_app() self.product_handler = app.get_products_handler() @@ -360,7 +360,7 @@ class ProductView(MasterView): g.set_sort_defaults('upc') - if self.print_labels and self.has_perm('print_labels'): + if self.expose_label_printing and self.has_perm('print_labels'): if use_buefy: g.more_actions.append(self.make_action( 'print_label', icon='print', url='#', @@ -661,7 +661,7 @@ class ProductView(MasterView): kwargs = super(ProductView, self).template_kwargs_index(**kwargs) model = self.model - if self.print_labels: + if self.expose_label_printing: kwargs['label_profiles'] = self.Session.query(model.LabelProfile)\ .filter(model.LabelProfile.visible == True)\ @@ -1704,6 +1704,37 @@ class ProductView(MasterView): self.request.response.body = product.image.bytes return self.request.response + def print_labels(self): + app = self.get_rattail_app() + label_handler = app.get_label_handler() + model = self.model + + profile = self.request.params.get('profile') + profile = self.Session.query(model.LabelProfile).get(profile) if profile else None + if not profile: + return {'error': "Label profile not found"} + + product = self.request.params.get('product') + product = self.Session.query(model.Product).get(product) if product else None + if not product: + return {'error': "Product not found"} + + quantity = self.request.params.get('quantity') + if not quantity.isdigit(): + return {'error': "Quantity must be numeric"} + quantity = int(quantity) + + printer = label_handler.get_printer(profile) + if not printer: + return {'error': "Couldn't get printer from label profile"} + + try: + printer.print_labels([({'product': product}, quantity)]) + except Exception as error: + log.warning("error occurred while printing labels", exc_info=True) + return {'error': six.text_type(error)} + return {'ok': True} + def search(self): """ Locate a product(s) by UPC. @@ -1964,10 +1995,18 @@ class ProductView(MasterView): template_prefix = cls.get_template_prefix() permission_prefix = cls.get_permission_prefix() model_title = cls.get_model_title() + model_title_plural = cls.get_model_title_plural() # print labels - config.add_tailbone_permission('products', 'products.print_labels', - "Print labels for products") + config.add_tailbone_permission(permission_prefix, + '{}.print_labels'.format(permission_prefix), + "Print labels for {}".format(model_title_plural)) + config.add_route('{}.print_labels'.format(route_prefix), + '{}/labels'.format(url_prefix)) + config.add_view(cls, attr='print_labels', + route_name='{}.print_labels'.format(route_prefix), + permission='{}.print_labels'.format(permission_prefix), + renderer='json') # view deleted products config.add_tailbone_permission('products', 'products.view_deleted', @@ -2250,39 +2289,6 @@ class PendingProductView(MasterView): permission='{}.resolve_product'.format(permission_prefix)) -def print_labels(request): - profile = request.params.get('profile') - profile = Session.query(model.LabelProfile).get(profile) if profile else None - if not profile: - return {'error': "Label profile not found"} - - product = request.params.get('product') - product = Session.query(model.Product).get(product) if product else None - if not product: - return {'error': "Product not found"} - - quantity = request.params.get('quantity') - if not quantity.isdigit(): - return {'error': "Quantity must be numeric"} - quantity = int(quantity) - - printer = profile.get_printer(request.rattail_config) - if not printer: - return {'error': "Couldn't get printer from label profile"} - - try: - printer.print_labels([(product, quantity, {})]) - except Exception as error: - log.warning("error occurred while printing labels", exc_info=True) - return {'error': six.text_type(error)} - return {'ok': True} - - def includeme(config): - - config.add_route('products.print_labels', '/products/labels') - config.add_view(print_labels, route_name='products.print_labels', - renderer='json', permission='products.print_labels') - ProductView.defaults(config) PendingProductView.defaults(config) From 23fb5e09d1e99df3fdab316c916c3fe575614c92 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 15 Jan 2022 12:47:08 -0600 Subject: [PATCH 0030/1119] 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 1910050e..915b5946 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,12 @@ CHANGELOG ========= +0.8.196 (2022-01-15) +-------------------- + +* Use the new label handler. + + 0.8.195 (2022-01-13) -------------------- diff --git a/tailbone/_version.py b/tailbone/_version.py index 27909901..c1902864 100644 --- a/tailbone/_version.py +++ b/tailbone/_version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8; -*- -__version__ = '0.8.195' +__version__ = '0.8.196' From f83fc18ebca1f76cf4ba1fe331d8d7a2c57627f2 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 18 Jan 2022 12:29:23 -0600 Subject: [PATCH 0031/1119] Use buefy input for quickie search not sure why this suddenly has poor style / formatting, but this fixes --- tailbone/templates/themes/falafel/base.mako | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tailbone/templates/themes/falafel/base.mako b/tailbone/templates/themes/falafel/base.mako index b50cfef7..5ab12a03 100644 --- a/tailbone/templates/themes/falafel/base.mako +++ b/tailbone/templates/themes/falafel/base.mako @@ -353,7 +353,14 @@
    - ${h.text('entry', placeholder=quickie.placeholder, autocomplete='off')} + % if use_buefy: + + + % else: + ${h.text('entry', placeholder=quickie.placeholder, autocomplete='off')} + % endif
    % endif + - ${self.render_execute_helper()} - +<%def name="render_auto_receive_helper()"> % if master.has_perm('auto_receive') and master.can_auto_receive(batch):
    @@ -329,8 +327,9 @@
    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 0053/1119] 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 0054/1119] 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 0055/1119] 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 0056/1119] 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 0057/1119] 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 0058/1119] 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 0059/1119] 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 0060/1119] 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 0061/1119] 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 0062/1119] 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 0063/1119] 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 0064/1119] 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 0065/1119] 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 0066/1119] 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 0067/1119] 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 0068/1119] 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 0069/1119] 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 0070/1119] 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 0071/1119] 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 0072/1119] 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 0073/1119] 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 0074/1119] 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 0075/1119] 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 0076/1119] 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 0077/1119] 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 0078/1119] 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 0079/1119] 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 0081/1119] 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 0082/1119] 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 @@