Allow changing status, adding notes for customer order items

This commit is contained in:
Lance Edgar 2021-09-27 13:25:02 -04:00
parent 7c6c2f7ded
commit ab517d1199
3 changed files with 555 additions and 12 deletions

View file

@ -0,0 +1,317 @@
## -*- coding: utf-8; -*-
<%inherit file="/master/view.mako" />
<%def name="render_buefy_form()">
<div class="form">
<${form.component} ref="mainForm"
% if master.has_perm('change_status'):
@change-status="showChangeStatus"
% endif
% if master.has_perm('add_note'):
@add-note="showAddNote"
% endif
>
</${form.component}>
</div>
</%def>
<%def name="page_content()">
${parent.page_content()}
% if master.has_perm('change_status'):
<b-modal :active.sync="showChangeStatusDialog">
<div class="card">
<div class="card-content">
<div class="level">
<div class="level-left">
<div class="level-item">
Current status is:&nbsp;
</div>
<div class="level-item has-text-weight-bold">
{{ orderItemStatuses[oldStatusCode] }}
</div>
<div class="level-item"
style="margin-left: 5rem;">
New status will be:
</div>
<b-field class="level-item"
:type="newStatusCode ? null : 'is-danger'">
<b-select v-model="newStatusCode">
<option v-for="item in orderItemStatusOptions"
:key="item.key"
:value="item.key">
{{ item.label }}
</option>
</b-select>
</b-field>
</div>
</div>
<div v-if="changeStatusGridData.length">
<p class="block">
Please indicate any other item(s) to which the new
status should be applied:
</p>
<b-table :data="changeStatusGridData"
checkable
:checked-rows.sync="changeStatusCheckedRows"
narrowed
class="is-size-7">
<template slot-scope="props">
<b-table-column field="product_brand" label="Brand">
<span v-html="props.row.product_brand"></span>
</b-table-column>
<b-table-column field="product_description" label="Product">
<span v-html="props.row.product_description"></span>
</b-table-column>
<!-- <b-table-column field="quantity" label="Quantity"> -->
<!-- <span v-html="props.row.quantity"></span> -->
<!-- </b-table-column> -->
<b-table-column field="product_case_quantity" label="cPack">
<span v-html="props.row.product_case_quantity"></span>
</b-table-column>
<b-table-column field="order_quantity" label="oQty">
<span v-html="props.row.order_quantity"></span>
</b-table-column>
<b-table-column field="order_uom" label="UOM">
<span v-html="props.row.order_uom"></span>
</b-table-column>
<b-table-column field="department_name" label="Department">
<span v-html="props.row.department_name"></span>
</b-table-column>
<b-table-column field="product_barcode" label="Product Barcode">
<span v-html="props.row.product_barcode"></span>
</b-table-column>
<b-table-column field="unit_price" label="Unit $">
<span v-html="props.row.unit_price"></span>
</b-table-column>
<b-table-column field="total_price" label="Total $">
<span v-html="props.row.total_price"></span>
</b-table-column>
<b-table-column field="order_date" label="Order Date">
<span v-html="props.row.order_date"></span>
</b-table-column>
<b-table-column field="status_code" label="Status">
<span v-html="props.row.status_code"></span>
</b-table-column>
<!-- <b-table-column field="flagged" label="Flagged"> -->
<!-- <span v-html="props.row.flagged"></span> -->
<!-- </b-table-column> -->
</template>
</b-table>
<br />
</div>
<p>
Please provide a note<span v-if="changeStatusGridData.length">
(will be applied to all selected items)</span>:
</p>
<b-input v-model="newStatusNote"
type="textarea" rows="2">
</b-input>
<br />
<div class="buttons">
<b-button type="is-primary"
:disabled="changeStatusSaveDisabled"
icon-pack="fas"
icon-left="save"
@click="statusChangeSave()">
{{ changeStatusSubmitText }}
</b-button>
<b-button @click="cancelStatusChange">
Cancel
</b-button>
</div>
</div>
</div>
</b-modal>
${h.form(master.get_action_url('change_status', instance), ref='changeStatusForm')}
${h.csrf_token(request)}
${h.hidden('new_status_code', **{'v-model': 'newStatusCode'})}
${h.hidden('uuids', **{':value': 'changeStatusCheckedRows.map((row) => {return row.uuid}).join()'})}
${h.hidden('note', **{':value': 'newStatusNote'})}
${h.end_form()}
% endif
% if master.has_perm('add_note'):
<b-modal has-modal-card
:active.sync="showAddNoteDialog">
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Add Note</p>
</header>
<section class="modal-card-body">
<b-field>
<b-input type="textarea" rows="8"
v-model="newNoteText"
ref="newNoteTextArea">
</b-input>
</b-field>
<b-field>
<b-checkbox v-model="newNoteApplyAll">
Apply to all items on this order
</b-checkbox>
</b-field>
</section>
<footer class="modal-card-foot">
<b-button type="is-primary"
@click="addNoteSave()"
:disabled="addNoteSaveDisabled"
icon-pack="fas"
icon-left="save">
{{ addNoteSubmitText }}
</b-button>
<b-button @click="showAddNoteDialog = false">
Cancel
</b-button>
</footer>
</div>
</b-modal>
% endif
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
${form.component_studly}Data.notesData = ${json.dumps(notes_data)|n}
% if master.has_perm('change_status'):
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}
ThisPageData.oldStatusCode = ${instance.status_code}
ThisPageData.showChangeStatusDialog = false
ThisPageData.newStatusCode = null
ThisPageData.changeStatusGridData = ${json.dumps(other_order_items_data)|n}
ThisPageData.changeStatusCheckedRows = []
ThisPageData.newStatusNote = null
ThisPageData.changeStatusSubmitText = "Update Status"
ThisPageData.changeStatusSubmitting = false
ThisPage.computed.changeStatusSaveDisabled = function() {
if (!this.newStatusCode) {
return true
}
if (this.changeStatusSubmitting) {
return true
}
return false
}
ThisPage.methods.showChangeStatus = function() {
this.newStatusCode = null
// clear out any checked rows
this.changeStatusCheckedRows.length = 0
this.newStatusNote = null
this.showChangeStatusDialog = true
}
ThisPage.methods.cancelStatusChange = function() {
this.showChangeStatusDialog = false
}
ThisPage.methods.statusChangeSave = function() {
if (this.newStatusCode == this.oldStatusCode) {
alert("You chose the same status it already had...")
return
}
this.changeStatusSubmitting = true
this.changeStatusSubmitText = "Working, please wait..."
this.$refs.changeStatusForm.submit()
}
% endif
% if master.has_perm('add_note'):
ThisPageData.showAddNoteDialog = false
ThisPageData.newNoteText = null
ThisPageData.newNoteApplyAll = false
ThisPageData.addNoteSubmitting = false
ThisPageData.addNoteSubmitText = "Save Note"
ThisPage.computed.addNoteSaveDisabled = function() {
if (!this.newNoteText) {
return true
}
if (this.addNoteSubmitting) {
return true
}
return false
}
ThisPage.methods.showAddNote = function() {
this.newNoteText = null
this.newNoteApplyAll = false
this.showAddNoteDialog = true
this.$nextTick(() => {
this.$refs.newNoteTextArea.focus()
})
}
ThisPage.methods.addNoteSave = function() {
this.addNoteSubmitting = true
this.addNoteSubmitText = "Working, please wait..."
let url = '${url('{}.add_note'.format(route_prefix), uuid=instance.uuid)}'
let params = {
note: this.newNoteText,
apply_all: this.newNoteApplyAll,
}
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(({ data }) => {
if (data.success) {
this.$refs.mainForm.notesData = data.notes
this.showAddNoteDialog = false
} else {
this.$buefy.toast.open({
message: "Save failed: " + (data.error || "(unknown error)"),
type: 'is-danger',
duration: 4000, // 4 seconds
})
}
this.addNoteSubmitting = false
this.addNoteSubmitText = "Save Note"
}).catch((error) => {
// TODO: should handle this better somehow..?
this.$buefy.toast.open({
message: "Save failed: (unknown error)",
type: 'is-danger',
duration: 4000, // 4 seconds
})
this.addNoteSubmitting = false
this.addNoteSubmitText = "Save Note"
})
}
% endif
</script>
</%def>
${parent.body()}

