From 8b044dbb2202aa960b01c9a92537bef7d2cedd4f Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 18 Oct 2021 18:28:28 -0500 Subject: [PATCH] Add basic "price needs confirmation" support for custorder --- tailbone/templates/custorders/create.mako | 34 +++++++- tailbone/templates/custorders/items/view.mako | 77 ++++++++++++++++ tailbone/views/custorders/items.py | 87 +++++++++++++++++-- tailbone/views/custorders/orders.py | 27 +++++- 4 files changed, 214 insertions(+), 11 deletions(-) diff --git a/tailbone/templates/custorders/create.mako b/tailbone/templates/custorders/create.mako index 2e89783c..655caf2b 100644 --- a/tailbone/templates/custorders/create.mako +++ b/tailbone/templates/custorders/create.mako @@ -500,7 +500,8 @@ -
+
@@ -544,8 +545,28 @@ +
+ + + + $4.20 / EA + + + + 2021-01-01 + + + + This price is questionable and should be confirmed + by someone before order proceeds. + +
+
+
@@ -619,7 +640,9 @@ - {{ props.row.total_price_display }} + + {{ props.row.total_price_display }} + @@ -742,6 +765,7 @@ defaultUOM: defaultUOM, productUOM: defaultUOM, productCaseSize: null, + productPriceNeedsConfirmation: false, ## 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}, @@ -1271,6 +1295,7 @@ this.productQuantity = 1 this.productUnitChoices = this.defaultUnitChoices this.productUOM = this.defaultUOM + this.productPriceNeedsConfirmation = false this.showingItemDialog = true this.$nextTick(() => { this.$refs.productDescriptionAutocomplete.focus() @@ -1287,6 +1312,7 @@ this.productQuantity = row.order_quantity this.productUnitChoices = row.order_uom_choices this.productUOM = row.order_uom + this.productPriceNeedsConfirmation = row.price_needs_confirmation this.showingItemDialog = true }, @@ -1319,6 +1345,7 @@ this.productDisplay = null this.productUPC = null this.productUnitChoices = this.defaultUnitChoices + this.productPriceNeedsConfirmation = false if (autofocus) { this.$nextTick(() => { this.$refs.productUPCInput.focus() @@ -1358,6 +1385,7 @@ this.productUPC = response.data.upc_pretty this.productDisplay = response.data.full_description this.setProductUnitChoices(response.data.uom_choices) + this.productPriceNeedsConfirmation = false } }) }, @@ -1379,6 +1407,7 @@ this.productUPC = response.data.upc_pretty this.productDisplay = response.data.full_description this.setProductUnitChoices(response.data.uom_choices) + this.productPriceNeedsConfirmation = false }) } else { this.clearProduct() @@ -1392,6 +1421,7 @@ product_uuid: this.productUUID, order_quantity: this.productQuantity, order_uom: this.productUOM, + price_needs_confirmation: this.productPriceNeedsConfirmation, } if (this.editingItem) { diff --git a/tailbone/templates/custorders/items/view.mako b/tailbone/templates/custorders/items/view.mako index 533d8f18..030b0ade 100644 --- a/tailbone/templates/custorders/items/view.mako +++ b/tailbone/templates/custorders/items/view.mako @@ -4,6 +4,9 @@ <%def name="render_buefy_form()">
<${form.component} ref="mainForm" + % if master.has_perm('confirm_price'): + @confirm-price="showConfirmPrice" + % endif % if master.has_perm('change_status'): @change-status="showChangeStatus" % endif @@ -18,6 +21,45 @@ <%def name="page_content()"> ${parent.page_content()} + % if master.has_perm('confirm_price'): + + + + ${h.form(master.get_action_url('confirm_price', instance), ref='confirmPriceForm')} + ${h.csrf_token(request)} + ${h.hidden('note', **{':value': 'confirmPriceNote'})} + ${h.end_form()} + % endif + % if master.has_perm('change_status'):
@@ -190,6 +232,41 @@ ${form.component_studly}Data.notesData = ${json.dumps(notes_data)|n} + % if master.has_perm('confirm_price'): + + ThisPageData.confirmPriceShowDialog = false + ThisPageData.confirmPriceNote = null + ThisPageData.confirmPriceSubmitting = false + + ThisPage.computed.confirmPriceSaveDisabled = function() { + if (this.confirmPriceSubmitting) { + return true + } + return false + } + + ThisPage.computed.confirmPriceSubmitText = function() { + if (this.confirmPriceSubmitting) { + return "Working, please wait..." + } + return "Confirm Price" + } + + ThisPage.methods.showConfirmPrice = function() { + this.confirmPriceNote = null + this.confirmPriceShowDialog = true + this.$nextTick(() => { + this.$refs.confirmPriceNoteField.focus() + }) + } + + ThisPage.methods.confirmPriceSave = function() { + this.confirmPriceSubmitting = true + this.$refs.confirmPriceForm.submit() + } + + % endif + % if master.has_perm('change_status'): ThisPageData.orderItemStatuses = ${json.dumps(enum.CUSTORDER_ITEM_STATUS)|n} diff --git a/tailbone/views/custorders/items.py b/tailbone/views/custorders/items.py index 2dcd43a5..737b1c20 100644 --- a/tailbone/views/custorders/items.py +++ b/tailbone/views/custorders/items.py @@ -98,6 +98,7 @@ class CustomerOrderItemView(MasterView): 'case_quantity', 'unit_price', 'total_price', + 'price_needs_confirmation', 'paid_amount', 'status_code', 'notes', @@ -135,7 +136,7 @@ class CustomerOrderItemView(MasterView): g.set_renderer('person', self.render_person_text) g.set_renderer('order_created', self.render_order_created) - g.set_enum('status_code', self.enum.CUSTORDER_ITEM_STATUS) + g.set_renderer('status_code', self.render_status_code_column) g.set_label('person', "Person Name") g.set_label('product_brand', "Brand") @@ -160,6 +161,13 @@ class CustomerOrderItemView(MasterView): value = localtime(self.rattail_config, item.order.created, from_utc=True) return raw_datetime(self.rattail_config, value) + def render_status_code_column(self, item, field): + text = self.enum.CUSTORDER_ITEM_STATUS.get(item.status_code, + six.text_type(item.status_code)) + if item.status_text: + return HTML.tag('span', title=item.status_text, c=[text]) + return text + def configure_form(self, f): super(CustomerOrderItemView, self).configure_form(f) use_buefy = self.get_use_buefy() @@ -178,12 +186,12 @@ class CustomerOrderItemView(MasterView): f.set_type('cases_ordered', 'quantity') f.set_type('units_ordered', 'quantity') f.set_type('order_quantity', 'quantity') - f.set_enum('order_uom', self.enum.UNIT_OF_MEASURE) - # currency fields - f.set_type('unit_price', 'currency') - f.set_type('total_price', 'currency') + # price fields + f.set_renderer('unit_price', self.render_price_with_confirmation) + f.set_renderer('total_price', self.render_price_with_confirmation) + f.set_renderer('price_needs_confirmation', self.render_price_needs_confirmation) f.set_type('paid_amount', 'currency') # person @@ -198,9 +206,37 @@ class CustomerOrderItemView(MasterView): else: f.remove('notes') + def render_price_with_confirmation(self, item, field): + price = getattr(item, field) + app = self.get_rattail_app() + text = app.render_currency(price) + if item.price_needs_confirmation: + return HTML.tag('span', class_='has-background-warning', + c=[text]) + return text + + def render_price_needs_confirmation(self, item, field): + + value = item.price_needs_confirmation + text = "Yes" if value else "No" + items = [text] + + if value and self.has_perm('confirm_price'): + button = HTML.tag('b-button', type='is-primary', c="Confirm Price", + style='margin-left: 1rem;', + icon_pack='fas', icon_left='check', + **{'@click': "$emit('confirm-price')"}) + items.append(button) + + left = HTML.tag('div', class_='level-left', c=items) + outer = HTML.tag('div', class_='level', c=[left]) + return outer + def render_status_code(self, item, field): use_buefy = self.get_use_buefy() text = self.enum.CUSTORDER_ITEM_STATUS[item.status_code] + if item.status_text: + text = "{} ({})".format(text, item.status_text) items = [HTML.tag('span', c=[text])] if use_buefy and self.has_perm('change_status'): @@ -298,6 +334,32 @@ class CustomerOrderItemView(MasterView): }) return notes + def confirm_price(self): + """ + View for confirming price of an order item. + """ + item = self.get_instance() + redirect = self.redirect(self.get_action_url('view', item)) + + # locate user responsible for change + user = self.request.user + + # grab user-provided note to attach to event + note = self.request.POST.get('note') + + # declare item no longer in need of price confirmation + item.price_needs_confirmation = False + item.add_event(self.enum.CUSTORDER_ITEM_EVENT_PRICE_CONFIRMED, + user, note=note) + + # advance item to next status + if item.status_code == self.enum.CUSTORDER_ITEM_STATUS_INITIATED: + item.status_code = self.enum.CUSTORDER_ITEM_STATUS_READY + item.status_text = "price has been confirmed" + + self.request.session.flash("Price has been confirmed.") + return redirect + def change_status(self): """ View for changing status of one or more order items. @@ -342,6 +404,9 @@ class CustomerOrderItemView(MasterView): # change status item.status_code = new_status_code + # nb. must blank this out, b/c user cannot specify new + # text and the old text no longer applies + item.status_text = None self.request.session.flash("Status has been updated to: {}".format( self.enum.CUSTORDER_ITEM_STATUS[new_status_code])) @@ -418,11 +483,23 @@ class CustomerOrderItemView(MasterView): 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() model_title_plural = cls.get_model_title_plural() # fix permission group name config.add_tailbone_permission_group(permission_prefix, model_title_plural) + # confirm price + config.add_tailbone_permission(permission_prefix, + '{}.confirm_price'.format(permission_prefix), + "Confirm price for a {}".format(model_title)) + config.add_route('{}.confirm_price'.format(route_prefix), + '{}/confirm-price'.format(instance_url_prefix), + request_method='POST') + config.add_view(cls, attr='confirm_price', + route_name='{}.confirm_price'.format(route_prefix), + permission='{}.confirm_price'.format(permission_prefix)) + # change status config.add_tailbone_permission(permission_prefix, '{}.change_status'.format(permission_prefix), diff --git a/tailbone/views/custorders/orders.py b/tailbone/views/custorders/orders.py index 0b72f377..0153126a 100644 --- a/tailbone/views/custorders/orders.py +++ b/tailbone/views/custorders/orders.py @@ -36,7 +36,7 @@ from rattail.db import model from rattail.util import pretty_quantity from rattail.batch import get_batch_handler -from webhelpers2.html import tags +from webhelpers2.html import tags, HTML from tailbone.db import Session from tailbone.views import MasterView @@ -189,10 +189,10 @@ class CustomerOrderView(MasterView): g.set_type('order_quantity', 'quantity') g.set_type('cases_ordered', 'quantity') g.set_type('units_ordered', 'quantity') - g.set_type('total_price', 'currency') + g.set_renderer('total_price', self.render_price_with_confirmation) g.set_enum('order_uom', self.enum.UNIT_OF_MEASURE) - g.set_enum('status_code', self.enum.CUSTORDER_ITEM_STATUS) + g.set_renderer('status_code', self.render_row_status_code) g.set_label('sequence', "Seq.") g.filters['sequence'].label = "Sequence" @@ -206,6 +206,22 @@ class CustomerOrderView(MasterView): g.set_link('product_brand') g.set_link('product_description') + def render_price_with_confirmation(self, item, field): + price = getattr(item, field) + app = self.get_rattail_app() + text = app.render_currency(price) + if item.price_needs_confirmation: + return HTML.tag('span', class_='has-background-warning', + c=[text]) + return text + + def render_row_status_code(self, item, field): + text = self.enum.CUSTORDER_ITEM_STATUS.get(item.status_code, + six.text_type(item.status_code)) + if item.status_text: + return HTML.tag('span', title=item.status_text, c=[text]) + return text + def get_batch_handler(self): return get_batch_handler( self.rattail_config, 'custorder', @@ -641,6 +657,7 @@ class CustomerOrderView(MasterView): 'unit_price_display': "${:0.2f}".format(row.unit_price) if row.unit_price is not None else None, 'total_price': six.text_type(row.total_price) if row.total_price is not None else None, 'total_price_display': "${:0.2f}".format(row.total_price) if row.total_price is not None else None, + 'price_needs_confirmation': row.price_needs_confirmation, 'status_code': row.status_code, 'status_text': row.status_text, @@ -684,7 +701,8 @@ class CustomerOrderView(MasterView): row = self.handler.add_product(batch, product, decimal.Decimal(data.get('order_quantity') or '0'), - data.get('order_uom')) + data.get('order_uom'), + price_needs_confirmation=data.get('price_needs_confirmation')) self.Session.flush() else: # product is not known @@ -719,6 +737,7 @@ class CustomerOrderView(MasterView): row.product = product row.order_quantity = decimal.Decimal(data.get('order_quantity') or '0') row.order_uom = data.get('order_uom') + row.price_needs_confirmation = data.get('price_needs_confirmation') self.handler.refresh_row(row) self.Session.flush() self.Session.refresh(row)