diff --git a/tailbone/templates/mobile/receiving/receive_row.mako b/tailbone/templates/mobile/receiving/receive_row.mako new file mode 100644 index 00000000..53d8820f --- /dev/null +++ b/tailbone/templates/mobile/receiving/receive_row.mako @@ -0,0 +1,151 @@ +## -*- coding: utf-8; -*- +<%inherit file="/mobile/master/view_row.mako" /> +<%namespace file="/mobile/keypad.mako" import="keypad" /> + +<%def name="title()">Receiving » ${batch.id_str} » ${master.render_product_key_value(row)} + +<%def name="page_title()">${h.link_to("Receiving", url('mobile.receiving'))} » ${h.link_to(batch.id_str, url('mobile.receiving.view', uuid=batch.uuid))} » ${master.render_product_key_value(row)} + + + +
+ % if instance.product: +

${instance.brand_name or ""}

+

${instance.description} ${instance.size or ''}

+ % if allow_cases: +

1 CS = ${h.pretty_quantity(row.case_quantity or 1)} ${unit_uom}

+ % endif + % else: +

${instance.description}

+ % endif +
+ % if product_image_url: +
+ ${h.image(product_image_url, "product image")} +
+ % endif + + + + + % if batch.order_quantities_known: + + ordered + + % if allow_cases: + ${h.pretty_quantity(row.cases_ordered or 0)} / + % endif + ${h.pretty_quantity(row.units_ordered or 0)} + + + % endif + + received + + % if allow_cases: + ${h.pretty_quantity(row.cases_received or 0)} / + % endif + ${h.pretty_quantity(row.units_received or 0)} + + + + damaged + + % if allow_cases: + ${h.pretty_quantity(row.cases_damaged or 0)} / + % endif + ${h.pretty_quantity(row.units_damaged or 0)} + + + % if allow_expired: + + expired + + % if allow_cases: + ${h.pretty_quantity(row.cases_expired or 0)} / + % endif + ${h.pretty_quantity(row.units_expired or 0)} + + + % endif + + + +% if request.session.peek_flash('receiving-warning'): + % for error in request.session.pop_flash('receiving-warning'): +
${error}
+ % endfor +% endif + +% if not batch.executed and not batch.complete: + + ${h.form(request.current_route_url(), class_='receiving-update')} + ${h.csrf_token(request)} + ${h.hidden('row', value=row.uuid)} + ${h.hidden('cases')} + ${h.hidden('units')} + + ## only show quick-receive if we have an identifiable product + % if quick_receive and instance.product: + % if quick_receive_all: + + % elif allow_cases: + +
+ ## TODO: probably should make these optional / configurable + + + +
+
+ % else: + + % endif + % endif + + ${keypad(unit_uom, uom, allow_cases=allow_cases)} + + + + + + + + + + + + + +
+
+ ${h.radio('mode', value='received', label="received", checked=True)} + ${h.radio('mode', value='damaged', label="damaged")} + % if allow_expired: + ${h.radio('mode', value='expired', label="expired")} + % endif +
+
+
+ + + ## +
+
+ + ${h.hidden('quick_receive', value='false')} + ${h.end_form()} + + % if master.mobile_rows_deletable and master.row_deletable(row) and request.has_perm('{}.delete_row'.format(permission_prefix)): + ${h.form(url('mobile.{}.delete_row'.format(route_prefix), uuid=batch.uuid, row_uuid=row.uuid), class_='receiving-update')} + ${h.csrf_token(request)} + ${h.submit('submit', "Delete this Row")} + ${h.end_form()} + % endif + +% endif diff --git a/tailbone/views/purchasing/receiving.py b/tailbone/views/purchasing/receiving.py index 982b1983..3f86de61 100644 --- a/tailbone/views/purchasing/receiving.py +++ b/tailbone/views/purchasing/receiving.py @@ -1115,6 +1115,15 @@ class ReceivingBatchView(PurchasingBatchView): description = row.product.full_description if row.product else row.description return "({}) {}".format(key, description) + def make_mobile_row_grid_kwargs(self, **kwargs): + kwargs = super(ReceivingBatchView, self).make_mobile_row_grid_kwargs(**kwargs) + + # use custom `receive_row` instead of `view_row` + # TODO: should still use `view_row` in some cases? e.g. executed batch + kwargs['url'] = lambda obj: self.get_row_action_url('receive', obj, mobile=True) + + return kwargs + def should_aggregate_products(self, batch): """ Must return a boolean indicating whether rows should be aggregated by @@ -1395,6 +1404,120 @@ class ReceivingBatchView(PurchasingBatchView): 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 mobile_receive_row(self): + """ + Mobile view for row-level receiving. + """ + self.mobile = True + self.viewing = True + row = self.get_row_instance() + batch = row.batch + permission_prefix = self.get_permission_prefix() + form = self.make_mobile_row_form(row) + 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), + 'form': form, + 'allow_expired': self.handler.allow_expired_credits(), + 'allow_cases': self.handler.allow_cases(), + 'quick_receive': False, + 'quick_receive_all': False, + } + + 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) + + if self.request.has_perm('{}.create_row'.format(permission_prefix)): + schema = MobileReceivingForm().bind(session=self.Session()) + update_form = forms.Form(schema=schema, request=self.request) + if update_form.validate(newstyle=True): + row = self.Session.query(model.PurchaseBatchRow).get(update_form.validated['row']) + mode = update_form.validated['mode'] + cases = update_form.validated['cases'] + units = update_form.validated['units'] + + # handler takes care of the row receiving logic for us + kwargs = dict(update_form.validated) + del kwargs['row'] + 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 + sticky_case = None + if not update_form.validated['quick_receive']: + 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 + + return self.redirect(self.get_action_url('view', batch, mobile=True)) + + # 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 = 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'] + + 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('receive_row', context, mobile=True) + def auto_receive(self): """ View which can "auto-receive" all items in the batch. Meant only as a @@ -1455,6 +1578,11 @@ class ReceivingBatchView(PurchasingBatchView): model_key = cls.get_model_key() permission_prefix = cls.get_permission_prefix() + # row-level receiving + 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)) + if cls.allow_truck_dump: # add TD child batch, from invoice file