diff --git a/tailbone/static/js/tailbone.mobile.receiving.js b/tailbone/static/js/tailbone.mobile.receiving.js index 2d5f9ab6..3075e00a 100644 --- a/tailbone/static/js/tailbone.mobile.receiving.js +++ b/tailbone/static/js/tailbone.mobile.receiving.js @@ -27,9 +27,9 @@ $(document).on('click', 'form[name="new-purchasing-batch"] #receive-truck-dump', }); -$(document).on('click', 'form.receiving-update #delete-receiving-row', function() { +$(document).on('click', 'form[name="new-purchasing-batch"] #receive-from-scratch', function() { var form = $(this).parents('form'); - form.find('input[name="delete_row"]').val('true'); + form.find('input[name="workflow"]').val('from_scratch'); form.submit(); }); @@ -68,11 +68,15 @@ $(document).on('click', 'form.receiving-update .receiving-actions button', funct }); -// quick-receive (1 CS) -$(document).on('click', 'form.receiving-update .receive-one-case', function() { +// quick-receive (1 case or unit) +$(document).on('click', 'form.receiving-update .quick-receive', function() { var form = $(this).parents('form:first'); form.find('[name="mode"]').val('received'); - form.find('[name="cases"]').val('1'); + if ($(this).data('uom') == 'CS') { + form.find('[name="cases"]').val('1'); + } else { + form.find('[name="units"]').val('1'); + } form.find('input[name="quick_receive"]').val('true'); form.submit(); }); diff --git a/tailbone/subscribers.py b/tailbone/subscribers.py index a6a62ffa..34d55043 100644 --- a/tailbone/subscribers.py +++ b/tailbone/subscribers.py @@ -149,7 +149,7 @@ def context_found(event): return False request.has_any_perm = has_any_perm - def get_referrer(default=None): + def get_referrer(default=None, mobile=False): if request.params.get('referrer'): return request.params['referrer'] if request.session.get('referrer'): @@ -157,7 +157,12 @@ def context_found(event): referrer = request.referrer if (not referrer or referrer == request.current_route_url() or not referrer.startswith(request.host_url)): - referrer = default or request.route_url('home') + if default: + referrer = default + elif mobile: + referrer = request.route_url('mobile.home') + else: + referrer = request.route_url('home') return referrer request.get_referrer = get_referrer diff --git a/tailbone/templates/mobile/master/edit_row.mako b/tailbone/templates/mobile/master/edit_row.mako index 0576989e..3c81eb55 100644 --- a/tailbone/templates/mobile/master/edit_row.mako +++ b/tailbone/templates/mobile/master/edit_row.mako @@ -16,3 +16,10 @@ ## ${form.render(buttons=capture(self.buttons))|n} ${form.render()|n} + +% if master.mobile_rows_deletable and request.has_perm('{}.delete_row'.format(permission_prefix)): + ${h.form(url('mobile.{}.delete_row'.format(route_prefix), uuid=parent_instance.uuid, row_uuid=row.uuid))} + ${h.csrf_token(request)} + ${h.submit('submit', "Delete this Row")} + ${h.end_form()} +% endif diff --git a/tailbone/templates/mobile/master/view.mako b/tailbone/templates/mobile/master/view.mako index 32c6466d..9bc18ce2 100644 --- a/tailbone/templates/mobile/master/view.mako +++ b/tailbone/templates/mobile/master/view.mako @@ -34,7 +34,7 @@ ${form.render()|n} % else: - ${h.text('quick_row_entry', placeholder=placeholder, autocomplete='off', **{'data-type': 'search', 'data-url': url('mobile.{}.quick_row'.format(route_prefix), uuid=instance.uuid), 'data-wedge': quick_row_keyboard_wedge})} + ${h.text('quick_row_entry', placeholder=placeholder, autocomplete='off', **{'data-type': 'search', 'data-url': url('mobile.{}.quick_row'.format(route_prefix), uuid=instance.uuid), 'data-wedge': 'true' if quick_row_keyboard_wedge else 'false'})} % endif ${h.end_form()} % endif diff --git a/tailbone/templates/mobile/master/view_row.mako b/tailbone/templates/mobile/master/view_row.mako index 98e3ff44..45d1bc7b 100644 --- a/tailbone/templates/mobile/master/view_row.mako +++ b/tailbone/templates/mobile/master/view_row.mako @@ -10,3 +10,10 @@ ${form.render()|n} % if master.mobile_rows_editable and instance_editable and request.has_perm('{}.edit_row'.format(permission_prefix)): ${h.link_to("Edit", url('mobile.{}.edit_row'.format(route_prefix), uuid=instance.batch_uuid, row_uuid=instance.uuid), class_='ui-btn')} % endif + +% if master.mobile_rows_deletable and request.has_perm('{}.delete_row'.format(permission_prefix)): + ${h.form(url('mobile.{}.delete_row'.format(route_prefix), uuid=parent_instance.uuid, row_uuid=row.uuid))} + ${h.csrf_token(request)} + ${h.submit('submit', "Delete this Row")} + ${h.end_form()} +% endif diff --git a/tailbone/templates/mobile/receiving/create.mako b/tailbone/templates/mobile/receiving/create.mako index 2d23a29d..d5e49b1b 100644 --- a/tailbone/templates/mobile/receiving/create.mako +++ b/tailbone/templates/mobile/receiving/create.mako @@ -31,7 +31,7 @@ ${h.csrf_token(request)} % endif % if master.allow_from_scratch: - + % endif % if master.allow_truck_dump: diff --git a/tailbone/templates/mobile/receiving/view_row.mako b/tailbone/templates/mobile/receiving/view_row.mako index 4fe16a27..33e4b169 100644 --- a/tailbone/templates/mobile/receiving/view_row.mako +++ b/tailbone/templates/mobile/receiving/view_row.mako @@ -11,37 +11,63 @@
% if instance.product:

${instance.brand_name or ""}

-

${instance.description} ${instance.size}

-

1 CS = ${h.pretty_quantity(row.case_quantity)} ${unit_uom}

+

${instance.description} ${instance.size or ''}

+ % if allow_cases: +

1 CS = ${h.pretty_quantity(row.case_quantity or 1)} ${unit_uom}

+ % endif % else:

${instance.description}

% endif
- ${h.image(product_image_url, "product image")} + % if product_image_url: + ${h.image(product_image_url, "product image")} + % endif
- % if not batch.truck_dump: + % if populated_from_purchase: - + % endif - + - - - - - + + % if allow_expired: + + + + + % endif
ordered${h.pretty_quantity(row.cases_ordered or 0)} / ${h.pretty_quantity(row.units_ordered or 0)} + % if allow_cases: + ${h.pretty_quantity(row.cases_ordered or 0)} / + % endif + ${h.pretty_quantity(row.units_ordered or 0)} +
received${h.pretty_quantity(row.cases_received or 0)} / ${h.pretty_quantity(row.units_received or 0)} + % if allow_cases: + ${h.pretty_quantity(row.cases_received or 0)} / + % endif + ${h.pretty_quantity(row.units_received or 0)} +
damaged${h.pretty_quantity(row.cases_damaged or 0)} / ${h.pretty_quantity(row.units_damaged or 0)}
expired${h.pretty_quantity(row.cases_expired or 0)} / ${h.pretty_quantity(row.units_expired or 0)} + % if allow_cases: + ${h.pretty_quantity(row.cases_damaged or 0)} / + % endif + ${h.pretty_quantity(row.units_damaged or 0)} +
expired + % if allow_cases: + ${h.pretty_quantity(row.cases_expired or 0)} / + % endif + ${h.pretty_quantity(row.units_expired or 0)} +
@@ -59,9 +85,13 @@ ${h.hidden('cases')} ${h.hidden('units')} - + % if allow_cases: + + % else: + + % endif - ${keypad(unit_uom, uom)} + ${keypad(unit_uom, uom, allow_cases=allow_cases)} @@ -70,7 +100,9 @@
${h.radio('mode', value='received', label="received", checked=True)} ${h.radio('mode', value='damaged', label="damaged")} - ${h.radio('mode', value='expired', label="expired")} + % if allow_expired: + ${h.radio('mode', value='expired', label="expired")} + % endif
@@ -95,11 +127,13 @@
${h.hidden('quick_receive', value='false')} + ${h.end_form()} - ${h.hidden('delete_row', value='false')} - % if request.has_perm('{}.delete_row'.format(permission_prefix)): - + % if master.mobile_rows_deletable and request.has_perm('{}.delete_row'.format(permission_prefix)): + ${h.form(url('mobile.{}.delete_row'.format(route_prefix), uuid=batch.uuid, row_uuid=row.uuid), class_='receiving-update')} + ${h.csrf_token(request)} + ${h.submit('submit', "Delete this Row")} + ${h.end_form()} % endif - ${h.end_form()} % endif diff --git a/tailbone/views/batch/core.py b/tailbone/views/batch/core.py index 0dfa1124..5589cc41 100644 --- a/tailbone/views/batch/core.py +++ b/tailbone/views/batch/core.py @@ -48,7 +48,6 @@ from rattail.progress import SocketProgress import colander import deform -from pyramid import httpexceptions from pyramid.renderers import render_to_response from pyramid.response import FileResponse from webhelpers2.html import HTML, tags @@ -1074,20 +1073,11 @@ class BatchMasterView(MasterView): def get_parent(self, row): return row.batch - def delete_row(self): + def delete_row_object(self, row): """ - "Delete" a row from the batch. This sets the ``removed`` flag on the - row but does not truly delete it. + Perform the actual deletion of given row object. """ - row = self.Session.query(self.model_row_class).get(self.request.matchdict['row_uuid']) - if not row: - raise httpexceptions.HTTPNotFound() - row.removed = True - batch = self.get_parent(row) - self.handler.refresh_batch_status(batch) - if batch.rowcount is not None: - batch.rowcount -= 1 - return self.redirect(self.get_action_url('view', batch)) + self.handler.remove_row(row) def bulk_delete_rows(self): """ @@ -1096,9 +1086,14 @@ class BatchMasterView(MasterView): """ batch = self.get_instance() query = self.get_effective_row_data(sort=False) + + # TODO: this should surely be handled by the handler... if batch.rowcount is not None: batch.rowcount -= query.count() query.update({'removed': True}, synchronize_session=False) + self.Session.refresh(batch) + self.handler.refresh_batch_status(batch) + return self.redirect(self.get_action_url('view', batch)) def execute(self): diff --git a/tailbone/views/master.py b/tailbone/views/master.py index 613934ab..a642f636 100644 --- a/tailbone/views/master.py +++ b/tailbone/views/master.py @@ -139,6 +139,7 @@ class MasterView(View): mobile_rows_filterable = False mobile_rows_viewable = False mobile_rows_editable = False + mobile_rows_deletable = False row_labels = {} @@ -2670,11 +2671,12 @@ class MasterView(View): parent = self.get_parent(row) return self.render_to_response('edit_row', { + 'row': row, 'instance': row, + 'parent_instance': parent, 'instance_title': self.get_row_instance_title(row), 'instance_url': instance_url, 'instance_deletable': self.row_deletable(row), - 'parent_instance': parent, 'parent_title': self.get_instance_title(parent), 'parent_url': self.get_action_url('view', parent, mobile=True), 'form': form}, @@ -2705,16 +2707,38 @@ class MasterView(View): """ return True + def delete_row_object(self, row): + """ + Perform the actual deletion of given row object. + """ + self.Session.delete(row) + def delete_row(self): """ - "Delete" a sub-row from the parent. + Desktop view which can "delete" a sub-row from the parent. """ row = self.Session.query(self.model_row_class).get(self.request.matchdict['row_uuid']) if not row: - raise httpexceptions.HTTPNotFound() - self.Session.delete(row) + raise self.notfound() + self.delete_row_object(row) return self.redirect(self.get_action_url('edit', self.get_parent(row))) + def mobile_delete_row(self): + """ + Mobile view which can "delete" a sub-row from the parent. + """ + if self.request.method == 'POST': + parent = self.get_instance() + row = self.get_row_instance() + if self.get_parent(row) is not parent: + raise RuntimeError("Can only delete rows which belong to current object") + + self.delete_row_object(row) + return self.redirect(self.get_action_url('view', parent, mobile=True)) + + self.session.flash("Must POST to delete a row", 'error') + return self.redirect(self.request.get_referrer(mobile=True)) + def get_parent(self, row): raise NotImplementedError @@ -3050,9 +3074,15 @@ class MasterView(View): permission='{}.edit_row'.format(permission_prefix)) # delete row - if cls.has_rows and cls.rows_deletable: - 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)) - config.add_tailbone_permission(permission_prefix, '{}.delete_row'.format(permission_prefix), - "Delete individual {} rows".format(model_title)) + if cls.has_rows: + if cls.rows_deletable or cls.mobile_rows_deletable: + config.add_tailbone_permission(permission_prefix, '{}.delete_row'.format(permission_prefix), + "Delete individual {} rows".format(model_title)) + if cls.rows_deletable: + 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)) + if cls.mobile_rows_deletable: + config.add_route('mobile.{}.delete_row'.format(route_prefix), '/mobile{}/{{uuid}}/rows/{{row_uuid}}/delete'.format(url_prefix)) + config.add_view(cls, attr='mobile_delete_row', route_name='mobile.{}.delete_row'.format(route_prefix), + permission='{}.delete_row'.format(permission_prefix)) diff --git a/tailbone/views/purchases/core.py b/tailbone/views/purchases/core.py index 6fae86fd..2878a461 100644 --- a/tailbone/views/purchases/core.py +++ b/tailbone/views/purchases/core.py @@ -160,16 +160,23 @@ class PurchaseView(MasterView): default_active=True, default_verb='contains') g.sorters['buyer'] = g.make_sorter(model.Person.display_name) - g.filters['date_ordered'].label = "Ordered" + # date_ordered g.filters['date_ordered'].default_active = True g.filters['date_ordered'].default_verb = 'equal' - + g.set_label('date_ordered', "Ordered") g.set_sort_defaults('date_ordered', 'desc') - g.set_enum('status', self.enum.PURCHASE_STATUS) - - g.set_label('date_ordered', "Ordered") + # date_received + g.filters['date_received'].default_active = True + g.filters['date_received'].default_verb = 'equal' g.set_label('date_received', "Received") + + # status + g.set_enum('status', self.enum.PURCHASE_STATUS) + g.filters['status'].default_active = True + g.filters['status'].verbs = ['equal', 'not_equal', 'is_any'] + g.filters['status'].default_verb = 'is_any' + g.set_label('invoice_number', "Invoice No.") def configure_form(self, f): diff --git a/tailbone/views/purchasing/ordering.py b/tailbone/views/purchasing/ordering.py index 5bef6803..e3b624af 100644 --- a/tailbone/views/purchasing/ordering.py +++ b/tailbone/views/purchasing/ordering.py @@ -55,6 +55,7 @@ class OrderingBatchView(PurchasingBatchView): mobile_rows_creatable = True mobile_rows_quickable = True mobile_rows_editable = True + mobile_rows_deletable = True has_worksheet = True mobile_form_fields = [ @@ -186,7 +187,7 @@ class OrderingBatchView(PurchasingBatchView): 'history': history, 'get_upc': lambda p: p.upc.pretty() if p.upc else '', 'header_columns': self.order_form_header_columns, - 'ignore_cases': self.handler.ignore_cases, + 'ignore_cases': not self.handler.allow_cases(), }) def get_order_form_history(self, batch, costs, count): diff --git a/tailbone/views/purchasing/receiving.py b/tailbone/views/purchasing/receiving.py index a6bdd9f9..1cec92bf 100644 --- a/tailbone/views/purchasing/receiving.py +++ b/tailbone/views/purchasing/receiving.py @@ -27,12 +27,14 @@ Views for 'receiving' (purchasing) batches from __future__ import unicode_literals, absolute_import import re +import logging import six import sqlalchemy as sa from rattail import pod from rattail.db import model, api +from rattail.db.util import maxlen from rattail.gpc import GPC from rattail.time import localtime from rattail.util import pretty_quantity, prettify, OrderedDict @@ -47,13 +49,16 @@ from tailbone import forms, grids from tailbone.views.purchasing import PurchasingBatchView +log = logging.getLogger(__name__) + + class MobileItemStatusFilter(grids.filters.MobileFilter): value_choices = ['incomplete', 'unexpected', 'damaged', 'expired', 'all'] def filter_equal(self, query, value): - # NOTE: this is only relevant for truck dump + # NOTE: this is only relevant for truck dump or "from scratch" if value == 'received': return query.filter(sa.or_( model.PurchaseBatchRow.cases_received != 0, @@ -105,6 +110,7 @@ class ReceivingBatchView(PurchasingBatchView): mobile_rows_filterable = True mobile_rows_creatable = True mobile_rows_quickable = True + mobile_rows_deletable = True allow_from_po = False allow_from_scratch = True @@ -354,13 +360,14 @@ class ReceivingBatchView(PurchasingBatchView): kwargs = super(ReceivingBatchView, self).get_batch_kwargs(batch, mobile=mobile) if mobile: - purchase = self.get_purchase(self.request.POST['purchase']) - if isinstance(purchase, model.Purchase): - kwargs['purchase'] = purchase + if 'purchase' in self.request.POST: + purchase = self.get_purchase(self.request.POST['purchase']) + if isinstance(purchase, model.Purchase): + kwargs['purchase'] = purchase - department = self.department_for_purchase(purchase) - if department: - kwargs['department'] = department + department = self.department_for_purchase(purchase) + if department: + kwargs['department'] = department else: # not mobile batch_type = self.request.POST['batch_type'] @@ -501,12 +508,19 @@ class ReceivingBatchView(PurchasingBatchView): """ batch = self.get_instance() filters = grids.filters.GridFilterSet() - if batch.truck_dump: - value_choices = ['received', 'damaged', 'expired', 'all'] - default_status = 'all' - else: + + # visible filter options will depend on whether batch came from purchase + if self.handler.populated_from_purchase(batch): value_choices = ['incomplete', 'unexpected', 'damaged', 'expired', 'all'] default_status = 'incomplete' + else: + value_choices = ['received', 'damaged', 'expired', 'all'] + default_status = 'all' + + # remove 'expired' filter option if not relevant + if 'expired' in value_choices and not self.handler.allow_expired_credits(): + value_choices.remove('expired') + filters['status'] = MobileItemStatusFilter('status', value_choices=value_choices, default_value=default_status) @@ -522,10 +536,24 @@ class ReceivingBatchView(PurchasingBatchView): mode = self.batch_mode data = {'mode': mode} - form = forms.Form(schema=MobileNewReceivingBatch(), request=self.request) + schema = MobileNewReceivingBatch().bind(session=self.Session()) + form = forms.Form(schema=schema, request=self.request) if form.validate(newstyle=True): - if form.validated['workflow'] == 'truck_dump': + if form.validated['workflow'] == 'from_scratch': + if not self.allow_from_scratch: + raise NotImplementedError("Requested workflow not supported: from_scratch") + batch = self.model_class() + batch.store = self.rattail_config.get_store(self.Session()) + batch.mode = mode + batch.vendor = self.Session.query(model.Vendor).get(form.validated['vendor']) + batch.created_by = self.request.user + batch.date_received = localtime(self.rattail_config).date() + kwargs = self.get_batch_kwargs(batch, mobile=True) + batch = self.handler.make_batch(self.Session(), **kwargs) + return self.redirect(self.request.route_url('mobile.receiving.view', uuid=batch.uuid)) + + elif form.validated['workflow'] == 'truck_dump': if not self.allow_truck_dump: raise NotImplementedError("Requested workflow not supported: truck_dump") batch = self.model_class() @@ -611,22 +639,36 @@ class ReceivingBatchView(PurchasingBatchView): def quick_locate_rows(self, batch, entry): rows = [] - # we prefer "exact" matches, i.e. those which assumed the entry already - # contained the check digit. - provided = GPC(entry, calc_check_digit=False) - for row in batch.active_rows(): - if row.upc == provided: - rows.append(row) - if rows: + # try to locate rows by product uuid match before other key + product = self.Session.query(model.Product).get(entry) + if product: + rows = [row for row in batch.active_rows() + if row.product_uuid == product.uuid] + if rows: + return rows + + key = self.rattail_config.product_key() + if key == 'upc': + + # we prefer "exact" UPC matches, i.e. those which assumed the entry + # already contained the check digit. + provided = GPC(entry, calc_check_digit=False) + rows = [row for row in batch.active_rows() + if row.upc == provided] + if rows: + return rows + + # if no "exact" UPC matches, we'll settle for those (UPC matches) + # which assume the entry lacked a check digit. + checked = GPC(entry, calc_check_digit='upc') + rows = [row for row in batch.active_rows() + if row.upc == checked] return rows - # if no "exact" matches, we'll settle for those which assume the entry - # lacked a check digit. - checked = GPC(entry, calc_check_digit='upc') - for row in batch.active_rows(): - if row.upc == checked: - rows.append(row) - return rows + elif key == 'item_id': + rows = [row for row in batch.active_rows() + if row.item_id == entry] + return rows def save_quick_row_form(self, form): batch = self.get_instance() @@ -652,30 +694,69 @@ class ReceivingBatchView(PurchasingBatchView): self.Session.flush() return row - # try to locate product by upc - provided = GPC(entry, calc_check_digit=False) - checked = GPC(entry, calc_check_digit='upc') - product = api.get_product_by_upc(self.Session(), provided) - if not product: - product = api.get_product_by_upc(self.Session(), checked) - if product: + # try to locate product by uuid before other, more specific key + product = self.Session.query(model.Product).get(entry) + if product and not product.deleted: row = model.PurchaseBatchRow() row.product = product self.handler.add_row(batch, row) self.Session.flush() return row - # check for "bad" upc - if len(entry) > 14: - return + key = self.rattail_config.product_key() + if key == 'upc': - # product not in system, but presumably sane upc, so add to batch anyway - row = model.PurchaseBatchRow() - row.upc = provided # TODO: why not checked? how to know? - row.description = "(unknown product)" - self.handler.add_row(batch, row) - self.Session.flush() - return row + # try to locate product by upc + provided = GPC(entry, calc_check_digit=False) + checked = GPC(entry, calc_check_digit='upc') + product = api.get_product_by_upc(self.Session(), provided) + if not product: + product = api.get_product_by_upc(self.Session(), checked) + if product: + row = model.PurchaseBatchRow() + row.product = product + self.handler.add_row(batch, row) + self.Session.flush() + return row + + # check for "bad" upc + if len(entry) > 14: + return + + # product not in system, but presumably sane upc, so add to batch anyway + row = model.PurchaseBatchRow() + row.upc = provided # TODO: why not checked? how to know? + row.item_id = entry + row.description = "(unknown product)" + self.handler.add_row(batch, row) + self.Session.flush() + return row + + elif key == 'item_id': + + # try to locate product by item_id + product = api.get_product_by_item_id(self.Session(), entry) + if product: + row = model.PurchaseBatchRow() + row.product = product + self.handler.add_row(batch, row) + self.Session.flush() + return row + + # check for "too long" item_id + if len(entry) > maxlen(model.PurchaseBatchRow.item_id): + return + + # product not in system, but presumably sane item_id, so add to batch anyway + row = model.PurchaseBatchRow() + row.item_id = entry + row.description = "(unknown product)" + self.handler.add_row(batch, row) + self.Session.flush() + return row + + else: + raise NotImplementedError("don't know how to handle product key: {}".format(key)) def redirect_after_quick_row(self, row, mobile=False): if mobile: @@ -695,11 +776,15 @@ class ReceivingBatchView(PurchasingBatchView): context = { 'row': row, 'batch': batch, + 'parent_instance': batch, 'instance': row, 'instance_title': self.get_row_instance_title(row), 'parent_model_title': self.get_model_title(), 'product_image_url': pod.get_image_url(self.rattail_config, row.upc), 'form': form, + 'populated_from_purchase': self.handler.populated_from_purchase(batch), + 'allow_expired': self.handler.allow_expired_credits(), + 'allow_cases': self.handler.allow_cases(), } if self.request.has_perm('{}.create_row'.format(permission_prefix)): @@ -707,56 +792,47 @@ class ReceivingBatchView(PurchasingBatchView): update_form = forms.Form(schema=schema, request=self.request) if update_form.validate(newstyle=True): row = self.Session.query(model.PurchaseBatchRow).get(update_form.validated['row']) + mode = update_form.validated['mode'] + cases = update_form.validated['cases'] + units = update_form.validated['units'] - # TODO: surely this (delete_row) should be split out to a separate view - if update_form.validated['delete_row']: - if not self.request.has_perm('{}.delete_row'.format(permission_prefix)): - raise httpexceptions.HTTPForbidden() - self.handler.remove_row(row) - return self.redirect(self.get_action_url('view', batch, mobile=True)) + # add values as-is to existing case/unit amounts. note + # that this can sometimes give us negative values! e.g. if + # user scans 1 CS and then subtracts 2 EA, then we would + # have 1 / -2 for our counts. but we consider that to be + # expected, and other logic must allow for the possibility + if cases: + setattr(row, 'cases_{}'.format(mode), + (getattr(row, 'cases_{}'.format(mode)) or 0) + cases) + if units: + setattr(row, 'units_{}'.format(mode), + (getattr(row, 'units_{}'.format(mode)) or 0) + units) - else: # not delete_row - mode = update_form.validated['mode'] - cases = update_form.validated['cases'] - units = update_form.validated['units'] + # if mode in ('damaged', 'expired', 'mispick'): + if mode in ('damaged', 'expired'): + self.attach_credit(row, mode, cases, units, + expiration_date=update_form.validated['expiration_date'], + # discarded=update_form.data['trash'], + # mispick_product=shipped_product) + ) - # add values as-is to existing case/unit amounts. note - # that this can sometimes give us negative values! e.g. if - # user scans 1 CS and then subtracts 2 EA, then we would - # have 1 / -2 for our counts. but we consider that to be - # expected, and other logic must allow for the possibility - if cases: - setattr(row, 'cases_{}'.format(mode), - (getattr(row, 'cases_{}'.format(mode)) or 0) + cases) - if units: - setattr(row, 'units_{}'.format(mode), - (getattr(row, 'units_{}'.format(mode)) or 0) + units) + # first undo any totals previously in effect for the row, then refresh + if row.invoice_total: + batch.invoice_total -= row.invoice_total + self.handler.refresh_row(row) - # if mode in ('damaged', 'expired', 'mispick'): - if mode in ('damaged', 'expired'): - self.attach_credit(row, mode, cases, units, - expiration_date=update_form.validated['expiration_date'], - # discarded=update_form.data['trash'], - # mispick_product=shipped_product) - ) + # keep track of last-used uom, although we just track + # whether or not it was 'CS' since the unit_uom can vary + sticky_case = None + if not update_form.validated['quick_receive']: + if cases and not units: + sticky_case = True + elif units and not cases: + sticky_case = False + if sticky_case is not None: + self.request.session['tailbone.mobile.receiving.sticky_uom_is_case'] = sticky_case - # first undo any totals previously in effect for the row, then refresh - if row.invoice_total: - batch.invoice_total -= row.invoice_total - self.handler.refresh_row(row) - - # keep track of last-used uom, although we just track - # whether or not it was 'CS' since the unit_uom can vary - sticky_case = None - if not update_form.validated['quick_receive']: - if cases and not units: - sticky_case = True - elif units and not cases: - sticky_case = False - if sticky_case is not None: - self.request.session['tailbone.mobile.receiving.sticky_uom_is_case'] = sticky_case - - return self.redirect(self.get_action_url('view', batch, mobile=True)) + return self.redirect(self.get_action_url('view', batch, mobile=True)) # unit_uom can vary by product context['unit_uom'] = 'LB' if row.product and row.product.weighed else 'EA' @@ -773,7 +849,7 @@ class ReceivingBatchView(PurchasingBatchView): if context['uom'] == 'CS' and row.units_ordered and not row.cases_ordered: context['uom'] = context['unit_uom'] - if not row.cases_ordered and not row.units_ordered and not batch.truck_dump: + if self.handler.populated_from_purchase(batch) and not row.cases_ordered and not row.units_ordered: self.request.session.flash("This item was NOT on the original purchase order.", 'receiving-warning') return self.render_to_response('view_row', context, mobile=True) @@ -844,9 +920,24 @@ class ReceivingBatchView(PurchasingBatchView): cls._defaults(config) +# TODO: this is a stopgap measure to fix an obvious bug, which exists when the +# session is not provided by the view at runtime (i.e. when it was instead +# being provided by the type instance, which was created upon app startup). +@colander.deferred +def valid_vendor(node, kw): + session = kw['session'] + def validate(node, value): + vendor = session.query(model.Vendor).get(value) + if not vendor: + raise colander.Invalid(node, "Vendor not found") + return vendor.uuid + return validate + + class MobileNewReceivingBatch(colander.MappingSchema): - vendor = colander.SchemaNode(forms.types.VendorType()) + vendor = colander.SchemaNode(colander.String(), + validator=valid_vendor) workflow = colander.SchemaNode(colander.String(), validator=colander.OneOf([ @@ -895,8 +986,6 @@ class MobileReceivingForm(colander.MappingSchema): quick_receive = colander.SchemaNode(colander.Boolean()) - delete_row = colander.SchemaNode(colander.Boolean()) - def includeme(config): ReceivingBatchView.defaults(config) diff --git a/tailbone/views/settings.py b/tailbone/views/settings.py index 508fd9c6..4bc135a7 100644 --- a/tailbone/views/settings.py +++ b/tailbone/views/settings.py @@ -40,6 +40,8 @@ class SettingsView(MasterView): Master view for the settings model. """ model_class = model.Setting + model_title = "Raw Setting" + model_title_plural = "Raw Settings" feedback = re.compile(r'^rattail\.mail\.user_feedback\..*') grid_columns = [