View file

@ -34,7 +34,7 @@ from sqlalchemy import orm
from rattail.db import model from rattail.db import model
from rattail.time import localtime from rattail.time import localtime
from webhelpers2.html import tags from webhelpers2.html import HTML, tags
from tailbone.views import MasterView from tailbone.views import MasterView
from tailbone.util import raw_datetime from tailbone.util import raw_datetime
@ -54,6 +54,7 @@ class CustomerOrderItemView(MasterView):
labels = { labels = {
'order_id': "Order ID", 'order_id': "Order ID",
'order_uom': "Order UOM", 'order_uom': "Order UOM",
'status_code': "Status",
} }
grid_columns = [ grid_columns = [
@ -99,6 +100,7 @@ class CustomerOrderItemView(MasterView):
'total_price', 'total_price',
'paid_amount', 'paid_amount',
'status_code', 'status_code',
'notes',
] ]
def query(self, session): def query(self, session):
@ -139,7 +141,6 @@ class CustomerOrderItemView(MasterView):
g.set_label('product_brand', "Brand") g.set_label('product_brand', "Brand")
g.set_label('product_description', "Description") g.set_label('product_description', "Description")
g.set_label('product_size', "Size") g.set_label('product_size', "Size")
g.set_label('status_code', "Status")
g.set_link('order_id') g.set_link('order_id')
g.set_link('person') g.set_link('person')
@ -161,6 +162,7 @@ class CustomerOrderItemView(MasterView):
def configure_form(self, f): def configure_form(self, f):
super(CustomerOrderItemView, self).configure_form(f) super(CustomerOrderItemView, self).configure_form(f)
use_buefy = self.get_use_buefy()
# order # order
f.set_renderer('order', self.render_order) f.set_renderer('order', self.render_order)
@ -187,10 +189,193 @@ class CustomerOrderItemView(MasterView):
# person # person
f.set_renderer('person', self.render_person) f.set_renderer('person', self.render_person)
f.set_enum('status_code', self.enum.CUSTORDER_ITEM_STATUS) # status_code
f.set_renderer('status_code', self.render_status_code)
# label overrides # notes
f.set_label('status_code', "Status") if use_buefy:
f.set_renderer('notes', self.render_notes)
else:
f.remove('notes')
def render_status_code(self, item, field):
use_buefy = self.get_use_buefy()
text = self.enum.CUSTORDER_ITEM_STATUS[item.status_code]
items = [HTML.tag('span', c=[text])]
if use_buefy and self.has_perm('change_status'):
button = HTML.tag('b-button', type='is-primary', c="Change Status",
style='margin-left: 1rem;',
icon_pack='fas', icon_left='edit',
**{'@click': "$emit('change-status')"})
items.append(button)
left = HTML.tag('div', class_='level-left', c=items)
outer = HTML.tag('div', class_='level', c=[left])
return outer
def render_notes(self, item, field):
route_prefix = self.get_route_prefix()
factory = self.get_grid_factory()
g = factory(
key='{}.notes'.format(route_prefix),
data=[],
columns=[
'text',
'created_by',
'created',
],
labels={
'text': "Note",
},
)
table = HTML.literal(
g.render_buefy_table_element(data_prop='notesData'))
elements = [table]
if self.has_perm('add_note'):
button = HTML.tag('b-button', type='is-primary', c="Add Note",
class_='is-pulled-right',
icon_pack='fas', icon_left='plus',
**{'@click': "$emit('add-note')"})
button_wrapper = HTML.tag('div', c=[button],
style='margin-top: 0.5rem;')
elements.append(button_wrapper)
return HTML.tag('div',
style='display: flex; flex-direction: column;',
c=elements)
def template_kwargs_view(self, **kwargs):
kwargs = super(CustomerOrderItemView, self).template_kwargs_view(**kwargs)
app = self.get_rattail_app()
item = kwargs['instance']
# fetch notes for current item
kwargs['notes_data'] = self.get_context_notes(item)
# fetch "other" order items, siblings of current one
order = item.order
other_items = self.Session.query(model.CustomerOrderItem)\
.filter(model.CustomerOrderItem.order == order)\
.filter(model.CustomerOrderItem.uuid != item.uuid)\
.all()
other_data = []
for other in other_items:
order_date = None
if order.created:
order_date = localtime(self.rattail_config, order.created, from_utc=True).date()
other_data.append({
'uuid': other.uuid,
'brand_name': other.product_brand,
'product_description': other.product_description,
'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],
'department_name': other.department_name,
'product_barcode': other.product_upc.pretty() if other.product_upc else None,
'unit_price': app.render_currency(other.unit_price),
'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],
})
kwargs['other_order_items_data'] = other_data
return kwargs
def get_context_notes(self, item):
notes = []
for note in reversed(item.notes):
created = localtime(self.rattail_config, note.created, from_utc=True)
notes.append({
'created': raw_datetime(self.rattail_config, created),
'created_by': note.created_by.display_name,
'text': note.text,
})
return notes
def change_status(self):
"""
View for changing status of one or more order items.
"""
order_item = self.get_instance()
redirect = self.redirect(self.get_action_url('view', order_item))
# validate new status
new_status_code = int(self.request.POST['new_status_code'])
if new_status_code not in self.enum.CUSTORDER_ITEM_STATUS:
self.request.session.flash("Invalid status code", 'error')
return redirect
# locate order items to which new status will be applied
order_items = [order_item]
uuids = self.request.POST['uuids']
if uuids:
for uuid in uuids.split(','):
item = self.Session.query(model.CustomerOrderItem).get(uuid)
if item:
order_items.append(item)
# locate user responsible for change
user = self.request.user
# maybe grab extra user-provided note to attach
extra_note = self.request.POST.get('note')
# apply new status to order item(s)
for item in order_items:
if item.status_code != new_status_code:
# attach event
note = "status changed from \"{}\" to \"{}\"".format(
self.enum.CUSTORDER_ITEM_STATUS[item.status_code],
self.enum.CUSTORDER_ITEM_STATUS[new_status_code])
if extra_note:
note = "{} - NOTE: {}".format(note, extra_note)
item.events.append(model.CustomerOrderItemEvent(
type_code=self.enum.CUSTORDER_ITEM_EVENT_STATUS_CHANGE,
user=user, note=note))
# change status
item.status_code = new_status_code
self.request.session.flash("Status has been updated to: {}".format(
self.enum.CUSTORDER_ITEM_STATUS[new_status_code]))
return redirect
def add_note(self):
"""
View for adding a new note to current order item, optinally
also adding it to all other items under the parent order.
"""
order_item = self.get_instance()
data = self.request.json_body
new_note = data['note']
apply_all = data['apply_all'] == True
user = self.request.user
if apply_all:
order_items = order_item.order.items
else:
order_items = [order_item]
for item in order_items:
item.notes.append(model.CustomerOrderItemNote(
created_by=user, text=new_note))
# # attach event
# item.events.append(model.CustomerOrderItemEvent(
# type_code=self.enum.CUSTORDER_ITEM_EVENT_ADDED_NOTE,
# user=user, note=new_note))
self.Session.flush()
self.Session.refresh(order_item)
return {'success': True,
'notes': self.get_context_notes(order_item)}
def render_order(self, item, field): def render_order(self, item, field):
order = item.order order = item.order
@ -210,16 +395,58 @@ class CustomerOrderItemView(MasterView):
def get_row_data(self, item): def get_row_data(self, item):
return self.Session.query(model.CustomerOrderItemEvent)\ return self.Session.query(model.CustomerOrderItemEvent)\
.filter(model.CustomerOrderItemEvent.item == item)\ .filter(model.CustomerOrderItemEvent.item == item)\
.order_by(model.CustomerOrderItemEvent.occurred, .order_by(model.CustomerOrderItemEvent.occurred.desc(),
model.CustomerOrderItemEvent.type_code) model.CustomerOrderItemEvent.type_code)
def configure_row_grid(self, g): def configure_row_grid(self, g):
super(CustomerOrderItemView, self).configure_row_grid(g) super(CustomerOrderItemView, self).configure_row_grid(g)
g.set_enum('type_code', self.enum.CUSTORDER_ITEM_EVENT)
g.set_label('occurred', "When") g.set_label('occurred', "When")
g.set_label('type_code', "What") # TODO: enum renderer g.set_label('type_code', "What") # TODO: enum renderer
g.set_label('user', "Who") g.set_label('user', "Who")
g.set_label('note', "Notes") g.set_label('note', "Notes")
@classmethod
def defaults(cls, config):
cls._order_item_defaults(config)
cls._defaults(config)
@classmethod
def _order_item_defaults(cls, config):
route_prefix = cls.get_route_prefix()
instance_url_prefix = cls.get_instance_url_prefix()
permission_prefix = cls.get_permission_prefix()
model_title_plural = cls.get_model_title_plural()
# fix permission group name
config.add_tailbone_permission_group(permission_prefix, model_title_plural)
# change status
config.add_tailbone_permission(permission_prefix,
'{}.change_status'.format(permission_prefix),
"Change status for 1 or more {}".format(model_title_plural))
config.add_route('{}.change_status'.format(route_prefix),
'{}/change-status'.format(instance_url_prefix),
request_method='POST')
config.add_view(cls, attr='change_status',
route_name='{}.change_status'.format(route_prefix),
permission='{}.change_status'.format(permission_prefix))
# add note
config.add_tailbone_permission(permission_prefix,
'{}.add_note'.format(permission_prefix),
"Add arbitrary notes for {}".format(model_title_plural))
config.add_route('{}.add_note'.format(route_prefix),
'{}/add-note'.format(instance_url_prefix),
request_method='POST')
config.add_view(cls, attr='add_note',
route_name='{}.add_note'.format(route_prefix),
renderer='json',
permission='{}.add_note'.format(permission_prefix))
# TODO: deprecate / remove this # TODO: deprecate / remove this
CustomerOrderItemsView = CustomerOrderItemView CustomerOrderItemsView = CustomerOrderItemView

