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:
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)}
+ |
% endif
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)}
+ |
+ % if allow_expired:
+
+ expired |
+
+ % if allow_cases:
+ ${h.pretty_quantity(row.cases_expired or 0)} /
+ % endif
+ ${h.pretty_quantity(row.units_expired or 0)}
+ |
+
+ % endif
@@ -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 @@
@@ -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 = [