diff --git a/tailbone/forms/__init__.py b/tailbone/forms/__init__.py
index 14e5b1a3..e24af4c3 100644
--- a/tailbone/forms/__init__.py
+++ b/tailbone/forms/__init__.py
@@ -2,7 +2,7 @@
################################################################################
#
# Rattail -- Retail Software Framework
-# Copyright © 2010-2015 Lance Edgar
+# Copyright © 2010-2016 Lance Edgar
#
# This file is part of Rattail.
#
@@ -24,6 +24,8 @@
Forms
"""
+from __future__ import unicode_literals, absolute_import
+
from formencode import Schema
from .core import Form, Field, FieldSet, GenericFieldSet
diff --git a/tailbone/templates/purchases/batches/receive_form.mako b/tailbone/templates/purchases/batches/receive_form.mako
new file mode 100644
index 00000000..0a76c390
--- /dev/null
+++ b/tailbone/templates/purchases/batches/receive_form.mako
@@ -0,0 +1,147 @@
+## -*- coding: utf-8 -*-
+<%inherit file="/base.mako" />
+
+<%def name="title()">Receiving Form (${batch.vendor})%def>
+
+<%def name="head_tags()">
+ ${parent.head_tags()}
+ ${h.javascript_link(request.static_url('tailbone:static/js/numeric.js'))}
+
+
+%def>
+
+
+<%def name="context_menu_items()">
+
${h.link_to("Back to Purchase Batch", url('purchases.batch.view', uuid=batch.uuid))}
+%def>
+
+
+
+
+
diff --git a/tailbone/templates/purchases/batches/view.mako b/tailbone/templates/purchases/batches/view.mako
index 3115a3b1..62fab912 100644
--- a/tailbone/templates/purchases/batches/view.mako
+++ b/tailbone/templates/purchases/batches/view.mako
@@ -18,13 +18,20 @@
location.href = '${url('purchases.batch.order_form', uuid=batch.uuid)}';
});
+ $('#receive-form').click(function() {
+ $(this).button('disable').button('option', 'label', "Working, please wait...");
+ location.href = '${url('purchases.batch.receiving_form', uuid=batch.uuid)}';
+ });
+
});
%def>
<%def name="leading_buttons()">
% if batch.mode == enum.PURCHASE_BATCH_MODE_NEW and not batch.complete and not batch.executed and request.has_perm('purchases.batch.order_form'):
-
+
+ % elif batch.mode == enum.PURCHASE_BATCH_MODE_RECEIVING and not batch.complete and not batch.executed and request.has_perm('purchases.batch.receiving_form'):
+
% endif
%def>
diff --git a/tailbone/views/products.py b/tailbone/views/products.py
index 14a762b1..34cd6d7e 100644
--- a/tailbone/views/products.py
+++ b/tailbone/views/products.py
@@ -335,6 +335,46 @@ class ProductsView(MasterView):
'instance_title': self.get_instance_title(instance),
'form': form})
+ def search(self):
+ """
+ Locate a product(s) by UPC.
+
+ Eventually this should be more generic, or at least offer more fields for
+ search. For now it operates only on the ``Product.upc`` field.
+ """
+ data = None
+ upc = self.request.GET.get('upc', '').strip()
+ upc = re.sub(r'\D', '', upc)
+ if upc:
+ product = api.get_product_by_upc(Session(), upc)
+ if not product:
+ # Try again, assuming caller did not include check digit.
+ upc = GPC(upc, calc_check_digit='upc')
+ product = api.get_product_by_upc(Session(), upc)
+ if product and (not product.deleted or self.request.has_perm('products.view_deleted')):
+ data = {
+ 'uuid': product.uuid,
+ 'upc': unicode(product.upc),
+ 'upc_pretty': product.upc.pretty(),
+ 'full_description': product.full_description,
+ 'image_url': pod.get_image_url(self.rattail_config, product.upc),
+ }
+ uuid = self.request.GET.get('with_vendor_cost')
+ if uuid:
+ vendor = Session.query(model.Vendor).get(uuid)
+ if not vendor:
+ return {'error': "Vendor not found"}
+ cost = product.cost_for_vendor(vendor)
+ if cost:
+ data['cost_found'] = True
+ if int(cost.case_size) == cost.case_size:
+ data['cost_case_size'] = int(cost.case_size)
+ else:
+ data['cost_case_size'] = '{:0.4f}'.format(cost.case_size)
+ else:
+ data['cost_found'] = False
+ return {'product': data}
+
def get_supported_batches(self):
return {
'labels': 'rattail.batch.labels:LabelBatchHandler',
@@ -479,6 +519,11 @@ class ProductsView(MasterView):
config.add_view(cls, attr='make_batch', route_name='products.create_batch',
renderer='/products/batch.mako', permission='batches.create')
+ # search (by upc)
+ config.add_route('products.search', '/products/search')
+ config.add_view(cls, attr='search', route_name='products.search',
+ renderer='json', permission='products.view')
+
cls._defaults(config)
@@ -535,34 +580,6 @@ class ProductsAutocomplete(AutocompleteView):
return product.full_description
-def products_search(request):
- """
- Locate a product(s) by UPC.
-
- Eventually this should be more generic, or at least offer more fields for
- search. For now it operates only on the ``Product.upc`` field.
- """
- product = None
- upc = request.GET.get('upc', '').strip()
- upc = re.sub(r'\D', '', upc)
- if upc:
- product = api.get_product_by_upc(Session(), upc)
- if not product:
- # Try again, assuming caller did not include check digit.
- upc = GPC(upc, calc_check_digit='upc')
- product = api.get_product_by_upc(Session(), upc)
- if product:
- if product.deleted and not request.has_perm('products.view_deleted'):
- product = None
- else:
- product = {
- 'uuid': product.uuid,
- 'upc': unicode(product.upc or ''),
- 'full_description': product.full_description,
- }
- return {'product': product}
-
-
def print_labels(request):
profile = request.params.get('profile')
profile = Session.query(model.LabelProfile).get(profile) if profile else None
@@ -600,9 +617,5 @@ def includeme(config):
config.add_view(print_labels, route_name='products.print_labels',
renderer='json', permission='products.print_labels')
- config.add_route('products.search', '/products/search')
- config.add_view(products_search, route_name='products.search',
- renderer='json', permission='products.list')
-
ProductsView.defaults(config)
version_defaults(config, ProductVersionView, 'product')
diff --git a/tailbone/views/purchases/batch.py b/tailbone/views/purchases/batch.py
index ea0cf1b2..eed1a745 100644
--- a/tailbone/views/purchases/batch.py
+++ b/tailbone/views/purchases/batch.py
@@ -26,9 +26,12 @@ Views for purchase order batches
from __future__ import unicode_literals, absolute_import
+import re
+import logging
+
from sqlalchemy import orm
-from rattail import enum
+from rattail import enum, pod
from rattail.db import model, api
from rattail.gpc import GPC
from rattail.time import localtime
@@ -36,6 +39,7 @@ from rattail.core import Object
from rattail.util import OrderedDict
import formalchemy as fa
+import formencode as fe
from pyramid import httpexceptions
from tailbone import forms
@@ -43,6 +47,21 @@ from tailbone.db import Session
from tailbone.views.batch import BatchMasterView
+log = logging.getLogger(__name__)
+
+
+class ReceivingForm(forms.Schema):
+ allow_extra_fields = True
+ filter_extra_fields = True
+ mode = fe.validators.OneOf([
+ 'received',
+ # 'damaged', 'expired', 'mispick',
+ ])
+ product = forms.validators.ValidProduct()
+ cases = fe.validators.Int()
+ units = fe.validators.Int()
+
+
class PurchaseBatchView(BatchMasterView):
"""
Master view for purchase order batches.
@@ -578,6 +597,95 @@ class PurchaseBatchView(BatchMasterView):
'batch_po_total': '${:0,.2f}'.format(batch.po_total),
}
+ def receiving_form(self):
+ """
+ Workflow view for receiving items on a purchase batch.
+ """
+ batch = self.get_instance()
+ if batch.executed:
+ return self.redirect(self.get_action_url('view', batch))
+
+ form = forms.SimpleForm(self.request, schema=ReceivingForm)
+ if form.validate():
+ assert form.data['mode'] == 'received' # TODO
+
+ product = form.data['product']
+ rows = [row for row in batch.active_rows() if row.product is product]
+ if rows:
+ if len(rows) > 1:
+ log.warning("found {} matching rows in batch {} for product: {}".format(
+ len(rows), batch.id_str, product.upc.pretty()))
+ row = rows[0]
+ else:
+ row = model.PurchaseBatchRow()
+ row.product = product
+
+ if form.data['cases']:
+ row.cases_received = (row.cases_received or 0) + form.data['cases']
+ if form.data['units']:
+ row.units_received = (row.units_received or 0) + form.data['units']
+
+ if not row.uuid:
+ batch.add_row(row)
+ self.handler.refresh_row(row)
+
+ self.request.session.flash("({}) {} cases, {} units: {} {}".format(
+ form.data['mode'], form.data['cases'] or 0, form.data['units'] or 0,
+ product.upc.pretty(), product))
+ return self.redirect(self.request.current_route_url())
+
+ title = self.get_instance_title(batch)
+ return self.render_to_response('receive_form', {
+ 'batch': batch,
+ 'instance': batch,
+ 'instance_title': title,
+ 'index_title': "{}: {}".format(self.get_model_title(), title),
+ 'index_url': self.get_action_url('view', batch),
+ 'vendor': batch.vendor,
+ 'form': forms.FormRenderer(form),
+ })
+
+ def receiving_lookup(self):
+ """
+ Try to locate a product by UPC, and validate it in the context of
+ current batch, returning some data for client JS.
+ """
+ batch = self.get_instance()
+ if batch.executed:
+ return {
+ 'error': "Current batch has already been executed",
+ 'redirect': self.get_action_url('view', batch),
+ }
+ data = None
+ upc = self.request.GET.get('upc', '').strip()
+ upc = re.sub(r'\D', '', upc)
+ if upc:
+ product = api.get_product_by_upc(Session(), upc)
+ if not product:
+ # Try again, assuming caller did not include check digit.
+ upc = GPC(upc, calc_check_digit='upc')
+ product = api.get_product_by_upc(Session(), upc)
+ if product and (not product.deleted or self.request.has_perm('products.view_deleted')):
+ data = {
+ 'uuid': product.uuid,
+ 'upc': unicode(product.upc),
+ 'upc_pretty': product.upc.pretty(),
+ 'full_description': product.full_description,
+ 'image_url': pod.get_image_url(self.rattail_config, product.upc),
+ }
+ cost = product.cost_for_vendor(batch.vendor)
+ if cost:
+ data['cost_found'] = True
+ if int(cost.case_size) == cost.case_size:
+ data['cost_case_size'] = int(cost.case_size)
+ else:
+ data['cost_case_size'] = '{:0.4f}'.format(cost.case_size)
+ else:
+ data['cost_found'] = False
+ data['found_in_batch'] = product in [row.product for row in batch.active_rows()]
+
+ return {'product': data}
+
@classmethod
def defaults(cls, config):
route_prefix = cls.get_route_prefix()
@@ -595,7 +703,7 @@ class PurchaseBatchView(BatchMasterView):
cls._batch_defaults(config)
cls._defaults(config)
- # order form
+ # ordering form
config.add_tailbone_permission(permission_prefix, '{}.order_form'.format(permission_prefix),
"Edit new {} in Order Form mode".format(model_title))
config.add_route('{}.order_form'.format(route_prefix), '{}/{{{}}}/order-form'.format(url_prefix, model_key))
@@ -605,6 +713,16 @@ class PurchaseBatchView(BatchMasterView):
config.add_view(cls, attr='order_form_update', route_name='{}.order_form_update'.format(route_prefix),
renderer='json', permission='{}.order_form'.format(permission_prefix))
+ # receiving form, lookup
+ config.add_tailbone_permission(permission_prefix, '{}.receiving_form'.format(permission_prefix),
+ "Edit 'receiving' {} in Receiving Form mode".format(model_title))
+ config.add_route('{}.receiving_form'.format(route_prefix), '{}/{{{}}}/receiving-form'.format(url_prefix, model_key))
+ config.add_view(cls, attr='receiving_form', route_name='{}.receiving_form'.format(route_prefix),
+ permission='{}.receiving_form'.format(permission_prefix))
+ config.add_route('{}.receiving_lookup'.format(route_prefix), '{}/{{{}}}/receiving-form/lookup'.format(url_prefix, model_key))
+ config.add_view(cls, attr='receiving_lookup', route_name='{}.receiving_lookup'.format(route_prefix),
+ renderer='json', permission='{}.receiving_form'.format(permission_prefix))
+
def includeme(config):
PurchaseBatchView.defaults(config)