feat: add tools to change order item status; add notes
This commit is contained in:
		
							parent
							
								
									b4deea76e0
								
							
						
					
					
						commit
						9d378a0c5f
					
				
					 6 changed files with 520 additions and 18 deletions
				
			
		| 
						 | 
				
			
			@ -370,8 +370,8 @@ ORDER_ITEM_EVENT = OrderedDict([
 | 
			
		|||
    (ORDER_ITEM_EVENT_CONTACTED,            "customer contacted"),
 | 
			
		||||
    (ORDER_ITEM_EVENT_CONTACT_FAILED,       "contact failed"),
 | 
			
		||||
    (ORDER_ITEM_EVENT_DELIVERED,            "delivered"),
 | 
			
		||||
    (ORDER_ITEM_EVENT_STATUS_CHANGE,        "status change"),
 | 
			
		||||
    (ORDER_ITEM_EVENT_NOTE_ADDED,           "note added"),
 | 
			
		||||
    (ORDER_ITEM_EVENT_STATUS_CHANGE,        "changed status"),
 | 
			
		||||
    (ORDER_ITEM_EVENT_NOTE_ADDED,           "added note"),
 | 
			
		||||
    (ORDER_ITEM_EVENT_CANCELED,             "canceled"),
 | 
			
		||||
    (ORDER_ITEM_EVENT_REFUND_PENDING,       "refund pending"),
 | 
			
		||||
    (ORDER_ITEM_EVENT_REFUNDED,             "refunded"),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -75,3 +75,24 @@ class OrderHandler(GenericHandler):
 | 
			
		|||
        unit_qty = self.app.render_quantity(order_qty)
 | 
			
		||||
        EA = enum.ORDER_UOM[enum.ORDER_UOM_UNIT]
 | 
			
		||||
        return f"{unit_qty} {EA}"
 | 
			
		||||
 | 
			
		||||
    def item_status_to_variant(self, status_code):
 | 
			
		||||
        """
 | 
			
		||||
        Return a Buefy style variant for the given status code.
 | 
			
		||||
 | 
			
		||||
        Default logic will return ``None`` for "normal" item status,
 | 
			
		||||
        but may return ``'warning'`` for some (e.g. canceled).
 | 
			
		||||
 | 
			
		||||
        :param status_code: The status code for an order item.
 | 
			
		||||
 | 
			
		||||
        :returns: Style variant string (e.g. ``'warning'``) or
 | 
			
		||||
           ``None``.
 | 
			
		||||
        """
 | 
			
		||||
        enum = self.app.enum
 | 
			
		||||
        if status_code in (enum.ORDER_ITEM_STATUS_CANCELED,
 | 
			
		||||
                           enum.ORDER_ITEM_STATUS_REFUND_PENDING,
 | 
			
		||||
                           enum.ORDER_ITEM_STATUS_REFUNDED,
 | 
			
		||||
                           enum.ORDER_ITEM_STATUS_RESTOCKED,
 | 
			
		||||
                           enum.ORDER_ITEM_STATUS_EXPIRED,
 | 
			
		||||
                           enum.ORDER_ITEM_STATUS_INACTIVE):
 | 
			
		||||
            return 'warning'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,11 +1,16 @@
 | 
			
		|||
## -*- coding: utf-8; -*-
 | 
			
		||||
<%inherit file="/master/view.mako" />
 | 
			
		||||
 | 
			
		||||
<%def name="content_title()">
 | 
			
		||||
  (${app.enum.ORDER_ITEM_STATUS[item.status_code]})
 | 
			
		||||
  ${instance_title}
 | 
			
		||||
</%def>
 | 
			
		||||
 | 
			
		||||
<%def name="extra_styles()">
 | 
			
		||||
  ${parent.extra_styles()}
 | 
			
		||||
  <style>
 | 
			
		||||
 | 
			
		||||
    .field .field-label .label {
 | 
			
		||||
    nav .field .field-label .label {
 | 
			
		||||
        white-space: nowrap;
 | 
			
		||||
        width: 10rem;
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -39,7 +44,86 @@
 | 
			
		|||
              <span>${app.render_currency(item.paid_amount)}</span>
 | 
			
		||||
            </b-field>
 | 
			
		||||
            <b-field horizontal label="Status">
 | 
			
		||||
              <span>${app.enum.ORDER_ITEM_STATUS[item.status_code]}</span>
 | 
			
		||||
              <div style="display: flex; gap: 1rem; align-items: center;">
 | 
			
		||||
                <span
 | 
			
		||||
                  % if item_status_variant:
 | 
			
		||||
                      class="has-background-${item_status_variant}"
 | 
			
		||||
                  % endif
 | 
			
		||||
                  % if master.has_perm('change_status'):
 | 
			
		||||
                      style="padding: 0.25rem;"
 | 
			
		||||
                  % endif
 | 
			
		||||
                  >
 | 
			
		||||
                  ${app.enum.ORDER_ITEM_STATUS[item.status_code]}
 | 
			
		||||
                </span>
 | 
			
		||||
                % if master.has_perm('change_status'):
 | 
			
		||||
                    <b-button type="is-primary"
 | 
			
		||||
                              icon-pack="fas"
 | 
			
		||||
                              icon-left="edit"
 | 
			
		||||
                              @click="changeStatusInit()">
 | 
			
		||||
                      Change Status
 | 
			
		||||
                    </b-button>
 | 
			
		||||
                    <${b}-modal
 | 
			
		||||
                      % if request.use_oruga:
 | 
			
		||||
                          v-model:active="changeStatusShowDialog"
 | 
			
		||||
                      % else:
 | 
			
		||||
                          :active.sync="changeStatusShowDialog"
 | 
			
		||||
                      % endif
 | 
			
		||||
                      >
 | 
			
		||||
                      <div class="card">
 | 
			
		||||
                        <div class="card-content">
 | 
			
		||||
 | 
			
		||||
                          <h4 class="block is-size-4">Change Item Status</h4>
 | 
			
		||||
 | 
			
		||||
                          <b-field horizontal label="Current Status">
 | 
			
		||||
                            <span>{{ changeStatusCodes[changeStatusOldCode] }}</span>
 | 
			
		||||
                          </b-field>
 | 
			
		||||
 | 
			
		||||
                          <br />
 | 
			
		||||
 | 
			
		||||
                          <b-field horizontal label="New Status"
 | 
			
		||||
                                   :type="changeStatusNewCode ? null : 'is-danger'">
 | 
			
		||||
                            <b-select v-model="changeStatusNewCode">
 | 
			
		||||
                              <option v-for="status in changeStatusCodeOptions"
 | 
			
		||||
                                      :key="status.key"
 | 
			
		||||
                                      :value="status.key">
 | 
			
		||||
                                {{ status.label }}
 | 
			
		||||
                              </option>
 | 
			
		||||
                            </b-select>
 | 
			
		||||
                          </b-field>
 | 
			
		||||
 | 
			
		||||
                          <b-field label="Note">
 | 
			
		||||
                            <b-input v-model="changeStatusNote"
 | 
			
		||||
                                     type="textarea" rows="4" />
 | 
			
		||||
                          </b-field>
 | 
			
		||||
 | 
			
		||||
                          <br />
 | 
			
		||||
 | 
			
		||||
                          <div class="buttons">
 | 
			
		||||
                            <b-button type="is-primary"
 | 
			
		||||
                                      :disabled="changeStatusSaveDisabled"
 | 
			
		||||
                                      icon-pack="fas"
 | 
			
		||||
                                      icon-left="save"
 | 
			
		||||
                                      @click="changeStatusSave()">
 | 
			
		||||
                              {{ changeStatusSubmitting ? "Working, please wait..." : "Update Status" }}
 | 
			
		||||
                            </b-button>
 | 
			
		||||
                            <b-button @click="changeStatusShowDialog = false">
 | 
			
		||||
                              Cancel
 | 
			
		||||
                            </b-button>
 | 
			
		||||
                          </div>
 | 
			
		||||
 | 
			
		||||
                        </div>
 | 
			
		||||
                      </div>
 | 
			
		||||
                    </${b}-modal>
 | 
			
		||||
 | 
			
		||||
                    ${h.form(master.get_action_url('change_status', item), ref='changeStatusForm')}
 | 
			
		||||
                    ${h.csrf_token(request)}
 | 
			
		||||
                    ${h.hidden('new_status', **{'v-model': 'changeStatusNewCode'})}
 | 
			
		||||
                    ## ${h.hidden('uuids', **{':value': 'changeStatusCheckedRows.map((row) => {return row.uuid}).join()'})}
 | 
			
		||||
                    ${h.hidden('note', **{':value': 'changeStatusNote'})}
 | 
			
		||||
                    ${h.end_form()}
 | 
			
		||||
 | 
			
		||||
                % endif
 | 
			
		||||
              </div>
 | 
			
		||||
            </b-field>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
| 
						 | 
				
			
			@ -154,7 +238,70 @@
 | 
			
		|||
 | 
			
		||||
  <div style="padding: 0 2rem;">
 | 
			
		||||
    <nav class="panel" style="width: 100%;">
 | 
			
		||||
      <p class="panel-heading">Events</p>
 | 
			
		||||
      <p class="panel-heading"
 | 
			
		||||
         % if master.has_perm('add_note'):
 | 
			
		||||
             style="display: flex; gap: 2rem; align-items: center;"
 | 
			
		||||
         % endif
 | 
			
		||||
         >
 | 
			
		||||
        <span>Events</span>
 | 
			
		||||
        % if master.has_perm('add_note'):
 | 
			
		||||
            <b-button type="is-primary"
 | 
			
		||||
                      icon-pack="fas"
 | 
			
		||||
                      icon-left="plus"
 | 
			
		||||
                      @click="addNoteInit()">
 | 
			
		||||
              Add Note
 | 
			
		||||
            </b-button>
 | 
			
		||||
            <${b}-modal has-modal-card
 | 
			
		||||
                        % if request.use_oruga:
 | 
			
		||||
                            v-model:active="addNoteShowDialog"
 | 
			
		||||
                        % else:
 | 
			
		||||
                            :active.sync="addNoteShowDialog"
 | 
			
		||||
                        % endif
 | 
			
		||||
                        >
 | 
			
		||||
              <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="addNoteText"
 | 
			
		||||
                             ref="addNoteText"
 | 
			
		||||
                             expanded />
 | 
			
		||||
                  </b-field>
 | 
			
		||||
##                   <b-field>
 | 
			
		||||
##                     <b-checkbox v-model="addNoteApplyAll">
 | 
			
		||||
##                       Apply to all products 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">
 | 
			
		||||
                    {{ addNoteSubmitting ? "Working, please wait..." : "Add Note" }}
 | 
			
		||||
                  </b-button>
 | 
			
		||||
                  <b-button @click="addNoteShowDialog = false">
 | 
			
		||||
                    Cancel
 | 
			
		||||
                  </b-button>
 | 
			
		||||
                </footer>
 | 
			
		||||
              </div>
 | 
			
		||||
            </${b}-modal>
 | 
			
		||||
 | 
			
		||||
            ${h.form(master.get_action_url('add_note', item), ref='addNoteForm')}
 | 
			
		||||
            ${h.csrf_token(request)}
 | 
			
		||||
            ${h.hidden('note', **{':value': 'addNoteText'})}
 | 
			
		||||
            ## ${h.hidden('uuids', **{':value': 'changeStatusCheckedRows.map((row) => {return row.uuid}).join()'})}
 | 
			
		||||
            ${h.end_form()}
 | 
			
		||||
 | 
			
		||||
        % endif
 | 
			
		||||
 | 
			
		||||
      </p>
 | 
			
		||||
      <div class="panel-block">
 | 
			
		||||
        <div style="width: 100%;">
 | 
			
		||||
          ${events_grid.render_table_element()}
 | 
			
		||||
| 
						 | 
				
			
			@ -174,6 +321,80 @@
 | 
			
		|||
  ${parent.modify_vue_vars()}
 | 
			
		||||
  <script>
 | 
			
		||||
 | 
			
		||||
    % if master.has_perm('add_note'):
 | 
			
		||||
 | 
			
		||||
        ThisPageData.addNoteShowDialog = false
 | 
			
		||||
        ThisPageData.addNoteText = null
 | 
			
		||||
        ## ThisPageData.addNoteApplyAll = false
 | 
			
		||||
        ThisPageData.addNoteSubmitting = false
 | 
			
		||||
 | 
			
		||||
        ThisPage.computed.addNoteSaveDisabled = function() {
 | 
			
		||||
            if (!this.addNoteText) {
 | 
			
		||||
                return true
 | 
			
		||||
            }
 | 
			
		||||
            if (this.addNoteSubmitting) {
 | 
			
		||||
                return true
 | 
			
		||||
            }
 | 
			
		||||
            return false
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        ThisPage.methods.addNoteInit = function() {
 | 
			
		||||
            this.addNoteText = null
 | 
			
		||||
            ## this.addNoteApplyAll = false
 | 
			
		||||
            this.addNoteShowDialog = true
 | 
			
		||||
            this.$nextTick(() => {
 | 
			
		||||
                this.$refs.addNoteText.focus()
 | 
			
		||||
            })
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        ThisPage.methods.addNoteSave = function() {
 | 
			
		||||
            this.addNoteSubmitting = true
 | 
			
		||||
            this.$refs.addNoteForm.submit()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    % endif
 | 
			
		||||
 | 
			
		||||
    % if master.has_perm('change_status'):
 | 
			
		||||
 | 
			
		||||
        ThisPageData.changeStatusCodes = ${json.dumps(app.enum.ORDER_ITEM_STATUS)|n}
 | 
			
		||||
        ThisPageData.changeStatusCodeOptions = ${json.dumps([dict(key=k, label=v) for k, v in app.enum.ORDER_ITEM_STATUS.items()])|n}
 | 
			
		||||
 | 
			
		||||
        ThisPageData.changeStatusShowDialog = false
 | 
			
		||||
        ThisPageData.changeStatusOldCode = ${instance.status_code}
 | 
			
		||||
        ThisPageData.changeStatusNewCode = null
 | 
			
		||||
        ThisPageData.changeStatusNote = null
 | 
			
		||||
        ThisPageData.changeStatusSubmitting = false
 | 
			
		||||
 | 
			
		||||
        ThisPage.computed.changeStatusSaveDisabled = function() {
 | 
			
		||||
            if (!this.changeStatusNewCode) {
 | 
			
		||||
                return true
 | 
			
		||||
            }
 | 
			
		||||
            if (this.changeStatusSubmitting) {
 | 
			
		||||
                return true
 | 
			
		||||
            }
 | 
			
		||||
            return false
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        ThisPage.methods.changeStatusInit = function() {
 | 
			
		||||
            this.changeStatusNewCode = null
 | 
			
		||||
            // clear out any checked rows
 | 
			
		||||
            // this.changeStatusCheckedRows.length = 0
 | 
			
		||||
            this.changeStatusNote = null
 | 
			
		||||
            this.changeStatusShowDialog = true
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        ThisPage.methods.changeStatusSave = function() {
 | 
			
		||||
            if (this.changeStatusNewCode == this.changeStatusOldCode) {
 | 
			
		||||
                alert("You chose the same status it already had...")
 | 
			
		||||
                return
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.changeStatusSubmitting = true
 | 
			
		||||
            this.$refs.changeStatusForm.submit()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    % endif
 | 
			
		||||
 | 
			
		||||
    ## TODO: ugh the hackiness
 | 
			
		||||
    ThisPageData.gridContext = {
 | 
			
		||||
        % for key, data in form.grid_vue_context.items():
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -30,7 +30,7 @@ import logging
 | 
			
		|||
import colander
 | 
			
		||||
from sqlalchemy import orm
 | 
			
		||||
 | 
			
		||||
from webhelpers2.html import tags
 | 
			
		||||
from webhelpers2.html import tags, HTML
 | 
			
		||||
 | 
			
		||||
from wuttaweb.views import MasterView
 | 
			
		||||
from wuttaweb.forms.schema import UserRef, WuttaMoney, WuttaQuantity, WuttaEnum, WuttaDictEnum
 | 
			
		||||
| 
						 | 
				
			
			@ -862,6 +862,15 @@ class OrderView(MasterView):
 | 
			
		|||
        # status_code
 | 
			
		||||
        g.set_renderer('status_code', self.render_status_code)
 | 
			
		||||
 | 
			
		||||
        # TODO: upstream should set this automatically
 | 
			
		||||
        g.row_class = self.row_grid_row_class
 | 
			
		||||
 | 
			
		||||
    def row_grid_row_class(self, item, data, i):
 | 
			
		||||
        """ """
 | 
			
		||||
        variant = self.order_handler.item_status_to_variant(item.status_code)
 | 
			
		||||
        if variant:
 | 
			
		||||
            return f'has-background-{variant}'
 | 
			
		||||
 | 
			
		||||
    def render_status_code(self, item, key, value):
 | 
			
		||||
        """ """
 | 
			
		||||
        enum = self.app.enum
 | 
			
		||||
| 
						 | 
				
			
			@ -1117,12 +1126,11 @@ class OrderItemView(MasterView):
 | 
			
		|||
        enum = self.app.enum
 | 
			
		||||
        return enum.ORDER_ITEM_STATUS[value]
 | 
			
		||||
 | 
			
		||||
    def get_instance_title(self, item):
 | 
			
		||||
    def grid_row_class(self, item, data, i):
 | 
			
		||||
        """ """
 | 
			
		||||
        enum = self.app.enum
 | 
			
		||||
        title = str(item)
 | 
			
		||||
        status = enum.ORDER_ITEM_STATUS[item.status_code]
 | 
			
		||||
        return f"({status}) {title}"
 | 
			
		||||
        variant = self.order_handler.item_status_to_variant(item.status_code)
 | 
			
		||||
        if variant:
 | 
			
		||||
            return f'has-background-{variant}'
 | 
			
		||||
 | 
			
		||||
    def configure_form(self, f):
 | 
			
		||||
        """ """
 | 
			
		||||
| 
						 | 
				
			
			@ -1185,6 +1193,7 @@ class OrderItemView(MasterView):
 | 
			
		|||
            context['order'] = item.order
 | 
			
		||||
            context['order_qty_uom_text'] = self.order_handler.get_order_qty_uom_text(
 | 
			
		||||
                item.order_qty, item.order_uom, case_size=item.case_size, html=True)
 | 
			
		||||
            context['item_status_variant'] = self.order_handler.item_status_to_variant(item.status_code)
 | 
			
		||||
 | 
			
		||||
            grid = self.make_grid(key=f'{route_prefix}.view.events',
 | 
			
		||||
                                  model_class=model.OrderItemEvent,
 | 
			
		||||
| 
						 | 
				
			
			@ -1201,6 +1210,7 @@ class OrderItemView(MasterView):
 | 
			
		|||
                                      'type_code': "Event Type",
 | 
			
		||||
                                  })
 | 
			
		||||
            grid.set_renderer('type_code', lambda e, k, v: enum.ORDER_ITEM_EVENT[v])
 | 
			
		||||
            grid.set_renderer('note', self.render_event_note)
 | 
			
		||||
            if self.request.has_perm('users.view'):
 | 
			
		||||
                grid.set_renderer('actor', lambda e, k, v: tags.link_to(
 | 
			
		||||
                    e.actor, self.request.route_url('users.view', uuid=e.actor.uuid)))
 | 
			
		||||
| 
						 | 
				
			
			@ -1209,6 +1219,15 @@ class OrderItemView(MasterView):
 | 
			
		|||
 | 
			
		||||
        return context
 | 
			
		||||
 | 
			
		||||
    def render_event_note(self, event, key, value):
 | 
			
		||||
        """ """
 | 
			
		||||
        enum = self.app.enum
 | 
			
		||||
        if event.type_code == enum.ORDER_ITEM_EVENT_NOTE_ADDED:
 | 
			
		||||
            return HTML.tag('span', class_='has-background-info-light',
 | 
			
		||||
                            style='padding: 0.25rem 0.5rem;',
 | 
			
		||||
                            c=[value])
 | 
			
		||||
        return value
 | 
			
		||||
 | 
			
		||||
    def get_xref_buttons(self, item):
 | 
			
		||||
        """ """
 | 
			
		||||
        buttons = super().get_xref_buttons(item)
 | 
			
		||||
| 
						 | 
				
			
			@ -1221,6 +1240,112 @@ class OrderItemView(MasterView):
 | 
			
		|||
 | 
			
		||||
        return buttons
 | 
			
		||||
 | 
			
		||||
    def add_note(self):
 | 
			
		||||
        """
 | 
			
		||||
        View which adds a note to an order item.  This is POST-only;
 | 
			
		||||
        will redirect back to the item view.
 | 
			
		||||
        """
 | 
			
		||||
        enum = self.app.enum
 | 
			
		||||
        item = self.get_instance()
 | 
			
		||||
 | 
			
		||||
        item.add_event(enum.ORDER_ITEM_EVENT_NOTE_ADDED, self.request.user,
 | 
			
		||||
                       note=self.request.POST['note'])
 | 
			
		||||
 | 
			
		||||
        return self.redirect(self.get_action_url('view', item))
 | 
			
		||||
 | 
			
		||||
    def change_status(self):
 | 
			
		||||
        """
 | 
			
		||||
        View which changes status for an order item.  This is
 | 
			
		||||
        POST-only; will redirect back to the item view.
 | 
			
		||||
        """
 | 
			
		||||
        model = self.app.model
 | 
			
		||||
        enum = self.app.enum
 | 
			
		||||
        main_item = self.get_instance()
 | 
			
		||||
        session = self.Session()
 | 
			
		||||
        redirect = self.redirect(self.get_action_url('view', main_item))
 | 
			
		||||
 | 
			
		||||
        extra_note = self.request.POST.get('note')
 | 
			
		||||
 | 
			
		||||
        # validate new status
 | 
			
		||||
        new_status_code = int(self.request.POST['new_status'])
 | 
			
		||||
        if new_status_code not in enum.ORDER_ITEM_STATUS:
 | 
			
		||||
            self.request.session.flash("Invalid status code", 'error')
 | 
			
		||||
            return redirect
 | 
			
		||||
        new_status_text = enum.ORDER_ITEM_STATUS[new_status_code]
 | 
			
		||||
 | 
			
		||||
        # locate all items to which new status will be applied
 | 
			
		||||
        items = [main_item]
 | 
			
		||||
        # uuids = self.request.POST.get('uuids')
 | 
			
		||||
        # if uuids:
 | 
			
		||||
        #     for uuid in uuids.split(','):
 | 
			
		||||
        #         item = Session.get(model.OrderItem, uuid)
 | 
			
		||||
        #         if item:
 | 
			
		||||
        #             items.append(item)
 | 
			
		||||
 | 
			
		||||
        # update item(s)
 | 
			
		||||
        for item in items:
 | 
			
		||||
            if item.status_code != new_status_code:
 | 
			
		||||
 | 
			
		||||
                # event: change status
 | 
			
		||||
                note = 'status changed from "{}" to "{}"'.format(
 | 
			
		||||
                    enum.ORDER_ITEM_STATUS[item.status_code],
 | 
			
		||||
                    new_status_text)
 | 
			
		||||
                item.add_event(enum.ORDER_ITEM_EVENT_STATUS_CHANGE,
 | 
			
		||||
                               self.request.user, note=note)
 | 
			
		||||
 | 
			
		||||
                # event: add note
 | 
			
		||||
                if extra_note:
 | 
			
		||||
                    item.add_event(enum.ORDER_ITEM_EVENT_NOTE_ADDED,
 | 
			
		||||
                                   self.request.user, note=extra_note)
 | 
			
		||||
 | 
			
		||||
                # new status
 | 
			
		||||
                item.status_code = new_status_code
 | 
			
		||||
 | 
			
		||||
        self.request.session.flash(f"Status has been updated to: {new_status_text}")
 | 
			
		||||
        return redirect
 | 
			
		||||
 | 
			
		||||
    @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()
 | 
			
		||||
        permission_prefix = cls.get_permission_prefix()
 | 
			
		||||
        instance_url_prefix = cls.get_instance_url_prefix()
 | 
			
		||||
        model_title = cls.get_model_title()
 | 
			
		||||
        model_title_plural = cls.get_model_title_plural()
 | 
			
		||||
 | 
			
		||||
        # fix perm group
 | 
			
		||||
        config.add_wutta_permission_group(permission_prefix,
 | 
			
		||||
                                          model_title_plural,
 | 
			
		||||
                                          overwrite=False)
 | 
			
		||||
 | 
			
		||||
        # add note
 | 
			
		||||
        config.add_route(f'{route_prefix}.add_note',
 | 
			
		||||
                         f'{instance_url_prefix}/add_note',
 | 
			
		||||
                         request_method='POST')
 | 
			
		||||
        config.add_view(cls, attr='add_note',
 | 
			
		||||
                        route_name=f'{route_prefix}.add_note',
 | 
			
		||||
                        renderer='json',
 | 
			
		||||
                        permission=f'{permission_prefix}.add_note')
 | 
			
		||||
        config.add_wutta_permission(permission_prefix,
 | 
			
		||||
                                    f'{permission_prefix}.add_note',
 | 
			
		||||
                                    f"Add note for {model_title}")
 | 
			
		||||
 | 
			
		||||
        # change status
 | 
			
		||||
        config.add_route(f'{route_prefix}.change_status',
 | 
			
		||||
                         f'{instance_url_prefix}/change-status',
 | 
			
		||||
                         request_method='POST')
 | 
			
		||||
        config.add_view(cls, attr='change_status',
 | 
			
		||||
                        route_name=f'{route_prefix}.change_status',
 | 
			
		||||
                        renderer='json',
 | 
			
		||||
                        permission=f'{permission_prefix}.change_status')
 | 
			
		||||
        config.add_wutta_permission(permission_prefix,
 | 
			
		||||
                                    f'{permission_prefix}.change_status',
 | 
			
		||||
                                    f"Change status for {model_title}")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def defaults(config, **kwargs):
 | 
			
		||||
    base = globals()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -38,3 +38,23 @@ class TestOrderHandler(DataTestCase):
 | 
			
		|||
        self.assertEqual(text, "2 Units")
 | 
			
		||||
        text = handler.get_order_qty_uom_text(2, enum.ORDER_UOM_UNIT, html=True)
 | 
			
		||||
        self.assertEqual(text, "2 Units")
 | 
			
		||||
 | 
			
		||||
    def test_item_status_to_variant(self):
 | 
			
		||||
        enum = self.app.enum
 | 
			
		||||
        handler = self.make_handler()
 | 
			
		||||
 | 
			
		||||
        # typical
 | 
			
		||||
        self.assertIsNone(handler.item_status_to_variant(enum.ORDER_ITEM_STATUS_INITIATED))
 | 
			
		||||
        self.assertIsNone(handler.item_status_to_variant(enum.ORDER_ITEM_STATUS_READY))
 | 
			
		||||
        self.assertIsNone(handler.item_status_to_variant(enum.ORDER_ITEM_STATUS_PLACED))
 | 
			
		||||
        self.assertIsNone(handler.item_status_to_variant(enum.ORDER_ITEM_STATUS_RECEIVED))
 | 
			
		||||
        self.assertIsNone(handler.item_status_to_variant(enum.ORDER_ITEM_STATUS_CONTACTED))
 | 
			
		||||
        self.assertIsNone(handler.item_status_to_variant(enum.ORDER_ITEM_STATUS_PAID))
 | 
			
		||||
 | 
			
		||||
        # warning
 | 
			
		||||
        self.assertEqual(handler.item_status_to_variant(enum.ORDER_ITEM_STATUS_CANCELED), 'warning')
 | 
			
		||||
        self.assertEqual(handler.item_status_to_variant(enum.ORDER_ITEM_STATUS_REFUND_PENDING), 'warning')
 | 
			
		||||
        self.assertEqual(handler.item_status_to_variant(enum.ORDER_ITEM_STATUS_REFUNDED), 'warning')
 | 
			
		||||
        self.assertEqual(handler.item_status_to_variant(enum.ORDER_ITEM_STATUS_RESTOCKED), 'warning')
 | 
			
		||||
        self.assertEqual(handler.item_status_to_variant(enum.ORDER_ITEM_STATUS_EXPIRED), 'warning')
 | 
			
		||||
        self.assertEqual(handler.item_status_to_variant(enum.ORDER_ITEM_STATUS_INACTIVE), 'warning')
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1183,6 +1183,19 @@ class TestOrderView(WebTestCase):
 | 
			
		|||
            view.configure_row_grid(grid)
 | 
			
		||||
            self.assertIn('product_scancode', grid.linked_columns)
 | 
			
		||||
 | 
			
		||||
    def test_row_grid_row_class(self):
 | 
			
		||||
        model = self.app.model
 | 
			
		||||
        enum = self.app.enum
 | 
			
		||||
        view = self.make_view()
 | 
			
		||||
 | 
			
		||||
        # typical
 | 
			
		||||
        item = model.OrderItem(status_code=enum.ORDER_ITEM_STATUS_READY)
 | 
			
		||||
        self.assertIsNone(view.row_grid_row_class(item, {}, 1))
 | 
			
		||||
 | 
			
		||||
        # warning
 | 
			
		||||
        item = model.OrderItem(status_code=enum.ORDER_ITEM_STATUS_CANCELED)
 | 
			
		||||
        self.assertEqual(view.row_grid_row_class(item, {}, 1), 'has-background-warning')
 | 
			
		||||
 | 
			
		||||
    def test_render_status_code(self):
 | 
			
		||||
        enum = self.app.enum
 | 
			
		||||
        view = self.make_view()
 | 
			
		||||
| 
						 | 
				
			
			@ -1291,17 +1304,18 @@ class TestOrderItemView(WebTestCase):
 | 
			
		|||
        self.assertEqual(view.render_status_code(None, None, enum.ORDER_ITEM_STATUS_INITIATED),
 | 
			
		||||
                         'initiated')
 | 
			
		||||
 | 
			
		||||
    def test_get_instance_title(self):
 | 
			
		||||
    def test_grid_row_class(self):
 | 
			
		||||
        model = self.app.model
 | 
			
		||||
        enum = self.app.enum
 | 
			
		||||
        view = self.make_view()
 | 
			
		||||
 | 
			
		||||
        item = model.OrderItem(product_brand='Bragg',
 | 
			
		||||
                               product_description='Vinegar',
 | 
			
		||||
                               product_size='32oz',
 | 
			
		||||
                               status_code=enum.ORDER_ITEM_STATUS_INITIATED)
 | 
			
		||||
        title = view.get_instance_title(item)
 | 
			
		||||
        self.assertEqual(title, "(initiated) Bragg Vinegar 32oz")
 | 
			
		||||
        # typical
 | 
			
		||||
        item = model.OrderItem(status_code=enum.ORDER_ITEM_STATUS_READY)
 | 
			
		||||
        self.assertIsNone(view.grid_row_class(item, {}, 1))
 | 
			
		||||
 | 
			
		||||
        # warning
 | 
			
		||||
        item = model.OrderItem(status_code=enum.ORDER_ITEM_STATUS_CANCELED)
 | 
			
		||||
        self.assertEqual(view.grid_row_class(item, {}, 1), 'has-background-warning')
 | 
			
		||||
 | 
			
		||||
    def test_configure_form(self):
 | 
			
		||||
        model = self.app.model
 | 
			
		||||
| 
						 | 
				
			
			@ -1349,6 +1363,24 @@ class TestOrderItemView(WebTestCase):
 | 
			
		|||
                self.assertIn('order_qty_uom_text', context)
 | 
			
		||||
                self.assertEqual(context['order_qty_uom_text'], "2 Cases (× 8 = 16 Units)")
 | 
			
		||||
 | 
			
		||||
    def test_render_event_note(self):
 | 
			
		||||
        model = self.app.model
 | 
			
		||||
        enum = self.app.enum
 | 
			
		||||
        view = self.make_view()
 | 
			
		||||
 | 
			
		||||
        # typical
 | 
			
		||||
        event = model.OrderItemEvent(type_code=enum.ORDER_ITEM_EVENT_READY, note='testing')
 | 
			
		||||
        result = view.render_event_note(event, 'note', 'testing')
 | 
			
		||||
        self.assertEqual(result, 'testing')
 | 
			
		||||
 | 
			
		||||
        # user note
 | 
			
		||||
        event = model.OrderItemEvent(type_code=enum.ORDER_ITEM_EVENT_NOTE_ADDED, note='testing2')
 | 
			
		||||
        result = view.render_event_note(event, 'note', 'testing2')
 | 
			
		||||
        self.assertNotEqual(result, 'testing2')
 | 
			
		||||
        self.assertIn('<span', result)
 | 
			
		||||
        self.assertIn('class="has-background-info-light"', result)
 | 
			
		||||
        self.assertIn('testing2', result)
 | 
			
		||||
 | 
			
		||||
    def test_get_xref_buttons(self):
 | 
			
		||||
        self.pyramid_config.add_route('orders.view', '/orders/{uuid}')
 | 
			
		||||
        model = self.app.model
 | 
			
		||||
| 
						 | 
				
			
			@ -1371,3 +1403,86 @@ class TestOrderItemView(WebTestCase):
 | 
			
		|||
            buttons = view.get_xref_buttons(item)
 | 
			
		||||
            self.assertEqual(len(buttons), 1)
 | 
			
		||||
            self.assertIn("View the Order", buttons[0])
 | 
			
		||||
 | 
			
		||||
    def test_add_note(self):
 | 
			
		||||
        self.pyramid_config.add_route('order_items.view', '/order-items/{uuid}')
 | 
			
		||||
        model = self.app.model
 | 
			
		||||
        enum = self.app.enum
 | 
			
		||||
        view = self.make_view()
 | 
			
		||||
 | 
			
		||||
        user = model.User(username='barney')
 | 
			
		||||
        self.session.add(user)
 | 
			
		||||
 | 
			
		||||
        order = model.Order(order_id=42, created_by=user)
 | 
			
		||||
        self.session.add(order)
 | 
			
		||||
        item = model.OrderItem(order_qty=1, order_uom=enum.ORDER_UOM_UNIT,
 | 
			
		||||
                               status_code=enum.ORDER_ITEM_STATUS_INITIATED)
 | 
			
		||||
        order.items.append(item)
 | 
			
		||||
        self.session.flush()
 | 
			
		||||
 | 
			
		||||
        with patch.object(view, 'Session', return_value=self.session):
 | 
			
		||||
            with patch.object(self.request, 'matchdict', new={'uuid': item.uuid}):
 | 
			
		||||
                with patch.object(self.request, 'POST', new={'note': 'testing'}):
 | 
			
		||||
                    self.assertEqual(len(item.events), 0)
 | 
			
		||||
                    result = view.add_note()
 | 
			
		||||
                    self.assertEqual(len(item.events), 1)
 | 
			
		||||
                    self.assertEqual(item.events[0].type_code, enum.ORDER_ITEM_EVENT_NOTE_ADDED)
 | 
			
		||||
                    self.assertEqual(item.events[0].note, 'testing')
 | 
			
		||||
 | 
			
		||||
    def test_change_status(self):
 | 
			
		||||
        self.pyramid_config.add_route('order_items.view', '/order-items/{uuid}')
 | 
			
		||||
        model = self.app.model
 | 
			
		||||
        enum = self.app.enum
 | 
			
		||||
        view = self.make_view()
 | 
			
		||||
 | 
			
		||||
        user = model.User(username='barney')
 | 
			
		||||
        self.session.add(user)
 | 
			
		||||
 | 
			
		||||
        order = model.Order(order_id=42, created_by=user)
 | 
			
		||||
        self.session.add(order)
 | 
			
		||||
        item = model.OrderItem(order_qty=1, order_uom=enum.ORDER_UOM_UNIT,
 | 
			
		||||
                               status_code=enum.ORDER_ITEM_STATUS_INITIATED)
 | 
			
		||||
        order.items.append(item)
 | 
			
		||||
        self.session.flush()
 | 
			
		||||
 | 
			
		||||
        with patch.object(view, 'Session', return_value=self.session):
 | 
			
		||||
            with patch.object(self.request, 'user', new=user):
 | 
			
		||||
                with patch.object(self.request, 'matchdict', new={'uuid': item.uuid}):
 | 
			
		||||
 | 
			
		||||
                    # just status change, no note
 | 
			
		||||
                    with patch.object(self.request, 'POST', new={
 | 
			
		||||
                            'new_status': enum.ORDER_ITEM_STATUS_PLACED}):
 | 
			
		||||
                        self.assertEqual(len(item.events), 0)
 | 
			
		||||
                        result = view.change_status()
 | 
			
		||||
                        self.assertIsInstance(result, HTTPFound)
 | 
			
		||||
                        self.assertFalse(self.request.session.peek_flash('error'))
 | 
			
		||||
                        self.assertEqual(len(item.events), 1)
 | 
			
		||||
                        self.assertEqual(item.events[0].type_code, enum.ORDER_ITEM_EVENT_STATUS_CHANGE)
 | 
			
		||||
                        self.assertEqual(item.events[0].note,
 | 
			
		||||
                                         "status changed from 'initiated' to 'placed'")
 | 
			
		||||
 | 
			
		||||
                    # status change plus note
 | 
			
		||||
                    with patch.object(self.request, 'POST', new={
 | 
			
		||||
                            'new_status': enum.ORDER_ITEM_STATUS_RECEIVED,
 | 
			
		||||
                            'note': 'check it out'}):
 | 
			
		||||
                        self.assertEqual(len(item.events), 1)
 | 
			
		||||
                        result = view.change_status()
 | 
			
		||||
                        self.assertIsInstance(result, HTTPFound)
 | 
			
		||||
                        self.assertFalse(self.request.session.peek_flash('error'))
 | 
			
		||||
                        self.assertEqual(len(item.events), 3)
 | 
			
		||||
                        self.assertEqual(item.events[0].type_code, enum.ORDER_ITEM_EVENT_STATUS_CHANGE)
 | 
			
		||||
                        self.assertEqual(item.events[0].note,
 | 
			
		||||
                                         "status changed from 'initiated' to 'placed'")
 | 
			
		||||
                        self.assertEqual(item.events[1].type_code, enum.ORDER_ITEM_EVENT_STATUS_CHANGE)
 | 
			
		||||
                        self.assertEqual(item.events[1].note,
 | 
			
		||||
                                         "status changed from 'placed' to 'received'")
 | 
			
		||||
                        self.assertEqual(item.events[2].type_code, enum.ORDER_ITEM_EVENT_NOTE_ADDED)
 | 
			
		||||
                        self.assertEqual(item.events[2].note, "check it out")
 | 
			
		||||
 | 
			
		||||
                    # invalid status
 | 
			
		||||
                    with patch.object(self.request, 'POST', new={'new_status': 23432143}):
 | 
			
		||||
                        self.assertEqual(len(item.events), 3)
 | 
			
		||||
                        result = view.change_status()
 | 
			
		||||
                        self.assertIsInstance(result, HTTPFound)
 | 
			
		||||
                        self.assertTrue(self.request.session.peek_flash('error'))
 | 
			
		||||
                        self.assertEqual(len(item.events), 3)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue