Overhaul desktop views for receiving, for efficiency
still could use even more i'm sure, but this takes advantage of buefy to add dialogs etc. from the "view receiving batch row" page. this batch no longer allows direct edit of rows but that's hopefully for the better.
This commit is contained in:
		
							parent
							
								
									2f676774e9
								
							
						
					
					
						commit
						340a177a29
					
				
					 14 changed files with 1014 additions and 157 deletions
				
			
		| 
						 | 
				
			
			@ -848,7 +848,10 @@ class Form(object):
 | 
			
		|||
            return ''
 | 
			
		||||
 | 
			
		||||
        # TODO: fair bit of duplication here, should merge with deform.mako
 | 
			
		||||
        label = HTML.tag('label', self.get_label(field_name), for_=field_name)
 | 
			
		||||
        label = kwargs.get('label')
 | 
			
		||||
        if not label:
 | 
			
		||||
            label = self.get_label(field_name)
 | 
			
		||||
        label = HTML.tag('label', label, for_=field_name)
 | 
			
		||||
        field = self.render_field_value(field_name) or ''
 | 
			
		||||
        field_div = HTML.tag('div', class_='field', c=[field])
 | 
			
		||||
        contents = [label, field_div]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -145,14 +145,14 @@
 | 
			
		|||
        </div><!-- card -->
 | 
			
		||||
 | 
			
		||||
        <div class="buttons">
 | 
			
		||||
          <once-button tag="a" href="${form.cancel_url}"
 | 
			
		||||
                       text="Cancel">
 | 
			
		||||
          </once-button>
 | 
			
		||||
          <b-button type="is-primary"
 | 
			
		||||
                    native-type="submit"
 | 
			
		||||
                    :disabled="formSubmitting">
 | 
			
		||||
            {{ formButtonText }}
 | 
			
		||||
          </b-button>
 | 
			
		||||
          <once-button tag="a" href="${form.cancel_url}"
 | 
			
		||||
                       text="Cancel">
 | 
			
		||||
          </once-button>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
      </div><!-- app-wrapper -->
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,7 +6,7 @@
 | 
			
		|||
    let FormPosterMixin = {
 | 
			
		||||
        methods: {
 | 
			
		||||
 | 
			
		||||
            submitForm(action, params, success) {
 | 
			
		||||
            submitForm(action, params, success, failure) {
 | 
			
		||||
 | 
			
		||||
                let csrftoken = ${json.dumps(request.session.get_csrf_token() or request.session.new_csrf_token())|n}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -21,18 +21,24 @@
 | 
			
		|||
 | 
			
		||||
                    } else {
 | 
			
		||||
                        this.$buefy.toast.open({
 | 
			
		||||
                            message: "Failed to send feedback:  " + response.data.error,
 | 
			
		||||
                            message: "Submit failed:  " + response.data.error,
 | 
			
		||||
                            type: 'is-danger',
 | 
			
		||||
                            duration: 4000, // 4 seconds
 | 
			
		||||
                        })
 | 
			
		||||
                        if (failure) {
 | 
			
		||||
                            failure(response)
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                }, response => {
 | 
			
		||||
                    this.$buefy.toast.open({
 | 
			
		||||
                        message: "Failed to submit form!  (unknown server error)",
 | 
			
		||||
                        message: "Submit failed!  (unknown server error)",
 | 
			
		||||
                        type: 'is-danger',
 | 
			
		||||
                        duration: 4000, // 4 seconds
 | 
			
		||||
                    })
 | 
			
		||||
                    if (failure) {
 | 
			
		||||
                        failure(response)
 | 
			
		||||
                    }
 | 
			
		||||
                })
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -203,7 +203,7 @@
 | 
			
		|||
              % for action in grid.main_actions + grid.more_actions:
 | 
			
		||||
                  <a v-if="props.row._action_url_${action.key}"
 | 
			
		||||
                     :href="props.row._action_url_${action.key}"
 | 
			
		||||
                     class="grid-action${' has-text-danger' if action.key == 'delete' else ''}"
 | 
			
		||||
                     class="grid-action${' has-text-danger' if action.key == 'delete' else ''} ${action.link_class or ''}"
 | 
			
		||||
                     % if action.click_handler:
 | 
			
		||||
                     @click.prevent="${action.click_handler}"
 | 
			
		||||
                     % endif
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,8 +1,8 @@
 | 
			
		|||
## -*- coding: utf-8 -*-
 | 
			
		||||
## -*- coding: utf-8; -*-
 | 
			
		||||
<%inherit file="/master/edit.mako" />
 | 
			
		||||
 | 
			
		||||
<%def name="context_menu_items()">
 | 
			
		||||
  <li>${h.link_to("Back to {}".format(model_title), index_url)}</li>
 | 
			
		||||
  <li>${h.link_to("Back to {}".format(parent_model_title), parent_url)}</li>
 | 
			
		||||
  % if master.rows_viewable and request.has_perm('{}.view'.format(row_permission_prefix)):
 | 
			
		||||
      <li>${h.link_to("View this {}".format(row_model_title), row_action_url('view', instance))}</li>
 | 
			
		||||
  % endif
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,7 +12,7 @@
 | 
			
		|||
  % if master.rows_editable and instance_editable and request.has_perm('{}.edit'.format(permission_prefix)):
 | 
			
		||||
      <li>${h.link_to("Edit this {}".format(model_title), action_url('edit', instance))}</li>
 | 
			
		||||
  % endif
 | 
			
		||||
  % if master.rows_deletable and instance_deletable and request.has_perm('{}.delete'.format(permission_prefix)):
 | 
			
		||||
  % if instance_deletable and master.has_perm('delete_row'):
 | 
			
		||||
      <li>${h.link_to("Delete this {}".format(model_title), action_url('delete', instance))}</li>
 | 
			
		||||
  % endif
 | 
			
		||||
  % if rows_creatable and request.has_perm('{}.create'.format(permission_prefix)):
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -32,6 +32,7 @@
 | 
			
		|||
 | 
			
		||||
    let ThisPage = {
 | 
			
		||||
        template: '#this-page-template',
 | 
			
		||||
        mixins: [FormPosterMixin],
 | 
			
		||||
        computed: {},
 | 
			
		||||
        methods: {},
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -284,7 +284,19 @@
 | 
			
		|||
</%def>
 | 
			
		||||
 | 
			
		||||
<%def name="object_helpers()">
 | 
			
		||||
  ${parent.object_helpers()}
 | 
			
		||||
  ${self.render_status_breakdown()}
 | 
			
		||||
 | 
			
		||||
  % if use_buefy and master.handler.has_purchase_order(batch) and master.handler.has_invoice_file(batch):
 | 
			
		||||
      <div class="object-helper">
 | 
			
		||||
        <h3>PO vs. Invoice</h3>
 | 
			
		||||
        <div class="object-helper-content">
 | 
			
		||||
          ${po_vs_invoice_breakdown_grid.render_buefy_table_element(data_prop='poVsInvoiceBreakdownData', empty_labels=True)|n}
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
  % endif
 | 
			
		||||
 | 
			
		||||
  ${self.render_execute_helper()}
 | 
			
		||||
 | 
			
		||||
  % if master.has_perm('auto_receive') and master.can_auto_receive(batch):
 | 
			
		||||
 | 
			
		||||
      <div class="object-helper">
 | 
			
		||||
| 
						 | 
				
			
			@ -292,7 +304,9 @@
 | 
			
		|||
        <div class="object-helper-content">
 | 
			
		||||
          % if use_buefy:
 | 
			
		||||
              <b-button type="is-primary"
 | 
			
		||||
                        @click="autoReceiveShowDialog = true">
 | 
			
		||||
                        @click="autoReceiveShowDialog = true"
 | 
			
		||||
                        icon-pack="fas"
 | 
			
		||||
                        icon-left="check">
 | 
			
		||||
                Auto-Receive All Items
 | 
			
		||||
              </b-button>
 | 
			
		||||
          % else:
 | 
			
		||||
| 
						 | 
				
			
			@ -334,7 +348,7 @@
 | 
			
		|||
                          :disabled="autoReceiveSubmitting"
 | 
			
		||||
                          @click="autoReceiveSubmitting = true"
 | 
			
		||||
                          icon-pack="fas"
 | 
			
		||||
                          icon-left="arrow-circle-right">
 | 
			
		||||
                          icon-left="check">
 | 
			
		||||
                  {{ autoReceiveSubmitting ? "Working, please wait..." : "Auto-Receive All Items" }}
 | 
			
		||||
                </b-button>
 | 
			
		||||
                ${h.end_form()}
 | 
			
		||||
| 
						 | 
				
			
			@ -352,6 +366,10 @@
 | 
			
		|||
    ThisPageData.autoReceiveShowDialog = false
 | 
			
		||||
    ThisPageData.autoReceiveSubmitting = false
 | 
			
		||||
 | 
			
		||||
    % if po_vs_invoice_breakdown_grid is not Undefined:
 | 
			
		||||
        ThisPageData.poVsInvoiceBreakdownData = ${json.dumps(po_vs_invoice_breakdown_grid.get_buefy_data()['data'])|n}
 | 
			
		||||
    % endif
 | 
			
		||||
 | 
			
		||||
  </script>
 | 
			
		||||
</%def>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,9 +5,37 @@
 | 
			
		|||
  ${parent.extra_styles()}
 | 
			
		||||
  <style type="text/css">
 | 
			
		||||
  % if use_buefy:
 | 
			
		||||
 | 
			
		||||
        nav.panel {
 | 
			
		||||
            margin: 0.5rem;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .header-fields {
 | 
			
		||||
            margin-top: 1rem;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .header-fields .field.is-horizontal {
 | 
			
		||||
            margin-left: 3rem;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .header-fields .field.is-horizontal .field-label .label {
 | 
			
		||||
            white-space: nowrap;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .quantity-form-fields {
 | 
			
		||||
            margin: 2rem auto;
 | 
			
		||||
            padding-left: 2rem;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .quantity-form-fields .field.is-horizontal .field-label .label {
 | 
			
		||||
            text-align: left;
 | 
			
		||||
            width: 8rem;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .remove-credit .field.is-horizontal .field-label .label {
 | 
			
		||||
            white-space: nowrap;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
  % endif
 | 
			
		||||
  </style>
 | 
			
		||||
</%def>
 | 
			
		||||
| 
						 | 
				
			
			@ -30,9 +58,20 @@
 | 
			
		|||
<%def name="page_content()">
 | 
			
		||||
  % if use_buefy:
 | 
			
		||||
 | 
			
		||||
      <b-field grouped>
 | 
			
		||||
        ${form.render_field_readonly('sequence')}
 | 
			
		||||
        ${form.render_field_readonly('status_code')}
 | 
			
		||||
      <b-field grouped class="header-fields">
 | 
			
		||||
 | 
			
		||||
        <b-field label="Sequence" horizontal>
 | 
			
		||||
          {{ rowData.sequence }}
 | 
			
		||||
        </b-field>
 | 
			
		||||
 | 
			
		||||
        <b-field label="Status" horizontal>
 | 
			
		||||
          {{ rowData.status }}
 | 
			
		||||
        </b-field>
 | 
			
		||||
 | 
			
		||||
        <b-field label="Calculated Total" horizontal>
 | 
			
		||||
          {{ rowData.invoice_total_calculated }}
 | 
			
		||||
        </b-field>
 | 
			
		||||
 | 
			
		||||
      </b-field>
 | 
			
		||||
 | 
			
		||||
      <div style="display: flex;">
 | 
			
		||||
| 
						 | 
				
			
			@ -42,18 +81,23 @@
 | 
			
		|||
          <div class="panel-block">
 | 
			
		||||
            <div style="display: flex;">
 | 
			
		||||
              <div>
 | 
			
		||||
                % if not row.product:
 | 
			
		||||
                    ${form.render_field_readonly('item_entry')}
 | 
			
		||||
                % endif
 | 
			
		||||
                % if row.product:
 | 
			
		||||
                    ${form.render_field_readonly('upc')}
 | 
			
		||||
                    ${form.render_field_readonly('product')}
 | 
			
		||||
                % else:
 | 
			
		||||
                    ${form.render_field_readonly('item_entry')}
 | 
			
		||||
                    ${form.render_field_readonly('upc')}
 | 
			
		||||
                    ${form.render_field_readonly('brand_name')}
 | 
			
		||||
                    ${form.render_field_readonly('description')}
 | 
			
		||||
                    ${form.render_field_readonly('size')}
 | 
			
		||||
                % endif
 | 
			
		||||
                ${form.render_field_readonly('vendor_code')}
 | 
			
		||||
                ${form.render_field_readonly('case_quantity')}
 | 
			
		||||
                ${form.render_field_readonly('catalog_unit_cost')}
 | 
			
		||||
              </div>
 | 
			
		||||
              % if image_url:
 | 
			
		||||
                  <div class="is-pulled-right">
 | 
			
		||||
                    ${h.image(image_url, "Product Image")}
 | 
			
		||||
                    ${h.image(image_url, "Product Image", width=150, height=150)}
 | 
			
		||||
                  </div>
 | 
			
		||||
              % endif
 | 
			
		||||
            </div>
 | 
			
		||||
| 
						 | 
				
			
			@ -64,88 +108,351 @@
 | 
			
		|||
          <p class="panel-heading">Quantities</p>
 | 
			
		||||
          <div class="panel-block">
 | 
			
		||||
            <div>
 | 
			
		||||
              ${form.render_field_readonly('ordered')}
 | 
			
		||||
              ${form.render_field_readonly('shipped')}
 | 
			
		||||
              ${form.render_field_readonly('received')}
 | 
			
		||||
              ${form.render_field_readonly('damaged')}
 | 
			
		||||
              ${form.render_field_readonly('expired')}
 | 
			
		||||
              ${form.render_field_readonly('mispick')}
 | 
			
		||||
              <div class="quantity-form-fields">
 | 
			
		||||
 | 
			
		||||
                <b-field label="Ordered" horizontal>
 | 
			
		||||
                  {{ rowData.ordered }}
 | 
			
		||||
                </b-field>
 | 
			
		||||
 | 
			
		||||
                <hr />
 | 
			
		||||
 | 
			
		||||
                <b-field label="Shipped" horizontal>
 | 
			
		||||
                  {{ rowData.shipped }}
 | 
			
		||||
                </b-field>
 | 
			
		||||
 | 
			
		||||
                <hr />
 | 
			
		||||
 | 
			
		||||
                <b-field label="Received" horizontal
 | 
			
		||||
                         v-if="rowData.received">
 | 
			
		||||
                  {{ rowData.received }}
 | 
			
		||||
                </b-field>
 | 
			
		||||
 | 
			
		||||
                <b-field label="Damaged" horizontal
 | 
			
		||||
                         v-if="rowData.damaged">
 | 
			
		||||
                  {{ rowData.damaged }}
 | 
			
		||||
                </b-field>
 | 
			
		||||
 | 
			
		||||
                <b-field label="Expired" horizontal
 | 
			
		||||
                         v-if="rowData.expired">
 | 
			
		||||
                  {{ rowData.expired }}
 | 
			
		||||
                </b-field>
 | 
			
		||||
 | 
			
		||||
                <b-field label="Mispick" horizontal
 | 
			
		||||
                         v-if="rowData.mispick">
 | 
			
		||||
                  {{ rowData.mispick }}
 | 
			
		||||
                </b-field>
 | 
			
		||||
 | 
			
		||||
                <b-field label="Missing" horizontal
 | 
			
		||||
                         v-if="rowData.missing">
 | 
			
		||||
                  {{ rowData.missing }}
 | 
			
		||||
                </b-field>
 | 
			
		||||
 | 
			
		||||
              <div class="buttons">
 | 
			
		||||
                <once-button type="is-primary"
 | 
			
		||||
                             tag="a" href="${url('{}.receive_row'.format(route_prefix), uuid=batch.uuid, row_uuid=row.uuid)}"
 | 
			
		||||
                             icon-left="download"
 | 
			
		||||
                             text="Receive Product">
 | 
			
		||||
                </once-button>
 | 
			
		||||
                <once-button type="is-primary"
 | 
			
		||||
                             tag="a" href="${url('{}.declare_credit'.format(route_prefix), uuid=batch.uuid, row_uuid=row.uuid)}"
 | 
			
		||||
                             icon-left="thumbs-down"
 | 
			
		||||
                             text="Declare Credit">
 | 
			
		||||
                </once-button>
 | 
			
		||||
              </div>
 | 
			
		||||
 | 
			
		||||
              % if master.has_perm('edit_row') and master.row_editable(row):
 | 
			
		||||
                  <div class="buttons">
 | 
			
		||||
                    <b-button type="is-primary"
 | 
			
		||||
                              @click="accountForProductInit()"
 | 
			
		||||
                              icon-pack="fas"
 | 
			
		||||
                              icon-left="check">
 | 
			
		||||
                      Account for Product
 | 
			
		||||
                    </b-button>
 | 
			
		||||
                    <b-button type="is-warning"
 | 
			
		||||
                              @click="declareCreditInit()"
 | 
			
		||||
                              :disabled="!rowData.received"
 | 
			
		||||
                              icon-pack="fas"
 | 
			
		||||
                              icon-left="thumbs-down">
 | 
			
		||||
                      Declare Credit
 | 
			
		||||
                    </b-button>
 | 
			
		||||
                  </div>
 | 
			
		||||
              % endif
 | 
			
		||||
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </nav>
 | 
			
		||||
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <b-modal has-modal-card
 | 
			
		||||
               :active.sync="accountForProductShowDialog">
 | 
			
		||||
        <div class="modal-card">
 | 
			
		||||
 | 
			
		||||
          <header class="modal-card-head">
 | 
			
		||||
            <p class="modal-card-title">Account for Product</p>
 | 
			
		||||
          </header>
 | 
			
		||||
 | 
			
		||||
          <section class="modal-card-body">
 | 
			
		||||
 | 
			
		||||
            <p class="block">
 | 
			
		||||
              This is for declaring that you have encountered some
 | 
			
		||||
              amount of the product.  Ideally you will just
 | 
			
		||||
              "receive" it normally, but you can indicate a "credit"
 | 
			
		||||
              state if there is something amiss.
 | 
			
		||||
            </p>
 | 
			
		||||
 | 
			
		||||
            <b-field grouped>
 | 
			
		||||
 | 
			
		||||
              <b-field label="Case Qty.">
 | 
			
		||||
                <span class="control">
 | 
			
		||||
                  {{ rowData.case_quantity }}
 | 
			
		||||
                </span>
 | 
			
		||||
              </b-field>
 | 
			
		||||
 | 
			
		||||
              <span class="control">
 | 
			
		||||
                 
 | 
			
		||||
              </span>
 | 
			
		||||
 | 
			
		||||
              <b-field label="Product State"
 | 
			
		||||
                       :type="accountForProductMode ? null : 'is-danger'">
 | 
			
		||||
                <b-select v-model="accountForProductMode">
 | 
			
		||||
                  <option v-for="mode in possibleReceivingModes"
 | 
			
		||||
                          :key="mode"
 | 
			
		||||
                          :value="mode">
 | 
			
		||||
                    {{ mode }}
 | 
			
		||||
                  </option>
 | 
			
		||||
                </b-select>
 | 
			
		||||
              </b-field>
 | 
			
		||||
 | 
			
		||||
              <b-field label="Expiration Date"
 | 
			
		||||
                       v-show="accountForProductMode == 'expired'"
 | 
			
		||||
                       :type="accountForProductExpiration ? null : 'is-danger'">
 | 
			
		||||
                <tailbone-datepicker v-model="accountForProductExpiration">
 | 
			
		||||
                </tailbone-datepicker>
 | 
			
		||||
              </b-field>
 | 
			
		||||
 | 
			
		||||
            </b-field>
 | 
			
		||||
 | 
			
		||||
            <div class="level">
 | 
			
		||||
              <div class="level-left">
 | 
			
		||||
 | 
			
		||||
                <div class="level-item">
 | 
			
		||||
                  <b-input v-model="accountForProductQuantity"
 | 
			
		||||
                           type="number" step="0.0001"
 | 
			
		||||
                           ref="accountForProductQuantityInput">
 | 
			
		||||
                  </b-input>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <div class="level-item">
 | 
			
		||||
                  <b-field>
 | 
			
		||||
                    <b-radio-button v-model="accountForProductUOM"
 | 
			
		||||
                                    @click.native="accountForProductUOMClicked('units')"
 | 
			
		||||
                                    native-value="units">
 | 
			
		||||
                      Units
 | 
			
		||||
                    </b-radio-button>
 | 
			
		||||
                    <b-radio-button v-model="accountForProductUOM"
 | 
			
		||||
                                    @click.native="accountForProductUOMClicked('cases')"
 | 
			
		||||
                                    native-value="cases">
 | 
			
		||||
                      Cases
 | 
			
		||||
                    </b-radio-button>
 | 
			
		||||
                  </b-field>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <div class="level-item"
 | 
			
		||||
                     v-if="accountForProductUOM == 'cases' && accountForProductQuantity">
 | 
			
		||||
                  = {{ accountForProductTotalUnits }}
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
          </section>
 | 
			
		||||
 | 
			
		||||
          <footer class="modal-card-foot">
 | 
			
		||||
            <b-button @click="accountForProductShowDialog = false">
 | 
			
		||||
              Cancel
 | 
			
		||||
            </b-button>
 | 
			
		||||
            <b-button type="is-primary"
 | 
			
		||||
                      @click="accountForProductSubmit()"
 | 
			
		||||
                      :disabled="accountForProductSubmitDisabled"
 | 
			
		||||
                      icon-pack="fas"
 | 
			
		||||
                      icon-left="check">
 | 
			
		||||
              {{ accountForProductSubmitting ? "Working, please wait..." : "Account for Product" }}
 | 
			
		||||
            </b-button>
 | 
			
		||||
          </footer>
 | 
			
		||||
        </div>
 | 
			
		||||
      </b-modal>
 | 
			
		||||
 | 
			
		||||
      <b-modal has-modal-card
 | 
			
		||||
               :active.sync="declareCreditShowDialog">
 | 
			
		||||
        <div class="modal-card">
 | 
			
		||||
 | 
			
		||||
          <header class="modal-card-head">
 | 
			
		||||
            <p class="modal-card-title">Declare Credit</p>
 | 
			
		||||
          </header>
 | 
			
		||||
 | 
			
		||||
          <section class="modal-card-body">
 | 
			
		||||
 | 
			
		||||
            <p class="block">
 | 
			
		||||
              This is for <span class="is-italic">converting</span>
 | 
			
		||||
              some amount you <span class="is-italic">already
 | 
			
		||||
              received</span>, and now declaring there is something
 | 
			
		||||
              wrong with it.
 | 
			
		||||
            </p>
 | 
			
		||||
 | 
			
		||||
            <b-field grouped>
 | 
			
		||||
 | 
			
		||||
              <b-field label="Received">
 | 
			
		||||
                <span class="control">
 | 
			
		||||
                  {{ rowData.received }}
 | 
			
		||||
                </span>
 | 
			
		||||
              </b-field>
 | 
			
		||||
 | 
			
		||||
              <span class="control">
 | 
			
		||||
                 
 | 
			
		||||
              </span>
 | 
			
		||||
 | 
			
		||||
              <b-field label="Credit Type"
 | 
			
		||||
                       :type="declareCreditType ? null : 'is-danger'">
 | 
			
		||||
                <b-select v-model="declareCreditType">
 | 
			
		||||
                  <option v-for="typ in possibleCreditTypes"
 | 
			
		||||
                          :key="typ"
 | 
			
		||||
                          :value="typ">
 | 
			
		||||
                    {{ typ }}
 | 
			
		||||
                  </option>
 | 
			
		||||
                </b-select>
 | 
			
		||||
              </b-field>
 | 
			
		||||
 | 
			
		||||
              <b-field label="Expiration Date"
 | 
			
		||||
                       v-show="declareCreditType == 'expired'"
 | 
			
		||||
                       :type="declareCreditExpiration ? null : 'is-danger'">
 | 
			
		||||
                <tailbone-datepicker v-model="declareCreditExpiration">
 | 
			
		||||
                </tailbone-datepicker>
 | 
			
		||||
              </b-field>
 | 
			
		||||
 | 
			
		||||
            </b-field>
 | 
			
		||||
 | 
			
		||||
            <div class="level">
 | 
			
		||||
              <div class="level-left">
 | 
			
		||||
 | 
			
		||||
                <div class="level-item">
 | 
			
		||||
                  <b-input v-model="declareCreditQuantity"
 | 
			
		||||
                           type="number" step="0.0001"
 | 
			
		||||
                           ref="declareCreditQuantityInput">
 | 
			
		||||
                  </b-input>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <div class="level-item">
 | 
			
		||||
                  <b-field>
 | 
			
		||||
                    <b-radio-button v-model="declareCreditUOM"
 | 
			
		||||
                                    @click.native="declareCreditUOMClicked('units')"
 | 
			
		||||
                                    native-value="units">
 | 
			
		||||
                      Units
 | 
			
		||||
                    </b-radio-button>
 | 
			
		||||
                    <b-radio-button v-model="declareCreditUOM"
 | 
			
		||||
                                    @click.native="declareCreditUOMClicked('cases')"
 | 
			
		||||
                                    native-value="cases">
 | 
			
		||||
                      Cases
 | 
			
		||||
                    </b-radio-button>
 | 
			
		||||
                  </b-field>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <div class="level-item"
 | 
			
		||||
                     v-if="declareCreditUOM == 'cases' && declareCreditQuantity">
 | 
			
		||||
                  = {{ declareCreditTotalUnits }}
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
          </section>
 | 
			
		||||
 | 
			
		||||
          <footer class="modal-card-foot">
 | 
			
		||||
            <b-button @click="declareCreditShowDialog = false">
 | 
			
		||||
              Cancel
 | 
			
		||||
            </b-button>
 | 
			
		||||
            <b-button type="is-warning"
 | 
			
		||||
                      @click="declareCreditSubmit()"
 | 
			
		||||
                      :disabled="declareCreditSubmitDisabled"
 | 
			
		||||
                      icon-pack="fas"
 | 
			
		||||
                      icon-left="thumbs-down">
 | 
			
		||||
              {{ declareCreditSubmitting ? "Working, please wait..." : "Declare this Credit" }}
 | 
			
		||||
            </b-button>
 | 
			
		||||
          </footer>
 | 
			
		||||
        </div>
 | 
			
		||||
      </b-modal>
 | 
			
		||||
 | 
			
		||||
      <nav class="panel" >
 | 
			
		||||
        <p class="panel-heading">Credits</p>
 | 
			
		||||
        <div class="panel-block">
 | 
			
		||||
          <div>
 | 
			
		||||
            ${form.render_field_value('credits')}
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </nav>
 | 
			
		||||
 | 
			
		||||
      <b-modal has-modal-card
 | 
			
		||||
               :active.sync="removeCreditShowDialog">
 | 
			
		||||
        <div class="modal-card remove-credit">
 | 
			
		||||
 | 
			
		||||
          <header class="modal-card-head">
 | 
			
		||||
            <p class="modal-card-title">Un-Declare Credit</p>
 | 
			
		||||
          </header>
 | 
			
		||||
 | 
			
		||||
          <section class="modal-card-body">
 | 
			
		||||
 | 
			
		||||
            <p class="block">
 | 
			
		||||
              If you un-declare this credit, the quantity below will
 | 
			
		||||
              be added back to the
 | 
			
		||||
              <span class="has-text-weight-bold">Received</span> tally.
 | 
			
		||||
            </p>
 | 
			
		||||
 | 
			
		||||
            <b-field label="Credit Type" horizontal>
 | 
			
		||||
              {{ removeCreditRow.credit_type }}
 | 
			
		||||
            </b-field>
 | 
			
		||||
 | 
			
		||||
            <b-field label="Quantity" horizontal>
 | 
			
		||||
              {{ removeCreditRow.shorted }}
 | 
			
		||||
            </b-field>
 | 
			
		||||
 | 
			
		||||
          </section>
 | 
			
		||||
 | 
			
		||||
          <footer class="modal-card-foot">
 | 
			
		||||
            <b-button @click="removeCreditShowDialog = false">
 | 
			
		||||
              Cancel
 | 
			
		||||
            </b-button>
 | 
			
		||||
            <b-button type="is-danger"
 | 
			
		||||
                      @click="removeCreditSubmit()"
 | 
			
		||||
                      :disabled="removeCreditSubmitting"
 | 
			
		||||
                      icon-pack="fas"
 | 
			
		||||
                      icon-left="trash">
 | 
			
		||||
              {{ removeCreditSubmitting ? "Working, please wait..." : "Un-Declare this Credit" }}
 | 
			
		||||
            </b-button>
 | 
			
		||||
          </footer>
 | 
			
		||||
        </div>
 | 
			
		||||
      </b-modal>
 | 
			
		||||
 | 
			
		||||
      <div style="display: flex;">
 | 
			
		||||
 | 
			
		||||
        % if master.batch_handler.has_purchase_order(batch):
 | 
			
		||||
            <nav class="panel" >
 | 
			
		||||
              <p class="panel-heading">Purchase Order</p>
 | 
			
		||||
              <div class="panel-block">
 | 
			
		||||
                <div>
 | 
			
		||||
                  ${form.render_field_readonly('po_line_number')}
 | 
			
		||||
                  ${form.render_field_readonly('po_unit_cost')}
 | 
			
		||||
                  ${form.render_field_readonly('po_case_size')}
 | 
			
		||||
                  ${form.render_field_readonly('po_total')}
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </nav>
 | 
			
		||||
        % endif
 | 
			
		||||
 | 
			
		||||
        % if master.batch_handler.has_invoice_file(batch):
 | 
			
		||||
            <nav class="panel" >
 | 
			
		||||
              <p class="panel-heading">Invoice</p>
 | 
			
		||||
              <div class="panel-block">
 | 
			
		||||
                <div>
 | 
			
		||||
                  ${form.render_field_readonly('invoice_line_number')}
 | 
			
		||||
                  ${form.render_field_readonly('invoice_unit_cost')}
 | 
			
		||||
              % if master.has_perm('edit_row'):
 | 
			
		||||
                  <div class="is-pulled-right">
 | 
			
		||||
                    <once-button type="is-primary"
 | 
			
		||||
                                 tag="a" href="${master.get_row_action_url('edit', row)}"
 | 
			
		||||
                                 ## @click="editUnitCost()"
 | 
			
		||||
                                 ## icon-pack="fas"
 | 
			
		||||
                                 icon-left="edit"
 | 
			
		||||
                                 text="Edit Unit Cost">
 | 
			
		||||
                    </once-button>
 | 
			
		||||
                  ${form.render_field_readonly('invoice_case_size')}
 | 
			
		||||
                  ${form.render_field_readonly('invoice_total', label="Invoice Total")}
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </nav>
 | 
			
		||||
        % endif
 | 
			
		||||
              ${form.render_field_readonly('invoice_cost_confirmed')}
 | 
			
		||||
              <div class="is-pulled-right">
 | 
			
		||||
                <b-button type="is-primary"
 | 
			
		||||
                          @click="confirmUnitCost()"
 | 
			
		||||
                          icon-pack="fas"
 | 
			
		||||
                          icon-left="check">
 | 
			
		||||
                  Confirm Unit Cost
 | 
			
		||||
                </b-button>
 | 
			
		||||
              </div>
 | 
			
		||||
              ${form.render_field_readonly('invoice_total')}
 | 
			
		||||
              ${form.render_field_readonly('invoice_total_calculated')}
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </nav>
 | 
			
		||||
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <nav class="panel" >
 | 
			
		||||
        <p class="panel-heading">Credits</p>
 | 
			
		||||
        <div class="panel-block">
 | 
			
		||||
          <div>
 | 
			
		||||
            ${form.render_field_readonly('credits')}
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </nav>
 | 
			
		||||
 | 
			
		||||
  % else:
 | 
			
		||||
      ## legacy / not buefy
 | 
			
		||||
      ${parent.page_content()}
 | 
			
		||||
| 
						 | 
				
			
			@ -164,6 +471,211 @@
 | 
			
		|||
        alert("TODO: not yet implemented")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ThisPageData.rowData = ${json.dumps(row_context)|n}
 | 
			
		||||
    ThisPageData.possibleReceivingModes = ${json.dumps(possible_receiving_modes)|n}
 | 
			
		||||
    ThisPageData.possibleCreditTypes = ${json.dumps(possible_credit_types)|n}
 | 
			
		||||
 | 
			
		||||
    ThisPageData.accountForProductShowDialog = false
 | 
			
		||||
    ThisPageData.accountForProductMode = null
 | 
			
		||||
    ThisPageData.accountForProductQuantity = null
 | 
			
		||||
    ThisPageData.accountForProductUOM = 'units'
 | 
			
		||||
    ThisPageData.accountForProductExpiration = null
 | 
			
		||||
    ThisPageData.accountForProductSubmitting = false
 | 
			
		||||
 | 
			
		||||
    ThisPage.computed.accountForProductTotalUnits = function() {
 | 
			
		||||
        return this.renderQuantity(this.accountForProductQuantity,
 | 
			
		||||
                                   this.accountForProductUOM)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ThisPage.computed.accountForProductSubmitDisabled = function() {
 | 
			
		||||
        if (!this.accountForProductMode) {
 | 
			
		||||
            return true
 | 
			
		||||
        }
 | 
			
		||||
        if (this.accountForProductMode == 'expired' && !this.accountForProductExpiration) {
 | 
			
		||||
            return true
 | 
			
		||||
        }
 | 
			
		||||
        if (!this.accountForProductQuantity) {
 | 
			
		||||
            return true
 | 
			
		||||
        }
 | 
			
		||||
        if (this.accountForProductSubmitting) {
 | 
			
		||||
            return true
 | 
			
		||||
        }
 | 
			
		||||
        return false
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ThisPage.methods.accountForProductInit = function() {
 | 
			
		||||
        this.accountForProductMode = 'received'
 | 
			
		||||
        this.accountForProductExpiration = null
 | 
			
		||||
        this.accountForProductQuantity = null
 | 
			
		||||
        this.accountForProductUOM = 'units'
 | 
			
		||||
        this.accountForProductShowDialog = true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ThisPage.methods.accountForProductUOMClicked = function(uom) {
 | 
			
		||||
 | 
			
		||||
        // TODO: this does not seem to work as expected..even though
 | 
			
		||||
        // the code appears to be correct
 | 
			
		||||
        this.$nextTick(() => {
 | 
			
		||||
            this.$refs.accountForProductQuantityInput.focus()
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ThisPage.methods.accountForProductSubmit = function() {
 | 
			
		||||
 | 
			
		||||
        let qty = parseFloat(this.accountForProductQuantity)
 | 
			
		||||
        if (qty == NaN || !qty) {
 | 
			
		||||
            this.$buefy.toast.open({
 | 
			
		||||
                message: "You must enter a quantity.",
 | 
			
		||||
                type: 'is-warning',
 | 
			
		||||
                duration: 4000, // 4 seconds
 | 
			
		||||
            })
 | 
			
		||||
            return
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this.accountForProductMode != 'received' && qty < 0) {
 | 
			
		||||
            this.$buefy.toast.open({
 | 
			
		||||
                message: "Negative amounts are only allowed for the \"received\" state.",
 | 
			
		||||
                type: 'is-warning',
 | 
			
		||||
                duration: 4000, // 4 seconds
 | 
			
		||||
            })
 | 
			
		||||
            return
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.accountForProductSubmitting = true
 | 
			
		||||
        let url = '${url('{}.receive_row'.format(route_prefix), uuid=batch.uuid, row_uuid=row.uuid)}'
 | 
			
		||||
        let params = {
 | 
			
		||||
            mode: this.accountForProductMode,
 | 
			
		||||
            quantity: {cases: null, units: null},
 | 
			
		||||
            expiration_date: this.accountForProductExpiration,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this.accountForProductUOM == 'cases') {
 | 
			
		||||
            params.quantity.cases = this.accountForProductQuantity
 | 
			
		||||
        } else {
 | 
			
		||||
            params.quantity.units = this.accountForProductQuantity
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.submitForm(url, params, response => {
 | 
			
		||||
            this.rowData = response.data.row
 | 
			
		||||
            this.accountForProductSubmitting = false
 | 
			
		||||
            this.accountForProductShowDialog = false
 | 
			
		||||
        }, response => {
 | 
			
		||||
            this.accountForProductSubmitting = false
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ThisPageData.declareCreditShowDialog = false
 | 
			
		||||
    ThisPageData.declareCreditType = null
 | 
			
		||||
    ThisPageData.declareCreditExpiration = null
 | 
			
		||||
    ThisPageData.declareCreditQuantity = null
 | 
			
		||||
    ThisPageData.declareCreditUOM = 'units'
 | 
			
		||||
    ThisPageData.declareCreditSubmitting = false
 | 
			
		||||
 | 
			
		||||
    ThisPage.methods.renderQuantity = function(qty, uom) {
 | 
			
		||||
        qty = parseFloat(qty)
 | 
			
		||||
        if (qty == NaN) {
 | 
			
		||||
            return "n/a"
 | 
			
		||||
        }
 | 
			
		||||
        if (uom == 'cases') {
 | 
			
		||||
            qty *= this.rowData.case_quantity
 | 
			
		||||
        }
 | 
			
		||||
        if (qty == NaN) {
 | 
			
		||||
            return "n/a"
 | 
			
		||||
        }
 | 
			
		||||
        if (qty == 1) {
 | 
			
		||||
            return "1 unit"
 | 
			
		||||
        }
 | 
			
		||||
        if (qty == -1) {
 | 
			
		||||
            return "-1 unit"
 | 
			
		||||
        }
 | 
			
		||||
        if (Math.round(qty) == qty) {
 | 
			
		||||
            return qty.toString() + " units"
 | 
			
		||||
        }
 | 
			
		||||
        return qty.toFixed(4) + " units"
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ThisPage.computed.declareCreditTotalUnits = function() {
 | 
			
		||||
        return this.renderQuantity(this.declareCreditQuantity,
 | 
			
		||||
                                   this.declareCreditUOM)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ThisPage.computed.declareCreditSubmitDisabled = function() {
 | 
			
		||||
        if (!this.declareCreditType) {
 | 
			
		||||
            return true
 | 
			
		||||
        }
 | 
			
		||||
        if (this.declareCreditType == 'expired' && !this.declareCreditExpiration) {
 | 
			
		||||
            return true
 | 
			
		||||
        }
 | 
			
		||||
        if (!this.declareCreditQuantity) {
 | 
			
		||||
            return true
 | 
			
		||||
        }
 | 
			
		||||
        if (this.declareCreditSubmitting) {
 | 
			
		||||
            return true
 | 
			
		||||
        }
 | 
			
		||||
        return false
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ThisPage.methods.declareCreditInit = function() {
 | 
			
		||||
        this.declareCreditType = null
 | 
			
		||||
        this.declareCreditExpiration = null
 | 
			
		||||
        if (this.rowData.cases_received) {
 | 
			
		||||
            this.declareCreditQuantity = this.rowData.cases_received
 | 
			
		||||
            this.declareCreditUOM = 'cases'
 | 
			
		||||
        } else {
 | 
			
		||||
            this.declareCreditQuantity = this.rowData.units_received
 | 
			
		||||
            this.declareCreditUOM = 'units'
 | 
			
		||||
        }
 | 
			
		||||
        this.declareCreditShowDialog = true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ThisPage.methods.declareCreditSubmit = function() {
 | 
			
		||||
        this.declareCreditSubmitting = true
 | 
			
		||||
        let url = '${url('{}.declare_credit'.format(route_prefix), uuid=batch.uuid, row_uuid=row.uuid)}'
 | 
			
		||||
        let params = {
 | 
			
		||||
            credit_type: this.declareCreditType,
 | 
			
		||||
            cases: null,
 | 
			
		||||
            units: null,
 | 
			
		||||
            expiration_date: this.declareCreditExpiration,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this.declareCreditUOM == 'cases') {
 | 
			
		||||
            params.cases = this.declareCreditQuantity
 | 
			
		||||
        } else {
 | 
			
		||||
            params.units = this.declareCreditQuantity
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.submitForm(url, params, response => {
 | 
			
		||||
            this.rowData = response.data.row
 | 
			
		||||
            this.declareCreditSubmitting = false
 | 
			
		||||
            this.declareCreditShowDialog = false
 | 
			
		||||
        }, response => {
 | 
			
		||||
            this.declareCreditSubmitting = false
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ThisPageData.removeCreditShowDialog = false
 | 
			
		||||
    ThisPageData.removeCreditRow = {}
 | 
			
		||||
    ThisPageData.removeCreditSubmitting = false
 | 
			
		||||
 | 
			
		||||
    ThisPage.methods.removeCreditInit = function(row) {
 | 
			
		||||
        this.removeCreditRow = row
 | 
			
		||||
        this.removeCreditShowDialog = true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ThisPage.methods.removeCreditSubmit = function() {
 | 
			
		||||
        this.removeCreditSubmitting = true
 | 
			
		||||
        let url = '${url('{}.undeclare_credit'.format(route_prefix), uuid=batch.uuid, row_uuid=row.uuid)}'
 | 
			
		||||
        let params = {
 | 
			
		||||
            uuid: this.removeCreditRow.uuid,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.submitForm(url, params, response => {
 | 
			
		||||
            this.rowData = response.data.row
 | 
			
		||||
            this.removeCreditSubmitting = false
 | 
			
		||||
            this.removeCreditShowDialog = false
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  </script>
 | 
			
		||||
</%def>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -31,6 +31,8 @@
 | 
			
		|||
  </head>
 | 
			
		||||
 | 
			
		||||
  <body>
 | 
			
		||||
    ${declare_formposter_mixin()}
 | 
			
		||||
 | 
			
		||||
    ${self.body()}
 | 
			
		||||
 | 
			
		||||
    <div id="whole-page-app">
 | 
			
		||||
| 
						 | 
				
			
			@ -517,7 +519,6 @@
 | 
			
		|||
</%def>
 | 
			
		||||
 | 
			
		||||
<%def name="declare_whole_page_vars()">
 | 
			
		||||
  ${declare_formposter_mixin()}
 | 
			
		||||
  ${h.javascript_link(request.static_url('tailbone:static/themes/falafel/js/tailbone.feedback.js') + '?ver={}'.format(tailbone.__version__))}
 | 
			
		||||
  <script type="text/javascript">
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -115,7 +115,9 @@ class BatchMasterView(MasterView):
 | 
			
		|||
 | 
			
		||||
    def __init__(self, request):
 | 
			
		||||
        super(BatchMasterView, self).__init__(request)
 | 
			
		||||
        self.handler = self.get_handler()
 | 
			
		||||
        self.batch_handler = self.get_handler()
 | 
			
		||||
        # TODO: deprecate / remove this (?)
 | 
			
		||||
        self.handler = self.batch_handler
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def get_handler_factory(cls, rattail_config):
 | 
			
		||||
| 
						 | 
				
			
			@ -1149,19 +1151,28 @@ class BatchMasterView(MasterView):
 | 
			
		|||
        """
 | 
			
		||||
        Batch rows are editable only until batch is complete or executed.
 | 
			
		||||
        """
 | 
			
		||||
        if not (self.rows_editable or self.rows_editable_but_not_directly):
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
        batch = self.get_parent(row)
 | 
			
		||||
        return self.rows_editable and not batch.executed and not batch.complete
 | 
			
		||||
        if batch.complete or batch.executed:
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
    def row_deletable(self, row):
 | 
			
		||||
        """
 | 
			
		||||
        Batch rows are deletable only until batch is complete or executed.
 | 
			
		||||
        """
 | 
			
		||||
        if self.rows_deletable:
 | 
			
		||||
            batch = self.get_parent(row)
 | 
			
		||||
            if not batch.executed and not batch.complete:
 | 
			
		||||
                return True
 | 
			
		||||
        if not self.rows_deletable:
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
        batch = self.get_parent(row)
 | 
			
		||||
        if batch.complete or batch.executed:
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
    def template_kwargs_view_row(self, **kwargs):
 | 
			
		||||
        kwargs['batch_model_title'] = kwargs['parent_model_title']
 | 
			
		||||
        # TODO: should these be set somewhere else?
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -166,6 +166,7 @@ class MasterView(View):
 | 
			
		|||
    rows_viewable = True
 | 
			
		||||
    rows_creatable = False
 | 
			
		||||
    rows_editable = False
 | 
			
		||||
    rows_editable_but_not_directly = False
 | 
			
		||||
    rows_deletable = False
 | 
			
		||||
    rows_deletable_speedbump = True
 | 
			
		||||
    rows_bulk_deletable = False
 | 
			
		||||
| 
						 | 
				
			
			@ -3852,6 +3853,7 @@ class MasterView(View):
 | 
			
		|||
        return self.render_to_response('edit_row', {
 | 
			
		||||
            'instance': row,
 | 
			
		||||
            'row_parent': parent,
 | 
			
		||||
            'parent_model_title': self.get_model_title(),
 | 
			
		||||
            'parent_title': self.get_instance_title(parent),
 | 
			
		||||
            'parent_url': self.get_action_url('view', parent),
 | 
			
		||||
            'parent_instance': parent,
 | 
			
		||||
| 
						 | 
				
			
			@ -3884,6 +3886,8 @@ class MasterView(View):
 | 
			
		|||
        considered "deletable".  Returns ``True`` by default; override as
 | 
			
		||||
        necessary.
 | 
			
		||||
        """
 | 
			
		||||
        if not self.rows_deletable:
 | 
			
		||||
            return False
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
    def delete_row_object(self, row):
 | 
			
		||||
| 
						 | 
				
			
			@ -4099,6 +4103,7 @@ class MasterView(View):
 | 
			
		|||
        config_title = cls.get_config_title()
 | 
			
		||||
        if cls.has_rows:
 | 
			
		||||
            row_model_title = cls.get_row_model_title()
 | 
			
		||||
            row_model_title_plural = cls.get_row_model_title_plural()
 | 
			
		||||
 | 
			
		||||
        config.add_tailbone_permission_group(permission_prefix, model_title_plural, overwrite=False)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -4386,9 +4391,10 @@ class MasterView(View):
 | 
			
		|||
 | 
			
		||||
        # edit row
 | 
			
		||||
        if cls.has_rows:
 | 
			
		||||
            if cls.rows_editable:
 | 
			
		||||
            if cls.rows_editable or cls.rows_editable_but_not_directly:
 | 
			
		||||
                config.add_tailbone_permission(permission_prefix, '{}.edit_row'.format(permission_prefix),
 | 
			
		||||
                                               "Edit individual {} rows".format(model_title))
 | 
			
		||||
                                               "Edit individual {}".format(row_model_title_plural))
 | 
			
		||||
            if cls.rows_editable:
 | 
			
		||||
                config.add_route('{}.edit_row'.format(route_prefix), '{}/{{uuid}}/rows/{{row_uuid}}/edit'.format(url_prefix))
 | 
			
		||||
                config.add_view(cls, attr='edit_row', route_name='{}.edit_row'.format(route_prefix),
 | 
			
		||||
                                permission='{}.edit_row'.format(permission_prefix))
 | 
			
		||||
| 
						 | 
				
			
			@ -4397,7 +4403,7 @@ class MasterView(View):
 | 
			
		|||
        if cls.has_rows:
 | 
			
		||||
            if cls.rows_deletable:
 | 
			
		||||
                config.add_tailbone_permission(permission_prefix, '{}.delete_row'.format(permission_prefix),
 | 
			
		||||
                                               "Delete individual {} rows".format(model_title))
 | 
			
		||||
                                               "Delete individual {}".format(row_model_title_plural))
 | 
			
		||||
                config.add_route('{}.delete_row'.format(route_prefix), '{}/{{uuid}}/rows/{{row_uuid}}/delete'.format(url_prefix))
 | 
			
		||||
                config.add_view(cls, attr='delete_row', route_name='{}.delete_row'.format(route_prefix),
 | 
			
		||||
                                permission='{}.delete_row'.format(permission_prefix))
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -99,8 +99,10 @@ class PurchasingBatchView(BatchMasterView):
 | 
			
		|||
        'upc': "UPC",
 | 
			
		||||
        'item_id': "Item ID",
 | 
			
		||||
        'brand_name': "Brand",
 | 
			
		||||
        'case_quantity': "Case Size",
 | 
			
		||||
        'po_line_number': "PO Line Number",
 | 
			
		||||
        'po_unit_cost': "PO Unit Cost",
 | 
			
		||||
        'po_case_size': "PO Case Size",
 | 
			
		||||
        'po_total': "PO Total",
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -144,6 +146,9 @@ class PurchasingBatchView(BatchMasterView):
 | 
			
		|||
        'mispick',
 | 
			
		||||
        'cases_mispick',
 | 
			
		||||
        'units_mispick',
 | 
			
		||||
        'missing',
 | 
			
		||||
        'cases_missing',
 | 
			
		||||
        'units_missing',
 | 
			
		||||
        'po_line_number',
 | 
			
		||||
        'po_unit_cost',
 | 
			
		||||
        'po_total',
 | 
			
		||||
| 
						 | 
				
			
			@ -710,8 +715,11 @@ class PurchasingBatchView(BatchMasterView):
 | 
			
		|||
        f.set_renderer('damaged', self.render_row_quantity)
 | 
			
		||||
        f.set_renderer('expired', self.render_row_quantity)
 | 
			
		||||
        f.set_renderer('mispick', self.render_row_quantity)
 | 
			
		||||
        f.set_renderer('missing', self.render_row_quantity)
 | 
			
		||||
 | 
			
		||||
        f.set_type('case_quantity', 'quantity')
 | 
			
		||||
        f.set_type('po_case_size', 'quantity')
 | 
			
		||||
        f.set_type('invoice_case_size', 'quantity')
 | 
			
		||||
        f.set_type('cases_ordered', 'quantity')
 | 
			
		||||
        f.set_type('units_ordered', 'quantity')
 | 
			
		||||
        f.set_type('cases_shipped', 'quantity')
 | 
			
		||||
| 
						 | 
				
			
			@ -724,6 +732,8 @@ class PurchasingBatchView(BatchMasterView):
 | 
			
		|||
        f.set_type('units_expired', 'quantity')
 | 
			
		||||
        f.set_type('cases_mispick', 'quantity')
 | 
			
		||||
        f.set_type('units_mispick', 'quantity')
 | 
			
		||||
        f.set_type('cases_missing', 'quantity')
 | 
			
		||||
        f.set_type('units_missing', 'quantity')
 | 
			
		||||
 | 
			
		||||
        # currency fields
 | 
			
		||||
        # nb. we only show "total" fields as currency, but not case or
 | 
			
		||||
| 
						 | 
				
			
			@ -746,6 +756,7 @@ class PurchasingBatchView(BatchMasterView):
 | 
			
		|||
 | 
			
		||||
        # credits
 | 
			
		||||
        f.set_readonly('credits')
 | 
			
		||||
        if self.viewing:
 | 
			
		||||
            f.set_renderer('credits', self.render_row_credits)
 | 
			
		||||
 | 
			
		||||
        if self.creating:
 | 
			
		||||
| 
						 | 
				
			
			@ -786,35 +797,57 @@ class PurchasingBatchView(BatchMasterView):
 | 
			
		|||
        app = self.get_rattail_app()
 | 
			
		||||
        cases = getattr(row, 'cases_{}'.format(field))
 | 
			
		||||
        units = getattr(row, 'units_{}'.format(field))
 | 
			
		||||
        if cases and units:
 | 
			
		||||
            return "{} cases + {} units".format(app.render_quantity(cases),
 | 
			
		||||
                                                app.render_quantity(units))
 | 
			
		||||
        if cases and not units:
 | 
			
		||||
            return "{} cases".format(app.render_quantity(cases))
 | 
			
		||||
        if units and not cases:
 | 
			
		||||
            return "{} units".format(app.render_quantity(units))
 | 
			
		||||
 | 
			
		||||
    def render_row_credits(self, row, field):
 | 
			
		||||
        if not row.credits:
 | 
			
		||||
            return ""
 | 
			
		||||
        return app.render_cases_units(cases, units)
 | 
			
		||||
 | 
			
		||||
    def make_row_credits_grid(self, row):
 | 
			
		||||
        use_buefy = self.get_use_buefy()
 | 
			
		||||
        route_prefix = self.get_route_prefix()
 | 
			
		||||
        factory = self.get_grid_factory()
 | 
			
		||||
 | 
			
		||||
        g = factory(
 | 
			
		||||
            key='{}.row_credits'.format(route_prefix),
 | 
			
		||||
            data=[] if use_buefy else row.credits,
 | 
			
		||||
            columns=[
 | 
			
		||||
                'credit_type',
 | 
			
		||||
            'cases_shorted',
 | 
			
		||||
            'units_shorted',
 | 
			
		||||
                # 'cases_shorted',
 | 
			
		||||
                # 'units_shorted',
 | 
			
		||||
                'shorted',
 | 
			
		||||
                'credit_total',
 | 
			
		||||
        ]
 | 
			
		||||
        g = grids.Grid(
 | 
			
		||||
            key='{}.row_credits'.format(route_prefix),
 | 
			
		||||
            data=row.credits,
 | 
			
		||||
            columns=columns,
 | 
			
		||||
            labels={'credit_type': "Type",
 | 
			
		||||
                'expiration_date',
 | 
			
		||||
                # 'mispick_upc',
 | 
			
		||||
                # 'mispick_brand_name',
 | 
			
		||||
                # 'mispick_description',
 | 
			
		||||
                # 'mispick_size',
 | 
			
		||||
            ],
 | 
			
		||||
            labels={
 | 
			
		||||
                'credit_type': "Type",
 | 
			
		||||
                'cases_shorted': "Cases",
 | 
			
		||||
                    'units_shorted': "Units"})
 | 
			
		||||
                'units_shorted': "Units",
 | 
			
		||||
                'shorted': "Quantity",
 | 
			
		||||
                'credit_total': "Total",
 | 
			
		||||
                'mispick_upc': "Mispick UPC",
 | 
			
		||||
                'mispick_brand_name': "MP Brand",
 | 
			
		||||
                'mispick_description': "MP Description",
 | 
			
		||||
                'mispick_size': "MP Size",
 | 
			
		||||
            })
 | 
			
		||||
 | 
			
		||||
        g.set_type('cases_shorted', 'quantity')
 | 
			
		||||
        g.set_type('units_shorted', 'quantity')
 | 
			
		||||
        g.set_type('credit_total', 'currency')
 | 
			
		||||
 | 
			
		||||
        return g
 | 
			
		||||
 | 
			
		||||
    def render_row_credits(self, row, field):
 | 
			
		||||
        use_buefy = self.get_use_buefy()
 | 
			
		||||
        if not use_buefy and not row.credits:
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        g = self.make_row_credits_grid(row)
 | 
			
		||||
 | 
			
		||||
        if use_buefy:
 | 
			
		||||
            return HTML.literal(
 | 
			
		||||
                g.render_buefy_table_element(data_prop='rowData.credits'))
 | 
			
		||||
        else:
 | 
			
		||||
            return HTML.literal(g.render_grid())
 | 
			
		||||
 | 
			
		||||
#     def item_lookup(self, value, field=None):
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -51,6 +51,21 @@ from tailbone.views.purchasing import PurchasingBatchView
 | 
			
		|||
 | 
			
		||||
log = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
POSSIBLE_RECEIVING_MODES = [
 | 
			
		||||
    'received',
 | 
			
		||||
    'damaged',
 | 
			
		||||
    'expired',
 | 
			
		||||
    # 'mispick',
 | 
			
		||||
    'missing',
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
POSSIBLE_CREDIT_TYPES = [
 | 
			
		||||
    'damaged',
 | 
			
		||||
    'expired',
 | 
			
		||||
    # 'mispick',
 | 
			
		||||
    'missing',
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ReceivingBatchView(PurchasingBatchView):
 | 
			
		||||
    """
 | 
			
		||||
| 
						 | 
				
			
			@ -63,7 +78,9 @@ class ReceivingBatchView(PurchasingBatchView):
 | 
			
		|||
    index_title = "Receiving"
 | 
			
		||||
    downloadable = True
 | 
			
		||||
    bulk_deletable = True
 | 
			
		||||
    rows_editable = True
 | 
			
		||||
    rows_editable = False
 | 
			
		||||
    rows_editable_but_not_directly = True
 | 
			
		||||
    rows_deletable = True
 | 
			
		||||
 | 
			
		||||
    default_uom_is_case = True
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -181,13 +198,18 @@ class ReceivingBatchView(PurchasingBatchView):
 | 
			
		|||
        'mispick',
 | 
			
		||||
        'cases_mispick',
 | 
			
		||||
        'units_mispick',
 | 
			
		||||
        'missing',
 | 
			
		||||
        'cases_missing',
 | 
			
		||||
        'units_missing',
 | 
			
		||||
        'catalog_unit_cost',
 | 
			
		||||
        'po_line_number',
 | 
			
		||||
        'po_unit_cost',
 | 
			
		||||
        'po_case_size',
 | 
			
		||||
        'po_total',
 | 
			
		||||
        'invoice_line_number',
 | 
			
		||||
        'invoice_unit_cost',
 | 
			
		||||
        'invoice_cost_confirmed',
 | 
			
		||||
        'invoice_case_size',
 | 
			
		||||
        'invoice_total',
 | 
			
		||||
        'invoice_total_calculated',
 | 
			
		||||
        'status_code',
 | 
			
		||||
| 
						 | 
				
			
			@ -322,17 +344,14 @@ class ReceivingBatchView(PurchasingBatchView):
 | 
			
		|||
        return self.render_to_response('create', context)
 | 
			
		||||
 | 
			
		||||
    def row_deletable(self, row):
 | 
			
		||||
 | 
			
		||||
        # first run it through the normal logic, if that doesn't like
 | 
			
		||||
        # it then we won't either
 | 
			
		||||
        if not super(ReceivingBatchView, self).row_deletable(row):
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
        batch = row.batch
 | 
			
		||||
 | 
			
		||||
        # don't allow if master view has disabled that entirely
 | 
			
		||||
        if not self.rows_deletable:
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
        # can never delete rows for complete/executed batches
 | 
			
		||||
        # TODO: not so sure about the 'complete' part though..?
 | 
			
		||||
        if batch.executed or batch.complete:
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
        # can always delete rows from truck dump parent
 | 
			
		||||
        if batch.is_truck_dump_parent():
 | 
			
		||||
            return True
 | 
			
		||||
| 
						 | 
				
			
			@ -362,7 +381,7 @@ class ReceivingBatchView(PurchasingBatchView):
 | 
			
		|||
        super(ReceivingBatchView, self).configure_form(f)
 | 
			
		||||
        model = self.model
 | 
			
		||||
        batch = f.model_instance
 | 
			
		||||
        allow_truck_dump = self.handler.allow_truck_dump_receiving()
 | 
			
		||||
        allow_truck_dump = self.batch_handler.allow_truck_dump_receiving()
 | 
			
		||||
        workflow = self.request.matchdict.get('workflow_key')
 | 
			
		||||
        route_prefix = self.get_route_prefix()
 | 
			
		||||
        use_buefy = self.get_use_buefy()
 | 
			
		||||
| 
						 | 
				
			
			@ -472,9 +491,9 @@ class ReceivingBatchView(PurchasingBatchView):
 | 
			
		|||
            and self.purchase_order_fieldname == 'purchase'):
 | 
			
		||||
            if use_buefy:
 | 
			
		||||
                f.replace('purchase', 'purchase_uuid')
 | 
			
		||||
                purchases = self.handler.get_eligible_purchases(
 | 
			
		||||
                purchases = self.batch_handler.get_eligible_purchases(
 | 
			
		||||
                    vendor, self.enum.PURCHASE_BATCH_MODE_RECEIVING)
 | 
			
		||||
                values = [(p.uuid, self.handler.render_eligible_purchase(p))
 | 
			
		||||
                values = [(p.uuid, self.batch_handler.render_eligible_purchase(p))
 | 
			
		||||
                          for p in purchases]
 | 
			
		||||
                f.set_widget('purchase_uuid', dfwidget.SelectWidget(values=values))
 | 
			
		||||
                f.set_label('purchase_uuid', "Purchase Order")
 | 
			
		||||
| 
						 | 
				
			
			@ -497,12 +516,11 @@ class ReceivingBatchView(PurchasingBatchView):
 | 
			
		|||
            f.remove('invoice_total_calculated')
 | 
			
		||||
 | 
			
		||||
        # hide all invoice fields if batch does not have invoice file
 | 
			
		||||
        if not self.creating and not self.handler.has_invoice_file(batch):
 | 
			
		||||
        if not self.creating and not self.batch_handler.has_invoice_file(batch):
 | 
			
		||||
            f.remove('invoice_file',
 | 
			
		||||
                     'invoice_date',
 | 
			
		||||
                     'invoice_number',
 | 
			
		||||
                     'invoice_total',
 | 
			
		||||
                     'invoice_total_calculated')
 | 
			
		||||
                     'invoice_total')
 | 
			
		||||
 | 
			
		||||
        # receiving_complete
 | 
			
		||||
        if self.creating:
 | 
			
		||||
| 
						 | 
				
			
			@ -517,9 +535,12 @@ class ReceivingBatchView(PurchasingBatchView):
 | 
			
		|||
                         'invoice_parser_key')
 | 
			
		||||
 | 
			
		||||
            elif workflow == 'from_invoice':
 | 
			
		||||
                f.remove('truck_dump_batch_uuid')
 | 
			
		||||
                f.set_required('invoice_file')
 | 
			
		||||
                f.set_required('invoice_parser_key')
 | 
			
		||||
                f.remove('truck_dump_batch_uuid',
 | 
			
		||||
                         'po_number',
 | 
			
		||||
                         'invoice_date',
 | 
			
		||||
                         'invoice_number')
 | 
			
		||||
 | 
			
		||||
            elif workflow == 'from_po':
 | 
			
		||||
                f.remove('truck_dump_batch_uuid',
 | 
			
		||||
| 
						 | 
				
			
			@ -531,9 +552,13 @@ class ReceivingBatchView(PurchasingBatchView):
 | 
			
		|||
                         'invoice_number')
 | 
			
		||||
 | 
			
		||||
            elif workflow == 'from_po_with_invoice':
 | 
			
		||||
                f.remove('truck_dump_batch_uuid')
 | 
			
		||||
                f.set_required('invoice_file')
 | 
			
		||||
                f.set_required('invoice_parser_key')
 | 
			
		||||
                f.remove('truck_dump_batch_uuid',
 | 
			
		||||
                         'date_ordered',
 | 
			
		||||
                         'po_number',
 | 
			
		||||
                         'invoice_date',
 | 
			
		||||
                         'invoice_number')
 | 
			
		||||
 | 
			
		||||
            elif workflow == 'truck_dump_children_first':
 | 
			
		||||
                f.remove('truck_dump_batch_uuid',
 | 
			
		||||
| 
						 | 
				
			
			@ -614,16 +639,92 @@ class ReceivingBatchView(PurchasingBatchView):
 | 
			
		|||
            raise NotImplementedError
 | 
			
		||||
        return kwargs
 | 
			
		||||
 | 
			
		||||
    def make_po_vs_invoice_breakdown(self, batch):
 | 
			
		||||
        """
 | 
			
		||||
        Returns a simple breakdown as list of 2-tuples, each of which
 | 
			
		||||
        has the display title as first member, and number of rows as
 | 
			
		||||
        second member.
 | 
			
		||||
        """
 | 
			
		||||
        grouped = {}
 | 
			
		||||
        labels = OrderedDict([
 | 
			
		||||
            ('both', "Found in both PO and Invoice"),
 | 
			
		||||
            ('po_not_invoice', "Found in PO but not Invoice"),
 | 
			
		||||
            ('invoice_not_po', "Found in Invoice but not PO"),
 | 
			
		||||
            ('neither', "Not found in PO nor Invoice"),
 | 
			
		||||
        ])
 | 
			
		||||
 | 
			
		||||
        for row in batch.active_rows():
 | 
			
		||||
            if row.po_line_number and not row.invoice_line_number:
 | 
			
		||||
                grouped.setdefault('po_not_invoice', []).append(row)
 | 
			
		||||
            elif row.invoice_line_number and not row.po_line_number:
 | 
			
		||||
                grouped.setdefault('invoice_not_po', []).append(row)
 | 
			
		||||
            elif row.po_line_number and row.invoice_line_number:
 | 
			
		||||
                grouped.setdefault('both', []).append(row)
 | 
			
		||||
            else:
 | 
			
		||||
                grouped.setdefault('neither', []).append(row)
 | 
			
		||||
 | 
			
		||||
        breakdown = []
 | 
			
		||||
 | 
			
		||||
        for key, label in labels.items():
 | 
			
		||||
            if key in grouped:
 | 
			
		||||
                breakdown.append({
 | 
			
		||||
                    'title': label,
 | 
			
		||||
                    'count': len(grouped[key]),
 | 
			
		||||
                })
 | 
			
		||||
 | 
			
		||||
        return breakdown
 | 
			
		||||
 | 
			
		||||
    def template_kwargs_view(self, **kwargs):
 | 
			
		||||
        kwargs = super(ReceivingBatchView, self).template_kwargs_view(**kwargs)
 | 
			
		||||
        batch = kwargs['instance']
 | 
			
		||||
 | 
			
		||||
        if self.handler.has_purchase_order(batch) and self.handler.has_invoice_file(batch):
 | 
			
		||||
            breakdown = self.make_po_vs_invoice_breakdown(batch)
 | 
			
		||||
 | 
			
		||||
            factory = self.get_grid_factory()
 | 
			
		||||
            kwargs['po_vs_invoice_breakdown_grid'] = factory(
 | 
			
		||||
                'batch_po_vs_invoice_breakdown',
 | 
			
		||||
                data=breakdown,
 | 
			
		||||
                columns=['title', 'count'])
 | 
			
		||||
 | 
			
		||||
        return kwargs
 | 
			
		||||
 | 
			
		||||
    def get_context_credits(self, row):
 | 
			
		||||
        app = self.get_rattail_app()
 | 
			
		||||
        credits_data = []
 | 
			
		||||
        for credit in row.credits:
 | 
			
		||||
            credits_data.append({
 | 
			
		||||
                'uuid': credit.uuid,
 | 
			
		||||
                'credit_type': credit.credit_type,
 | 
			
		||||
                'expiration_date': six.text_type(credit.expiration_date) if credit.expiration_date else None,
 | 
			
		||||
                'cases_shorted': app.render_quantity(credit.cases_shorted),
 | 
			
		||||
                'units_shorted': app.render_quantity(credit.units_shorted),
 | 
			
		||||
                'shorted': app.render_cases_units(credit.cases_shorted,
 | 
			
		||||
                                                  credit.units_shorted),
 | 
			
		||||
                'credit_total': app.render_currency(credit.credit_total),
 | 
			
		||||
                'mispick_upc': '-',
 | 
			
		||||
                'mispick_brand_name': '-',
 | 
			
		||||
                'mispick_description': '-',
 | 
			
		||||
                'mispick_size': '-',
 | 
			
		||||
            })
 | 
			
		||||
        return credits_data
 | 
			
		||||
 | 
			
		||||
    def template_kwargs_view_row(self, **kwargs):
 | 
			
		||||
        kwargs = super(ReceivingBatchView, self).template_kwargs_view_row(**kwargs)
 | 
			
		||||
        use_buefy = self.get_use_buefy()
 | 
			
		||||
        app = self.get_rattail_app()
 | 
			
		||||
        handler = app.get_products_handler()
 | 
			
		||||
        products_handler = app.get_products_handler()
 | 
			
		||||
        row = kwargs['instance']
 | 
			
		||||
 | 
			
		||||
        if row.product:
 | 
			
		||||
            kwargs['image_url'] = handler.get_image_url(row.product)
 | 
			
		||||
            kwargs['image_url'] = products_handler.get_image_url(row.product)
 | 
			
		||||
        elif row.upc:
 | 
			
		||||
            kwargs['image_url'] = handler.get_image_url(upc=row.upc)
 | 
			
		||||
            kwargs['image_url'] = products_handler.get_image_url(upc=row.upc)
 | 
			
		||||
 | 
			
		||||
        if use_buefy:
 | 
			
		||||
            kwargs['row_context'] = self.get_context_row(row)
 | 
			
		||||
            kwargs['possible_receiving_modes'] = POSSIBLE_RECEIVING_MODES
 | 
			
		||||
            kwargs['possible_credit_types'] = POSSIBLE_CREDIT_TYPES
 | 
			
		||||
 | 
			
		||||
        return kwargs
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -849,6 +950,24 @@ class ReceivingBatchView(PurchasingBatchView):
 | 
			
		|||
                if row.product and row.product.is_pack_item():
 | 
			
		||||
                    return self.get_row_action_url('transform_unit', row)
 | 
			
		||||
 | 
			
		||||
    def make_row_credits_grid(self, row):
 | 
			
		||||
 | 
			
		||||
        # first make grid like normal
 | 
			
		||||
        g = super(ReceivingBatchView, self).make_row_credits_grid(row)
 | 
			
		||||
 | 
			
		||||
        if (self.get_use_buefy()
 | 
			
		||||
            and self.has_perm('edit_row')
 | 
			
		||||
            and self.row_editable(row)):
 | 
			
		||||
 | 
			
		||||
            # add the Un-Declare action
 | 
			
		||||
            g.main_actions.append(self.make_action(
 | 
			
		||||
                'remove', label="Un-Declare",
 | 
			
		||||
                url='#', icon='trash',
 | 
			
		||||
                link_class='has-text-danger',
 | 
			
		||||
                click_handler='removeCreditInit(props.row)'))
 | 
			
		||||
 | 
			
		||||
        return g
 | 
			
		||||
 | 
			
		||||
    def vuejs_convert_quantity(self, cstruct):
 | 
			
		||||
        result = dict(cstruct)
 | 
			
		||||
        if result['cases'] is colander.null:
 | 
			
		||||
| 
						 | 
				
			
			@ -872,6 +991,55 @@ class ReceivingBatchView(PurchasingBatchView):
 | 
			
		|||
        self.viewing = True
 | 
			
		||||
        use_buefy = self.get_use_buefy()
 | 
			
		||||
        row = self.get_row_instance()
 | 
			
		||||
 | 
			
		||||
        # things are a bit different now w/ buefy support..
 | 
			
		||||
        if use_buefy:
 | 
			
		||||
 | 
			
		||||
            # don't even bother showing this page if that's all the
 | 
			
		||||
            # request was about
 | 
			
		||||
            if self.request.method == 'GET':
 | 
			
		||||
                return self.redirect(self.get_row_action_url('view', row))
 | 
			
		||||
 | 
			
		||||
            # make sure edit is allowed
 | 
			
		||||
            if not (self.has_perm('edit_row') and self.row_editable(row)):
 | 
			
		||||
                raise self.forbidden()
 | 
			
		||||
 | 
			
		||||
            # check for JSON POST, which is submitted via AJAX from
 | 
			
		||||
            # the "view row" page
 | 
			
		||||
            if self.request.method == 'POST' and not self.request.POST:
 | 
			
		||||
                data = self.request.json_body
 | 
			
		||||
                kwargs = dict(data)
 | 
			
		||||
 | 
			
		||||
                # TODO: for some reason quantities can come through as strings?
 | 
			
		||||
                cases = kwargs['quantity']['cases']
 | 
			
		||||
                if cases is not None:
 | 
			
		||||
                    if cases == '':
 | 
			
		||||
                        cases = None
 | 
			
		||||
                    else:
 | 
			
		||||
                        cases = decimal.Decimal(cases)
 | 
			
		||||
                kwargs['cases'] = cases
 | 
			
		||||
                units = kwargs['quantity']['units']
 | 
			
		||||
                if units is not None:
 | 
			
		||||
                    if units == '':
 | 
			
		||||
                        units = None
 | 
			
		||||
                    else:
 | 
			
		||||
                        units = decimal.Decimal(units)
 | 
			
		||||
                kwargs['units'] = units
 | 
			
		||||
                del kwargs['quantity']
 | 
			
		||||
 | 
			
		||||
                # handler takes care of the receiving logic for us
 | 
			
		||||
                try:
 | 
			
		||||
                    self.batch_handler.receive_row(row, **kwargs)
 | 
			
		||||
 | 
			
		||||
                except Exception as error:
 | 
			
		||||
                    return self.json_response({'error': six.text_type(error)})
 | 
			
		||||
 | 
			
		||||
                self.Session.flush()
 | 
			
		||||
                self.Session.refresh(row)
 | 
			
		||||
                return self.json_response({
 | 
			
		||||
                    'ok': True,
 | 
			
		||||
                    'row': self.get_context_row(row)})
 | 
			
		||||
 | 
			
		||||
        batch = row.batch
 | 
			
		||||
        permission_prefix = self.get_permission_prefix()
 | 
			
		||||
        possible_modes = [
 | 
			
		||||
| 
						 | 
				
			
			@ -1024,11 +1192,59 @@ class ReceivingBatchView(PurchasingBatchView):
 | 
			
		|||
        """
 | 
			
		||||
        use_buefy = self.get_use_buefy()
 | 
			
		||||
        row = self.get_row_instance()
 | 
			
		||||
 | 
			
		||||
        # things are a bit different now w/ buefy support..
 | 
			
		||||
        if use_buefy:
 | 
			
		||||
 | 
			
		||||
            # don't even bother showing this page if that's all the
 | 
			
		||||
            # request was about
 | 
			
		||||
            if self.request.method == 'GET':
 | 
			
		||||
                return self.redirect(self.get_row_action_url('view', row))
 | 
			
		||||
 | 
			
		||||
            # make sure edit is allowed
 | 
			
		||||
            if not (self.has_perm('edit_row') and self.row_editable(row)):
 | 
			
		||||
                raise self.forbidden()
 | 
			
		||||
 | 
			
		||||
            # check for JSON POST, which is submitted via AJAX from
 | 
			
		||||
            # the "view row" page
 | 
			
		||||
            if self.request.method == 'POST' and not self.request.POST:
 | 
			
		||||
                data = self.request.json_body
 | 
			
		||||
                kwargs = dict(data)
 | 
			
		||||
 | 
			
		||||
                # TODO: for some reason quantities can come through as strings?
 | 
			
		||||
                if kwargs['cases'] is not None:
 | 
			
		||||
                    if kwargs['cases'] == '':
 | 
			
		||||
                        kwargs['cases'] = None
 | 
			
		||||
                    else:
 | 
			
		||||
                        kwargs['cases'] = decimal.Decimal(kwargs['cases'])
 | 
			
		||||
                if kwargs['units'] is not None:
 | 
			
		||||
                    if kwargs['units'] == '':
 | 
			
		||||
                        kwargs['units'] = None
 | 
			
		||||
                    else:
 | 
			
		||||
                        kwargs['units'] = decimal.Decimal(kwargs['units'])
 | 
			
		||||
 | 
			
		||||
                try:
 | 
			
		||||
                    result = self.handler.can_declare_credit(row, **kwargs)
 | 
			
		||||
 | 
			
		||||
                except Exception as error:
 | 
			
		||||
                    return self.json_response({'error': six.text_type(error)})
 | 
			
		||||
 | 
			
		||||
                else:
 | 
			
		||||
                    if result:
 | 
			
		||||
                        self.handler.declare_credit(row, **kwargs)
 | 
			
		||||
 | 
			
		||||
                    else:
 | 
			
		||||
                        return self.json_response({
 | 
			
		||||
                            'error': "Handler says you can't declare that credit; "
 | 
			
		||||
                            "not sure why"})
 | 
			
		||||
 | 
			
		||||
                self.Session.flush()
 | 
			
		||||
                self.Session.refresh(row)
 | 
			
		||||
                return self.json_response({
 | 
			
		||||
                    'ok': True,
 | 
			
		||||
                    'row': self.get_context_row(row)})
 | 
			
		||||
 | 
			
		||||
        batch = row.batch
 | 
			
		||||
        possible_credit_types = [
 | 
			
		||||
            'damaged',
 | 
			
		||||
            'expired',
 | 
			
		||||
        ]
 | 
			
		||||
        context = {
 | 
			
		||||
            'row': row,
 | 
			
		||||
            'batch': batch,
 | 
			
		||||
| 
						 | 
				
			
			@ -1044,9 +1260,10 @@ class ReceivingBatchView(PurchasingBatchView):
 | 
			
		|||
        schema = DeclareCreditForm()
 | 
			
		||||
        form = forms.Form(schema=schema, request=self.request,
 | 
			
		||||
                          use_buefy=use_buefy)
 | 
			
		||||
        form.cancel_url = self.get_row_action_url('view', row)
 | 
			
		||||
 | 
			
		||||
        # credit_type
 | 
			
		||||
        values = [(m, m) for m in possible_credit_types]
 | 
			
		||||
        values = [(m, m) for m in POSSIBLE_CREDIT_TYPES]
 | 
			
		||||
        if use_buefy:
 | 
			
		||||
            widget = dfwidget.SelectWidget(values=values)
 | 
			
		||||
        else:
 | 
			
		||||
| 
						 | 
				
			
			@ -1085,6 +1302,54 @@ class ReceivingBatchView(PurchasingBatchView):
 | 
			
		|||
        context['parent_title'] = self.get_instance_title(batch)
 | 
			
		||||
        return self.render_to_response('declare_credit', context)
 | 
			
		||||
 | 
			
		||||
    def undeclare_credit(self):
 | 
			
		||||
        """
 | 
			
		||||
        View for un-declaring a credit, i.e. moving the credit amounts
 | 
			
		||||
        back into the "received" tally.
 | 
			
		||||
        """
 | 
			
		||||
        model = self.model
 | 
			
		||||
        row = self.get_row_instance()
 | 
			
		||||
        data = self.request.json_body
 | 
			
		||||
 | 
			
		||||
        # make sure edit is allowed
 | 
			
		||||
        if not (self.has_perm('edit_row') and self.row_editable(row)):
 | 
			
		||||
            raise self.forbidden()
 | 
			
		||||
 | 
			
		||||
        # figure out which credit to un-declare
 | 
			
		||||
        credit = None
 | 
			
		||||
        uuid = data.get('uuid')
 | 
			
		||||
        if uuid:
 | 
			
		||||
            credit = self.Session.query(model.PurchaseBatchCredit).get(uuid)
 | 
			
		||||
        if not credit:
 | 
			
		||||
            return {'error': "Credit not found"}
 | 
			
		||||
 | 
			
		||||
        # un-declare it
 | 
			
		||||
        self.batch_handler.undeclare_credit(row, credit)
 | 
			
		||||
        self.Session.flush()
 | 
			
		||||
        self.Session.refresh(row)
 | 
			
		||||
 | 
			
		||||
        return {'ok': True,
 | 
			
		||||
                'row': self.get_context_row(row)}
 | 
			
		||||
 | 
			
		||||
    def get_context_row(self, row):
 | 
			
		||||
        app = self.get_rattail_app()
 | 
			
		||||
        return {
 | 
			
		||||
            'sequence': row.sequence,
 | 
			
		||||
            'case_quantity': float(row.case_quantity) if row.case_quantity is not None else None,
 | 
			
		||||
            'ordered': self.render_row_quantity(row, 'ordered'),
 | 
			
		||||
            'shipped': self.render_row_quantity(row, 'shipped'),
 | 
			
		||||
            'received': self.render_row_quantity(row, 'received'),
 | 
			
		||||
            'cases_received': float(row.cases_received) if row.cases_received is not None else None,
 | 
			
		||||
            'units_received': float(row.units_received) if row.units_received is not None else None,
 | 
			
		||||
            'damaged': self.render_row_quantity(row, 'damaged'),
 | 
			
		||||
            'expired': self.render_row_quantity(row, 'expired'),
 | 
			
		||||
            'mispick': self.render_row_quantity(row, 'mispick'),
 | 
			
		||||
            'missing': self.render_row_quantity(row, 'missing'),
 | 
			
		||||
            'credits': self.get_context_credits(row),
 | 
			
		||||
            'invoice_total_calculated': app.render_currency(row.invoice_total_calculated),
 | 
			
		||||
            'status': row.STATUS[row.status_code],
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    def transform_unit_row(self):
 | 
			
		||||
        """
 | 
			
		||||
        View which transforms the given row, which is assumed to associate with
 | 
			
		||||
| 
						 | 
				
			
			@ -1593,6 +1858,14 @@ class ReceivingBatchView(PurchasingBatchView):
 | 
			
		|||
        config.add_view(cls, attr='declare_credit', route_name='{}.declare_credit'.format(route_prefix),
 | 
			
		||||
                        permission='{}.edit_row'.format(permission_prefix))
 | 
			
		||||
 | 
			
		||||
        # un-declare credit
 | 
			
		||||
        config.add_route('{}.undeclare_credit'.format(route_prefix),
 | 
			
		||||
                         '{}/rows/{{row_uuid}}/undeclare-credit'.format(instance_url_prefix))
 | 
			
		||||
        config.add_view(cls, attr='undeclare_credit',
 | 
			
		||||
                        route_name='{}.undeclare_credit'.format(route_prefix),
 | 
			
		||||
                        permission='{}.edit_row'.format(permission_prefix),
 | 
			
		||||
                        renderer='json')
 | 
			
		||||
 | 
			
		||||
        # update row cost
 | 
			
		||||
        config.add_route('{}.update_row_cost'.format(route_prefix), '{}/update-row-cost'.format(instance_url_prefix))
 | 
			
		||||
        config.add_view(cls, attr='update_row_cost', route_name='{}.update_row_cost'.format(route_prefix),
 | 
			
		||||
| 
						 | 
				
			
			@ -1649,12 +1922,8 @@ class NewReceivingBatch(colander.Schema):
 | 
			
		|||
class ReceiveRowForm(colander.MappingSchema):
 | 
			
		||||
 | 
			
		||||
    mode = colander.SchemaNode(colander.String(),
 | 
			
		||||
                               validator=colander.OneOf([
 | 
			
		||||
                                   'received',
 | 
			
		||||
                                   'damaged',
 | 
			
		||||
                                   'expired',
 | 
			
		||||
                                   # 'mispick',
 | 
			
		||||
                               ]))
 | 
			
		||||
                               validator=colander.OneOf(
 | 
			
		||||
                                   POSSIBLE_RECEIVING_MODES))
 | 
			
		||||
 | 
			
		||||
    quantity = forms.types.ProductQuantity()
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1677,11 +1946,8 @@ class ReceiveRowForm(colander.MappingSchema):
 | 
			
		|||
class DeclareCreditForm(colander.MappingSchema):
 | 
			
		||||
 | 
			
		||||
    credit_type = colander.SchemaNode(colander.String(),
 | 
			
		||||
                                      validator=colander.OneOf([
 | 
			
		||||
                                          'damaged',
 | 
			
		||||
                                          'expired',
 | 
			
		||||
                                          # 'mispick',
 | 
			
		||||
                                      ]))
 | 
			
		||||
                                      validator=colander.OneOf(
 | 
			
		||||
                                          POSSIBLE_CREDIT_TYPES))
 | 
			
		||||
 | 
			
		||||
    quantity = forms.types.ProductQuantity()
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue