diff --git a/tailbone/templates/custorders/items/view.mako b/tailbone/templates/custorders/items/view.mako new file mode 100644 index 00000000..533d8f18 --- /dev/null +++ b/tailbone/templates/custorders/items/view.mako @@ -0,0 +1,317 @@ +## -*- coding: utf-8; -*- +<%inherit file="/master/view.mako" /> + +<%def name="render_buefy_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 + > + +
+ + +<%def name="page_content()"> + ${parent.page_content()} + + % if master.has_perm('change_status'): + +
+
+
+
+ +
+ Current status is:  +
+ +
+ {{ orderItemStatuses[oldStatusCode] }} +
+ +
+ New status will be: +
+ + + + + + + +
+
+ +
+ +

+ Please indicate any other item(s) to which the new + status should be applied: +

+ + + + + +
+
+ +

+ Please provide a note + (will be applied to all selected items): +

+ + + +
+ +
+ + {{ changeStatusSubmitText }} + + + Cancel + +
+ +
+
+
+ ${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'): + + + + % endif + + +<%def name="modify_this_page_vars()"> + ${parent.modify_this_page_vars()} + + + +${parent.body()} diff --git a/tailbone/views/custorders/items.py b/tailbone/views/custorders/items.py index 8756d538..2dcd43a5 100644 --- a/tailbone/views/custorders/items.py +++ b/tailbone/views/custorders/items.py @@ -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 diff --git a/tailbone/views/custorders/orders.py b/tailbone/views/custorders/orders.py index 4bab7740..4ae9666f 100644 --- a/tailbone/views/custorders/orders.py +++ b/tailbone/views/custorders/orders.py @@ -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')