Add basic "receive row" desktop view for receiving batches

not terribly polished yet, but works
This commit is contained in:
Lance Edgar 2019-03-13 18:31:57 -05:00
parent 7fab472fc4
commit c869516449
8 changed files with 343 additions and 9 deletions

View file

@ -1111,6 +1111,9 @@ class BatchMasterView(MasterView):
def template_kwargs_view_row(self, **kwargs):
kwargs['batch_model_title'] = kwargs['parent_model_title']
# TODO: should these be set somewhere else?
kwargs['row'] = kwargs['instance']
kwargs['batch'] = kwargs['row'].batch
return kwargs
def get_parent(self, row):

View file

@ -800,6 +800,159 @@ class ReceivingBatchView(PurchasingBatchView):
if row.product and row.product.is_pack_item():
return self.get_row_action_url('transform_unit', row)
def receive_row(self, mobile=False):
"""
Primary desktop view for row-level receiving.
"""
# TODO: this code was largely copied from mobile_receive_row() but it
# tries to pave the way for shared logic, i.e. where the latter would
# simply invoke this method and return the result. however we're not
# there yet...for now it's only tested for desktop
self.mobile = mobile
self.viewing = True
row = self.get_row_instance()
batch = row.batch
permission_prefix = self.get_permission_prefix()
possible_modes = [
'received',
'damaged',
'expired',
]
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': self.get_row_image_url(row),
'allow_expired': self.handler.allow_expired_credits(),
'allow_cases': self.handler.allow_cases(),
'quick_receive': False,
'quick_receive_all': False,
}
if mobile:
context['quick_receive'] = self.rattail_config.getbool('rattail.batch', 'purchase.mobile_quick_receive',
default=True)
if batch.order_quantities_known:
context['quick_receive_all'] = self.rattail_config.getbool('rattail.batch', 'purchase.mobile_quick_receive_all',
default=False)
schema = ReceiveRowForm().bind(session=self.Session())
form = forms.Form(schema=schema, request=self.request)
form.set_widget('mode', forms.widgets.JQuerySelectWidget(values=[(m, m) for m in possible_modes]))
form.set_widget('quantity', forms.widgets.CasesUnitsWidget(amount_required=True,
one_amount_only=True))
form.set_type('expiration_date', 'date_jquery')
if not mobile:
form.remove_field('quick_receive')
if form.validate(newstyle=True):
# handler takes care of the row receiving logic for us
kwargs = dict(form.validated)
kwargs['cases'] = kwargs['quantity']['cases']
kwargs['units'] = kwargs['quantity']['units']
del kwargs['quantity']
self.handler.receive_row(row, **kwargs)
# keep track of last-used uom, although we just track
# whether or not it was 'CS' since the unit_uom can vary
# TODO: should this be done for desktop too somehow?
sticky_case = None
if mobile and not form.validated['quick_receive']:
cases = form.validated['cases']
units = form.validated['units']
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
if mobile:
return self.redirect(self.get_action_url('view', batch, mobile=True))
else:
return self.redirect(self.get_row_action_url('view', row))
# unit_uom can vary by product
context['unit_uom'] = 'LB' if row.product and row.product.weighed else 'EA'
if context['quick_receive'] and context['quick_receive_all']:
if context['allow_cases']:
context['quick_receive_uom'] = 'CS'
raise NotImplementedError("TODO: add CS support for quick_receive_all")
else:
context['quick_receive_uom'] = context['unit_uom']
accounted_for = self.handler.get_units_accounted_for(row)
remainder = self.handler.get_units_ordered(row) - accounted_for
if accounted_for:
# some product accounted for; button should receive "remainder" only
if remainder:
remainder = pretty_quantity(remainder)
context['quick_receive_quantity'] = remainder
context['quick_receive_text'] = "Receive Remainder ({} {})".format(remainder, context['unit_uom'])
else:
# unless there is no remainder, in which case disable it
context['quick_receive'] = False
else: # nothing yet accounted for, button should receive "all"
if not remainder:
raise ValueError("why is remainder empty?")
remainder = pretty_quantity(remainder)
context['quick_receive_quantity'] = remainder
context['quick_receive_text'] = "Receive ALL ({} {})".format(remainder, context['unit_uom'])
# effective uom can vary in a few ways...the basic default is 'CS' if
# self.default_uom_is_case is true, otherwise whatever unit_uom is.
sticky_case = None
if mobile:
# TODO: should do this for desktop also, but rename the session variable
sticky_case = self.request.session.get('tailbone.mobile.receiving.sticky_uom_is_case')
if sticky_case is None:
context['uom'] = 'CS' if self.default_uom_is_case else context['unit_uom']
elif sticky_case:
context['uom'] = 'CS'
else:
context['uom'] = context['unit_uom']
if context['uom'] == 'CS' and row.units_ordered and not row.cases_ordered:
context['uom'] = context['unit_uom']
# TODO: should do this for desktop in addition to mobile?
if mobile and 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')
# TODO: should do this for desktop in addition to mobile?
if mobile:
# maybe alert user if they've already received some of this product
alert_received = self.rattail_config.getbool('tailbone', 'receiving.alert_already_received',
default=False)
if alert_received:
if self.handler.get_units_confirmed(row):
msg = "You have already received some of this product; last update was {}.".format(
humanize.naturaltime(make_utc() - row.modified))
self.request.session.flash(msg, 'receiving-warning')
context['form'] = form
context['dform'] = form.make_deform_form()
context['parent_url'] = self.get_action_url('view', batch, mobile=mobile)
context['parent_title'] = self.get_instance_title(batch)
return self.render_to_response('receive_row', context, mobile=mobile)
def transform_unit_row(self):
"""
View which transforms the given row, which is assumed to associate with
@ -853,15 +1006,16 @@ class ReceivingBatchView(PurchasingBatchView):
super(ReceivingBatchView, self).configure_row_form(f)
batch = self.get_instance()
f.set_readonly('cases_ordered')
f.set_readonly('units_ordered')
f.set_readonly('po_unit_cost')
f.set_readonly('po_total')
# allow input for certain fields only; all others are readonly
mutable = [
'invoice_unit_cost',
]
for name in f.fields:
if name not in mutable:
f.set_readonly(name)
# invoice totals
f.set_readonly('invoice_total')
f.set_label('invoice_total', "Invoice Total (Orig.)")
f.set_readonly('invoice_total_calculated')
f.set_label('invoice_total_calculated', "Invoice Total (Calc.)")
# claims
@ -1629,6 +1783,9 @@ class ReceivingBatchView(PurchasingBatchView):
permission_prefix = cls.get_permission_prefix()
# row-level receiving
config.add_route('{}.receive_row'.format(route_prefix), '{}/{{uuid}}/rows/{{row_uuid}}/receive'.format(url_prefix))
config.add_view(cls, attr='receive_row', route_name='{}.receive_row'.format(route_prefix),
permission='{}.edit_row'.format(permission_prefix))
config.add_route('mobile.{}.receive_row'.format(route_prefix), '/mobile{}/{{uuid}}/rows/{{row_uuid}}/receive'.format(url_prefix))
config.add_view(cls, attr='mobile_receive_row', route_name='mobile.{}.receive_row'.format(route_prefix),
permission='{}.edit_row'.format(permission_prefix))
@ -1711,6 +1868,25 @@ def valid_purchase_batch_row(node, kw):
return validate
class ReceiveRowForm(colander.MappingSchema):
mode = colander.SchemaNode(colander.String(),
validator=colander.OneOf([
'received',
'damaged',
'expired',
# 'mispick',
]))
quantity = forms.types.ProductQuantity()
expiration_date = colander.SchemaNode(colander.Date(),
widget=dfwidget.TextInputWidget(),
missing=colander.null)
quick_receive = colander.SchemaNode(colander.Boolean())
class MobileReceivingForm(colander.MappingSchema):
row = colander.SchemaNode(colander.String(),