From ccb4661b39641f52881f4fc6d6e4ae921be36212 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 9 Sep 2023 14:14:23 -0500 Subject: [PATCH 001/499] Add custom hook for grid "apply filters" so a page can know when the data set changes.. this seems a bit hacky, may need a better solution some day --- tailbone/templates/grids/buefy.mako | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tailbone/templates/grids/buefy.mako b/tailbone/templates/grids/buefy.mako index 25b8abca..519c16d8 100644 --- a/tailbone/templates/grids/buefy.mako +++ b/tailbone/templates/grids/buefy.mako @@ -604,8 +604,11 @@ params = new URLSearchParams(params) this.loadAsyncData(params) + this.appliedFiltersHook() }, + appliedFiltersHook() {}, + clearFilters() { // explicitly deactivate all filters From a9fbf480531ebea8ee2e0ea9283c568b9dfd457f Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 9 Sep 2023 16:18:39 -0500 Subject: [PATCH 002/499] Use common POST logic for submitting new customer order --- tailbone/templates/custorders/create.mako | 28 +++-------------------- 1 file changed, 3 insertions(+), 25 deletions(-) diff --git a/tailbone/templates/custorders/create.mako b/tailbone/templates/custorders/create.mako index 77129fb8..055957bb 100644 --- a/tailbone/templates/custorders/create.mako +++ b/tailbone/templates/custorders/create.mako @@ -1091,6 +1091,7 @@ const CustomerOrderCreator = { template: '#customer-order-creator-template', + mixins: [SimpleRequestMixin], data() { let defaultUnitChoices = ${json.dumps(default_uom_choices)|n} @@ -1198,9 +1199,6 @@ pendingProduct: {}, departmentOptions: ${json.dumps(department_options)|n}, - ## TODO: should find a better way to handle CSRF token - csrftoken: ${json.dumps(request.session.get_csrf_token() or request.session.new_csrf_token())|n}, - submittingOrder: false, } }, @@ -1500,31 +1498,11 @@ submitBatchData(params, success, failure) { let url = ${json.dumps(request.current_route_url())|n} - let headers = { - ## TODO: should find a better way to handle CSRF token - 'X-CSRF-TOKEN': this.csrftoken, - } - - ## TODO: should find a better way to handle CSRF token - this.$http.post(url, params, {headers: headers}).then((response) => { - if (response.data.error) { - this.$buefy.toast.open({ - message: response.data.error, - type: 'is-danger', - duration: 2000, // 2 seconds - }) - if (failure) { - failure(response) - } - } else if (success) { + this.simplePOST(url, params, response => { + if (success) { success(response) } }, response => { - this.$buefy.toast.open({ - message: "Unexpected error occurred", - type: 'is-danger', - duration: 2000, // 2 seconds - }) if (failure) { failure(response) } From 64c58a3cf8357624567eb6084c421e618fabcb40 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 10 Sep 2023 07:44:13 -0500 Subject: [PATCH 003/499] Optionally configure SQLAlchemy Session with `future=True` this avoids the need for setting `cascade_backrefs=False` everywhere https://docs.sqlalchemy.org/en/14/errors.html#error-s9r1 https://docs.sqlalchemy.org/en/14/orm/session_api.html#sqlalchemy.orm.Session.params.future --- tailbone/app.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tailbone/app.py b/tailbone/app.py index 4d4f435c..6f41a8de 100644 --- a/tailbone/app.py +++ b/tailbone/app.py @@ -70,6 +70,10 @@ def make_rattail_config(settings): if hasattr(rattail_config, 'tempmon_engine'): tailbone.db.TempmonSession.configure(bind=rattail_config.tempmon_engine) + # maybe set "future" behavior for SQLAlchemy + if rattail_config.getbool('rattail.db', 'sqlalchemy_future_mode', usedb=False): + tailbone.db.Session.configure(future=True) + # create session wrappers for each "extra" Trainwreck engine for key, engine in rattail_config.trainwreck_engines.items(): if key != 'default': From 48daa042d17827e133f7107b139251ce460e85b0 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 10 Sep 2023 07:45:25 -0500 Subject: [PATCH 004/499] Show related customer orders for Pending Product view and similar tweaks --- tailbone/templates/products/pending/view.mako | 22 +++---- tailbone/views/products.py | 64 ++++++++++++++++--- 2 files changed, 65 insertions(+), 21 deletions(-) diff --git a/tailbone/templates/products/pending/view.mako b/tailbone/templates/products/pending/view.mako index 90d9c687..be61a44f 100644 --- a/tailbone/templates/products/pending/view.mako +++ b/tailbone/templates/products/pending/view.mako @@ -53,24 +53,22 @@ diff --git a/tailbone/views/products.py b/tailbone/views/products.py index 92c99c34..e0183d14 100644 --- a/tailbone/views/products.py +++ b/tailbone/views/products.py @@ -2229,6 +2229,7 @@ class PendingProductView(MasterView): form_fields = [ '_product_key_', + 'product', 'brand_name', 'brand', 'description', @@ -2237,14 +2238,34 @@ class PendingProductView(MasterView): 'department', 'vendor_name', 'vendor', + 'vendor_item_code', 'unit_cost', 'case_size', 'regular_price_amount', 'special_order', 'notes', + 'status_code', 'created', 'user', - 'status_code', + 'resolved', + 'resolved_by', + ] + + has_rows = True + model_row_class = model.CustomerOrderItem + rows_title = "Customer Orders" + # TODO: add support for this someday + rows_viewable = False + + # TODO: this clearly needs help + row_grid_columns = [ + # 'upc', + 'brand_name', + 'description', + 'size', + 'vendor_name', + # 'regular_price', + # 'current_price', ] def configure_grid(self, g): @@ -2264,6 +2285,10 @@ class PendingProductView(MasterView): model = self.model pending = f.model_instance + # product + f.set_readonly('product') # TODO + f.set_renderer('product', self.render_product) + # department if self.creating or self.editing: if 'department' in f: @@ -2342,13 +2367,6 @@ class PendingProductView(MasterView): else: f.set_readonly('created') - # user - if self.creating: - f.remove('user') - else: - f.set_readonly('user') - f.set_renderer('user', self.render_user) - # status_code if self.creating: f.remove('status_code') @@ -2356,7 +2374,23 @@ class PendingProductView(MasterView): # f.set_readonly('status_code') f.set_enum('status_code', self.enum.PENDING_PRODUCT_STATUS) + # user + if self.creating: + f.remove('user') + else: + f.set_readonly('user') + f.set_renderer('user', self.render_user) + + # resolved* + if self.creating: + f.remove('resolved', 'resolved_by') + else: + if not pending.resolved: + f.remove('resolved', 'resolved_by') + def editable_instance(self, pending): + if self.request.is_root: + return True if pending.status_code == self.enum.PENDING_PRODUCT_STATUS_RESOLVED: return False return True @@ -2419,9 +2453,21 @@ class PendingProductView(MasterView): app = self.get_rattail_app() products_handler = app.get_products_handler() - products_handler.resolve_product(pending, product, self.request.user) + kwargs = self.get_resolve_product_kwargs() + products_handler.resolve_product(pending, product, self.request.user, **kwargs) return redirect + def get_resolve_product_kwargs(self, **kwargs): + return kwargs + + def get_row_data(self, pending): + model = self.model + return self.Session.query(model.CustomerOrderItem)\ + .filter(model.CustomerOrderItem.pending_product == pending) + + def get_parent(self, item): + return item.pending_product + @classmethod def defaults(cls, config): cls._defaults(config) From e255c35e8663f78f12f3c231c2d3a625fdf63a10 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 10 Sep 2023 13:51:11 -0500 Subject: [PATCH 005/499] Set stacklevel for all deprecation warnings --- tailbone/grids/core.py | 2 +- tailbone/views/handheld.py | 6 ++---- tailbone/views/labels/batch.py | 6 ++---- tailbone/views/vendors/catalogs.py | 6 ++---- tailbone/views/vendors/invoices.py | 6 ++---- 5 files changed, 9 insertions(+), 17 deletions(-) diff --git a/tailbone/grids/core.py b/tailbone/grids/core.py index a6ba34d1..4a748536 100644 --- a/tailbone/grids/core.py +++ b/tailbone/grids/core.py @@ -309,7 +309,7 @@ class Grid(object): """ warnings.warn("Grid.hide_column() is deprecated; please use " "Grid.remove() instead.", - DeprecationWarning) + DeprecationWarning, stacklevel=2) self.remove(key) def hide_columns(self, *keys): diff --git a/tailbone/views/handheld.py b/tailbone/views/handheld.py index 4d702c92..34211c30 100644 --- a/tailbone/views/handheld.py +++ b/tailbone/views/handheld.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2022 Lance Edgar +# Copyright © 2010-2023 Lance Edgar # # This file is part of Rattail. # @@ -24,8 +24,6 @@ (DEPRECATED) Views for handheld batches """ -from __future__ import unicode_literals, absolute_import - import warnings # nb. this is imported only for sake of legacy callers @@ -35,5 +33,5 @@ from tailbone.views.batch.handheld import HandheldBatchView def includeme(config): warnings.warn("tailbone.views.handheld is a deprecated module; " "please use tailbone.views.batch.handheld instead", - DeprecationWarning) + DeprecationWarning, stacklevel=2) config.include('tailbone.views.batch.handheld') diff --git a/tailbone/views/labels/batch.py b/tailbone/views/labels/batch.py index b4910466..e9d2971b 100644 --- a/tailbone/views/labels/batch.py +++ b/tailbone/views/labels/batch.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2019 Lance Edgar +# Copyright © 2010-2023 Lance Edgar # # This file is part of Rattail. # @@ -26,8 +26,6 @@ Please use `tailbone.views.batch.labels` instead. """ -from __future__ import unicode_literals, absolute_import - import warnings @@ -35,6 +33,6 @@ def includeme(config): warnings.warn("The `tailbone.views.labels.batch` module is deprecated, " "please use `tailbone.views.batch.labels` instead.", - DeprecationWarning) + DeprecationWarning, stacklevel=2) config.include('tailbone.views.batch.labels') diff --git a/tailbone/views/vendors/catalogs.py b/tailbone/views/vendors/catalogs.py index e021a88a..2471ad47 100644 --- a/tailbone/views/vendors/catalogs.py +++ b/tailbone/views/vendors/catalogs.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2019 Lance Edgar +# Copyright © 2010-2023 Lance Edgar # # This file is part of Rattail. # @@ -26,13 +26,11 @@ Please use `tailbone.views.batch.vendorcatalog` instead. """ -from __future__ import unicode_literals, absolute_import - import warnings def includeme(config): warnings.warn("The `tailbone.views.vendors.catalogs` module is deprecated, " "please use `tailbone.views.batch.vendorcatalog` instead.", - DeprecationWarning) + DeprecationWarning, stacklevel=2) config.include('tailbone.views.batch.vendorcatalog') diff --git a/tailbone/views/vendors/invoices.py b/tailbone/views/vendors/invoices.py index e61329f6..40fe0365 100644 --- a/tailbone/views/vendors/invoices.py +++ b/tailbone/views/vendors/invoices.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2019 Lance Edgar +# Copyright © 2010-2023 Lance Edgar # # This file is part of Rattail. # @@ -26,13 +26,11 @@ Please use `tailbone.views.batch.vendorinvoice` instead. """ -from __future__ import unicode_literals, absolute_import - import warnings def includeme(config): warnings.warn("The `tailbone.views.vendors.invoices` module is deprecated, " "please use `tailbone.views.batch.vendorinvoice` instead.", - DeprecationWarning) + DeprecationWarning, stacklevel=2) config.include('tailbone.views.batch.vendorinvoice') From e49e0edc5719386cf688c5e10b957ee52937d27b Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 10 Sep 2023 17:06:14 -0500 Subject: [PATCH 006/499] Misc. improvements for Customer Orders view --- tailbone/templates/custorders/view.mako | 3 + tailbone/views/custorders/items.py | 32 ++++++--- tailbone/views/custorders/orders.py | 93 +++++++++++++++++++++++-- 3 files changed, 113 insertions(+), 15 deletions(-) create mode 100644 tailbone/templates/custorders/view.mako diff --git a/tailbone/templates/custorders/view.mako b/tailbone/templates/custorders/view.mako new file mode 100644 index 00000000..e2af7bf4 --- /dev/null +++ b/tailbone/templates/custorders/view.mako @@ -0,0 +1,3 @@ +## -*- coding: utf-8; -*- +<%inherit file="/master/view.mako" /> +${parent.body()} diff --git a/tailbone/views/custorders/items.py b/tailbone/views/custorders/items.py index 5dc61e4d..5d4f6049 100644 --- a/tailbone/views/custorders/items.py +++ b/tailbone/views/custorders/items.py @@ -85,8 +85,10 @@ class CustomerOrderItemView(MasterView): form_fields = [ 'order', - 'sequence', + 'customer', 'person', + 'sequence', + '_product_key_', 'product', 'pending_product', 'product_brand', @@ -97,9 +99,11 @@ class CustomerOrderItemView(MasterView): 'case_quantity', 'unit_price', 'total_price', + 'special_order', 'price_needs_confirmation', 'paid_amount', 'status_code', + 'flagged', 'notes', ] @@ -167,13 +171,30 @@ class CustomerOrderItemView(MasterView): return HTML.tag('span', title=item.status_text, c=[text]) return text + def get_batch_handler(self): + app = self.get_rattail_app() + return app.get_batch_handler( + 'custorder', + default='rattail.batch.custorder:CustomerOrderBatchHandler') + def configure_form(self, f): - super(CustomerOrderItemView, self).configure_form(f) + super().configure_form(f) item = f.model_instance # order f.set_renderer('order', self.render_order) + # contact + batch_handler = self.get_batch_handler() + if batch_handler.new_order_requires_customer(): + f.remove('person') + else: + f.remove('customer') + + # product key + key = self.get_product_key_field() + f.set_renderer(key, lambda item, field: getattr(item, f'product_{key}')) + # (pending) product f.set_renderer('product', self.render_product) f.set_renderer('pending_product', self.render_pending_product) @@ -192,13 +213,6 @@ class CustomerOrderItemView(MasterView): 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('cases_ordered', 'quantity') f.set_type('units_ordered', 'quantity') diff --git a/tailbone/views/custorders/orders.py b/tailbone/views/custorders/orders.py index cdf765a6..abbcf87c 100644 --- a/tailbone/views/custorders/orders.py +++ b/tailbone/views/custorders/orders.py @@ -52,7 +52,7 @@ class CustomerOrderView(MasterView): configurable = True labels = { - 'id': "ID", + 'id': "Order ID", 'status_code': "Status", } @@ -60,8 +60,9 @@ class CustomerOrderView(MasterView): 'id', 'customer', 'person', - 'created', 'status_code', + 'created', + 'created_by', ] form_fields = [ @@ -88,14 +89,17 @@ class CustomerOrderView(MasterView): row_grid_columns = [ 'sequence', + '_product_key_', 'product_brand', 'product_description', 'product_size', 'order_quantity', 'order_uom', 'case_quantity', + 'department_name', 'total_price', 'status_code', + 'flagged', ] def __init__(self, request): @@ -107,11 +111,19 @@ class CustomerOrderView(MasterView): .options(orm.joinedload(model.CustomerOrder.customer)) def configure_grid(self, g): - super(CustomerOrderView, self).configure_grid(g) + super().configure_grid(g) + + # id + g.set_link('id') + g.filters['id'].default_active = True + g.filters['id'].default_verb = 'equal' + + # import ipdb; ipdb.set_trace() # customer or person if self.batch_handler.new_order_requires_customer(): g.remove('person') + g.set_link('customer') 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, @@ -120,6 +132,7 @@ class CustomerOrderView(MasterView): default_verb='contains') else: g.remove('customer') + g.set_link('person') 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, @@ -127,13 +140,14 @@ class CustomerOrderView(MasterView): default_active=True, default_verb='contains') + # status_code g.set_enum('status_code', self.enum.CUSTORDER_STATUS) + # created g.set_sort_defaults('created', 'desc') - g.set_link('id') - g.set_link('customer') - g.set_link('person') + def get_instance_title(self, order): + return f"#{order.id} for {order.customer or order.person}" def configure_form(self, f): super(CustomerOrderView, self).configure_form(f) @@ -232,6 +246,10 @@ class CustomerOrderView(MasterView): 'custorder', default='rattail.batch.custorder:CustomerOrderBatchHandler') + # product key + key = self.get_product_key_field() + g.set_renderer(key, lambda item, field: getattr(item, f'product_{key}')) + g.set_type('case_quantity', 'quantity') g.set_type('order_quantity', 'quantity') g.set_type('cases_ordered', 'quantity') @@ -962,6 +980,61 @@ class CustomerOrderView(MasterView): def execute_new_order_batch(self, batch, data): return self.batch_handler.do_execute(batch, self.request.user) + def fetch_order_data(self): + app = self.get_rattail_app() + model = self.model + + order = None + uuid = self.request.GET.get('uuid') + if uuid: + order = self.Session.get(model.CustomerOrder, uuid) + if not order: + # raise self.notfound() + return {'error': "Customer order not found"} + + address = None + if self.batch_handler.new_order_requires_customer(): + contact = order.customer + else: + contact = order.person + if contact and contact.address: + a = contact.address + address = { + 'street_1': a.street, + 'street_2': a.street2, + 'city': a.city, + 'state': a.state, + 'zip': a.zipcode, + } + + # gather all the order items + items = [] + grand_total = 0 + for item in order.items: + item_data = { + 'uuid': item.uuid, + 'special_order': False, # TODO + 'product_description': item.product_description, + 'order_quantity': app.render_quantity(item.order_quantity), + 'department': item.department_name, + 'price': app.render_currency(item.unit_price), + 'total': app.render_currency(item.total_price), + } + items.append(item_data) + grand_total += item.total_price + + return { + 'uuid': order.uuid, + 'id': order.id, + 'created_display': app.render_datetime(app.localtime(order.created, from_utc=True)), + 'contact_display': str(contact or ''), + 'address': address, + 'phone_display': str(contact.phone) if contact and contact.phone else "", + 'email_display': str(contact.email) if contact and contact.email else "", + 'items': items, + 'grand_total_display': app.render_currency(grand_total), + } + def configure_get_simple_settings(self): return [ @@ -1048,6 +1121,14 @@ class CustomerOrderView(MasterView): renderer='json', permission='products.list') + # fetch order data + config.add_route(f'{route_prefix}.fetch_order_data', + f'{url_prefix}/fetch-order-data') + config.add_view(cls, attr='fetch_order_data', + route_name=f'{route_prefix}.fetch_order_data', + renderer='json', + permission=f'{permission_prefix}.view') + # TODO: deprecate / remove this CustomerOrdersView = CustomerOrderView From ddb8e3656fe10c4b1f7d83dd8113029b3f035c1f Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 10 Sep 2023 17:49:29 -0500 Subject: [PATCH 007/499] Add support for toggling custorder item "flagged" --- tailbone/templates/custorders/items/view.mako | 6 ++ tailbone/views/custorders/items.py | 62 +++++++++++++++++-- 2 files changed, 64 insertions(+), 4 deletions(-) diff --git a/tailbone/templates/custorders/items/view.mako b/tailbone/templates/custorders/items/view.mako index 9eb239ed..e82b567f 100644 --- a/tailbone/templates/custorders/items/view.mako +++ b/tailbone/templates/custorders/items/view.mako @@ -324,6 +324,12 @@ this.$refs.changeStatusForm.submit() } + ${form.component_studly}Data.changeFlaggedSubmitting = false + + ${form.component_studly}.methods.changeFlaggedSubmit = function() { + this.changeFlaggedSubmitting = true + } + % endif % if master.has_perm('add_note'): diff --git a/tailbone/views/custorders/items.py b/tailbone/views/custorders/items.py index 5d4f6049..baec4151 100644 --- a/tailbone/views/custorders/items.py +++ b/tailbone/views/custorders/items.py @@ -34,7 +34,7 @@ from rattail.time import localtime from webhelpers2.html import HTML, tags from tailbone.views import MasterView -from tailbone.util import raw_datetime +from tailbone.util import raw_datetime, csrf_token class CustomerOrderItemView(MasterView): @@ -231,9 +231,53 @@ class CustomerOrderItemView(MasterView): # status_code f.set_renderer('status_code', self.render_status_code) + # flagged + f.set_renderer('flagged', self.render_flagged) + # notes f.set_renderer('notes', self.render_notes) + def render_flagged(self, item, field): + text = "Yes" if item.flagged else "No" + items = [HTML.tag('span', c=text)] + + if self.has_perm('change_status'): + button_text = "Un-Flag This" if item.flagged else "Flag This" + form = [ + tags.form(self.get_action_url('change_flagged', item), + **{'@submit': 'changeFlaggedSubmit'}), + csrf_token(self.request), + tags.hidden('new_flagged', + value='false' if item.flagged else 'true'), + HTML.tag('b-button', + type='is-warning' if item.flagged else 'is-primary', + c=f"{{{{ changeFlaggedSubmitting ? 'Working, please wait...' : '{button_text}' }}}}", + native_type='submit', + style='margin-left: 1rem;', + icon_pack='fas', icon_left='flag', + **{':disabled': 'changeFlaggedSubmitting'}), + tags.end_form(), + ] + items.append(HTML.literal('').join(form)) + + left = HTML.tag('div', class_='level-left', c=items) + outer = HTML.tag('div', class_='level', c=[left]) + return outer + + def change_flagged(self): + """ + View for changing "flagged" status of one or more order products. + """ + item = self.get_instance() + redirect = self.redirect(self.get_action_url('view', item)) + + new_flagged = self.request.POST['new_flagged'] == 'true' + item.flagged = new_flagged + + flagged = "FLAGGED" if new_flagged else "UN-FLAGGED" + self.request.session.flash(f"Order item has been {flagged}") + return redirect + def highlight_pending_field(self, item, field, value=None): if value is None: value = getattr(item, field) @@ -299,14 +343,16 @@ class CustomerOrderItemView(MasterView): factory = self.get_grid_factory() g = factory( - key='{}.notes'.format(route_prefix), + key=f'{route_prefix}.notes', data=[], columns=[ - 'text', - 'created_by', 'created', + 'created_by', + 'text', ], labels={ + 'created': "Date/Time", + 'created_by': "Added by", 'text': "Note", }, ) @@ -555,6 +601,14 @@ class CustomerOrderItemView(MasterView): route_name='{}.change_status'.format(route_prefix), permission='{}.change_status'.format(permission_prefix)) + # change flagged + config.add_route(f'{route_prefix}.change_flagged', + f'{instance_url_prefix}/change-flagged', + request_method='POST') + config.add_view(cls, attr='change_flagged', + route_name=f'{route_prefix}.change_flagged', + permission=f'{permission_prefix}.change_status') + # add note config.add_tailbone_permission(permission_prefix, '{}.add_note'.format(permission_prefix), From 67ec6f7773147fbe2be0cd5c8b9315c0875f71b4 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 10 Sep 2023 19:55:48 -0500 Subject: [PATCH 008/499] Add support for "mark received" when viewing custorder item --- tailbone/templates/custorders/items/view.mako | 118 ++++++++++++++---- tailbone/views/custorders/items.py | 57 ++++++++- 2 files changed, 149 insertions(+), 26 deletions(-) diff --git a/tailbone/templates/custorders/items/view.mako b/tailbone/templates/custorders/items/view.mako index e82b567f..c1aaf970 100644 --- a/tailbone/templates/custorders/items/view.mako +++ b/tailbone/templates/custorders/items/view.mako @@ -9,6 +9,7 @@ % endif % if master.has_perm('change_status'): @change-status="showChangeStatus" + @mark-received="markReceivedInit" % endif % if master.has_perm('add_note'): @add-note="showAddNote" @@ -61,6 +62,67 @@ % endif % if master.has_perm('change_status'): + + ## TODO ## + <% contact = instance.order.person %> + <% email_address = rattail_app.get_contact_email_address(contact) %> + + + + + ${h.form(url(f'{route_prefix}.mark_received'), ref='markReceivedForm')} + ${h.csrf_token(request)} + ${h.hidden('order_item_uuids', value=instance.uuid)} + ${h.end_form()} +
@@ -106,21 +168,30 @@ :checked-rows.sync="changeStatusCheckedRows" narrowed class="is-size-7"> - - + {{ props.row.product_key }} - + + + - - - + + + + + + @@ -129,33 +200,18 @@ v-slot="props"> - - - - - - - - - - - - - - - + + {{ props.row.flagged ? "FLAG" : "" }} +
@@ -278,6 +334,18 @@ % if master.has_perm('change_status'): + ThisPageData.markReceivedShowDialog = false + ThisPageData.markReceivedSubmitting = false + + ThisPage.methods.markReceivedInit = function() { + this.markReceivedShowDialog = true + } + + ThisPage.methods.markReceivedSubmit = function() { + this.markReceivedSubmitting = true + this.$refs.markReceivedForm.submit() + } + ThisPageData.orderItemStatuses = ${json.dumps(enum.CUSTORDER_ITEM_STATUS)|n} ThisPageData.orderItemStatusOptions = ${json.dumps([dict(key=k, label=v) for k, v in six.iteritems(enum.CUSTORDER_ITEM_STATUS)])|n} diff --git a/tailbone/views/custorders/items.py b/tailbone/views/custorders/items.py index baec4151..84fe615a 100644 --- a/tailbone/views/custorders/items.py +++ b/tailbone/views/custorders/items.py @@ -328,6 +328,16 @@ class CustomerOrderItemView(MasterView): items = [HTML.tag('span', c=[text])] if self.has_perm('change_status'): + + # Mark Received + if self.can_be_received(item): + button = HTML.tag('b-button', type='is-primary', c="Mark Received", + style='margin-left: 1rem;', + icon_pack='fas', icon_left='check', + **{'@click': "$emit('mark-received')"}) + items.append(button) + + # Change Status button = HTML.tag('b-button', type='is-primary', c="Change Status", style='margin-left: 1rem;', icon_pack='fas', icon_left='edit', @@ -338,6 +348,16 @@ class CustomerOrderItemView(MasterView): outer = HTML.tag('div', class_='level', c=[left]) return outer + def can_be_received(self, item): + + # TODO: is this generic enough? probably belongs in handler anyway.. + if item.status_code in (self.enum.CUSTORDER_ITEM_STATUS_INITIATED, + self.enum.CUSTORDER_ITEM_STATUS_READY, + self.enum.CUSTORDER_ITEM_STATUS_PLACED): + return True + + return False + def render_notes(self, item, field): route_prefix = self.get_route_prefix() @@ -389,6 +409,7 @@ class CustomerOrderItemView(MasterView): .filter(model.CustomerOrderItem.uuid != item.uuid)\ .all() other_data = [] + product_key_field = self.get_product_key_field() for other in other_items: order_date = None @@ -397,8 +418,10 @@ class CustomerOrderItemView(MasterView): other_data.append({ 'uuid': other.uuid, + 'product_key': getattr(other, f'product_{product_key_field}'), 'brand_name': other.product_brand, 'product_description': other.product_description, + 'product_size': other.product_size, 'product_case_quantity': app.render_quantity(other.case_quantity), 'order_quantity': app.render_quantity(other.order_quantity), 'order_uom': self.enum.UNIT_OF_MEASURE[other.order_uom], @@ -408,6 +431,7 @@ class CustomerOrderItemView(MasterView): 'total_price': app.render_currency(other.total_price), 'order_date': app.render_date(order_date), 'status_code': self.enum.CUSTORDER_ITEM_STATUS[other.status_code], + 'flagged': other.flagged, }) kwargs['other_order_items_data'] = other_data @@ -450,6 +474,28 @@ class CustomerOrderItemView(MasterView): self.request.session.flash("Price has been confirmed.") return redirect + def mark_received(self): + """ + View to mark some order item(s) as having been received. + """ + app = self.get_rattail_app() + model = self.model + uuids = self.request.POST['order_item_uuids'].split(',') + + order_items = self.Session.query(model.CustomerOrderItem)\ + .filter(model.CustomerOrderItem.uuid.in_(uuids))\ + .all() + + handler = app.get_custorder_handler() + handler.mark_received(order_items, self.request.user) + + msg = self.mark_received_get_flash(order_items) + self.request.session.flash(msg) + return self.redirect(self.request.get_referrer(default=self.get_index_url())) + + def mark_received_get_flash(self, order_items): + return "Order item statuses have been updated." + def change_status(self): """ View for changing status of one or more order items. @@ -550,7 +596,7 @@ class CustomerOrderItemView(MasterView): def get_row_data(self, item): return self.Session.query(model.CustomerOrderItemEvent)\ .filter(model.CustomerOrderItemEvent.item == item)\ - .order_by(model.CustomerOrderItemEvent.occurred.desc(), + .order_by(model.CustomerOrderItemEvent.occurred, model.CustomerOrderItemEvent.type_code) def configure_row_grid(self, g): @@ -571,6 +617,7 @@ class CustomerOrderItemView(MasterView): @classmethod def _order_item_defaults(cls, config): route_prefix = cls.get_route_prefix() + url_prefix = cls.get_url_prefix() instance_url_prefix = cls.get_instance_url_prefix() permission_prefix = cls.get_permission_prefix() model_title = cls.get_model_title() @@ -590,6 +637,14 @@ class CustomerOrderItemView(MasterView): route_name='{}.confirm_price'.format(route_prefix), permission='{}.confirm_price'.format(permission_prefix)) + # mark received + config.add_route(f'{route_prefix}.mark_received', + f'{url_prefix}/mark-received', + request_method='POST') + config.add_view(cls, attr='mark_received', + route_name=f'{route_prefix}.mark_received', + permission=f'{permission_prefix}.change_status') + # change status config.add_tailbone_permission(permission_prefix, '{}.change_status'.format(permission_prefix), From e793ba66308cbc8120dcd1a947815dc54aee6798 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 11 Sep 2023 15:24:00 -0500 Subject: [PATCH 009/499] Improve grids for custorder items main grid as well as rows grid for Pending Product --- tailbone/templates/products/pending/view.mako | 21 ------ tailbone/views/custorders/items.py | 75 ++++++++++++------- tailbone/views/products.py | 65 ++++++++++++++-- 3 files changed, 103 insertions(+), 58 deletions(-) diff --git a/tailbone/templates/products/pending/view.mako b/tailbone/templates/products/pending/view.mako index be61a44f..2b9852d9 100644 --- a/tailbone/templates/products/pending/view.mako +++ b/tailbone/templates/products/pending/view.mako @@ -3,27 +3,6 @@ <%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'): -<%def name="render_form()"> +<%def name="render_form_template()"> @@ -113,7 +113,7 @@ }, data() { return { - groupPermissions: ${json.dumps(buefy_perms.get(selected_group, {}).get('permissions', []))|n}, + groupPermissions: ${json.dumps(perms_data.get(selected_group, {}).get('permissions', []))|n}, permissionGroupTerm: '', permissionTerm: '', selectedGroup: ${json.dumps(selected_group)|n}, diff --git a/tailbone/templates/products/batch.mako b/tailbone/templates/products/batch.mako index 868ad9b1..e0b93bd6 100644 --- a/tailbone/templates/products/batch.mako +++ b/tailbone/templates/products/batch.mako @@ -54,7 +54,7 @@ ${h.end_form()} -<%def name="render_form()"> +<%def name="render_form_template()"> diff --git a/tailbone/templates/products/view.mako b/tailbone/templates/products/view.mako index 5de6d099..c4da08ba 100644 --- a/tailbone/templates/products/view.mako +++ b/tailbone/templates/products/view.mako @@ -108,7 +108,7 @@ <%def name="lookup_codes_grid()"> - ${lookup_codes['grid'].render_buefy_table_element(data_prop='lookupCodesData')|n} + ${lookup_codes['grid'].render_table_element(data_prop='lookupCodesData')|n} <%def name="lookup_codes_panel()"> @@ -121,7 +121,7 @@ <%def name="sources_grid()"> - ${vendor_sources['grid'].render_buefy_table_element(data_prop='vendorSourcesData')|n} + ${vendor_sources['grid'].render_table_element(data_prop='vendorSourcesData')|n} <%def name="sources_panel()"> @@ -175,7 +175,7 @@