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) node = colander.SchemaNode(nodeinfo, **kwargs)
self.nodes[key] = node self.nodes[key] = node
def set_type(self, key, type_): def set_type(self, key, type_, **kwargs):
if type_ == 'datetime': if type_ == 'datetime':
self.set_renderer(key, self.render_datetime) self.set_renderer(key, self.render_datetime)
elif type_ == 'datetime_local': elif type_ == 'datetime_local':
@ -599,9 +599,11 @@ class Form(object):
self.set_widget(key, dfwidget.TextAreaWidget(cols=80, rows=8)) self.set_widget(key, dfwidget.TextAreaWidget(cols=80, rows=8))
elif type_ == 'file': elif type_ == 'file':
tmpstore = SessionFileUploadTempStore(self.request) tmpstore = SessionFileUploadTempStore(self.request)
self.set_node(key, colander.SchemaNode(deform.FileData(), kw = {'widget': dfwidget.FileUploadWidget(tmpstore),
widget=dfwidget.FileUploadWidget(tmpstore), 'title': self.get_label(key)}
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: else:
raise ValueError("unknown type for '{}' field: {}".format(key, type_)) raise ValueError("unknown type for '{}' field: {}".format(key, type_))
@ -619,6 +621,12 @@ class Form(object):
def get_enum(self, key): def get_enum(self, key):
return self.enums.get(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): def set_renderer(self, key, renderer):
if renderer is None: if renderer is None:
if key in self.renderers: if key in self.renderers:

View file

@ -48,13 +48,24 @@
$('.field-wrapper.invoice_date').show(); $('.field-wrapper.invoice_date').show();
$('.field-wrapper.invoice_number').show(); $('.field-wrapper.invoice_number').show();
} else if (batch_type == 'truck_dump') { } else if (batch_type == 'truck_dump_children_first') {
$('.field-wrapper.truck_dump_batch_uuid').show(); $('.field-wrapper.truck_dump_batch_uuid').hide();
$('.field-wrapper.invoice_file').show(); $('.field-wrapper.invoice_file').hide();
$('.field-wrapper.invoice_parser_key').show(); $('.field-wrapper.invoice_parser_key').hide();
$('.field-wrapper.vendor_uuid').hide(); $('.field-wrapper.vendor_uuid').show();
$('.field-wrapper.date_ordered').hide(); $('.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.po_number').hide();
$('.field-wrapper.invoice_date').hide(); $('.field-wrapper.invoice_date').hide();
$('.field-wrapper.invoice_number').hide(); $('.field-wrapper.invoice_number').hide();

View file

@ -54,6 +54,22 @@
% endif % endif
</%def> </%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()} ${parent.body()}
% if master.allow_truck_dump and request.has_perm('{}.edit_row'.format(permission_prefix)): % 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): def make_row_grid_tools(self, batch):
return (self.make_default_row_grid_tools(batch) or '') + (self.make_batch_row_grid_tools(batch) or '') return (self.make_default_row_grid_tools(batch) or '') + (self.make_batch_row_grid_tools(batch) or '')
def get_mobile_row_data(self, batch): def sort_mobile_row_data(self, query):
return super(BatchMasterView, self).get_mobile_row_data(batch)\ return query.order_by(self.model_row_class.sequence)
.order_by(self.model_row_class.sequence)
def redirect_after_edit(self, batch): def redirect_after_edit(self, batch):
""" """

View file

@ -1325,7 +1325,11 @@ class MasterView(View):
return form.validate(newstyle=True) return form.validate(newstyle=True)
def get_mobile_row_data(self, parent): 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): def mobile_row_route_url(self, route_name, **kwargs):
route_name = 'mobile.{}.{}_row'.format(self.get_route_prefix(), route_name) 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) # query = super(PurchasingBatchView, self).get_row_data(batch)
# return query.options(orm.joinedload(model.PurchaseBatchRow.credits)) # 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): def sort_mobile_row_data(self, query):
return query.order_by(model.PurchaseBatchRow.modified.desc()) return query.order_by(model.PurchaseBatchRow.modified.desc())

View file

