From 0715bd632195c86e609acd18c7b63c7aba219b41 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 15 Nov 2019 14:50:03 -0600 Subject: [PATCH] Add basic "receive" handler logic for receiving API --- tailbone/api/batch/receiving.py | 66 +++++++++++++++++++++++- tailbone/forms/core.py | 14 ++++-- tailbone/forms/receiving.py | 69 ++++++++++++++++++++++++++ tailbone/views/purchasing/receiving.py | 45 ++--------------- 4 files changed, 150 insertions(+), 44 deletions(-) create mode 100644 tailbone/forms/receiving.py diff --git a/tailbone/api/batch/receiving.py b/tailbone/api/batch/receiving.py index b422b9e1..88a52bf2 100644 --- a/tailbone/api/batch/receiving.py +++ b/tailbone/api/batch/receiving.py @@ -26,13 +26,23 @@ Tailbone Web API - Receiving Batches from __future__ import unicode_literals, absolute_import +import logging + import six import humanize +from rattail import pod from rattail.db import model from rattail.time import make_utc +from deform import widget as dfwidget + +from tailbone import forms from tailbone.api.batch import APIBatchView, APIBatchRowView +from tailbone.forms.receiving import ReceiveRow + + +log = logging.getLogger(__name__) class ReceivingBatchViews(APIBatchView): @@ -244,10 +254,19 @@ class ReceivingBatchRowViews(APIBatchRowView): data['size'] = row.size data['full_description'] = row.product.full_description if row.product else row.description + # only provide image url if so configured + if self.rattail_config.getbool('rattail.batch', 'purchase.mobile_images', default=True): + data['image_url'] = pod.get_image_url(self.rattail_config, row.upc) if row.upc else None + + # unit_uom can vary by product + data['unit_uom'] = 'LB' if row.product and row.product.weighed else 'EA' + data['case_quantity'] = row.case_quantity - data['unit_uom'] = 'EA' # TODO data['order_quantities_known'] = batch.order_quantities_known + data['cases_ordered'] = row.cases_ordered + data['units_ordered'] = row.units_ordered + data['cases_shipped'] = row.cases_shipped data['units_shipped'] = row.units_shipped @@ -294,6 +313,51 @@ class ReceivingBatchRowViews(APIBatchRowView): def get(self): return self._get() + def receive(self): + """ + View which handles "receiving" against a particular batch row. + """ + # first do basic input validation + schema = ReceiveRow().bind(session=self.Session()) + form = forms.Form(schema=schema, request=self.request) + # TODO: this seems hacky, but avoids "complex" date value parsing + form.set_widget('expiration_date', dfwidget.TextInputWidget()) + if not form.validate(newstyle=True): + log.debug("form did not validate: %s", + form.make_deform_form().error) + return {'error': "Form did not validate"} + + # fetch / validate row object + row = self.Session.query(model.PurchaseBatchRow).get(form.validated['row']) + if row is not self.get_object(): + return {'error': "Specified row does not match the route!"} + + # handler takes care of the row receiving logic for us + kwargs = dict(form.validated) + del kwargs['row'] + self.handler.receive_row(row, **kwargs) + + self.Session.flush() + return self._get(obj=row) + + @classmethod + def defaults(cls, config): + cls._batch_row_defaults(config) + cls._receiving_batch_row_defaults(config) + + @classmethod + def _receiving_batch_row_defaults(cls, config): + route_prefix = cls.get_route_prefix() + permission_prefix = cls.get_permission_prefix() + object_url_prefix = cls.get_object_url_prefix() + + # receive (row) + config.add_route('{}.receive'.format(route_prefix), '{}/{{uuid}}/receive'.format(object_url_prefix), + request_method=('OPTIONS', 'POST')) + config.add_view(cls, attr='receive', route_name='{}.receive'.format(route_prefix), + permission='{}.edit_row'.format(permission_prefix), + renderer='json') + def includeme(config): ReceivingBatchViews.defaults(config) diff --git a/tailbone/forms/core.py b/tailbone/forms/core.py index 109cc2df..a923346c 100644 --- a/tailbone/forms/core.py +++ b/tailbone/forms/core.py @@ -924,14 +924,22 @@ class Form(object): if self.request.is_xhr and not self.request.POST: controls = self.request.json_body.items() - # TODO: why in the hell is this necessary? some colander forms - # won't validate if a `None` sneaks its way through? note, in - # particular this was needed to allow anonymous user feedback + # unfortunately the normal form logic (i.e. peppercorn) is + # expecting all values to be strings, whereas the JSON body we + # just parsed, may have given us some Pythonic objects. so + # here we must convert them *back* to strings... + # TODO: this seems like a hack, i must be missing something controls = [[key, val] for key, val in controls] for i in range(len(controls)): key, value = controls[i] if value is None: controls[i][1] = '' + elif value is True: + controls[i][1] = 'true' + elif value is False: + controls[i][1] = 'false' + elif not isinstance(value, six.string_types): + controls[i][1] = six.text_type(value) else: controls = self.request.POST.items() diff --git a/tailbone/forms/receiving.py b/tailbone/forms/receiving.py new file mode 100644 index 00000000..40fa35fe --- /dev/null +++ b/tailbone/forms/receiving.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2019 Lance Edgar +# +# This file is part of Rattail. +# +# Rattail is free software: you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation, either version 3 of the License, or (at your option) any later +# version. +# +# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# Rattail. If not, see . +# +################################################################################ +""" +Forms for Receiving +""" + +from __future__ import unicode_literals, absolute_import + +from rattail.db import model + +import colander + + +@colander.deferred +def valid_purchase_batch_row(node, kw): + session = kw['session'] + def validate(node, value): + row = session.query(model.PurchaseBatchRow).get(value) + if not row: + raise colander.Invalid(node, "Batch row not found") + if row.batch.executed: + raise colander.Invalid(node, "Batch has already been executed") + return row.uuid + return validate + + +class ReceiveRow(colander.MappingSchema): + + row = colander.SchemaNode(colander.String(), + validator=valid_purchase_batch_row) + + mode = colander.SchemaNode(colander.String(), + validator=colander.OneOf([ + 'received', + 'damaged', + 'expired', + # 'mispick', + ])) + + cases = colander.SchemaNode(colander.Decimal(), + missing=colander.null) + + units = colander.SchemaNode(colander.Decimal(), + missing=colander.null) + + expiration_date = colander.SchemaNode(colander.Date(), + missing=colander.null) + + quick_receive = colander.SchemaNode(colander.Boolean()) diff --git a/tailbone/views/purchasing/receiving.py b/tailbone/views/purchasing/receiving.py index ee21a616..bab99147 100644 --- a/tailbone/views/purchasing/receiving.py +++ b/tailbone/views/purchasing/receiving.py @@ -47,6 +47,7 @@ from webhelpers2.html import tags, HTML from tailbone import forms, grids from tailbone.views.purchasing import PurchasingBatchView +from tailbone.forms.receiving import ReceiveRow as MobileReceivingForm log = logging.getLogger(__name__) @@ -1455,6 +1456,8 @@ class ReceivingBatchView(PurchasingBatchView): 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) + # TODO: this seems hacky, but avoids "complex" date value parsing + update_form.set_widget('expiration_date', dfwidget.TextInputWidget()) if update_form.validate(newstyle=True): row = self.Session.query(model.PurchaseBatchRow).get(update_form.validated['row']) mode = update_form.validated['mode'] @@ -1569,6 +1572,8 @@ class ReceivingBatchView(PurchasingBatchView): 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) + # TODO: this seems hacky, but avoids "complex" date value parsing + update_form.set_widget('expiration_date', dfwidget.TextInputWidget()) if update_form.validate(newstyle=True): row = self.Session.query(model.PurchaseBatchRow).get(update_form.validated['row']) mode = update_form.validated['mode'] @@ -1794,22 +1799,6 @@ class MobileNewReceivingFromPO(colander.MappingSchema): purchase = colander.SchemaNode(colander.String()) -# TODO: this is a stopgap measure to fix an obvious bug, which exists when the -# session is not provided by the view at runtime (i.e. when it was instead -# being provided by the type instance, which was created upon app startup). -@colander.deferred -def valid_purchase_batch_row(node, kw): - session = kw['session'] - def validate(node, value): - row = session.query(model.PurchaseBatchRow).get(value) - if not row: - raise colander.Invalid(node, "Batch row not found") - if row.batch.executed: - raise colander.Invalid(node, "Batch has already been executed") - return row.uuid - return validate - - class ReceiveRowForm(colander.MappingSchema): mode = colander.SchemaNode(colander.String(), @@ -1845,29 +1834,5 @@ class DeclareCreditForm(colander.MappingSchema): missing=colander.null) -class MobileReceivingForm(colander.MappingSchema): - - row = colander.SchemaNode(colander.String(), - validator=valid_purchase_batch_row) - - mode = colander.SchemaNode(colander.String(), - validator=colander.OneOf([ - 'received', - 'damaged', - 'expired', - # 'mispick', - ])) - - cases = colander.SchemaNode(colander.Decimal(), missing=None) - - units = colander.SchemaNode(colander.Decimal(), missing=None) - - expiration_date = colander.SchemaNode(colander.Date(), - widget=dfwidget.TextInputWidget(), - missing=colander.null) - - quick_receive = colander.SchemaNode(colander.Boolean()) - - def includeme(config): ReceivingBatchView.defaults(config)