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.time import localtime
from webhelpers2.html import tags
from webhelpers2.html import HTML, tags
from tailbone.views import MasterView
from tailbone.util import raw_datetime
@ -54,6 +54,7 @@ class CustomerOrderItemView(MasterView):
labels = {
'order_id': "Order ID",
'order_uom': "Order UOM",
'status_code': "Status",
}
grid_columns = [
@ -99,6 +100,7 @@ class CustomerOrderItemView(MasterView):
'total_price',
'paid_amount',
'status_code',
'notes',
]
def query(self, session):
@ -139,7 +141,6 @@ class CustomerOrderItemView(MasterView):
g.set_label('product_brand', "Brand")
g.set_label('product_description', "Description")
g.set_label('product_size', "Size")
g.set_label('status_code', "Status")
g.set_link('order_id')
g.set_link('person')
@ -161,6 +162,7 @@ class CustomerOrderItemView(MasterView):
def configure_form(self, f):
super(CustomerOrderItemView, self).configure_form(f)
use_buefy = self.get_use_buefy()
# order
f.set_renderer('order', self.render_order)
@ -187,10 +189,193 @@ class CustomerOrderItemView(MasterView):
# 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
f.set_label('status_code', "Status")
# notes
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):
order = item.order
@ -210,16 +395,58 @@ 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,
.order_by(model.CustomerOrderItemEvent.occurred.desc(),
model.CustomerOrderItemEvent.type_code)
def configure_row_grid(self, 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('type_code', "What") # TODO: enum renderer
g.set_label('user', "Who")
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
CustomerOrderItemsView = CustomerOrderItemView

View file

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