View file

@ -50,6 +50,11 @@ class CustomerOrderView(MasterView):
route_prefix = 'custorders' route_prefix = 'custorders'
editable = False editable = False
labels = {
'id': "ID",
'status_code': "Status",
}
grid_columns = [ grid_columns = [
'id', 'id',
'customer', 'customer',
@ -117,10 +122,6 @@ class CustomerOrderView(MasterView):
g.set_sort_defaults('created', 'desc') g.set_sort_defaults('created', 'desc')
# TODO: enum choices renderer
g.set_label('status_code', "Status")
g.set_label('id', "ID")
g.set_link('id') g.set_link('id')
g.set_link('customer') g.set_link('customer')
g.set_link('person') g.set_link('person')
@ -129,7 +130,6 @@ class CustomerOrderView(MasterView):
super(CustomerOrderView, self).configure_form(f) super(CustomerOrderView, self).configure_form(f)
f.set_readonly('id') f.set_readonly('id')
f.set_label('id', "ID")
f.set_renderer('store', self.render_store) f.set_renderer('store', self.render_store)
f.set_renderer('customer', self.render_customer) f.set_renderer('customer', self.render_customer)
@ -138,7 +138,6 @@ class CustomerOrderView(MasterView):
f.set_type('total_price', 'currency') f.set_type('total_price', 'currency')
f.set_enum('status_code', self.enum.CUSTORDER_STATUS) f.set_enum('status_code', self.enum.CUSTORDER_STATUS)
f.set_label('status_code', "Status")
f.set_readonly('created') f.set_readonly('created')