Introduce support for "children first" truck dump receiving
still needs more testing to see what's left...
This commit is contained in:
parent
4af971b83c
commit
a45ce2ced2
|
@ -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:
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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)):
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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())
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue