Add support for editing catalog cost in receiving batch, per new theme
had to add several "under the hood" features to make this work, to embed a Vue component within grid `<td>` cells, etc.
This commit is contained in:
		
							parent
							
								
									ec71f532a1
								
							
						
					
					
						commit
						2e3823364c
					
				
					 7 changed files with 296 additions and 27 deletions
				
			
		| 
						 | 
					@ -48,7 +48,7 @@ from pyramid_deform import SessionFileUploadTempStore
 | 
				
			||||||
from pyramid.renderers import render
 | 
					from pyramid.renderers import render
 | 
				
			||||||
from webhelpers2.html import tags, HTML
 | 
					from webhelpers2.html import tags, HTML
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from tailbone.util import raw_datetime
 | 
					from tailbone.util import raw_datetime, get_form_data
 | 
				
			||||||
from . import types
 | 
					from . import types
 | 
				
			||||||
from .widgets import ReadonlyWidget, PlainDateWidget, JQueryDateWidget, JQueryTimeWidget
 | 
					from .widgets import ReadonlyWidget, PlainDateWidget, JQueryDateWidget, JQueryTimeWidget
 | 
				
			||||||
from tailbone.exceptions import TailboneJSONFieldError
 | 
					from tailbone.exceptions import TailboneJSONFieldError
 | 
				
			||||||
| 
						 | 
					@ -1071,17 +1071,15 @@ class Form(object):
 | 
				
			||||||
            if self.request.method != 'POST':
 | 
					            if self.request.method != 'POST':
 | 
				
			||||||
                return False
 | 
					                return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # use POST or JSON body, whichever is present
 | 
					            controls = get_form_data(self.request).items()
 | 
				
			||||||
            # TODO: per docs, some JS libraries may not set this flag?
 | 
					 | 
				
			||||||
            # https://docs.pylonsproject.org/projects/pyramid/en/latest/api/request.html#pyramid.request.Request.is_xhr
 | 
					 | 
				
			||||||
            if self.request.is_xhr and not self.request.POST:
 | 
					 | 
				
			||||||
                controls = self.request.json_body.items()
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # unfortunately the normal form logic (i.e. peppercorn) is
 | 
					            # unfortunately the normal form logic (i.e. peppercorn) is
 | 
				
			||||||
                # expecting all values to be strings, whereas the JSON body we
 | 
					            # expecting all values to be strings, whereas if our data
 | 
				
			||||||
                # just parsed, may have given us some Pythonic objects.  so
 | 
					            # came from JSON body, may have given us some Pythonic
 | 
				
			||||||
                # here we must convert them *back* to strings...
 | 
					            # objects.  so here we must convert them *back* to strings
 | 
				
			||||||
            # TODO: this seems like a hack, i must be missing something
 | 
					            # TODO: this seems like a hack, i must be missing something
 | 
				
			||||||
 | 
					            # TODO: also this uses same "JSON" check as get_form_data()
 | 
				
			||||||
 | 
					            if self.request.is_xhr and not self.request.POST:
 | 
				
			||||||
                controls = [[key, val] for key, val in controls]
 | 
					                controls = [[key, val] for key, val in controls]
 | 
				
			||||||
                for i in range(len(controls)):
 | 
					                for i in range(len(controls)):
 | 
				
			||||||
                    key, value = controls[i]
 | 
					                    key, value = controls[i]
 | 
				
			||||||
| 
						 | 
					@ -1094,9 +1092,6 @@ class Form(object):
 | 
				
			||||||
                    elif not isinstance(value, six.string_types):
 | 
					                    elif not isinstance(value, six.string_types):
 | 
				
			||||||
                        controls[i][1] = six.text_type(value)
 | 
					                        controls[i][1] = six.text_type(value)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            else:
 | 
					 | 
				
			||||||
                controls = self.request.POST.items()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            dform = self.make_deform_form()
 | 
					            dform = self.make_deform_form()
 | 
				
			||||||
            try:
 | 
					            try:
 | 
				
			||||||
                self.validated = dform.validate(controls)
 | 
					                self.validated = dform.validate(controls)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -68,11 +68,49 @@ class FieldList(list):
 | 
				
			||||||
