Introduce support for "children first" truck dump receiving

still needs more testing to see what's left...
This commit is contained in:
Lance Edgar 2019-02-06 16:50:40 -06:00
parent 4af971b83c
commit a45ce2ced2
7 changed files with 201 additions and 34 deletions

View file

@ -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:

View file

@ -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();

View file

@ -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:
<div class="object-helper">
<h3>Development Tools</h3>
<div class="object-helper-content">
${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()}
</div>
</div>
% endif
</%def>
${parent.body()}
% if master.allow_truck_dump and request.has_perm('{}.edit_row'.format(permission_prefix)):

View file

@ -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):
"""

View file

@ -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)

View file

@ -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())

View file

@ -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:
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)