diff --git a/tailbone/forms/core.py b/tailbone/forms/core.py
index fb2a60d9..2ecdbb5c 100644
--- a/tailbone/forms/core.py
+++ b/tailbone/forms/core.py
@@ -565,7 +565,7 @@ class Form(object):
node = colander.SchemaNode(nodeinfo, **kwargs)
self.nodes[key] = node
- def set_type(self, key, type_):
+ def set_type(self, key, type_, **kwargs):
if type_ == 'datetime':
self.set_renderer(key, self.render_datetime)
elif type_ == 'datetime_local':
@@ -599,9 +599,11 @@ class Form(object):
self.set_widget(key, dfwidget.TextAreaWidget(cols=80, rows=8))
elif type_ == 'file':
tmpstore = SessionFileUploadTempStore(self.request)
- self.set_node(key, colander.SchemaNode(deform.FileData(),
- widget=dfwidget.FileUploadWidget(tmpstore),
- title=self.get_label(key)))
+ kw = {'widget': dfwidget.FileUploadWidget(tmpstore),
+ 'title': self.get_label(key)}
+ if 'required' in kwargs and not kwargs['required']:
+ kw['missing'] = colander.null
+ self.set_node(key, colander.SchemaNode(deform.FileData(), **kw))
else:
raise ValueError("unknown type for '{}' field: {}".format(key, type_))
@@ -619,6 +621,12 @@ class Form(object):
def get_enum(self, key):
return self.enums.get(key)
+ # TODO: i don't think this is actually being used anywhere..?
+ def set_enum_value(self, key, enum_key, enum_value):
+ enum = self.enums.get(key)
+ if enum:
+ enum[enum_key] = enum_value
+
def set_renderer(self, key, renderer):
if renderer is None:
if key in self.renderers:
diff --git a/tailbone/templates/receiving/create.mako b/tailbone/templates/receiving/create.mako
index d05634b9..a8055188 100644
--- a/tailbone/templates/receiving/create.mako
+++ b/tailbone/templates/receiving/create.mako
@@ -48,13 +48,24 @@
$('.field-wrapper.invoice_date').show();
$('.field-wrapper.invoice_number').show();
- } else if (batch_type == 'truck_dump') {
- $('.field-wrapper.truck_dump_batch_uuid').show();
- $('.field-wrapper.invoice_file').show();
- $('.field-wrapper.invoice_parser_key').show();
- $('.field-wrapper.vendor_uuid').hide();
+ } else if (batch_type == 'truck_dump_children_first') {
+ $('.field-wrapper.truck_dump_batch_uuid').hide();
+ $('.field-wrapper.invoice_file').hide();
+ $('.field-wrapper.invoice_parser_key').hide();
+ $('.field-wrapper.vendor_uuid').show();
$('.field-wrapper.date_ordered').hide();
- $('.field-wrapper.date_received').hide();
+ $('.field-wrapper.date_received').show();
+ $('.field-wrapper.po_number').hide();
+ $('.field-wrapper.invoice_date').hide();
+ $('.field-wrapper.invoice_number').hide();
+
+ } else if (batch_type == 'truck_dump_children_last') {
+ $('.field-wrapper.truck_dump_batch_uuid').hide();
+ $('.field-wrapper.invoice_file').hide();
+ $('.field-wrapper.invoice_parser_key').hide();
+ $('.field-wrapper.vendor_uuid').show();
+ $('.field-wrapper.date_ordered').hide();
+ $('.field-wrapper.date_received').show();
$('.field-wrapper.po_number').hide();
$('.field-wrapper.invoice_date').hide();
$('.field-wrapper.invoice_number').hide();
diff --git a/tailbone/templates/receiving/view.mako b/tailbone/templates/receiving/view.mako
index 2204cd77..62c0d6cc 100644
--- a/tailbone/templates/receiving/view.mako
+++ b/tailbone/templates/receiving/view.mako
@@ -54,6 +54,22 @@
% endif
%def>
+<%def name="object_helpers()">
+ ${parent.object_helpers()}
+ % if not request.rattail_config.production() and not batch.executed and not batch.complete and request.has_perm('admin') and batch.is_truck_dump_parent() and batch.truck_dump_children_first:
+
+
Development Tools
+
+ ${h.form(url('{}.auto_receive'.format(route_prefix), uuid=batch.uuid), class_='autodisable')}
+ ${h.csrf_token(request)}
+ ${h.submit('submit', "Auto-Receive All Items")}
+ ${h.end_form()}
+
+
+ % endif
+%def>
+
+
${parent.body()}
% if master.allow_truck_dump and request.has_perm('{}.edit_row'.format(permission_prefix)):
diff --git a/tailbone/views/batch/core.py b/tailbone/views/batch/core.py
index 5c2df5f7..0e8d5344 100644
--- a/tailbone/views/batch/core.py
+++ b/tailbone/views/batch/core.py
@@ -674,9 +674,8 @@ class BatchMasterView(MasterView):
def make_row_grid_tools(self, batch):
return (self.make_default_row_grid_tools(batch) or '') + (self.make_batch_row_grid_tools(batch) or '')
- def get_mobile_row_data(self, batch):
- return super(BatchMasterView, self).get_mobile_row_data(batch)\
- .order_by(self.model_row_class.sequence)
+ def sort_mobile_row_data(self, query):
+ return query.order_by(self.model_row_class.sequence)
def redirect_after_edit(self, batch):
"""
diff --git a/tailbone/views/master.py b/tailbone/views/master.py
index c524da7a..b8ae34e4 100644
--- a/tailbone/views/master.py
+++ b/tailbone/views/master.py
@@ -1325,7 +1325,11 @@ class MasterView(View):
return form.validate(newstyle=True)
def get_mobile_row_data(self, parent):
- return self.get_row_data(parent)
+ query = self.get_row_data(parent)
+ return self.sort_mobile_row_data(query)
+
+ def sort_mobile_row_data(self, query):
+ return query
def mobile_row_route_url(self, route_name, **kwargs):
route_name = 'mobile.{}.{}_row'.format(self.get_route_prefix(), route_name)
diff --git a/tailbone/views/purchasing/batch.py b/tailbone/views/purchasing/batch.py
index e2b9e379..92c787b8 100644
--- a/tailbone/views/purchasing/batch.py
+++ b/tailbone/views/purchasing/batch.py
@@ -591,10 +591,6 @@ class PurchasingBatchView(BatchMasterView):
# query = super(PurchasingBatchView, self).get_row_data(batch)
# return query.options(orm.joinedload(model.PurchaseBatchRow.credits))
- def get_mobile_row_data(self, parent):
- query = self.get_row_data(parent)
- return self.sort_mobile_row_data(query)
-
def sort_mobile_row_data(self, query):
return query.order_by(model.PurchaseBatchRow.modified.desc())
diff --git a/tailbone/views/purchasing/receiving.py b/tailbone/views/purchasing/receiving.py
index 8c935587..2c7969bc 100644
--- a/tailbone/views/purchasing/receiving.py
+++ b/tailbone/views/purchasing/receiving.py
@@ -33,12 +33,13 @@ import six
import sqlalchemy as sa
from rattail import pod
-from rattail.db import model, api
+from rattail.db import model, api, Session as RattailSession
from rattail.db.util import maxlen
from rattail.gpc import GPC
from rattail.time import localtime
from rattail.util import pretty_quantity, prettify, OrderedDict
from rattail.vendors.invoices import iter_invoice_parsers, require_invoice_parser
+from rattail.threads import Thread
import colander
from deform import widget as dfwidget
@@ -47,6 +48,7 @@ from webhelpers2.html import tags, HTML
from tailbone import forms, grids
from tailbone.views.purchasing import PurchasingBatchView
+from tailbone.progress import SessionProgress
log = logging.getLogger(__name__)
@@ -64,13 +66,15 @@ class MobileItemStatusFilter(grids.filters.MobileFilter):
model.PurchaseBatchRow.cases_received != 0,
model.PurchaseBatchRow.units_received != 0))
- # TODO: is this accurate (enough) ?
if value == 'incomplete':
+ # looking for any rows with "ordered" quantity, but where the
+ # status does *not* signify a "settled" row so to speak
return query.filter(sa.or_(model.PurchaseBatchRow.cases_ordered != 0,
model.PurchaseBatchRow.units_ordered != 0))\
.filter(~model.PurchaseBatchRow.status_code.in_((
model.PurchaseBatchRow.STATUS_OK,
- model.PurchaseBatchRow.STATUS_PRODUCT_NOT_FOUND)))
+ model.PurchaseBatchRow.STATUS_PRODUCT_NOT_FOUND,
+ model.PurchaseBatchRow.STATUS_TRUCKDUMP_CLAIMED)))
if value == 'invalid':
return query.filter(model.PurchaseBatchRow.status_code.in_((
@@ -81,13 +85,22 @@ class MobileItemStatusFilter(grids.filters.MobileFilter):
)))
if value == 'unexpected':
+ # looking for any rows which have "received" quantity but which
+ # do *not* have any "ordered" quantity
return query.filter(sa.and_(
sa.or_(
model.PurchaseBatchRow.cases_ordered == None,
model.PurchaseBatchRow.cases_ordered == 0),
sa.or_(
model.PurchaseBatchRow.units_ordered == None,
- model.PurchaseBatchRow.units_ordered == 0)))
+ model.PurchaseBatchRow.units_ordered == 0),
+ sa.or_(
+ model.PurchaseBatchRow.cases_received != 0,
+ model.PurchaseBatchRow.units_received != 0,
+ model.PurchaseBatchRow.cases_damaged != 0,
+ model.PurchaseBatchRow.units_damaged != 0,
+ model.PurchaseBatchRow.cases_expired != 0,
+ model.PurchaseBatchRow.units_expired != 0)))
if value == 'damaged':
return query.filter(sa.or_(
@@ -154,11 +167,13 @@ class ReceivingBatchView(PurchasingBatchView):
form_fields = [
'id',
'batch_type',
- 'description',
'store',
'vendor',
+ 'description',
'truck_dump',
+ 'truck_dump_children_first',
'truck_dump_children',
+ 'truck_dump_ready',
'truck_dump_batch',
'invoice_file',
'invoice_parser_key',
@@ -288,9 +303,15 @@ class ReceivingBatchView(PurchasingBatchView):
# batch_type
if self.creating:
- f.set_enum('batch_type', OrderedDict([
- ('from_scratch', "New from Scratch"),
- ]))
+ batch_types = OrderedDict()
+ if self.allow_from_scratch:
+ batch_types['from_scratch'] = "From Scratch"
+ if self.allow_from_po:
+ batch_types['from_po'] = "From PO"
+ if self.allow_truck_dump:
+ batch_types['truck_dump_children_first'] = "Truck Dump (children FIRST)"
+ batch_types['truck_dump_children_last'] = "Truck Dump (children LAST)"
+ f.set_enum('batch_type', batch_types)
else:
f.remove_field('batch_type')
@@ -305,6 +326,10 @@ class ReceivingBatchView(PurchasingBatchView):
else:
f.set_readonly('truck_dump')
+ # truck_dump_children_first
+ if self.creating or not batch.truck_dump:
+ f.remove_field('truck_dump_children_first')
+
# truck_dump_children
if self.viewing:
if batch.truck_dump:
@@ -314,6 +339,10 @@ class ReceivingBatchView(PurchasingBatchView):
else:
f.remove_field('truck_dump_children')
+ # truck_dump_ready
+ if self.creating or not batch.truck_dump:
+ f.remove_field('truck_dump_ready')
+
# truck_dump_batch
if self.creating:
f.replace('truck_dump_batch', 'truck_dump_batch_uuid')
@@ -344,12 +373,14 @@ class ReceivingBatchView(PurchasingBatchView):
else:
f.remove_fields('truck_dump',
+ 'truck_dump_children_first',
'truck_dump_children',
+ 'truck_dump_ready',
'truck_dump_batch')
# invoice_file
if self.creating:
- f.set_type('invoice_file', 'file')
+ f.set_type('invoice_file', 'file', required=False)
else:
f.set_readonly('invoice_file')
f.set_renderer('invoice_file', self.render_downloadable_file)
@@ -366,9 +397,10 @@ class ReceivingBatchView(PurchasingBatchView):
# store
if self.creating:
store = self.rattail_config.get_store(self.Session())
- f.set_widget('store_uuid', forms.widgets.ReadonlyWidget())
f.set_default('store_uuid', store.uuid)
+ # TODO: seems like set_hidden() should also set HiddenWidget
f.set_hidden('store_uuid')
+ f.set_widget('store_uuid', dfwidget.HiddenWidget())
# purchase
if self.creating:
@@ -402,6 +434,19 @@ class ReceivingBatchView(PurchasingBatchView):
if batch_type == 'from_scratch':
kwargs.pop('truck_dump_batch', None)
kwargs.pop('truck_dump_batch_uuid', None)
+ elif batch_type == 'truck_dump_children_first':
+ kwargs['truck_dump'] = True
+ kwargs['truck_dump_children_first'] = True
+ kwargs['order_quantities_known'] = True
+ # TODO: this makes sense in some cases, but all?
+ # (should just omit that field when not relevant)
+ kwargs['date_ordered'] = None
+ elif batch_type == 'truck_dump_children_last':
+ kwargs['truck_dump'] = True
+ kwargs['truck_dump_ready'] = True
+ # TODO: this makes sense in some cases, but all?
+ # (should just omit that field when not relevant)
+ kwargs['date_ordered'] = None
elif batch_type.startswith('truck_dump_child'):
truck_dump = self.get_instance()
kwargs['store'] = truck_dump.store
@@ -451,7 +496,7 @@ class ReceivingBatchView(PurchasingBatchView):
url = self.request.route_url('receiving.view', uuid=child.uuid)
items.append(HTML.tag('li', c=[tags.link_to(text, url)]))
contents.append(HTML.tag('ul', c=items))
- if batch.complete and not batch.executed:
+ if not batch.executed and (batch.complete or batch.truck_dump_children_first):
buttons = self.make_truck_dump_child_buttons(batch)
if buttons:
buttons = HTML.literal(' ').join(buttons)
@@ -476,7 +521,7 @@ class ReceivingBatchView(PurchasingBatchView):
if batch.executed:
self.request.session.flash("Batch has already been executed: {}".format(batch))
return self.redirect(self.get_action_url('view', batch))
- if not batch.complete:
+ if not batch.complete and not batch.truck_dump_children_first:
self.request.session.flash("Batch is not marked as complete: {}".format(batch))
return self.redirect(self.get_action_url('view', batch))
self.creating = True
@@ -687,10 +732,10 @@ class ReceivingBatchView(PurchasingBatchView):
super(ReceivingBatchView, self).configure_row_grid(g)
g.set_label('department_name', "Department")
- # hide 'ordered' columns for truck dump parent, since that batch type
- # is only concerned with receiving
+ # hide 'ordered' columns for truck dump parent, if its "children first"
+ # flag is set, since that batch type is only concerned with receiving
batch = self.get_instance()
- if batch.is_truck_dump_parent():
+ if batch.is_truck_dump_parent() and not batch.truck_dump_children_first:
g.hide_column('cases_ordered')
g.hide_column('units_ordered')
@@ -1213,6 +1258,15 @@ class ReceivingBatchView(PurchasingBatchView):
if self.rattail_config.getbool('rattail.batch', 'purchase.mobile_images', default=True):
return pod.get_image_url(self.rattail_config, row.upc)
+ def get_mobile_data(self, session=None):
+ query = super(ReceivingBatchView, self).get_mobile_data(session=session)
+
+ # do not expose truck dump child batches on mobile
+ # TODO: is there any case where we *would* want to?
+ query = query.filter(model.PurchaseBatch.truck_dump_batch == None)
+
+ return query
+
def mobile_view_row(self):
"""
Mobile view for receiving batch row items. Note that this also handles
@@ -1279,6 +1333,13 @@ class ReceivingBatchView(PurchasingBatchView):
batch.invoice_total -= row.invoice_total
self.handler.refresh_row(row)
+ # if current batch is a truck dump parent with "children last"
+ # then we now must let handler "make claims" between them
+ if (batch.is_truck_dump_parent()
+ and batch.truck_dump_children_first
+ and row.product):
+ self.handler.make_truck_dump_claims_for_parent_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
@@ -1334,9 +1395,72 @@ class ReceivingBatchView(PurchasingBatchView):
context['uom'] = context['unit_uom']
if batch.order_quantities_known 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')
+ warn = True
+ if batch.is_truck_dump_parent() and row.product:
+ uuids = [child.uuid for child in batch.truck_dump_children]
+ if uuids:
+ count = self.Session.query(model.PurchaseBatchRow)\
+ .filter(model.PurchaseBatchRow.batch_uuid.in_(uuids))\
+ .filter(model.PurchaseBatchRow.product == row.product)\
+ .count()
+ if count:
+ warn = False
+ if warn:
+ 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)
+ def auto_receive(self):
+ """
+ View which can "auto-receive" all items in the batch. Meant only as a
+ convenience for developers.
+ """
+ batch = self.get_instance()
+ key = '{}.receive_all'.format(self.get_grid_key())
+ progress = SessionProgress(self.request, key)
+ kwargs = {'progress': progress}
+ thread = Thread(target=self.auto_receive_thread, args=(batch.uuid, self.request.user.uuid), kwargs=kwargs)
+ thread.start()
+
+ return self.render_progress(progress, {
+ 'instance': batch,
+ 'cancel_url': self.get_action_url('view', batch),
+ 'cancel_msg': "Auto-receive was canceled",
+ })
+
+ def auto_receive_thread(self, uuid, user_uuid, progress=None):
+ """
+ Thread target for receiving all items on the given batch.
+ """
+ session = RattailSession()
+ batch = session.query(model.PurchaseBatch).get(uuid)
+ user = session.query(model.User).get(user_uuid)
+ try:
+ self.handler.auto_receive_all_items(batch, progress=progress)
+
+ # if anything goes wrong, rollback and log the error etc.
+ except Exception as error:
+ session.rollback()
+ log.exception("auto-receive failed for: %s".format(batch))
+ session.close()
+ if progress:
+ progress.session.load()
+ progress.session['error'] = True
+ progress.session['error_msg'] = "Auto-receive failed: {}: {}".format(
+ type(error).__name__, error)
+ progress.session.save()
+
+ # if no error, check result flag (false means user canceled)
+ else:
+ session.commit()
+ session.refresh(batch)
+ success_url = self.get_action_url('view', batch)
+ session.close()
+ if progress:
+ progress.session.load()
+ progress.session['complete'] = True
+ progress.session['success_url'] = success_url
+ progress.session.save()
+
def attach_credit(self, row, credit_type, cases, units, expiration_date=None, discarded=None, mispick_product=None):
batch = row.batch
credit = model.PurchaseBatchCredit()
@@ -1386,6 +1510,7 @@ class ReceivingBatchView(PurchasingBatchView):
@classmethod
def _receiving_defaults(cls, config):
+ rattail_config = config.registry.settings.get('rattail_config')
route_prefix = cls.get_route_prefix()
url_prefix = cls.get_url_prefix()
model_key = cls.get_model_key()
@@ -1403,6 +1528,14 @@ class ReceivingBatchView(PurchasingBatchView):
config.add_view(cls, attr='transform_unit_row', route_name='{}.transform_unit_row'.format(route_prefix),
permission='{}.edit_row'.format(permission_prefix), renderer='json')
+ # auto-receive all items
+ if not rattail_config.production():
+ config.add_route('{}.auto_receive'.format(route_prefix), '{}/{{{}}}/auto-receive'.format(url_prefix, model_key),
+ request_method='POST')
+ config.add_view(cls, attr='auto_receive', route_name='{}.auto_receive'.format(route_prefix),
+ permission='admin')
+
+
@classmethod
def defaults(cls, config):
cls._receiving_defaults(config)