From de373a683bdb2e0917d763671ea11867b5f7d56e Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 1 Sep 2023 11:20:30 -0500 Subject: [PATCH 001/511] Add grid filter type for BigInteger columns so we can filter by larger values --- tailbone/grids/core.py | 2 ++ tailbone/grids/filters.py | 15 ++++++++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/tailbone/grids/core.py b/tailbone/grids/core.py index abbac793..a6ba34d1 100644 --- a/tailbone/grids/core.py +++ b/tailbone/grids/core.py @@ -689,6 +689,8 @@ class Grid(object): factory = gridfilters.AlchemyStringFilter elif isinstance(column.type, sa.Numeric): factory = gridfilters.AlchemyNumericFilter + elif isinstance(column.type, sa.BigInteger): + factory = gridfilters.AlchemyBigIntegerFilter elif isinstance(column.type, sa.Integer): factory = gridfilters.AlchemyIntegerFilter elif isinstance(column.type, sa.Boolean): diff --git a/tailbone/grids/filters.py b/tailbone/grids/filters.py index 59e20d78..c8815f9f 100644 --- a/tailbone/grids/filters.py +++ b/tailbone/grids/filters.py @@ -659,6 +659,7 @@ class AlchemyIntegerFilter(AlchemyNumericFilter): """ Integer filter for SQLAlchemy. """ + bigint = False def value_invalid(self, value): if value: @@ -666,9 +667,10 @@ class AlchemyIntegerFilter(AlchemyNumericFilter): return True if not value.isdigit(): return True - # TODO: this one is to avoid DataError from PG, but perhaps that - # isn't a good enough reason to make this global logic? - if int(value) > 2147483647: + # normal Integer columns have a max value, beyond which PG + # will throw an error if we try to query for larger values + # TODO: this seems hacky, how to better handle it? + if not self.bigint and int(value) > 2147483647: return True return False @@ -678,6 +680,13 @@ class AlchemyIntegerFilter(AlchemyNumericFilter): return int(value) +class AlchemyBigIntegerFilter(AlchemyIntegerFilter): + """ + BigInteger filter for SQLAlchemy. + """ + bigint = True + + class AlchemyBooleanFilter(AlchemyGridFilter): """ Boolean filter for SQLAlchemy. From 75caface6b8b184411583cfa6a65cca907488ec8 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 2 Sep 2023 10:56:06 -0500 Subject: [PATCH 002/511] Add products API route to fetch label profiles for use w/ printing --- tailbone/api/products.py | 25 +++++++++++++++++++++++++ tailbone/views/products.py | 11 ++++------- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/tailbone/api/products.py b/tailbone/api/products.py index a2e2db73..3f29ff54 100644 --- a/tailbone/api/products.py +++ b/tailbone/api/products.py @@ -125,6 +125,24 @@ class ProductView(APIMasterView): return {'ok': True, 'product': self.normalize(product)} + def label_profiles(self): + """ + Returns the set of label profiles available for use with + printing label for product. + """ + app = self.get_rattail_app() + label_handler = app.get_label_handler() + model = self.model + + profiles = [] + for profile in label_handler.get_label_profiles(self.Session()): + profiles.append({ + 'uuid': profile.uuid, + 'description': profile.description, + }) + + return {'label_profiles': profiles} + def print_labels(self): app = self.get_rattail_app() label_handler = app.get_label_handler() @@ -176,6 +194,13 @@ class ProductView(APIMasterView): permission='{}.list'.format(permission_prefix)) config.add_cornice_service(quick_lookup) + # label profiles + label_profiles = Service(name=f'{route_prefix}.label_profiles', + path=f'{collection_url_prefix}/label-profiles') + label_profiles.add_view('GET', 'label_profiles', klass=cls, + permission=f'{permission_prefix}.print_labels') + config.add_cornice_service(label_profiles) + # print labels print_labels = Service(name='{}.print_labels'.format(route_prefix), path='{}/print-labels'.format(collection_url_prefix)) diff --git a/tailbone/views/products.py b/tailbone/views/products.py index 1cfa528a..92c99c34 100644 --- a/tailbone/views/products.py +++ b/tailbone/views/products.py @@ -658,16 +658,13 @@ class ProductView(MasterView): return pretty_quantity(inventory.on_order) def template_kwargs_index(self, **kwargs): - kwargs = super(ProductView, self).template_kwargs_index(**kwargs) + kwargs = super().template_kwargs_index(**kwargs) + app = self.get_rattail_app() + label_handler = app.get_label_handler() model = self.model if self.expose_label_printing: - - kwargs['label_profiles'] = self.Session.query(model.LabelProfile)\ - .filter(model.LabelProfile.visible == True)\ - .order_by(model.LabelProfile.ordinal)\ - .all() - + kwargs['label_profiles'] = label_handler.get_label_profiles(self.Session()) kwargs['quick_label_speedbump_threshold'] = self.rattail_config.getint( 'tailbone', 'products.quick_labels.speedbump_threshold') From bd7e6f9f8a2bc932e47f0d74c8884ceaaeccf48c Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 2 Sep 2023 11:39:49 -0500 Subject: [PATCH 003/511] Tweaks for cost editing within a receiving batch never show PO Cost column in row grid, since Invoice Cost is what receiving is most concerned with add "zig-zag" entry behavior when both catalog and invoice costs are editable --- tailbone/templates/receiving/view.mako | 82 ++++++++++++++------------ tailbone/views/purchasing/receiving.py | 9 --- 2 files changed, 44 insertions(+), 47 deletions(-) diff --git a/tailbone/templates/receiving/view.mako b/tailbone/templates/receiving/view.mako index 77560ac1..b01436ba 100644 --- a/tailbone/templates/receiving/view.mako +++ b/tailbone/templates/receiving/view.mako @@ -177,6 +177,7 @@ let ReceivingCostEditor = { template: '#receiving-cost-editor-template', + mixins: [SimpleRequestMixin], props: { row: Object, 'field': String, @@ -232,41 +233,21 @@ submitEdit() { let url = '${url('{}.update_row_cost'.format(route_prefix), uuid=batch.uuid)}' - // TODO: should get csrf token from parent component? - let csrftoken = ${json.dumps(request.session.get_csrf_token() or request.session.new_csrf_token())|n} - let headers = {'${csrf_header_name}': csrftoken} - let params = { row_uuid: this.$props.row.uuid, } params[this.$props.field] = this.inputValue - this.$http.post(url, params, {headers: headers}).then(response => { - if (!response.data.error) { + this.simplePOST(url, params, response => { - // let parent know cost value has changed - // (this in turn will update data in *this* - // component, and display will refresh) - this.$emit('input', response.data.row[this.$props.field], - this.$props.row._index) + // let parent know cost value has changed + // (this in turn will update data in *this* + // component, and display will refresh) + this.$emit('input', response.data.row[this.$props.field], + this.$props.row._index) - // and hide the input box - this.editing = false - - } else { - this.$buefy.toast.open({ - message: "Submit failed: " + response.data.error, - type: 'is-warning', - duration: 4000, // 4 seconds - }) - } - - }, response => { - this.$buefy.toast.open({ - message: "Submit failed: (unknown error)", - type: 'is-warning', - duration: 4000, // 4 seconds - }) + // and hide the input box + this.editing = false }) }, }, @@ -289,11 +270,23 @@ // update display to indicate cost was confirmed this.addRowClass(index, 'catalog_cost_confirmed') - // start editing next row, unless there are no more - let nextRow = index + 1 - if (this.data.length > nextRow) { - nextRow = this.data[nextRow] - this.$refs['catalogUnitCost_' + nextRow.uuid].startEdit() + // advance to next editable cost input... + + // first try invoice cost within same row + let thisRow = this.data[index] + let cost = this.$refs['invoiceUnitCost_' + thisRow.uuid] + if (!cost) { + + // or, try catalog cost from next row + let nextRow = this.data[index + 1] + if (nextRow) { + cost = this.$refs['catalogUnitCost_' + nextRow.uuid] + } + } + + // start editing next cost if found + if (cost) { + cost.startEdit() } } @@ -312,11 +305,24 @@ // update display to indicate cost was confirmed this.addRowClass(index, 'invoice_cost_confirmed') - // start editing next row, unless there are no more - let nextRow = index + 1 - if (this.data.length > nextRow) { - nextRow = this.data[nextRow] - this.$refs['invoiceUnitCost_' + nextRow.uuid].startEdit() + // advance to next editable cost input... + + // nb. always advance to next row, regardless of field + let nextRow = this.data[index + 1] + if (nextRow) { + + // first try catalog cost from next row + let cost = this.$refs['catalogUnitCost_' + nextRow.uuid] + if (!cost) { + + // or, try invoice cost from next row + cost = this.$refs['invoiceUnitCost_' + nextRow.uuid] + } + + // start editing next cost if found + if (cost) { + cost.startEdit() + } } } diff --git a/tailbone/views/purchasing/receiving.py b/tailbone/views/purchasing/receiving.py index 35e1d6b4..909ded3f 100644 --- a/tailbone/views/purchasing/receiving.py +++ b/tailbone/views/purchasing/receiving.py @@ -162,7 +162,6 @@ class ReceivingBatchView(PurchasingBatchView): 'cases_received', 'units_received', 'catalog_unit_cost', - 'po_unit_cost', 'invoice_unit_cost', 'invoice_total_calculated', 'credits', @@ -979,14 +978,6 @@ class ReceivingBatchView(PurchasingBatchView): g.set_click_handler('invoice_unit_cost', 'this.invoiceUnitCostClicked') - # nb. only show PO *or* invoice cost; prefer the latter unless - # we have a PO and no invoice - if (self.batch_handler.has_purchase_order(batch) - and not self.batch_handler.has_invoice_file(batch)): - g.remove('invoice_unit_cost') - else: - g.remove('po_unit_cost') - # credits # note that sorting by credits involves a subquery with group by clause. # seems likely there may be a better way? but this seems to work fine From b1ec1b881706b4ef460122871f3223fe4cfc9965 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 2 Sep 2023 13:56:10 -0500 Subject: [PATCH 004/511] 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 ca3c5342..2c3388b4 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,16 @@ CHANGELOG ========= +0.9.45 (2023-09-02) +------------------- + +* Add grid filter type for BigInteger columns. + +* Add products API route to fetch label profiles for use w/ printing. + +* Tweaks for cost editing within a receiving batch. + + 0.9.44 (2023-08-31) ------------------- diff --git a/tailbone/_version.py b/tailbone/_version.py index da259c44..f9f23ea3 100644 --- a/tailbone/_version.py +++ b/tailbone/_version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8; -*- -__version__ = '0.9.44' +__version__ = '0.9.45' From ecf46fa6fed1d817e9c8329395d7cb4c16143a11 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 7 Sep 2023 17:41:47 -0500 Subject: [PATCH 005/511] Improve display for member equity payments --- tailbone/views/members.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/tailbone/views/members.py b/tailbone/views/members.py index a004b5a3..0cacaf04 100644 --- a/tailbone/views/members.py +++ b/tailbone/views/members.py @@ -144,9 +144,10 @@ class MemberView(MasterView): rows_title = "Equity Payments" row_grid_columns = [ - 'amount', 'received', + 'amount', 'description', + 'source', 'transaction_identifier', ] @@ -408,18 +409,22 @@ class MemberEquityPaymentView(MasterView): has_versions = True grid_columns = [ + 'received', + '_member_key_', 'member', 'amount', - 'received', 'description', + 'source', 'transaction_identifier', ] form_fields = [ + '_member_key_', 'member', 'amount', 'received', 'description', + 'source', 'transaction_identifier', ] @@ -435,14 +440,31 @@ class MemberEquityPaymentView(MasterView): super().configure_grid(g) model = self.model + # member_key + field = self.get_member_key_field() + attr = getattr(model.Member, field) + g.set_renderer(field, self.render_member_key) + g.set_filter(field, attr, + label=self.get_member_key_label(), + default_active=True) + g.set_sorter(field, attr) + + # member (name) g.set_joiner('member', lambda q: q.outerjoin(model.Person)) g.set_sorter('member', model.Person.display_name) g.set_link('member') + g.set_filter('member', model.Person.display_name, + label="Member Name") g.set_type('amount', 'currency') g.set_sort_defaults('received', 'desc') g.set_link('received') + g.set_link('transaction_identifier') + + def render_member_key(self, payment, field): + key = getattr(payment.member, field) + return key def configure_form(self, f): super().configure_form(f) From f732e04f49b0d802f568c93256ba5b7cdfc3a386 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 7 Sep 2023 18:36:02 -0500 Subject: [PATCH 006/511] 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 2c3388b4..16b70517 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,12 @@ CHANGELOG ========= +0.9.46 (2023-09-07) +------------------- + +* Improve display for member equity payments. + + 0.9.45 (2023-09-02) ------------------- diff --git a/tailbone/_version.py b/tailbone/_version.py index f9f23ea3..f089641a 100644 --- a/tailbone/_version.py +++ b/tailbone/_version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8; -*- -__version__ = '0.9.45' +__version__ = '0.9.46' From f717bc47e56d48b210079a4ce7dce389ff4cff04 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 7 Sep 2023 20:57:33 -0500 Subject: [PATCH 007/511] Fallback to None when getting values for merge preview --- tailbone/views/master.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tailbone/views/master.py b/tailbone/views/master.py index 98408420..5027f230 100644 --- a/tailbone/views/master.py +++ b/tailbone/views/master.py @@ -2141,7 +2141,7 @@ class MasterView(View): if self.merge_handler: return self.merge_handler.get_merge_preview_data(obj) - return dict([(f, getattr(obj, f)) + return dict([(f, getattr(obj, f, None)) for f in self.get_merge_fields()]) def get_merge_resulting_data(self, remove, keep): From 84de5e09a2f81c878007dd00e22f2583e9caea7e Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 7 Sep 2023 21:00:40 -0500 Subject: [PATCH 008/511] 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 16b70517..2668119c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,12 @@ CHANGELOG ========= +0.9.47 (2023-09-07) +------------------- + +* Fallback to None when getting values for merge preview. + + 0.9.46 (2023-09-07) ------------------- diff --git a/tailbone/_version.py b/tailbone/_version.py index f089641a..bedab6b6 100644 --- a/tailbone/_version.py +++ b/tailbone/_version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8; -*- -__version__ = '0.9.46' +__version__ = '0.9.47' From 6e50288bd4f55921b682e5f29a0173611b7f43ad Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 8 Sep 2023 08:49:43 -0500 Subject: [PATCH 009/511] Add grid link for equity payment description --- tailbone/views/members.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tailbone/views/members.py b/tailbone/views/members.py index 0cacaf04..85ffa99c 100644 --- a/tailbone/views/members.py +++ b/tailbone/views/members.py @@ -460,6 +460,10 @@ class MemberEquityPaymentView(MasterView): g.set_sort_defaults('received', 'desc') g.set_link('received') + + # description + g.set_link('description') + g.set_link('transaction_identifier') def render_member_key(self, payment, field): From 7221400b8850710cc80a9220a545101a48986908 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 8 Sep 2023 10:56:25 -0500 Subject: [PATCH 010/511] Fix msg body display, download link for email bounces --- tailbone/templates/email-bounces/view.mako | 4 +- tailbone/views/bouncer.py | 45 ++++++++-------------- tailbone/views/master.py | 4 +- 3 files changed, 18 insertions(+), 35 deletions(-) diff --git a/tailbone/templates/email-bounces/view.mako b/tailbone/templates/email-bounces/view.mako index 610118ed..f8372c88 100644 --- a/tailbone/templates/email-bounces/view.mako +++ b/tailbone/templates/email-bounces/view.mako @@ -48,9 +48,7 @@ <%def name="render_this_page()"> ${parent.render_this_page()} - + diff --git a/tailbone/views/bouncer.py b/tailbone/views/bouncer.py index 628ed07c..3416bbed 100644 --- a/tailbone/views/bouncer.py +++ b/tailbone/views/bouncer.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,18 +24,12 @@ Views for Email Bounces """ -from __future__ import unicode_literals, absolute_import - import os import datetime -import six - from rattail.db import model -from rattail.bouncer import get_handler from rattail.bouncer.config import get_profile_keys -from pyramid.response import FileResponse from webhelpers2.html import HTML, tags from tailbone.views import MasterView @@ -50,6 +44,7 @@ class EmailBounceView(MasterView): url_prefix = '/email-bounces' creatable = False editable = False + downloadable = True labels = { 'config_key': "Source", @@ -70,7 +65,8 @@ class EmailBounceView(MasterView): self.handler_options = sorted(get_profile_keys(self.rattail_config)) def get_handler(self, bounce): - return get_handler(self.rattail_config, bounce.config_key) + app = self.get_rattail_app() + return app.get_bounce_handler(bounce.config_key) def configure_grid(self, g): super(EmailBounceView, self).configure_grid(g) @@ -142,11 +138,16 @@ class EmailBounceView(MasterView): path = handler.msgpath(bounce) if os.path.exists(path): with open(path, 'rb') as f: - kwargs['message'] = f.read() + # TODO: how to determine encoding? (is utf_8 guaranteed?) + kwargs['message'] = f.read().decode('utf_8') else: kwargs['message'] = "(file not found)" return kwargs + def download_path(self, bounce, filename): + handler = self.get_handler(bounce) + return handler.msgpath(bounce) + # TODO: should require POST here def process(self): """ @@ -169,20 +170,13 @@ class EmailBounceView(MasterView): self.request.session.flash("Email bounce has been marked UN-processed.") return self.redirect(self.get_action_url('view', bounce)) - def download(self): - """ - View for downloading the message file associated with a bounce. - """ - bounce = self.get_instance() - handler = self.get_handler(bounce) - path = handler.msgpath(bounce) - response = FileResponse(path, request=self.request) - response.headers[b'Content-Length'] = six.binary_type(os.path.getsize(path)) - response.headers[b'Content-Disposition'] = b'attachment; filename="bounce.eml"' - return response - @classmethod def defaults(cls, config): + cls._bounce_defaults(config) + cls._defaults(config) + + @classmethod + def _bounce_defaults(cls, config): config.add_tailbone_permission_group('emailbounces', "Email Bounces", overwrite=False) @@ -200,15 +194,6 @@ class EmailBounceView(MasterView): config.add_tailbone_permission('emailbounces', 'emailbounces.unprocess', "Mark Email Bounce as UN-processed") - # download raw email - config.add_route('emailbounces.download', '/email-bounces/{uuid}/download') - config.add_view(cls, attr='download', route_name='emailbounces.download', - permission='emailbounces.download') - config.add_tailbone_permission('emailbounces', 'emailbounces.download', - "Download raw message of Email Bounce") - - cls._defaults(config) - def defaults(config, **kwargs): base = globals() diff --git a/tailbone/views/master.py b/tailbone/views/master.py index 5027f230..4aacc9f1 100644 --- a/tailbone/views/master.py +++ b/tailbone/views/master.py @@ -1593,9 +1593,9 @@ class MasterView(View): """ obj = self.get_instance() filename = self.request.GET.get('filename', None) - if not filename: - raise self.notfound() path = self.download_path(obj, filename) + if not path or not os.path.exists(path): + raise self.notfound() response = FileResponse(path, request=self.request) response.content_length = os.path.getsize(path) content_type = self.download_content_type(path, filename) From 669e50e40658caca5dd66913739aa13a50fa3b8c Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 8 Sep 2023 19:53:10 -0500 Subject: [PATCH 011/511] Fix member key display for equity payment form --- tailbone/views/members.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tailbone/views/members.py b/tailbone/views/members.py index 85ffa99c..d2a0e455 100644 --- a/tailbone/views/members.py +++ b/tailbone/views/members.py @@ -475,6 +475,10 @@ class MemberEquityPaymentView(MasterView): model = self.model payment = f.model_instance + # member_key + field = self.get_member_key_field() + f.set_renderer(field, self.render_member_key) + # member if self.creating: f.replace('member', 'member_uuid') From c5344d2df62284bb5b12fbf99dd0f1331bcc122f Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 8 Sep 2023 19:55:14 -0500 Subject: [PATCH 012/511] 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 2668119c..fc858f6b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,16 @@ CHANGELOG ========= +0.9.48 (2023-09-08) +------------------- + +* Add grid link for equity payment description. + +* Fix msg body display, download link for email bounces. + +* Fix member key display for equity payment form. + + 0.9.47 (2023-09-07) ------------------- diff --git a/tailbone/_version.py b/tailbone/_version.py index bedab6b6..014d9357 100644 --- a/tailbone/_version.py +++ b/tailbone/_version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8; -*- -__version__ = '0.9.47' +__version__ = '0.9.48' From ccb4661b39641f52881f4fc6d6e4ae921be36212 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 9 Sep 2023 14:14:23 -0500 Subject: [PATCH 013/511] 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 014/511] 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 015/511] 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 016/511] 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 017/511] 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 018/511] 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 019/511] 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 020/511] 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 021/511] 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 @@