@ -33,12 +33,13 @@ import six
import sqlalchemy as sa import sqlalchemy as sa
from rattail import pod 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.db.util import maxlen
from rattail.gpc import GPC from rattail.gpc import GPC
from rattail.time import localtime from rattail.time import localtime
from rattail.util import pretty_quantity, prettify, OrderedDict from rattail.util import pretty_quantity, prettify, OrderedDict
from rattail.vendors.invoices import iter_invoice_parsers, require_invoice_parser from rattail.vendors.invoices import iter_invoice_parsers, require_invoice_parser
from rattail.threads import Thread
import colander import colander
from deform import widget as dfwidget from deform import widget as dfwidget
@ -47,6 +48,7 @@ from webhelpers2.html import tags, HTML
from tailbone import forms, grids from tailbone import forms, grids
from tailbone.views.purchasing import PurchasingBatchView from tailbone.views.purchasing import PurchasingBatchView
from tailbone.progress import SessionProgress
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -64,13 +66,15 @@ class MobileItemStatusFilter(grids.filters.MobileFilter):
model.PurchaseBatchRow.cases_received != 0, model.PurchaseBatchRow.cases_received != 0,
model.PurchaseBatchRow.units_received != 0)) model.PurchaseBatchRow.units_received != 0))
# TODO: is this accurate (enough) ?
if value == 'incomplete': 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, return query.filter(sa.or_(model.PurchaseBatchRow.cases_ordered != 0,
model.PurchaseBatchRow.units_ordered != 0))\ model.PurchaseBatchRow.units_ordered != 0))\
.filter(~model.PurchaseBatchRow.status_code.in_(( .filter(~model.PurchaseBatchRow.status_code.in_((
model.PurchaseBatchRow.STATUS_OK, model.PurchaseBatchRow.STATUS_OK,
model.PurchaseBatchRow.STATUS_PRODUCT_NOT_FOUND))) model.PurchaseBatchRow.STATUS_PRODUCT_NOT_FOUND,
model.PurchaseBatchRow.STATUS_TRUCKDUMP_CLAIMED)))
if value == 'invalid': if value == 'invalid':
return query.filter(model.PurchaseBatchRow.status_code.in_(( return query.filter(model.PurchaseBatchRow.status_code.in_((
@ -81,13 +85,22 @@ class MobileItemStatusFilter(grids.filters.MobileFilter):
))) )))
if value == 'unexpected': if value == 'unexpected':
# looking for any rows which have "received" quantity but which
# do *not* have any "ordered" quantity
return query.filter(sa.and_( return query.filter(sa.and_(
sa.or_( sa.or_(
model.PurchaseBatchRow.cases_ordered == None, model.PurchaseBatchRow.cases_ordered == None,
model.PurchaseBatchRow.cases_ordered == 0), model.PurchaseBatchRow.cases_ordered == 0),
sa.or_( sa.or_(
model.PurchaseBatchRow.units_ordered == None, 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': if value == 'damaged':
return query.filter(sa.or_( return query.filter(sa.or_(
@ -154,11 +167,13 @@ class ReceivingBatchView(PurchasingBatchView):
form_fields = [ form_fields = [
'id', 'id',
'batch_type', 'batch_type',
'description',
'store', 'store',
'vendor', 'vendor',
'description',
'truck_dump', 'truck_dump',
'truck_dump_children_first',
'truck_dump_children', 'truck_dump_children',
'truck_dump_ready',
'truck_dump_batch', 'truck_dump_batch',
'invoice_file', 'invoice_file',
'invoice_parser_key', 'invoice_parser_key',
@ -288,9 +303,15 @@ class ReceivingBatchView(PurchasingBatchView):
# batch_type # batch_type
if self.creating: if self.creating:
f.set_enum('batch_type', OrderedDict([ batch_types = OrderedDict()
('from_scratch', "New from Scratch"), 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: else:
f.remove_field('batch_type') f.remove_field('batch_type')
@ -305,6 +326,10 @@ class ReceivingBatchView(PurchasingBatchView):
else: else:
f.set_readonly('truck_dump') 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 # truck_dump_children
if self.viewing: if self.viewing:
if batch.truck_dump: if batch.truck_dump:
@ -314,6 +339,10 @@ class ReceivingBatchView(PurchasingBatchView):
else: else:
f.remove_field('truck_dump_children') 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 # truck_dump_batch
if self.creating: if self.creating:
f.replace('truck_dump_batch', 'truck_dump_batch_uuid') f.replace('truck_dump_batch', 'truck_dump_batch_uuid')
@ -344,12 +373,14 @@ class ReceivingBatchView(PurchasingBatchView):
else: else:
f.remove_fields('truck_dump', f.remove_fields('truck_dump',
'truck_dump_children_first',
'truck_dump_children', 'truck_dump_children',
'truck_dump_ready',
'truck_dump_batch') 'truck_dump_batch')
# invoice_file # invoice_file
if self.creating: if self.creating:
f.set_type('invoice_file', 'file') f.set_type('invoice_file', 'file', required=False)
else: else:
f.set_readonly('invoice_file') f.set_readonly('invoice_file')
f.set_renderer('invoice_file', self.render_downloadable_file) f.set_renderer('invoice_file', self.render_downloadable_file)
@ -366,9 +397,10 @@ class ReceivingBatchView(PurchasingBatchView):
# store # store
if self.creating: if self.creating:
store = self.rattail_config.get_store(self.Session()) store = self.rattail_config.get_store(self.Session())
f.set_widget('store_uuid', forms.widgets.ReadonlyWidget())
f.set_default('store_uuid', store.uuid) f.set_default('store_uuid', store.uuid)
# TODO: seems like set_hidden() should also set HiddenWidget
f.set_hidden('store_uuid') f.set_hidden('store_uuid')
f.set_widget('store_uuid', dfwidget.HiddenWidget())
# purchase # purchase
if self.creating: if self.creating:
@ -402,6 +434,19 @@ class ReceivingBatchView(PurchasingBatchView):
if batch_type == 'from_scratch': if batch_type == 'from_scratch':
kwargs.pop('truck_dump_batch', None) kwargs.pop('truck_dump_batch', None)
kwargs.pop('truck_dump_batch_uuid', 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'): elif batch_type.startswith('truck_dump_child'):
truck_dump = self.get_instance() truck_dump = self.get_instance()
kwargs['store'] = truck_dump.store kwargs['store'] = truck_dump.store
@ -451,7 +496,7 @@ class ReceivingBatchView(PurchasingBatchView):
url = self.request.route_url('receiving.view', uuid=child.uuid) url = self.request.route_url('receiving.view', uuid=child.uuid)
items.append(HTML.tag('li', c=[tags.link_to(text, url)])) items.append(HTML.tag('li', c=[tags.link_to(text, url)]))
contents.append(HTML.tag('ul', c=items)) 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) buttons = self.make_truck_dump_child_buttons(batch)
if buttons: if buttons:
buttons = HTML.literal(' ').join(buttons) buttons = HTML.literal(' ').join(buttons)
@ -476,7 +521,7 @@ class ReceivingBatchView(PurchasingBatchView):
if batch.executed: if batch.executed:
self.request.session.flash("Batch has already been executed: {}".format(batch)) self.request.session.flash("Batch has already been executed: {}".format(batch))
return self.redirect(self.get_action_url('view', 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)) self.request.session.flash("Batch is not marked as complete: {}".format(batch))
return self.redirect(self.get_action_url('view', batch)) return self.redirect(self.get_action_url('view', batch))
self.creating = True self.creating = True
@ -687,10 +732,10 @@ class ReceivingBatchView(PurchasingBatchView):
super(ReceivingBatchView, self).configure_row_grid(g) super(ReceivingBatchView, self).configure_row_grid(g)
g.set_label('department_name', "Department") g.set_label('department_name', "Department")
# hide 'ordered' columns for truck dump parent, since that batch type # hide 'ordered' columns for truck dump parent, if its "children first"
# is only concerned with receiving # flag is set, since that batch type is only concerned with receiving
batch = self.get_instance() 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('cases_ordered')
g.hide_column('units_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): if self.rattail_config.getbool('rattail.batch', 'purchase.mobile_images', default=True):
return pod.get_image_url(self.rattail_config, row.upc) 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): def mobile_view_row(self):
""" """
Mobile view for receiving batch row items. Note that this also handles 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 batch.invoice_total -= row.invoice_total
self.handler.refresh_row(row) 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 # keep track of last-used uom, although we just track
# whether or not it was 'CS' since the unit_uom can vary # whether or not it was 'CS' since the unit_uom can vary
sticky_case = None sticky_case = None
@ -1334,9 +1395,72 @@ class ReceivingBatchView(PurchasingBatchView):
context['uom'] = context['unit_uom'] context['uom'] = context['unit_uom']
if batch.order_quantities_known and not row.cases_ordered and not row.units_ordered: 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') 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) 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): def attach_credit(self, row, credit_type, cases, units, expiration_date=None, discarded=None, mispick_product=None):
batch = row.batch batch = row.batch
credit = model.PurchaseBatchCredit() credit = model.PurchaseBatchCredit()
@ -1386,6 +1510,7 @@ class ReceivingBatchView(PurchasingBatchView):
@classmethod @classmethod
def _receiving_defaults(cls, config): def _receiving_defaults(cls, config):
rattail_config = config.registry.settings.get('rattail_config')
route_prefix = cls.get_route_prefix() route_prefix = cls.get_route_prefix()
url_prefix = cls.get_url_prefix() url_prefix = cls.get_url_prefix()
model_key = cls.get_model_key() 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), config.add_view(cls, attr='transform_unit_row', route_name='{}.transform_unit_row'.format(route_prefix),
permission='{}.edit_row'.format(permission_prefix), renderer='json') 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 @classmethod
def defaults(cls, config): def defaults(cls, config):
cls._receiving_defaults(config) cls._receiving_defaults(config)