class Grid(object):
 | 
					class Grid(object):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Core grid class.  In sore need of documentation.
 | 
					    Core grid class.  In sore need of documentation.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .. attribute:: raw_renderers
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					       Dict of "raw" field renderers.  See also
 | 
				
			||||||
 | 
					       :meth:`set_raw_renderer()`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					       When present, these are rendered "as-is" into the grid
 | 
				
			||||||
 | 
					       template, whereas the more typical scenario involves rendering
 | 
				
			||||||
 | 
					       each field "into" a span element, like:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					       .. code-block:: html
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <span v-html="RENDERED-FIELD"></span>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					       So instead of injecting into a span, any "raw" fields defined
 | 
				
			||||||
 | 
					       via this dict, will be injected as-is, like:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					       .. code-block:: html
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          RENDERED-FIELD
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					       Note that each raw renderer is called only once, and *without*
 | 
				
			||||||
 | 
					       any arguments.  Likely the only use case for this, is to inject
 | 
				
			||||||
 | 
					       a Vue component into the field.  A basic example::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          from webhelpers2.html import HTML
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          def myrender():
 | 
				
			||||||
 | 
					              return HTML.tag('my-component', **{'v-model': 'props.row.myfield'})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          grid = Grid(
 | 
				
			||||||
 | 
					              # ..normal constructor args here..
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              raw_renderers={
 | 
				
			||||||
 | 
					                  'myfield': myrender,
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					          )
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, key, data, columns=None, width='auto', request=None,
 | 
					    def __init__(self, key, data, columns=None, width='auto', request=None,
 | 
				
			||||||
                 model_class=None, model_title=None, model_title_plural=None,
 | 
					                 model_class=None, model_title=None, model_title_plural=None,
 | 
				
			||||||
                 enums={}, labels={}, assume_local_times=False, renderers={}, invisible=[],
 | 
					                 enums={}, labels={}, assume_local_times=False, renderers={}, invisible=[],
 | 
				
			||||||
 | 
					                 raw_renderers={},
 | 
				
			||||||
                 extra_row_class=None, linked_columns=[], url='#',
 | 
					                 extra_row_class=None, linked_columns=[], url='#',
 | 
				
			||||||
                 joiners={}, filterable=False, filters={}, use_byte_string_filters=False,
 | 
					                 joiners={}, filterable=False, filters={}, use_byte_string_filters=False,
 | 
				
			||||||
                 searchable={},
 | 
					                 searchable={},
 | 
				
			||||||
| 
						 | 
					@ -109,6 +147,7 @@ class Grid(object):
 | 
				
			||||||
        self.labels = labels or {}
 | 
					        self.labels = labels or {}
 | 
				
			||||||
        self.assume_local_times = assume_local_times
 | 
					        self.assume_local_times = assume_local_times
 | 
				
			||||||
        self.renderers = self.make_default_renderers(renderers or {})
 | 
					        self.renderers = self.make_default_renderers(renderers or {})
 | 
				
			||||||
 | 
					        self.raw_renderers = raw_renderers or {}
 | 
				
			||||||
        self.invisible = invisible or []
 | 
					        self.invisible = invisible or []
 | 
				
			||||||
        self.extra_row_class = extra_row_class
 | 
					        self.extra_row_class = extra_row_class
 | 
				
			||||||
        self.linked_columns = linked_columns or []
 | 
					        self.linked_columns = linked_columns or []
 | 
				
			||||||
| 
						 | 
					@ -286,6 +325,21 @@ class Grid(object):
 | 
				
			||||||
    def set_renderer(self, key, renderer):
 | 
					    def set_renderer(self, key, renderer):
 | 
				
			||||||
        self.renderers[key] = renderer
 | 
					        self.renderers[key] = renderer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def set_raw_renderer(self, key, renderer):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Set or remove the "raw" renderer for the given field.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        See :attr:`raw_renderers` for more about these.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param key: Field name.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param renderer: Either a renderer callable, or ``None``.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if renderer:
 | 
				
			||||||
 | 
					            self.raw_renderers[key] = renderer
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            self.raw_renderers.pop(key, None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def set_type(self, key, type_):
 | 
					    def set_type(self, key, type_):
 | 
				
			||||||
        if type_ == 'boolean':
 | 
					        if type_ == 'boolean':
 | 
				
			||||||
            self.set_renderer(key, self.render_boolean)
 | 
					            self.set_renderer(key, self.render_boolean)
 | 
				
			||||||
| 
						 | 
					@ -1313,7 +1367,10 @@ class Grid(object):
 | 
				
			||||||
        # iterate over data rows
 | 
					        # iterate over data rows
 | 
				
			||||||
        for i in range(count):
 | 
					        for i in range(count):
 | 
				
			||||||
            rowobj = raw_data[i]
 | 
					            rowobj = raw_data[i]
 | 
				
			||||||
            row = {}
 | 
					
 | 
				
			||||||
 | 
					            # nb. cache 0-based index on the row, in case client-side
 | 
				
			||||||
 | 
					            # logic finds it useful
 | 
				
			||||||
 | 
					            row = {'_index': i}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # sometimes we need to include some "raw" data columns in our
 | 
					            # sometimes we need to include some "raw" data columns in our
 | 
				
			||||||
            # result set, even though the column is not displayed as part of
 | 
					            # result set, even though the column is not displayed as part of
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -221,8 +221,14 @@
 | 
				
			||||||
                            % if grid.is_searchable(column['field']):
 | 
					                            % if grid.is_searchable(column['field']):
 | 
				
			||||||
                            searchable
 | 
					                            searchable
 | 
				
			||||||
                            % endif
 | 
					                            % endif
 | 
				
			||||||
 | 
					                            cell-class="${column['field']}"
 | 
				
			||||||
 | 
					                            % if grid.has_click_handler(column['field']):
 | 
				
			||||||
 | 
					                            @click.native="${grid.click_handlers[column['field']]}"
 | 
				
			||||||
 | 
					                            % endif
 | 
				
			||||||
                            :visible="${json.dumps(column['visible'])}">
 | 
					                            :visible="${json.dumps(column['visible'])}">
 | 
				
			||||||
              % if grid.is_linked(column['field']):
 | 
					              % if column['field'] in grid.raw_renderers:
 | 
				
			||||||
 | 
					                  ${grid.raw_renderers[column['field']]()}
 | 
				
			||||||
 | 
					              % elif grid.is_linked(column['field']):
 | 
				
			||||||
                  <a :href="props.row._action_url_view" v-html="props.row.${column['field']}"></a>
 | 
					                  <a :href="props.row._action_url_view" v-html="props.row.${column['field']}"></a>
 | 
				
			||||||
              % else:
 | 
					              % else:
 | 
				
			||||||
                  <span v-html="props.row.${column['field']}"></span>
 | 
					                  <span v-html="props.row.${column['field']}"></span>
 | 
				
			||||||
| 
						 | 
					@ -349,6 +355,18 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      methods: {
 | 
					      methods: {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          addRowClass(index, className) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              // TODO: this may add duplicated name to class string
 | 
				
			||||||
 | 
					              // (not a serious problem i think, but could be improved)
 | 
				
			||||||
 | 
					              this.rowStatusMap[index] = (this.rowStatusMap[index] || '')
 | 
				
			||||||
 | 
					                  + ' ' + className
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              // nb. for some reason b-table does not always "notice"
 | 
				
			||||||
 | 
					              // when we update status; so we force it to refresh
 | 
				
			||||||
 | 
					              this.$forceUpdate()
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          getRowClass(row, index) {
 | 
					          getRowClass(row, index) {
 | 
				
			||||||
              return this.rowStatusMap[index]
 | 
					              return this.rowStatusMap[index]
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -115,12 +115,23 @@
 | 
				
			||||||
      </b-checkbox>
 | 
					      </b-checkbox>
 | 
				
			||||||
    </b-field>
 | 
					    </b-field>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <b-field>
 | 
				
			||||||
      <b-checkbox name="rattail.batch.purchase.receiving.should_autofix_invoice_case_vs_unit"
 | 
					      <b-checkbox name="rattail.batch.purchase.receiving.should_autofix_invoice_case_vs_unit"
 | 
				
			||||||
                  v-model="simpleSettings['rattail.batch.purchase.receiving.should_autofix_invoice_case_vs_unit']"
 | 
					                  v-model="simpleSettings['rattail.batch.purchase.receiving.should_autofix_invoice_case_vs_unit']"
 | 
				
			||||||
                  native-value="true"
 | 
					                  native-value="true"
 | 
				
			||||||
                  @input="settingsNeedSaved = true">
 | 
					                  @input="settingsNeedSaved = true">
 | 
				
			||||||
        Try to auto-correct "case vs. unit" mistakes from invoice parser
 | 
					        Try to auto-correct "case vs. unit" mistakes from invoice parser
 | 
				
			||||||
      </b-checkbox>
 | 
					      </b-checkbox>
 | 
				
			||||||
 | 
					    </b-field>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <b-field>
 | 
				
			||||||
 | 
					      <b-checkbox name="rattail.batch.purchase.receiving.allow_edit_catalog_unit_cost"
 | 
				
			||||||
 | 
					                  v-model="simpleSettings['rattail.batch.purchase.receiving.allow_edit_catalog_unit_cost']"
 | 
				
			||||||
 | 
					                  native-value="true"
 | 
				
			||||||
 | 
					                  @input="settingsNeedSaved = true">
 | 
				
			||||||
 | 
					        Allow edit of Catalog Unit Cost
 | 
				
			||||||
 | 
					      </b-checkbox>
 | 
				
			||||||
 | 
					    </b-field>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,7 +3,7 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<%def name="extra_javascript()">
 | 
					<%def name="extra_javascript()">
 | 
				
			||||||
  ${parent.extra_javascript()}
 | 
					  ${parent.extra_javascript()}
 | 
				
			||||||
  % if master.has_perm('edit_row'):
 | 
					  % if not use_buefy and master.has_perm('edit_row'):
 | 
				
			||||||
      ${h.javascript_link(request.static_url('tailbone:static/js/numeric.js'))}
 | 
					      ${h.javascript_link(request.static_url('tailbone:static/js/numeric.js'))}
 | 
				
			||||||
      <script type="text/javascript">
 | 
					      <script type="text/javascript">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -264,7 +264,21 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<%def name="extra_styles()">
 | 
					<%def name="extra_styles()">
 | 
				
			||||||
  ${parent.extra_styles()}
 | 
					  ${parent.extra_styles()}
 | 
				
			||||||
  % if not batch.executed and master.has_perm('edit_row'):
 | 
					  % if use_buefy and allow_edit_catalog_unit_cost:
 | 
				
			||||||
 | 
					      <style type="text/css">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        td.catalog_unit_cost {
 | 
				
			||||||
 | 
					            cursor: pointer;
 | 
				
			||||||
 | 
					            background-color: #fcc;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        tr.catalog_cost_confirmed td.catalog_unit_cost {
 | 
				
			||||||
 | 
					            /* cursor: pointer; */
 | 
				
			||||||
 | 
					            background-color: #cfc;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      </style>
 | 
				
			||||||
 | 
					  % elif not use_buefy and not batch.executed and master.has_perm('edit_row'):
 | 
				
			||||||
      <style type="text/css">
 | 
					      <style type="text/css">
 | 
				
			||||||
        .grid tr:not(.header) td.catalog_unit_cost,
 | 
					        .grid tr:not(.header) td.catalog_unit_cost,
 | 
				
			||||||
        .grid tr:not(.header) td.invoice_unit_cost {
 | 
					        .grid tr:not(.header) td.invoice_unit_cost {
 | 
				
			||||||
| 
						 | 
					@ -357,6 +371,26 @@
 | 
				
			||||||
  % endif
 | 
					  % endif
 | 
				
			||||||
</%def>
 | 
					</%def>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<%def name="render_this_page_template()">
 | 
				
			||||||
 | 
					  ${parent.render_this_page_template()}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  % if allow_edit_catalog_unit_cost:
 | 
				
			||||||
 | 
					      <script type="text/x-template" id="receiving-cost-editor-template">
 | 
				
			||||||
 | 
					        <div>
 | 
				
			||||||
 | 
					          <span v-show="!editing">
 | 
				
			||||||
 | 
					            {{ value }}
 | 
				
			||||||
 | 
					          </span>
 | 
				
			||||||
 | 
					          <b-input v-model="inputValue"
 | 
				
			||||||
 | 
					                   ref="input"
 | 
				
			||||||
 | 
					                   v-show="editing"
 | 
				
			||||||
 | 
					                   @keydown.native="inputKeyDown"
 | 
				
			||||||
 | 
					                   @blur="inputBlur">
 | 
				
			||||||
 | 
					          </b-input>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </script>
 | 
				
			||||||
 | 
					  % endif
 | 
				
			||||||
 | 
					</%def>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<%def name="object_helpers()">
 | 
					<%def name="object_helpers()">
 | 
				
			||||||
  ${self.render_status_breakdown()}
 | 
					  ${self.render_status_breakdown()}
 | 
				
			||||||
  ${self.render_po_vs_invoice_helper()}
 | 
					  ${self.render_po_vs_invoice_helper()}
 | 
				
			||||||
| 
						 | 
					@ -418,13 +452,128 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    % endif
 | 
					    % endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    % if allow_edit_catalog_unit_cost:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let ReceivingCostEditor = {
 | 
				
			||||||
 | 
					            template: '#receiving-cost-editor-template',
 | 
				
			||||||
 | 
					            props: {
 | 
				
			||||||
 | 
					                row: Object,
 | 
				
			||||||
 | 
					                value: String,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            data() {
 | 
				
			||||||
 | 
					                return {
 | 
				
			||||||
 | 
					                    inputValue: this.value,
 | 
				
			||||||
 | 
					                    editing: false,
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            methods: {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                startEdit() {
 | 
				
			||||||
 | 
					                    this.inputValue = this.value
 | 
				
			||||||
 | 
					                    this.editing = true
 | 
				
			||||||
 | 
					                    this.$nextTick(() => {
 | 
				
			||||||
 | 
					                        this.$refs.input.focus()
 | 
				
			||||||
 | 
					                    })
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                inputKeyDown(event) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    // when user presses Enter while editing cost value, submit
 | 
				
			||||||
 | 
					                    // value to server for immediate persistence
 | 
				
			||||||
 | 
					                    if (event.which == 13) {
 | 
				
			||||||
 | 
					                        this.submitEdit()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    // when user presses Escape, cancel the edit
 | 
				
			||||||
 | 
					                    } else if (event.which == 27) {
 | 
				
			||||||
 | 
					                        this.cancelEdit()
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                inputBlur(event) {
 | 
				
			||||||
 | 
					                    // always assume user meant to cancel
 | 
				
			||||||
 | 
					                    this.cancelEdit()
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                cancelEdit() {
 | 
				
			||||||
 | 
					                    // reset input to discard any user entry
 | 
				
			||||||
 | 
					                    this.inputValue = this.value
 | 
				
			||||||
 | 
					                    this.editing = false
 | 
				
			||||||
 | 
					                    this.$emit('cancel-edit')
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                submitEdit() {
 | 
				
			||||||
 | 
					                    let url = '${url('{}.update_row_cost'.format(route_prefix), uuid=batch.uuid)}'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    // TODO: should get csrf token from parent component?
 | 
				
			||||||
 | 
					                    let csrftoken = ${json.dumps(request.session.get_csrf_token() or request.session.new_csrf_token())|n}
 | 
				
			||||||
 | 
					                    let headers = {'${csrf_header_name}': csrftoken}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    let params = {
 | 
				
			||||||
 | 
					                        row_uuid: this.$props.row.uuid,
 | 
				
			||||||
 | 
					                        catalog_unit_cost: this.inputValue,
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    this.$http.post(url, params, {headers: headers}).then(response => {
 | 
				
			||||||
 | 
					                        if (!response.data.error) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            // let parent know cost value has changed
 | 
				
			||||||
 | 
					                            // (this in turn will update data in *this*
 | 
				
			||||||
 | 
					                            // component, and display will refresh)
 | 
				
			||||||
 | 
					                            this.$emit('input', response.data.row.catalog_unit_cost,
 | 
				
			||||||
 | 
					                                       this.$props.row._index)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            // and hide the input box
 | 
				
			||||||
 | 
					                            this.editing = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        } else {
 | 
				
			||||||
 | 
					                            this.$buefy.toast.open({
 | 
				
			||||||
 | 
					                                message: "Submit failed:  " + response.data.error,
 | 
				
			||||||
 | 
					                                type: 'is-warning',
 | 
				
			||||||
 | 
					                                duration: 4000, // 4 seconds
 | 
				
			||||||
 | 
					                            })
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    }, response => {
 | 
				
			||||||
 | 
					                        this.$buefy.toast.open({
 | 
				
			||||||
 | 
					                            message: "Submit failed:  (unknown error)",
 | 
				
			||||||
 | 
					                            type: 'is-warning',
 | 
				
			||||||
 | 
					                            duration: 4000, // 4 seconds
 | 
				
			||||||
 | 
					                        })
 | 
				
			||||||
 | 
					                    })
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Vue.component('receiving-cost-editor', ReceivingCostEditor)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        ${rows_grid.component_studly}.methods.catalogUnitCostClicked = function(row) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // start edit for clicked cell
 | 
				
			||||||
 | 
					            this.$refs['catalogUnitCost_' + row.uuid].startEdit()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        ${rows_grid.component_studly}.methods.catalogCostConfirmed = function(amount, index) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // update display to indicate cost was confirmed
 | 
				
			||||||
 | 
					            this.addRowClass(index, 'catalog_cost_confirmed')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // start editing next row, unless there are no more
 | 
				
			||||||
 | 
					            let nextRow = index + 1
 | 
				
			||||||
 | 
					            if (this.data.length > nextRow) {
 | 
				
			||||||
 | 
					                nextRow = this.data[nextRow]
 | 
				
			||||||
 | 
					                this.$refs['catalogUnitCost_' + nextRow.uuid].startEdit()
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    % endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  </script>
 | 
					  </script>
 | 
				
			||||||
</%def>
 | 
					</%def>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
${parent.body()}
 | 
					${parent.body()}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
% if master.handler.allow_truck_dump_receiving() and master.has_perm('edit_row'):
 | 
					% if not use_buefy and master.handler.allow_truck_dump_receiving() and master.has_perm('edit_row'):
 | 
				
			||||||
    ${h.form(url('{}.transform_unit_row'.format(route_prefix), uuid=batch.uuid), name='transform-unit-form')}
 | 
					    ${h.form(url('{}.transform_unit_row'.format(route_prefix), uuid=batch.uuid), name='transform-unit-form')}
 | 
				
			||||||
    ${h.csrf_token(request)}
 | 
					    ${h.csrf_token(request)}
 | 
				
			||||||
    ${h.hidden('row_uuid')}
 | 
					    ${h.hidden('row_uuid')}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -64,6 +64,21 @@ def csrf_token(request, name='_csrf'):
 | 
				
			||||||
    return HTML.tag("div", tags.hidden(name, value=token), style="display:none;")
 | 
					    return HTML.tag("div", tags.hidden(name, value=token), style="display:none;")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_form_data(request):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Returns the effective form data for the given request.  Mostly
 | 
				
			||||||
 | 
					    this is a convenience, to return either POST or JSON depending on
 | 
				
			||||||
 | 
					    the type of request.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    # nb. we prefer JSON only if no POST is present
 | 
				
			||||||
 | 
					    # TODO: this seems to work for our use case at least, but perhaps
 | 
				
			||||||
 | 
					    # there is a better way?  see also
 | 
				
			||||||
 | 
					    # https://docs.pylonsproject.org/projects/pyramid/en/latest/api/request.html#pyramid.request.Request.is_xhr
 | 
				
			||||||
 | 
					    if request.is_xhr and not request.POST:
 | 
				
			||||||
 | 
					        return request.json_body
 | 
				
			||||||
 | 
					    return request.POST
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def should_use_buefy(request):
 | 
					def should_use_buefy(request):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Returns a flag indicating whether or not the current theme supports (and
 | 
					    Returns a flag indicating whether or not the current theme supports (and
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -46,6 +46,7 @@ from pyramid import httpexceptions
 | 
				
			||||||
from webhelpers2.html import tags, HTML
 | 
					from webhelpers2.html import tags, HTML
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from tailbone import forms, grids
 | 
					from tailbone import forms, grids
 | 
				
			||||||
 | 
					from tailbone.util import get_form_data
 | 
				
			||||||
from tailbone.views.purchasing import PurchasingBatchView
 | 
					from tailbone.views.purchasing import PurchasingBatchView
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -715,6 +716,11 @@ class ReceivingBatchView(PurchasingBatchView):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return breakdown
 | 
					        return breakdown
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def allow_edit_catalog_unit_cost(self, batch):
 | 
				
			||||||
 | 
					        return (not batch.executed
 | 
				
			||||||
 | 
					                and self.has_perm('edit_row')
 | 
				
			||||||
 | 
					                and self.batch_handler.allow_receiving_edit_catalog_unit_cost())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def template_kwargs_view(self, **kwargs):
 | 
					    def template_kwargs_view(self, **kwargs):
 | 
				
			||||||
        kwargs = super(ReceivingBatchView, self).template_kwargs_view(**kwargs)
 | 
					        kwargs = super(ReceivingBatchView, self).template_kwargs_view(**kwargs)
 | 
				
			||||||
        batch = kwargs['instance']
 | 
					        batch = kwargs['instance']
 | 
				
			||||||
| 
						 | 
					@ -739,6 +745,8 @@ class ReceivingBatchView(PurchasingBatchView):
 | 
				
			||||||
                    data=breakdown,
 | 
					                    data=breakdown,
 | 
				
			||||||
                    columns=['title', 'count'])
 | 
					                    columns=['title', 'count'])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        kwargs['allow_edit_catalog_unit_cost'] = self.allow_edit_catalog_unit_cost(batch)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return kwargs
 | 
					        return kwargs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_context_credits(self, row):
 | 
					    def get_context_credits(self, row):
 | 
				
			||||||
| 
						 | 
					@ -933,6 +941,7 @@ class ReceivingBatchView(PurchasingBatchView):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def configure_row_grid(self, g):
 | 
					    def configure_row_grid(self, g):
 | 
				
			||||||
        super(ReceivingBatchView, self).configure_row_grid(g)
 | 
					        super(ReceivingBatchView, self).configure_row_grid(g)
 | 
				
			||||||
 | 
					        use_buefy = self.get_use_buefy()
 | 
				
			||||||
        batch = self.get_instance()
 | 
					        batch = self.get_instance()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # vendor_code
 | 
					        # vendor_code
 | 
				
			||||||
| 
						 | 
					@ -943,6 +952,10 @@ class ReceivingBatchView(PurchasingBatchView):
 | 
				
			||||||
        if (self.handler.has_purchase_order(batch)
 | 
					        if (self.handler.has_purchase_order(batch)
 | 
				
			||||||
            or self.handler.has_invoice_file(batch)):
 | 
					            or self.handler.has_invoice_file(batch)):
 | 
				
			||||||
            g.remove('catalog_unit_cost')
 | 
					            g.remove('catalog_unit_cost')
 | 
				
			||||||
 | 
					        elif use_buefy and self.allow_edit_catalog_unit_cost(batch):
 | 
				
			||||||
 | 
					            g.set_raw_renderer('catalog_unit_cost', self.render_catalog_unit_cost)
 | 
				
			||||||
 | 
					            g.set_click_handler('catalog_unit_cost',
 | 
				
			||||||
 | 
					                                'catalogUnitCostClicked(props.row)')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # po_unit_cost
 | 
					        # po_unit_cost
 | 
				
			||||||
        if self.handler.has_invoice_file(batch):
 | 
					        if self.handler.has_invoice_file(batch):
 | 
				
			||||||
| 
						 | 
					@ -1001,6 +1014,14 @@ class ReceivingBatchView(PurchasingBatchView):
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            g.set_enum('truck_dump_status', model.PurchaseBatchRow.STATUS)
 | 
					            g.set_enum('truck_dump_status', model.PurchaseBatchRow.STATUS)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def render_catalog_unit_cost(self):
 | 
				
			||||||
 | 
					        return HTML.tag('receiving-cost-editor', **{
 | 
				
			||||||
 | 
					            'v-model': 'props.row.catalog_unit_cost',
 | 
				
			||||||
 | 
					            ':ref': "'catalogUnitCost_' + props.row.uuid",
 | 
				
			||||||
 | 
					            ':row': 'props.row',
 | 
				
			||||||
 | 
					            '@input': 'catalogCostConfirmed',
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def row_grid_extra_class(self, row, i):
 | 
					    def row_grid_extra_class(self, row, i):
 | 
				
			||||||
        css_class = super(ReceivingBatchView, self).row_grid_extra_class(row, i)
 | 
					        css_class = super(ReceivingBatchView, self).row_grid_extra_class(row, i)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1790,10 +1811,10 @@ class ReceivingBatchView(PurchasingBatchView):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def update_row_cost(self):
 | 
					    def update_row_cost(self):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        AJAX view for updating the invoice (actual) unit cost for a row.
 | 
					        AJAX view for updating various cost fields in a data row.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        batch = self.get_instance()
 | 
					        batch = self.get_instance()
 | 
				
			||||||
        data = dict(self.request.POST)
 | 
					        data = dict(get_form_data(self.request))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # validate row
 | 
					        # validate row
 | 
				
			||||||
        uuid = data.get('row_uuid')
 | 
					        uuid = data.get('row_uuid')
 | 
				
			||||||
| 
						 | 
					@ -1939,6 +1960,9 @@ class ReceivingBatchView(PurchasingBatchView):
 | 
				
			||||||
            {'section': 'rattail.batch',
 | 
					            {'section': 'rattail.batch',
 | 
				
			||||||
             'option': 'purchase.receiving.should_autofix_invoice_case_vs_unit',
 | 
					             'option': 'purchase.receiving.should_autofix_invoice_case_vs_unit',
 | 
				
			||||||
             'type': bool},
 | 
					             'type': bool},
 | 
				
			||||||
 | 
					            {'section': 'rattail.batch',
 | 
				
			||||||
 | 
					             'option': 'purchase.receiving.allow_edit_catalog_unit_cost',
 | 
				
			||||||
 | 
					             'type': bool},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # mobile interface
 | 
					            # mobile interface
 | 
				
			||||||
            {'section': 'rattail.batch',
 | 
					            {'section': 'rattail.batch',
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue